package kustomization import ( "bytes" "fmt" "sigs.k8s.io/kustomize/api/filters/filtersutil" "sigs.k8s.io/kustomize/api/filters/fsslice" "sigs.k8s.io/kustomize/api/filters/namespace" "sigs.k8s.io/kustomize/api/filters/refvar" "sigs.k8s.io/kustomize/api/filters/suffix" "sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/kustomize/kyaml/kio" "sigs.k8s.io/kustomize/kyaml/resid" "sigs.k8s.io/kustomize/kyaml/yaml" "edge-infra.dev/pkg/k8s/decoder" "edge-infra.dev/pkg/k8s/unstructured" ) // ProcessManifests applies a set of mutations to the supplied manifests which // allows the manifests to be safely deployed to a shared test cluster by using // the unique test id and namespace isolation. This includes: // - mutating the namespace to test specific namespace // - mutating ClusterRole and ClusterRoleBinding names to include the test uid // - replacing the string `$(TEST_NAMESPACE)` from any config map or env var with the unique test namespace // - removing priority class from deployments // // See test/f2/examples/embed/kustomization/kustomization_test.go // for example usage. func ProcessManifests(uid string, manifests []byte, namespace string) ([]*unstructured.Unstructured, error) { buf := bytes.Buffer{} filters := append( processName(uid), processNamespace(namespace), ) filters = append(filters, processNamespaceVar(namespace)...) filters = append(filters, processPriorityClass()...) err := kio.Pipeline{ Inputs: []kio.Reader{&kio.ByteReader{Reader: bytes.NewReader(manifests)}}, Outputs: []kio.Writer{kio.ByteWriter{Writer: &buf}}, Filters: filters, }.Execute() if err != nil { return nil, fmt.Errorf("failed to update manifests: %w", err) } return decoder.DecodeYAML(buf.Bytes()) } // processNamespace returns a kio.Filter which can be used to transform the // namespace on all objects to the provided namespace func processNamespace(ns string) kio.Filter { fss := types.FsSlice{ { Path: "metadata/namespace", CreateIfNotPresent: true, }, } return namespace.Filter{ Namespace: ns, FsSlice: fss, SetRoleBindingSubjects: namespace.AllServiceAccountSubjects, } } // processName is used to create a set of kio.Filter which can update the names // of any cluster scoped objects to include a unique id suffix to avoid // collisions during parallel test runs. It also updates all references to those // objects in any other objects, such as the ClusterRole reference within the // ClusterRoleBinding. func processName(uid string) []kio.Filter { filters := []kio.Filter{} fsSlice := types.FsSlice{ { Path: "metadata/name", Gvk: resid.Gvk{ Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "ClusterRole", }, }, { Path: "metadata/name", Gvk: resid.Gvk{ Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "ClusterRoleBinding", }, }, // It may be possible to use sigs.k8s.io/kustomize/api/filters/nameref // instead of using this, but I wasn't able to work out that package { Path: "roleRef/name", Gvk: resid.Gvk{ Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "ClusterRoleBinding", }, }, } for _, fieldSpec := range fsSlice { filters = append(filters, suffix.Filter{ // TODO: Unique names should include the namespace as well as the uid Suffix: fmt.Sprintf("-%s", uid), FieldSpec: fieldSpec, }) } return filters } // processNamespaceVar can be used to inject the test namespace into any // environment variables or config map's by replacing any `TEST_NAMESPACE` // string from these locations func processNamespaceVar(ns string) []kio.Filter { envVarName := "TEST_NAMESPACE" return []kio.Filter{ refvar.Filter{ FieldSpec: types.FieldSpec{Path: "spec/template/spec/containers/env/value"}, MappingFunc: refvar.MakePrimitiveReplacer(map[string]int{}, map[string]interface{}{ envVarName: ns, }), }, refvar.Filter{ FieldSpec: types.FieldSpec{Path: "data"}, MappingFunc: refvar.MakePrimitiveReplacer(map[string]int{}, map[string]interface{}{ envVarName: ns, }), }, } } // Removes any priority class from Daemonsets and Deployments - kind and gke // clusters for integration tests won't have any priority classes defined, so // will fail to deploy if this is set. func processPriorityClass() []kio.Filter { return []kio.Filter{kio.FilterAll(fsslice.Filter{ CreateKind: yaml.ScalarNode, SetValue: filtersutil.SetScalar(""), FsSlice: []types.FieldSpec{ {Path: "spec/template/spec/priorityClassName"}, }, })} }