Skip to content

Commit

Permalink
Add support for testing Helm lookup function. (#82)
Browse files Browse the repository at this point in the history
Also some documentation fixes.
  • Loading branch information
porridge authored Jan 23, 2024
1 parent 80ce849 commit 31a7fde
Show file tree
Hide file tree
Showing 6 changed files with 52 additions and 9 deletions.
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,23 @@ release: # Overrides for the Helm release properties. These are applied in root
isInstall: bool # override for the "IsInstall" property of the release options
isUpgrade: bool # override for the "IsUpgrade" property of the release options
server:
visibleSchema: # openAPI schema which is visible to helm, i.e. to check API resource availability
visibleSchemas: # openAPI schema which is visible to helm, i.e. to check API resource availability
# all valid schemas are:
- kubernetes-1.20.2
- openshift-3.11.0
- openshift-4.1.0
- com.coreos
availableSchemas: [] # openAPI schema to validate against, i.e. to validate if rendered objects could be applied
objects: # objects visible to Helm's k8s client, for example via the `lookup` function
# example object specification:
- apiVersion: string
kind: string
metadata:
name: string
namespace: string # optional for cluster-scoped objects
noInherit: bool # indicates that server-side settings should *not* be inherited from the enclosing scope
capabilities: # represents the .Capabilities in Helm
kubeVersion: string # the kubernetes version which is discoverable via `.Capabilities.KubeVersion`
values: # values as consumed by Helm via the `-f` CLI flag.
key: value
set: # alternative format for Helm values, as consumed via the `--set` CLI flag.
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ require (
gopkg.in/yaml.v3 v3.0.1
helm.sh/helm/v3 v3.14.0
k8s.io/apimachinery v0.29.1
k8s.io/client-go v0.29.1
k8s.io/kube-openapi v0.0.0-20240105020646-a37d4de58910
k8s.io/kubectl v0.29.1
k8s.io/utils v0.0.0-20240102154912-e7106e64919e
Expand Down Expand Up @@ -239,7 +240,6 @@ require (
k8s.io/api v0.29.1 // indirect
k8s.io/apiextensions-apiserver v0.29.0 // indirect
k8s.io/cli-runtime v0.29.1 // indirect
k8s.io/client-go v0.29.1 // indirect
k8s.io/klog/v2 v2.110.1 // indirect
mvdan.cc/gofumpt v0.5.0 // indirect
mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed // indirect
Expand Down
8 changes: 8 additions & 0 deletions integration_test/testdata/helmtest/suite.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,15 @@ server:

tests:
- name: "Expect secret to be rendered on upgrades"
server:
objects:
- apiVersion: test.stackrox.io
kind: FakeResource
metadata:
name: example-fr
namespace: loadbalancer
release:
isUpgrade: true
expect: |
.secrets["some-secret-on-upgrade"] | assertThat(. != null)
.notesRaw | assertThat(. | contains("BTW, lookup saw 1 FakeResource"))
2 changes: 2 additions & 0 deletions integration_test/testdata/nginx-example/templates/NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,5 @@
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
{{- end }}

BTW, lookup saw {{ (lookup "test.stackrox.io" "FakeResource" .Release.Namespace "").items | len }} FakeResource
35 changes: 28 additions & 7 deletions pkg/framework/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,13 @@ import (
"github.com/stretchr/testify/require"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/engine"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/yaml"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/dynamic/fake"
"k8s.io/kubectl/pkg/util/openapi"
"k8s.io/kubectl/pkg/validation"
k8sYaml "sigs.k8s.io/yaml"
Expand Down Expand Up @@ -112,7 +116,18 @@ func (r *runner) readAndValidateYAML(fileName, fileContents string, resources op
return objs
}

func (r *runner) instantiateWorld(renderVals chartutil.Values, resources openapi.Resources) map[string]interface{} {
type clientProviderFromDynamicClient struct {
dynIface dynamic.Interface
}

func (c clientProviderFromDynamicClient) GetClientFor(apiVersion, kind string) (dynamic.NamespaceableResourceInterface, bool, error) {
// This is suboptimal, but the best kind->resource translation we can do without a discovery client.
gvr, _ := meta.UnsafeGuessKindToResource(schema.FromAPIVersionAndKind(apiVersion, kind))
namespaced := true // we infer this from the namespace argument to the lookup function
return c.dynIface.Resource(gvr), namespaced, nil
}

func (r *runner) instantiateWorld(renderVals chartutil.Values, resources openapi.Resources, objects []runtime.Object) map[string]interface{} {
world := make(map[string]interface{})

renderValsBytes, err := json.Marshal(renderVals)
Expand All @@ -123,9 +138,12 @@ func (r *runner) instantiateWorld(renderVals chartutil.Values, resources openapi
if err := json.Unmarshal(renderValsBytes, &helmRenderVals); err != nil {
panic(errors.Wrap(err, "unmarshaling Helm render values"))
}

client := fake.NewSimpleDynamicClient(runtime.NewScheme(), objects...)
clientProvider := clientProviderFromDynamicClient{dynIface: client}
world["helm"] = helmRenderVals

renderedTemplates, err := (&engine.Engine{}).Render(r.tgt.Chart, renderVals)
renderedTemplates, err := engine.RenderWithClientProvider(r.tgt.Chart, renderVals, clientProvider)

if *r.test.ExpectError {
r.Require().Error(err, "expected rendering to fail")
Expand Down Expand Up @@ -177,7 +195,7 @@ func (r *runner) instantiateWorld(renderVals chartutil.Values, resources openapi
return world
}

func (r *runner) loadSchemas() (visible, available schemas.Schemas) {
func (r *runner) loadServerSettings() (visible, available schemas.Schemas, objects []runtime.Object) {
var visibleSchemaNames, availableSchemaNames []string
r.test.forEachScopeTopDown(func(t *Test) {
server := t.Server
Expand All @@ -198,6 +216,10 @@ func (r *runner) loadSchemas() (visible, available schemas.Schemas) {
// Every visible schema is also available (but not vice versa)
availableSchemaNames = append(availableSchemaNames, schemaName)
}
for _, o := range server.Objects {
obj := &unstructured.Unstructured{Object: o}
objects = append(objects, obj.DeepCopyObject())
}
})

availableSchemaNames = sliceutils.StringUnique(availableSchemaNames)
Expand All @@ -219,7 +241,7 @@ func (r *runner) loadSchemas() (visible, available schemas.Schemas) {
visible = append(visible, schema)
}

return visible, available
return visible, available, objects
}

func (r *runner) Run() {
Expand All @@ -241,7 +263,7 @@ func (r *runner) Run() {
rel.apply(&releaseOpts)
})

visibleSchemas, availableSchemas := r.loadSchemas()
visibleSchemas, availableSchemas, availableObjects := r.loadServerSettings()

caps := r.tgt.Capabilities
if caps == nil {
Expand All @@ -265,8 +287,7 @@ func (r *runner) Run() {

renderVals, err := chartutil.ToRenderValues(r.tgt.Chart, values, releaseOpts, caps)
r.Require().NoError(err, "failed to obtain render values")

world := r.instantiateWorld(renderVals, availableSchemas)
world := r.instantiateWorld(renderVals, availableSchemas, availableObjects)
r.evaluatePredicates(world)
}

Expand Down
2 changes: 2 additions & 0 deletions pkg/framework/spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ type ServerSpec struct {
// VisibleSchemas are the names of schemas that are available on the server AND discoverable via
// `.Capabilities.APIVersions`.
VisibleSchemas []string `json:"visibleSchemas,omitempty" yaml:"visibleSchemas,omitempty"`
// Objects are definitions of objects visible to Helm's k8s client, for example via the `lookup` function.
Objects []map[string]interface{} `json:"objects,omitempty" yaml:"objects,omitempty"`

// NoInherit indicates that server-side settings should *not* be inherited from the enclosing scope.
NoInherit bool `json:"noInherit,omitempty" yaml:"noInherit,omitempty"`
Expand Down

0 comments on commit 31a7fde

Please sign in to comment.