diff --git a/api/filters/annotations/annotations.go b/api/filters/annotations/annotations.go index d834ecd02..b868d3acf 100644 --- a/api/filters/annotations/annotations.go +++ b/api/filters/annotations/annotations.go @@ -4,15 +4,18 @@ package annotations import ( + "sigs.k8s.io/kustomize/api/filters/filtersutil" "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 annoMap map[string]string + type Filter struct { // Annotations is the set of annotations to apply to the inputs - Annotations map[string]string `yaml:"annotations,omitempty"` + Annotations annoMap `yaml:"annotations,omitempty"` // FsSlice contains the FieldSpecs to locate the namespace field FsSlice types.FsSlice @@ -21,24 +24,19 @@ type Filter struct { var _ kio.Filter = Filter{} func (f Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) { - for i := range nodes { - if err := f.run(nodes[i]); err != nil { - return nil, err - } - } - return nodes, nil -} - -// run applies the filter to a single node. -func (f Filter) run(node *yaml.RNode) error { - for key, value := range f.Annotations { - if err := node.PipeE(fsslice.Filter{ - FsSlice: f.FsSlice, - SetValue: fsslice.SetEntry(key, value), - CreateKind: yaml.MappingNode, // Annotations are MappingNodes. - }); err != nil { - return err - } - } - return nil + keys := filtersutil.SortedMapKeys(f.Annotations) + _, err := kio.FilterAll(yaml.FilterFunc( + func(node *yaml.RNode) (*yaml.RNode, error) { + for _, k := range keys { + if err := node.PipeE(fsslice.Filter{ + FsSlice: f.FsSlice, + SetValue: fsslice.SetEntry(k, f.Annotations[k]), + CreateKind: yaml.MappingNode, // Annotations are MappingNodes. + }); err != nil { + return nil, err + } + } + return node, nil + })).Filter(nodes) + return nodes, err } diff --git a/api/filters/annotations/annotations_test.go b/api/filters/annotations/annotations_test.go index b370f56d0..e1acb5f6e 100644 --- a/api/filters/annotations/annotations_test.go +++ b/api/filters/annotations/annotations_test.go @@ -4,17 +4,17 @@ package annotations import ( - "bytes" "strings" "testing" "github.com/stretchr/testify/assert" "sigs.k8s.io/kustomize/api/internal/plugins/builtinconfig" + filtertest_test "sigs.k8s.io/kustomize/api/testutils/filtertest" "sigs.k8s.io/kustomize/api/types" - "sigs.k8s.io/kustomize/kyaml/kio" - "sigs.k8s.io/kustomize/kyaml/yaml" ) +var annosFs = builtinconfig.MakeDefaultConfig().CommonAnnotations + func TestAnnotations_Filter(t *testing.T) { testCases := map[string]struct { input string @@ -28,11 +28,9 @@ apiVersion: example.com/v1 kind: Foo metadata: name: instance ---- -apiVersion: example.com/v1 -kind: Bar -metadata: - name: instance + annotations: + hero: batman + fiend: riddler `, expectedOutput: ` apiVersion: example.com/v1 @@ -40,17 +38,18 @@ kind: Foo metadata: name: instance annotations: - sleater: kinney ---- -apiVersion: example.com/v1 -kind: Bar -metadata: - name: instance - annotations: - sleater: kinney + hero: batman + fiend: riddler + auto: ford + bean: cannellini + clown: emmett kelley + dragon: smaug `, - filter: Filter{Annotations: map[string]string{ - "sleater": "kinney", + filter: Filter{Annotations: annoMap{ + "clown": "emmett kelley", + "auto": "ford", + "dragon": "smaug", + "bean": "cannellini", }}, }, "update": { @@ -60,7 +59,8 @@ kind: Foo metadata: name: instance annotations: - foo: foo + hero: batman + fiend: riddler `, expectedOutput: ` apiVersion: example.com/v1 @@ -68,10 +68,16 @@ kind: Foo metadata: name: instance annotations: - foo: bar + hero: superman + fiend: luthor + bean: cannellini + clown: emmett kelley `, - filter: Filter{Annotations: map[string]string{ - "foo": "bar", + filter: Filter{Annotations: annoMap{ + "clown": "emmett kelley", + "hero": "superman", + "fiend": "luthor", + "bean": "cannellini", }}, }, "data-fieldspecs": { @@ -107,7 +113,7 @@ a: b: sleater: kinney `, - filter: Filter{Annotations: map[string]string{ + filter: Filter{Annotations: annoMap{ "sleater": "kinney", }}, fsslice: []types.FieldSpec{ @@ -121,126 +127,13 @@ a: for tn, tc := range testCases { t.Run(tn, func(t *testing.T) { - config := builtinconfig.MakeDefaultConfig() - filter := tc.filter - filter.FsSlice = append(config.CommonAnnotations, tc.fsslice...) - - var out bytes.Buffer - rw := kio.ByteReadWriter{ - Reader: bytes.NewBufferString(tc.input), - Writer: &out, - } - - err := kio.Pipeline{ - Inputs: []kio.Reader{&rw}, - Filters: []kio.Filter{filter}, - Outputs: []kio.Writer{&rw}, - }.Execute() - if !assert.NoError(t, err) { - t.FailNow() - } - + filter.FsSlice = append(annosFs, tc.fsslice...) if !assert.Equal(t, strings.TrimSpace(tc.expectedOutput), - strings.TrimSpace(out.String())) { + strings.TrimSpace(filtertest_test.RunFilter(t, tc.input, filter))) { t.FailNow() } }) } } - -func TestAnnotations_Filter_Multiple(t *testing.T) { - input := ` -apiVersion: example.com/v1 -kind: Foo -metadata: - name: instance ---- -apiVersion: example.com/v1 -kind: Bar -metadata: - name: instance -` - annos := map[string]string{ - "sleater": "kinney", - "sonic": "youth", - } - config := builtinconfig.MakeDefaultConfig() - filter := Filter{Annotations: annos} - filter.FsSlice = config.CommonAnnotations - - var out bytes.Buffer - rw := kio.ByteReadWriter{ - Reader: bytes.NewBufferString(input), - Writer: &out, - } - - err := kio.Pipeline{ - Inputs: []kio.Reader{&rw}, - Filters: []kio.Filter{filter}, - Outputs: []kio.Writer{&rw}, - }.Execute() - if !assert.NoError(t, err) { - t.FailNow() - } - - assertHasAnnotation(t, out.String(), annos) -} - -func assertHasAnnotation(t *testing.T, y string, exp map[string]string) bool { - var out bytes.Buffer - rw := kio.ByteReadWriter{ - Reader: bytes.NewBufferString(y), - Writer: &out, - } - filter := &captureAnnotationFilter{ - annotations: make(map[string]annotations), - } - err := kio.Pipeline{ - Inputs: []kio.Reader{&rw}, - Filters: []kio.Filter{filter}, - Outputs: []kio.Writer{&rw}, - }.Execute() - if err != nil { - t.Error(err) - return false - } - - for name, annos := range filter.annotations { - for key, val := range exp { - v, found := annos[key] - if !found { - t.Errorf("expected annotation with key %s in object %s, but didn't find it", - key, name) - return false - } - if want, got := val, v; got != want { - t.Errorf("exected annotation %s in object %s to have value %s, but found %s", - key, name, want, got) - return false - } - } - } - return true -} - -type annotations map[string]string - -type captureAnnotationFilter struct { - annotations map[string]annotations -} - -func (c captureAnnotationFilter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, - error) { - for _, n := range nodes { - meta, err := n.GetMeta() - if err != nil { - return nodes, err - } - name := meta.Name - annos := meta.Annotations - c.annotations[name] = annos - } - return nodes, nil -} diff --git a/api/filters/filtersutil/filtersutil.go b/api/filters/filtersutil/filtersutil.go index e238f7bc0..09c1abd89 100644 --- a/api/filters/filtersutil/filtersutil.go +++ b/api/filters/filtersutil/filtersutil.go @@ -5,11 +5,25 @@ package filtersutil import ( "encoding/json" + "sort" "sigs.k8s.io/kustomize/kyaml/kio" "sigs.k8s.io/kustomize/kyaml/yaml" ) +// SortedMapKeys returns a sorted slice of keys to the given map. +// Writing this function never gets old. +func SortedMapKeys(m map[string]string) []string { + keys := make([]string, len(m)) + i := 0 + for k := range m { + keys[i] = k + i++ + } + sort.Strings(keys) + return keys +} + // ApplyToJSON applies the filter to the json objects. func ApplyToJSON(filter kio.Filter, objs ...marshalerUnmarshaler) error { var nodes []*yaml.RNode diff --git a/api/filters/filtersutil/filtersutil_test.go b/api/filters/filtersutil/filtersutil_test.go index 97471e25e..685bec74e 100644 --- a/api/filters/filtersutil/filtersutil_test.go +++ b/api/filters/filtersutil/filtersutil_test.go @@ -11,6 +11,32 @@ import ( "sigs.k8s.io/kustomize/kyaml/yaml" ) +func TestSortedKeys(t *testing.T) { + testCases := map[string]struct { + input map[string]string + expected []string + }{ + "empty": { + input: map[string]string{}, + expected: []string{}}, + "one": { + input: map[string]string{"a": "aaa"}, + expected: []string{"a"}}, + "three": { + input: map[string]string{"c": "ccc", "b": "bbb", "a": "aaa"}, + expected: []string{"a", "b", "c"}}, + } + for tn, tc := range testCases { + t.Run(tn, func(t *testing.T) { + if !assert.Equal(t, + filtersutil.SortedMapKeys(tc.input), + tc.expected) { + t.FailNow() + } + }) + } +} + func TestApplyToJSON(t *testing.T) { instance1 := bytes.NewBufferString(`{"kind": "Foo"}`) instance2 := bytes.NewBufferString(`{"kind": "Bar"}`) diff --git a/api/filters/labels/doc.go b/api/filters/labels/doc.go new file mode 100644 index 000000000..978033c7e --- /dev/null +++ b/api/filters/labels/doc.go @@ -0,0 +1,6 @@ +// Copyright 2020 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +// Package labels contains a kio.Filter implementation of the kustomize +// labels transformer. +package labels diff --git a/api/filters/labels/example_test.go b/api/filters/labels/example_test.go new file mode 100644 index 000000000..b419b2810 --- /dev/null +++ b/api/filters/labels/example_test.go @@ -0,0 +1,55 @@ +// Copyright 2020 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package labels + +import ( + "bytes" + "log" + "os" + + "sigs.k8s.io/kustomize/api/internal/plugins/builtinconfig" + "sigs.k8s.io/kustomize/kyaml/kio" +) + +func ExampleFilter() { + fss := builtinconfig.MakeDefaultConfig().CommonLabels + 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 +`)}}, + Filters: []kio.Filter{Filter{ + Labels: map[string]string{ + "foo": "bar", + }, + 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 + // labels: + // foo: bar + // --- + // apiVersion: example.com/v1 + // kind: Bar + // metadata: + // name: instance + // labels: + // foo: bar +} diff --git a/api/filters/labels/labels.go b/api/filters/labels/labels.go new file mode 100644 index 000000000..d9b0ad992 --- /dev/null +++ b/api/filters/labels/labels.go @@ -0,0 +1,43 @@ +// Copyright 2020 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package labels + +import ( + "sigs.k8s.io/kustomize/api/filters/filtersutil" + "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 labelMap map[string]string + +// Filter sets labels. +type Filter struct { + // Labels is the set of labels to apply to the inputs + Labels labelMap `yaml:"labels,omitempty"` + + // FsSlice identifies the label fields. + FsSlice types.FsSlice +} + +var _ kio.Filter = Filter{} + +func (f Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) { + keys := filtersutil.SortedMapKeys(f.Labels) + _, err := kio.FilterAll(yaml.FilterFunc( + func(node *yaml.RNode) (*yaml.RNode, error) { + for _, k := range keys { + if err := node.PipeE(fsslice.Filter{ + FsSlice: f.FsSlice, + SetValue: fsslice.SetEntry(k, f.Labels[k]), + CreateKind: yaml.MappingNode, // Labels are MappingNodes. + }); err != nil { + return nil, err + } + } + return node, nil + })).Filter(nodes) + return nodes, err +} diff --git a/api/filters/labels/labels_test.go b/api/filters/labels/labels_test.go new file mode 100644 index 000000000..2758dc215 --- /dev/null +++ b/api/filters/labels/labels_test.go @@ -0,0 +1,139 @@ +// Copyright 2020 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package labels + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "sigs.k8s.io/kustomize/api/internal/plugins/builtinconfig" + filtertest_test "sigs.k8s.io/kustomize/api/testutils/filtertest" + "sigs.k8s.io/kustomize/api/types" +) + +var labelsFs = builtinconfig.MakeDefaultConfig().CommonLabels + +func TestLabels_Filter(t *testing.T) { + testCases := map[string]struct { + input string + expectedOutput string + filter Filter + fsSlice types.FsSlice + }{ + "add": { + input: ` +apiVersion: example.com/v1 +kind: Foo +metadata: + name: instance + labels: + hero: batman + fiend: riddler +`, + expectedOutput: ` +apiVersion: example.com/v1 +kind: Foo +metadata: + name: instance + labels: + hero: batman + fiend: riddler + auto: ford + bean: cannellini + clown: emmett kelley + dragon: smaug +`, + filter: Filter{Labels: labelMap{ + "clown": "emmett kelley", + "auto": "ford", + "dragon": "smaug", + "bean": "cannellini", + }}, + }, + "update": { + input: ` +apiVersion: example.com/v1 +kind: Foo +metadata: + name: instance + labels: + hero: batman + fiend: riddler +`, + expectedOutput: ` +apiVersion: example.com/v1 +kind: Foo +metadata: + name: instance + labels: + hero: superman + fiend: luthor + bean: cannellini + clown: emmett kelley +`, + filter: Filter{Labels: labelMap{ + "clown": "emmett kelley", + "hero": "superman", + "fiend": "luthor", + "bean": "cannellini", + }}, + }, + "data-fieldspecs": { + input: ` +apiVersion: example.com/v1 +kind: Foo +metadata: + name: instance +--- +apiVersion: example.com/v1 +kind: Bar +metadata: + name: instance +`, + expectedOutput: ` +apiVersion: example.com/v1 +kind: Foo +metadata: + name: instance + labels: + sleater: kinney +a: + b: + sleater: kinney +--- +apiVersion: example.com/v1 +kind: Bar +metadata: + name: instance + labels: + sleater: kinney +a: + b: + sleater: kinney +`, + filter: Filter{Labels: labelMap{ + "sleater": "kinney", + }}, + fsSlice: []types.FieldSpec{ + { + Path: "a/b", + CreateIfNotPresent: true, + }, + }, + }, + } + + for tn, tc := range testCases { + t.Run(tn, func(t *testing.T) { + filter := tc.filter + filter.FsSlice = append(labelsFs, tc.fsSlice...) + if !assert.Equal(t, + strings.TrimSpace(tc.expectedOutput), + strings.TrimSpace(filtertest_test.RunFilter(t, tc.input, filter))) { + t.FailNow() + } + }) + } +} diff --git a/api/filters/namespace/namespace.go b/api/filters/namespace/namespace.go index 34bb1fe2f..c8a610b22 100644 --- a/api/filters/namespace/namespace.go +++ b/api/filters/namespace/namespace.go @@ -21,19 +21,14 @@ type Filter struct { 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 + return kio.FilterAll(yaml.FilterFunc(ns.run)).Filter(nodes) } // Run runs the filter on a single node rather than a slice -func (ns Filter) run(node *yaml.RNode) error { +func (ns Filter) run(node *yaml.RNode) (*yaml.RNode, error) { // hacks for hardcoded types -- :( if err := ns.hacks(node); err != nil { - return err + return nil, err } // Remove the fieldspecs that are for hardcoded fields. The fieldspecs @@ -45,11 +40,12 @@ func (ns Filter) run(node *yaml.RNode) error { ns.FsSlice = ns.removeFieldSpecsForHacks(ns.FsSlice) // transformations based on data -- :) - return node.PipeE(fsslice.Filter{ + err := node.PipeE(fsslice.Filter{ FsSlice: ns.FsSlice, SetValue: fsslice.SetScalar(ns.Namespace), CreateKind: yaml.ScalarNode, // Namespace is a ScalarNode }) + return node, err } // hacks applies the namespace transforms that are hardcoded rather diff --git a/api/filters/namespace/namespace_test.go b/api/filters/namespace/namespace_test.go index a68a394e3..f3d1613cc 100644 --- a/api/filters/namespace/namespace_test.go +++ b/api/filters/namespace/namespace_test.go @@ -4,15 +4,14 @@ 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" + filtertest_test "sigs.k8s.io/kustomize/api/testutils/filtertest" "sigs.k8s.io/kustomize/api/types" - "sigs.k8s.io/kustomize/kyaml/kio" ) var tests = []TestCase{ @@ -270,27 +269,10 @@ func TestNamespace_Filter(t *testing.T) { 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())) { + strings.TrimSpace( + filtertest_test.RunFilter(t, test.input, test.filter))) { t.FailNow() } }) diff --git a/api/testutils/filtertest/runfilter.go b/api/testutils/filtertest/runfilter.go new file mode 100644 index 000000000..aea5901fb --- /dev/null +++ b/api/testutils/filtertest/runfilter.go @@ -0,0 +1,30 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package filtertest_test + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + "sigs.k8s.io/kustomize/kyaml/kio" +) + +func RunFilter(t *testing.T, input string, f kio.Filter) string { + var out bytes.Buffer + rw := kio.ByteReadWriter{ + Reader: bytes.NewBufferString(input), + Writer: &out, + } + + err := kio.Pipeline{ + Inputs: []kio.Reader{&rw}, + Filters: []kio.Filter{f}, + Outputs: []kio.Writer{&rw}, + }.Execute() + if !assert.NoError(t, err) { + t.FailNow() + } + return out.String() +}