From 7629a03dd6b9b37deea0ff62a078e800a7d07bbd Mon Sep 17 00:00:00 2001 From: Phillip Wittrock Date: Mon, 23 Mar 2020 11:15:03 -0700 Subject: [PATCH] namespace transformer implementation using kyaml --- api/filters/namespace/doc.go | 9 + api/filters/namespace/example_test.go | 50 ++++ api/filters/namespace/namespace.go | 171 ++++++++++++++ api/filters/namespace/namespace_test.go | 298 ++++++++++++++++++++++++ 4 files changed, 528 insertions(+) create mode 100644 api/filters/namespace/doc.go create mode 100644 api/filters/namespace/example_test.go create mode 100644 api/filters/namespace/namespace.go create mode 100644 api/filters/namespace/namespace_test.go diff --git a/api/filters/namespace/doc.go b/api/filters/namespace/doc.go new file mode 100644 index 000000000..539758b28 --- /dev/null +++ b/api/filters/namespace/doc.go @@ -0,0 +1,9 @@ +// Copyright 2020 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +// Package namespace contains a kio.Filter implementation of the kustomize +// namespace transformer. +// +// Special cases for known Kubernetes resources have been hardcoded in addition +// to those defined by the FsSlice. +package namespace diff --git a/api/filters/namespace/example_test.go b/api/filters/namespace/example_test.go new file mode 100644 index 000000000..04afc314e --- /dev/null +++ b/api/filters/namespace/example_test.go @@ -0,0 +1,50 @@ +// Copyright 2020 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package namespace_test + +import ( + "bytes" + "log" + "os" + + "sigs.k8s.io/kustomize/api/filters/namespace" + "sigs.k8s.io/kustomize/api/internal/plugins/builtinconfig" + "sigs.k8s.io/kustomize/kyaml/kio" +) + +func ExampleFilter() { + fss := builtinconfig.MakeDefaultConfig().NameSpace + err := kio.Pipeline{ + Inputs: []kio.Reader{&kio.ByteReader{Reader: bytes.NewBufferString(` +apiVersion: example.com/v1 +kind: Foo +metadata: + name: instance +--- +apiVersion: example.com/v1 +kind: Bar +metadata: + name: instance + namespace: bar +`)}}, + Filters: []kio.Filter{namespace.Filter{Namespace: "app", FsSlice: fss}}, + Outputs: []kio.Writer{kio.ByteWriter{Writer: os.Stdout}}, + }.Execute() + if err != nil { + log.Fatal(err) + } + + // Output: + // apiVersion: example.com/v1 + // kind: Foo + // metadata: + // name: instance + // namespace: app + // --- + // apiVersion: example.com/v1 + // kind: Bar + // metadata: + // name: instance + // namespace: app +} diff --git a/api/filters/namespace/namespace.go b/api/filters/namespace/namespace.go new file mode 100644 index 000000000..34bb1fe2f --- /dev/null +++ b/api/filters/namespace/namespace.go @@ -0,0 +1,171 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package namespace + +import ( + "sigs.k8s.io/kustomize/api/filters/fsslice" + "sigs.k8s.io/kustomize/api/types" + "sigs.k8s.io/kustomize/kyaml/kio" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +type Filter struct { + // Namespace is the namespace to apply to the inputs + Namespace string `yaml:"namespace,omitempty"` + + // FsSlice contains the FieldSpecs to locate the namespace field + FsSlice types.FsSlice +} + +var _ kio.Filter = Filter{} + +func (ns Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) { + for i := range nodes { + if err := ns.run(nodes[i]); err != nil { + return nil, err + } + } + return nodes, nil +} + +// Run runs the filter on a single node rather than a slice +func (ns Filter) run(node *yaml.RNode) error { + // hacks for hardcoded types -- :( + if err := ns.hacks(node); err != nil { + return err + } + + // Remove the fieldspecs that are for hardcoded fields. The fieldspecs + // exist for backwards compatibility with other implementations + // of this transformation. + // This implementation of the namespace transformation + // Does not use the fieldspecs for implementing cases which + // require hardcoded logic. + ns.FsSlice = ns.removeFieldSpecsForHacks(ns.FsSlice) + + // transformations based on data -- :) + return node.PipeE(fsslice.Filter{ + FsSlice: ns.FsSlice, + SetValue: fsslice.SetScalar(ns.Namespace), + CreateKind: yaml.ScalarNode, // Namespace is a ScalarNode + }) +} + +// hacks applies the namespace transforms that are hardcoded rather +// than specified through FieldSpecs. +func (ns Filter) hacks(obj *yaml.RNode) error { + meta, err := obj.GetMeta() + if err != nil { + return err + } + + if err := ns.metaNamespaceHack(obj, meta); err != nil { + return err + } + + return ns.roleBindingHack(obj, meta) +} + +// metaNamespaceHack is a hack for implementing the namespace transform +// for the metadata.namespace field on namespace scoped resources. +// namespace scoped resources are determined by NOT being present +// in a blacklist of cluster-scoped resource types (by apiVersion and kind). +// +// This hack should be updated to allow individual resources to specify +// if they are cluster scoped through either an annotation on the resources, +// or through inlined OpenAPI on the resource as a YAML comment. +func (ns Filter) metaNamespaceHack(obj *yaml.RNode, meta yaml.ResourceMeta) error { + gvk := fsslice.GetGVK(meta) + if !gvk.IsNamespaceableKind() { + return nil + } + f := fsslice.Filter{ + FsSlice: []types.FieldSpec{ + {Path: metaNamespaceField, CreateIfNotPresent: true}, + }, + SetValue: fsslice.SetScalar(ns.Namespace), + CreateKind: yaml.ScalarNode, // Namespace is a ScalarNode + } + _, err := f.Filter(obj) + return err +} + +// roleBindingHack is a hack for implementing the namespace transform +// for RoleBinding and ClusterRoleBinding resource types. +// RoleBinding and ClusterRoleBinding have namespace set on +// elements of the "subjects" field if and only if the subject elements +// "name" is "default". Otherwise the namespace is not set. +// +// Example: +// +// kind: RoleBinding +// subjects: +// - name: "default" # this will have the namespace set +// ... +// - name: "something-else" # this will not have the namespace set +// ... +func (ns Filter) roleBindingHack(obj *yaml.RNode, meta yaml.ResourceMeta) error { + if meta.Kind != roleBindingKind && meta.Kind != clusterRoleBindingKind { + return nil + } + + // Lookup the namespace field on all elements. + // We should change the fieldspec so this isn't necessary. + obj, err := obj.Pipe(yaml.Lookup(subjectsField)) + if err != nil || yaml.IsEmpty(obj) { + return err + } + + // add the namespace to each "subject" with name: default + err = obj.VisitElements(func(o *yaml.RNode) error { + // copied from kunstruct based kustomize NamespaceTransformer plugin + // The only case we need to force the namespace + // if for the "service account". "default" is + // kind of hardcoded here for right now. + name, err := o.Pipe( + yaml.Lookup("name"), yaml.Match("default"), + ) + if err != nil || yaml.IsEmpty(name) { + return err + } + + // set the namespace for the default account + v := yaml.NewScalarRNode(ns.Namespace) + return o.PipeE( + yaml.LookupCreate(yaml.ScalarNode, "namespace"), + yaml.FieldSetter{Value: v}, + ) + }) + + return err +} + +// removeFieldSpecsForHacks removes from the list fieldspecs that +// have hardcoded implementations +func (ns Filter) removeFieldSpecsForHacks(fs types.FsSlice) types.FsSlice { + var val types.FsSlice + for i := range fs { + // implemented by metaNamespaceHack + if fs[i].Path == metaNamespaceField { + continue + } + // implemented by roleBindingHack + if fs[i].Kind == roleBindingKind && fs[i].Path == subjectsField { + continue + } + // implemented by roleBindingHack + if fs[i].Kind == clusterRoleBindingKind && fs[i].Path == subjectsField { + continue + } + val = append(val, fs[i]) + } + return val +} + +const ( + metaNamespaceField = "metadata/namespace" + subjectsField = "subjects" + roleBindingKind = "RoleBinding" + clusterRoleBindingKind = "ClusterRoleBinding" +) diff --git a/api/filters/namespace/namespace_test.go b/api/filters/namespace/namespace_test.go new file mode 100644 index 000000000..a68a394e3 --- /dev/null +++ b/api/filters/namespace/namespace_test.go @@ -0,0 +1,298 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package namespace_test + +import ( + "bytes" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "sigs.k8s.io/kustomize/api/filters/namespace" + "sigs.k8s.io/kustomize/api/internal/plugins/builtinconfig" + "sigs.k8s.io/kustomize/api/types" + "sigs.k8s.io/kustomize/kyaml/kio" +) + +var tests = []TestCase{ + { + name: "add", + input: ` +apiVersion: example.com/v1 +kind: Foo +metadata: + name: instance +--- +apiVersion: example.com/v1 +kind: Bar +metadata: + name: instance +`, + expected: ` +apiVersion: example.com/v1 +kind: Foo +metadata: + name: instance + namespace: foo +--- +apiVersion: example.com/v1 +kind: Bar +metadata: + name: instance + namespace: foo +`, + filter: namespace.Filter{Namespace: "foo"}, + }, + + { + name: "add-recurse", + input: ` +apiVersion: example.com/v1 +kind: Foo +--- +apiVersion: example.com/v1 +kind: Bar +`, + expected: ` +apiVersion: example.com/v1 +kind: Foo +metadata: + namespace: foo +--- +apiVersion: example.com/v1 +kind: Bar +metadata: + namespace: foo +`, + filter: namespace.Filter{Namespace: "foo"}, + }, + + { + name: "update", + input: ` +apiVersion: example.com/v1 +kind: Foo +metadata: + name: instance + # update this namespace + namespace: bar +--- +apiVersion: example.com/v1 +kind: Bar +metadata: + name: instance + namespace: bar +`, + expected: ` +apiVersion: example.com/v1 +kind: Foo +metadata: + name: instance + # update this namespace + namespace: foo +--- +apiVersion: example.com/v1 +kind: Bar +metadata: + name: instance + namespace: foo +`, + filter: namespace.Filter{Namespace: "foo"}, + }, + + { + name: "update-rolebinding", + input: ` +apiVersion: example.com/v1 +kind: RoleBinding +subjects: +- name: default +--- +apiVersion: example.com/v1 +kind: RoleBinding +subjects: +- name: default + namespace: foo +--- +apiVersion: example.com/v1 +kind: RoleBinding +subjects: +- name: something +--- +apiVersion: example.com/v1 +kind: RoleBinding +subjects: +- name: something + namespace: foo +`, + expected: ` +apiVersion: example.com/v1 +kind: RoleBinding +subjects: +- name: default + namespace: bar +metadata: + namespace: bar +--- +apiVersion: example.com/v1 +kind: RoleBinding +subjects: +- name: default + namespace: bar +metadata: + namespace: bar +--- +apiVersion: example.com/v1 +kind: RoleBinding +subjects: +- name: something +metadata: + namespace: bar +--- +apiVersion: example.com/v1 +kind: RoleBinding +subjects: +- name: something + namespace: foo +metadata: + namespace: bar +`, + filter: namespace.Filter{Namespace: "bar"}, + }, + + { + name: "update-clusterrolebinding", + input: ` +apiVersion: example.com/v1 +kind: ClusterRoleBinding +subjects: +- name: default +--- +apiVersion: example.com/v1 +kind: ClusterRoleBinding +subjects: +- name: default + namespace: foo +--- +apiVersion: example.com/v1 +kind: ClusterRoleBinding +subjects: +- name: something +--- +apiVersion: example.com/v1 +kind: ClusterRoleBinding +subjects: +- name: something + namespace: foo +`, + expected: ` +apiVersion: example.com/v1 +kind: ClusterRoleBinding +subjects: +- name: default + namespace: bar +--- +apiVersion: example.com/v1 +kind: ClusterRoleBinding +subjects: +- name: default + namespace: bar +--- +apiVersion: example.com/v1 +kind: ClusterRoleBinding +subjects: +- name: something +--- +apiVersion: example.com/v1 +kind: ClusterRoleBinding +subjects: +- name: something + namespace: foo +`, + filter: namespace.Filter{Namespace: "bar"}, + }, + + { + name: "data-fieldspecs", + input: ` +apiVersion: example.com/v1 +kind: Foo +metadata: + name: instance +--- +apiVersion: example.com/v1 +kind: Bar +metadata: + name: instance +`, + expected: ` +apiVersion: example.com/v1 +kind: Foo +metadata: + name: instance + namespace: foo +a: + b: + c: foo +--- +apiVersion: example.com/v1 +kind: Bar +metadata: + name: instance + namespace: foo +a: + b: + c: foo +`, + filter: namespace.Filter{Namespace: "foo"}, + fsslice: []types.FieldSpec{ + { + Path: "a/b/c", + CreateIfNotPresent: true, + }, + }, + }, +} + +type TestCase struct { + name string + input string + expected string + filter namespace.Filter + fsslice types.FsSlice +} + +var config = builtinconfig.MakeDefaultConfig() + +func TestNamespace_Filter(t *testing.T) { + for i := range tests { + test := tests[i] + t.Run(test.name, func(t *testing.T) { + test.filter.FsSlice = append(config.NameSpace, test.fsslice...) + + out := &bytes.Buffer{} + rw := &kio.ByteReadWriter{ + Reader: bytes.NewBufferString(test.input), + Writer: out, + } + + // run the filter + err := kio.Pipeline{ + Inputs: []kio.Reader{rw}, + Filters: []kio.Filter{test.filter}, + Outputs: []kio.Writer{rw}, + }.Execute() + if !assert.NoError(t, err) { + t.FailNow() + } + + // check results + if !assert.Equal(t, + strings.TrimSpace(test.expected), + strings.TrimSpace(out.String())) { + t.FailNow() + } + }) + } +}