diff --git a/api/builtins/PrefixSuffixTransformer.go b/api/builtins/PrefixSuffixTransformer.go index 15bb83ce44..148c68a1a4 100644 --- a/api/builtins/PrefixSuffixTransformer.go +++ b/api/builtins/PrefixSuffixTransformer.go @@ -5,31 +5,27 @@ package builtins import ( "errors" - "fmt" - - "sigs.k8s.io/kustomize/api/transform" - "sigs.k8s.io/kustomize/api/types" + "sigs.k8s.io/kustomize/api/filters/prefixsuffix" "sigs.k8s.io/kustomize/api/resid" "sigs.k8s.io/kustomize/api/resmap" + "sigs.k8s.io/kustomize/api/types" + "sigs.k8s.io/kustomize/kyaml/filtersutil" "sigs.k8s.io/yaml" ) // Add the given prefix and suffix to the field. type PrefixSuffixTransformerPlugin struct { - Prefix string `json:"prefix,omitempty" yaml:"prefix,omitempty"` - Suffix string `json:"suffix,omitempty" yaml:"suffix,omitempty"` - FieldSpecs []types.FieldSpec `json:"fieldSpecs,omitempty" yaml:"fieldSpecs,omitempty"` + Prefix string `json:"prefix,omitempty" yaml:"prefix,omitempty"` + Suffix string `json:"suffix,omitempty" yaml:"suffix,omitempty"` + FieldSpecs types.FsSlice `json:"fieldSpecs,omitempty" yaml:"fieldSpecs,omitempty"` } -// Not placed in a file yet due to lack of demand. -var prefixSuffixFieldSpecsToSkip = []types.FieldSpec{ - { - Gvk: resid.Gvk{Kind: "CustomResourceDefinition"}, - }, - { - Gvk: resid.Gvk{Group: "apiregistration.k8s.io", Kind: "APIService"}, - }, +// A Gvk skip list for prefix/suffix modification. +// hard coded for now - eventually should be part of config. +var prefixSuffixFieldSpecsToSkip = types.FsSlice{ + {Gvk: resid.Gvk{Kind: "CustomResourceDefinition"}}, + {Gvk: resid.Gvk{Group: "apiregistration.k8s.io", Kind: "APIService"}}, } func (p *PrefixSuffixTransformerPlugin) Config( @@ -48,29 +44,24 @@ func (p *PrefixSuffixTransformerPlugin) Config( } func (p *PrefixSuffixTransformerPlugin) Transform(m resmap.ResMap) error { - // Even if both the Prefix and Suffix are empty we want // to proceed with the transformation. This allows to add contextual // information to the resources (AddNamePrefix and AddNameSuffix). - for _, r := range m.Resources() { + // TODO: move this test into the filter (i.e. make a better filter) if p.shouldSkip(r.OrgId()) { - // Don't change the actual definition - // of a CRD. continue } id := r.OrgId() // current default configuration contains // only one entry: "metadata/name" with no GVK - for _, path := range p.FieldSpecs { - if !id.IsSelected(&path.Gvk) { - // With the currrent default configuration, - // because no Gvk is specified, so a wild - // card + for _, fs := range p.FieldSpecs { + // TODO: this is redundant to filter (but needed for now) + if !id.IsSelected(&fs.Gvk) { continue } - - if smellsLikeANameChange(&path) { + // TODO: move this test into the filter. + if smellsLikeANameChange(&fs) { // "metadata/name" is the only field. // this will add a prefix and a suffix // to the resource even if those are @@ -78,15 +69,11 @@ func (p *PrefixSuffixTransformerPlugin) Transform(m resmap.ResMap) error { r.AddNamePrefix(p.Prefix) r.AddNameSuffix(p.Suffix) } - - // the addPrefixSuffix method will not - // change the name if both the prefix and suffix - // are empty. - err := transform.MutateField( - r.Map(), - path.PathSlice(), - path.CreateIfNotPresent, - p.addPrefixSuffix) + err := filtersutil.ApplyToJSON(prefixsuffix.Filter{ + Prefix: p.Prefix, + Suffix: p.Suffix, + FieldSpec: fs, + }, r) if err != nil { return err } @@ -99,8 +86,7 @@ func smellsLikeANameChange(fs *types.FieldSpec) bool { return fs.Path == "metadata/name" } -func (p *PrefixSuffixTransformerPlugin) shouldSkip( - id resid.ResId) bool { +func (p *PrefixSuffixTransformerPlugin) shouldSkip(id resid.ResId) bool { for _, path := range prefixSuffixFieldSpecsToSkip { if id.IsSelected(&path.Gvk) { return true @@ -109,15 +95,6 @@ func (p *PrefixSuffixTransformerPlugin) shouldSkip( return false } -func (p *PrefixSuffixTransformerPlugin) addPrefixSuffix( - in interface{}) (interface{}, error) { - s, ok := in.(string) - if !ok { - return nil, fmt.Errorf("%#v is expected to be %T", in, s) - } - return fmt.Sprintf("%s%s%s", p.Prefix, s, p.Suffix), nil -} - func NewPrefixSuffixTransformerPlugin() resmap.TransformerPlugin { return &PrefixSuffixTransformerPlugin{} } diff --git a/api/filters/annotations/annotations.go b/api/filters/annotations/annotations.go index 7e4600103f..759e76e5d9 100644 --- a/api/filters/annotations/annotations.go +++ b/api/filters/annotations/annotations.go @@ -30,7 +30,7 @@ func (f Filter) Filter(nodes []*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], yaml.StringTag), + SetValue: filtersutil.SetEntry(k, f.Annotations[k], yaml.StringTag), CreateKind: yaml.MappingNode, // Annotations are MappingNodes. CreateTag: "!!map", }); err != nil { diff --git a/api/filters/fieldspec/doc.go b/api/filters/fieldspec/doc.go new file mode 100644 index 0000000000..6f643630a5 --- /dev/null +++ b/api/filters/fieldspec/doc.go @@ -0,0 +1,6 @@ +// Copyright 2020 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +// Package fieldspec contains a yaml.Filter to modify a resource +// that matches the FieldSpec. +package fieldspec diff --git a/api/filters/fieldspec/example_test.go b/api/filters/fieldspec/example_test.go new file mode 100644 index 0000000000..53c37a6e5d --- /dev/null +++ b/api/filters/fieldspec/example_test.go @@ -0,0 +1,61 @@ +// Copyright 2020 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package fieldspec_test + +import ( + "bytes" + "log" + "os" + + . "sigs.k8s.io/kustomize/api/filters/fieldspec" + "sigs.k8s.io/kustomize/api/filters/filtersutil" + "sigs.k8s.io/kustomize/api/types" + "sigs.k8s.io/kustomize/kyaml/kio" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +func ExampleFilter() { + in := &kio.ByteReader{ + Reader: bytes.NewBufferString(` +apiVersion: example.com/v1 +kind: Foo +metadata: + name: instance +--- +apiVersion: example.com/v1 +kind: Bar +metadata: + name: instance +`), + } + fltr := Filter{ + CreateKind: yaml.ScalarNode, + SetValue: filtersutil.SetScalar("green"), + FieldSpec: types.FieldSpec{Path: "a/b", CreateIfNotPresent: true}, + } + + err := kio.Pipeline{ + Inputs: []kio.Reader{in}, + Filters: []kio.Filter{kio.FilterAll(fltr)}, + 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 + // a: + // b: green + // --- + // apiVersion: example.com/v1 + // kind: Bar + // metadata: + // name: instance + // a: + // b: green +} diff --git a/api/filters/fsslice/fieldspec_filter.go b/api/filters/fieldspec/fieldspec.go similarity index 88% rename from api/filters/fsslice/fieldspec_filter.go rename to api/filters/fieldspec/fieldspec.go index ca5b0a13aa..dbff33573f 100644 --- a/api/filters/fsslice/fieldspec_filter.go +++ b/api/filters/fieldspec/fieldspec.go @@ -1,24 +1,27 @@ // Copyright 2019 The Kubernetes Authors. // SPDX-License-Identifier: Apache-2.0 -package fsslice +package fieldspec import ( "strings" + "sigs.k8s.io/kustomize/api/filters/filtersutil" "sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/kustomize/kyaml/errors" "sigs.k8s.io/kustomize/kyaml/yaml" ) -// fieldSpecFilter applies a single fieldSpec to a single object -// fieldSpecFilter stores internal state and should not be reused -type fieldSpecFilter struct { +var _ yaml.Filter = Filter{} + +// Filter applies a single fieldSpec to a single object +// Filter stores internal state and should not be reused +type Filter struct { // FieldSpec contains the path to the value to set. FieldSpec types.FieldSpec `yaml:"fieldSpec"` // Set the field using this function - SetValue SetFn + SetValue filtersutil.SetFn // CreateKind defines the type of node to create if the field is not found CreateKind yaml.Kind @@ -29,7 +32,7 @@ type fieldSpecFilter struct { path []string } -func (fltr fieldSpecFilter) Filter(obj *yaml.RNode) (*yaml.RNode, error) { +func (fltr Filter) Filter(obj *yaml.RNode) (*yaml.RNode, error) { // check if the FieldSpec applies to the object if match, err := isMatchGVK(fltr.FieldSpec, obj); !match || err != nil { return obj, errors.Wrap(err) @@ -43,7 +46,7 @@ func (fltr fieldSpecFilter) Filter(obj *yaml.RNode) (*yaml.RNode, error) { return obj, nil } -func (fltr fieldSpecFilter) filter(obj *yaml.RNode) error { +func (fltr Filter) filter(obj *yaml.RNode) error { if len(fltr.path) == 0 { // found the field -- set its value return fltr.SetValue(obj) @@ -60,7 +63,7 @@ func (fltr fieldSpecFilter) filter(obj *yaml.RNode) error { } // field calls filter on the field matching the next path element -func (fltr fieldSpecFilter) field(obj *yaml.RNode) error { +func (fltr Filter) field(obj *yaml.RNode) error { fieldName, isSeq := isSequenceField(fltr.path[0]) // lookup the field matching the next path element @@ -104,9 +107,9 @@ func (fltr fieldSpecFilter) field(obj *yaml.RNode) error { } // seq calls filter on all sequence elements -func (fltr fieldSpecFilter) seq(obj *yaml.RNode) error { +func (fltr Filter) seq(obj *yaml.RNode) error { if err := obj.VisitElements(func(node *yaml.RNode) error { - // recurse on each element -- re-allocating a fieldSpecFilter is + // recurse on each element -- re-allocating a Filter is // not strictly required, but is more consistent with field // and less likely to have side effects // keep the entire path -- it does not contain parts for sequences diff --git a/api/filters/fieldspec/fieldspec_test.go b/api/filters/fieldspec/fieldspec_test.go new file mode 100644 index 0000000000..740105e929 --- /dev/null +++ b/api/filters/fieldspec/fieldspec_test.go @@ -0,0 +1,380 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package fieldspec_test + +import ( + "bytes" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "sigs.k8s.io/kustomize/api/filters/fieldspec" + "sigs.k8s.io/kustomize/api/filters/filtersutil" + "sigs.k8s.io/kustomize/kyaml/kio" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +type TestCase struct { + name string + input string + expected string + filter fieldspec.Filter + fieldSpec string + error string +} + +var tests = []TestCase{ + { + name: "update", + fieldSpec: ` +path: a/b +group: foo +kind: Bar +`, + input: ` +apiVersion: foo/v1beta1 +kind: Bar +a: + b: c +`, + expected: ` +apiVersion: foo/v1beta1 +kind: Bar +a: + b: e +`, + filter: fieldspec.Filter{ + SetValue: filtersutil.SetScalar("e"), + }, + }, + + { + name: "update-kind-not-match", + fieldSpec: ` +path: a/b +group: foo +kind: Bar1 +`, + input: ` +apiVersion: foo/v1beta1 +kind: Bar2 +a: + b: c +`, + expected: ` +apiVersion: foo/v1beta1 +kind: Bar2 +a: + b: c +`, + filter: fieldspec.Filter{ + SetValue: filtersutil.SetScalar("e"), + }, + }, + + { + name: "update-group-not-match", + fieldSpec: ` +path: a/b +group: foo1 +kind: Bar +`, + input: ` +apiVersion: foo2/v1beta1 +kind: Bar +a: + b: c +`, + expected: ` +apiVersion: foo2/v1beta1 +kind: Bar +a: + b: c +`, + filter: fieldspec.Filter{ + SetValue: filtersutil.SetScalar("e"), + }, + }, + + { + name: "update-version-not-match", + fieldSpec: ` +path: a/b +group: foo +version: v1beta1 +kind: Bar +`, + input: ` +apiVersion: foo/v1beta2 +kind: Bar +a: + b: c +`, + expected: ` +apiVersion: foo/v1beta2 +kind: Bar +a: + b: c +`, + filter: fieldspec.Filter{ + SetValue: filtersutil.SetScalar("e"), + }, + }, + + { + name: "bad-version", + fieldSpec: ` +path: a/b +group: foo +version: v1beta1 +kind: Bar +`, + input: ` +apiVersion: foo/v1beta2/something +kind: Bar +a: + b: c +`, + expected: ` +apiVersion: foo/v1beta2/something +kind: Bar +a: + b: c +`, + filter: fieldspec.Filter{ + SetValue: filtersutil.SetScalar("e"), + }, + }, + + { + name: "bad-meta", + fieldSpec: ` +path: a/b +group: foo +version: v1beta1 +kind: Bar +`, + input: ` +a: + b: c +`, + filter: fieldspec.Filter{ + SetValue: filtersutil.SetScalar("e"), + }, + error: "missing Resource metadata", + }, + + { + name: "miss-match-type", + fieldSpec: ` +path: a/b/c +kind: Bar +`, + input: ` +kind: Bar +a: + b: a +`, + error: "obj kind: Bar\na:\n b: a\n at path a/b/c: unsupported yaml node", + filter: fieldspec.Filter{ + SetValue: filtersutil.SetScalar("e"), + }, + }, + + { + name: "add", + fieldSpec: ` +path: a/b/c/d +group: foo +create: true +kind: Bar +`, + input: ` +apiVersion: foo/v1beta1 +kind: Bar +a: {} +`, + expected: ` +apiVersion: foo/v1beta1 +kind: Bar +a: {b: {c: {d: e}}} +`, + filter: fieldspec.Filter{ + SetValue: filtersutil.SetScalar("e"), + CreateKind: yaml.ScalarNode, + }, + }, + + { + name: "update-in-sequence", + fieldSpec: ` +path: a/b[]/c/d +group: foo +kind: Bar +`, + input: ` +apiVersion: foo/v1beta1 +kind: Bar +a: + b: + - c: + d: a +`, + expected: ` +apiVersion: foo/v1beta1 +kind: Bar +a: + b: + - c: + d: e +`, + filter: fieldspec.Filter{ + SetValue: filtersutil.SetScalar("e"), + }, + }, + + // Don't create a sequence + { + name: "empty-sequence-no-create", + fieldSpec: ` +path: a/b[]/c/d +group: foo +create: true +kind: Bar +`, + input: ` +apiVersion: foo/v1beta1 +kind: Bar +a: {} +`, + expected: ` +apiVersion: foo/v1beta1 +kind: Bar +a: {} +`, + filter: fieldspec.Filter{ + SetValue: filtersutil.SetScalar("e"), + CreateKind: yaml.ScalarNode, + }, + }, + + // Create a new field for an element in a sequence + { + name: "empty-sequence-create", + fieldSpec: ` +path: a/b[]/c/d +group: foo +create: true +kind: Bar +`, + input: ` +apiVersion: foo/v1beta1 +kind: Bar +a: + b: + - c: {} +`, + expected: ` +apiVersion: foo/v1beta1 +kind: Bar +a: + b: + - c: {d: e} +`, + filter: fieldspec.Filter{ + SetValue: filtersutil.SetScalar("e"), + CreateKind: yaml.ScalarNode, + }, + }, + + { + name: "group v1", + fieldSpec: ` +path: a/b +group: v1 +create: true +kind: Bar +`, + input: ` +apiVersion: v1 +kind: Bar +`, + expected: ` +apiVersion: v1 +kind: Bar +`, + filter: fieldspec.Filter{ + SetValue: filtersutil.SetScalar("e"), + CreateKind: yaml.ScalarNode, + }, + }, + + { + name: "version v1", + fieldSpec: ` +path: a/b +version: v1 +create: true +kind: Bar +`, + input: ` +apiVersion: v1 +kind: Bar +`, + expected: ` +apiVersion: v1 +kind: Bar +a: + b: e +`, + filter: fieldspec.Filter{ + SetValue: filtersutil.SetScalar("e"), + CreateKind: yaml.ScalarNode, + }, + }, +} + +func TestFilter_Filter(t *testing.T) { + for i := range tests { + test := tests[i] + t.Run(test.name, func(t *testing.T) { + err := yaml.Unmarshal([]byte(test.fieldSpec), &test.filter.FieldSpec) + if !assert.NoError(t, err) { + t.FailNow() + } + + out := &bytes.Buffer{} + rw := &kio.ByteReadWriter{ + Reader: bytes.NewBufferString(test.input), + Writer: out, + OmitReaderAnnotations: true, + } + + // run the filter + err = kio.Pipeline{ + Inputs: []kio.Reader{rw}, + Filters: []kio.Filter{kio.FilterAll(test.filter)}, + Outputs: []kio.Writer{rw}, + }.Execute() + if test.error != "" { + if !assert.EqualError(t, err, test.error) { + t.FailNow() + } + // stop rest of test + return + } + + if !assert.NoError(t, err) { + t.FailNow() + } + + // check results + if !assert.Equal(t, + strings.TrimSpace(test.expected), + strings.TrimSpace(out.String())) { + t.FailNow() + } + }) + } +} diff --git a/api/filters/fsslice/gvk.go b/api/filters/fieldspec/gvk.go similarity index 98% rename from api/filters/fsslice/gvk.go rename to api/filters/fieldspec/gvk.go index c130eb31a0..42e21a80dc 100644 --- a/api/filters/fsslice/gvk.go +++ b/api/filters/fieldspec/gvk.go @@ -1,6 +1,6 @@ // Copyright 2019 The Kubernetes Authors. // SPDX-License-Identifier: Apache-2.0 -package fsslice +package fieldspec import ( "strings" diff --git a/api/filters/fsslice/gvk_test.go b/api/filters/fieldspec/gvk_test.go similarity index 99% rename from api/filters/fsslice/gvk_test.go rename to api/filters/fieldspec/gvk_test.go index 57894ce5d0..0f9d35117a 100644 --- a/api/filters/fsslice/gvk_test.go +++ b/api/filters/fieldspec/gvk_test.go @@ -1,6 +1,6 @@ // Copyright 2019 The Kubernetes Authors. // SPDX-License-Identifier: Apache-2.0 -package fsslice +package fieldspec import ( "strings" diff --git a/api/filters/filtersutil/setters.go b/api/filters/filtersutil/setters.go new file mode 100644 index 0000000000..1785218b58 --- /dev/null +++ b/api/filters/filtersutil/setters.go @@ -0,0 +1,33 @@ +package filtersutil + +import ( + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +// SetFn is a function that accepts an RNode to possibly modify. +type SetFn func(*yaml.RNode) error + +// SetScalar returns a SetFn to set a scalar value +func SetScalar(value string) SetFn { + return func(node *yaml.RNode) error { + return node.PipeE(yaml.FieldSetter{StringValue: value}) + } +} + +// SetEntry returns a SetFn to set an entry in a map +func SetEntry(key, value, tag string) SetFn { + n := &yaml.Node{ + Kind: yaml.ScalarNode, + Value: value, + Tag: tag, + } + if tag == yaml.StringTag && yaml.IsYaml1_1NonString(n) { + n.Style = yaml.DoubleQuotedStyle + } + return func(node *yaml.RNode) error { + return node.PipeE(yaml.FieldSetter{ + Name: key, + Value: yaml.NewRNode(n), + }) + } +} diff --git a/api/filters/fsslice/doc.go b/api/filters/fsslice/doc.go index 407f11c473..b0f197722e 100644 --- a/api/filters/fsslice/doc.go +++ b/api/filters/fsslice/doc.go @@ -1,6 +1,6 @@ // Copyright 2020 The Kubernetes Authors. // SPDX-License-Identifier: Apache-2.0 -// Package fsslice contains a yaml.Filter to modify a resource using an -// FsSlice to identify fields to be updated within the resource. +// Package fsslice contains a yaml.Filter to modify a resource if +// it matches one or more FieldSpec entries. package fsslice diff --git a/api/filters/fsslice/example_test.go b/api/filters/fsslice/example_test.go index ce14d4a58b..cbcbbc45e6 100644 --- a/api/filters/fsslice/example_test.go +++ b/api/filters/fsslice/example_test.go @@ -8,6 +8,7 @@ import ( "log" "os" + "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" @@ -30,7 +31,7 @@ metadata: } fltr := fsslice.Filter{ CreateKind: yaml.ScalarNode, - SetValue: fsslice.SetScalar("green"), + SetValue: filtersutil.SetScalar("green"), FsSlice: []types.FieldSpec{ {Path: "a/b", CreateIfNotPresent: true}, }, diff --git a/api/filters/fsslice/fsslice.go b/api/filters/fsslice/fsslice.go index 2a1044cc5e..9eb5c13133 100644 --- a/api/filters/fsslice/fsslice.go +++ b/api/filters/fsslice/fsslice.go @@ -4,48 +4,22 @@ package fsslice import ( + "sigs.k8s.io/kustomize/api/filters/fieldspec" + "sigs.k8s.io/kustomize/api/filters/filtersutil" "sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/kustomize/kyaml/yaml" ) -// SetFn sets a value -type SetFn func(*yaml.RNode) error - -// SetScalar returns a SetFn to set a scalar value -func SetScalar(value string) SetFn { - return func(node *yaml.RNode) error { - return node.PipeE(yaml.FieldSetter{StringValue: value}) - } -} - -// SetEntry returns a SetFn to set an entry in a map -func SetEntry(key, value, tag string) SetFn { - n := &yaml.Node{ - Kind: yaml.ScalarNode, - Value: value, - Tag: tag, - } - if tag == yaml.StringTag && yaml.IsYaml1_1NonString(n) { - n.Style = yaml.DoubleQuotedStyle - } - return func(node *yaml.RNode) error { - return node.PipeE(yaml.FieldSetter{ - Name: key, - Value: yaml.NewRNode(n), - }) - } - -} - var _ yaml.Filter = Filter{} -// Filter uses an FsSlice to modify fields on a single object +// Filter ranges over an FsSlice to modify fields on a single object. +// An FsSlice is a range of FieldSpecs. A FieldSpec is a GVK plus a path. type Filter struct { // FieldSpecList list of FieldSpecs to set FsSlice types.FsSlice `yaml:"fsSlice"` // SetValue is called on each field that matches one of the FieldSpecs - SetValue SetFn + SetValue filtersutil.SetFn // CreateKind is used to create fields that do not exist CreateKind yaml.Kind @@ -59,7 +33,7 @@ func (fltr Filter) Filter(obj *yaml.RNode) (*yaml.RNode, error) { // apply this FieldSpec // create a new filter for each iteration because they // store internal state about the field paths - _, err := (&fieldSpecFilter{ + _, err := (&fieldspec.Filter{ FieldSpec: fltr.FsSlice[i], SetValue: fltr.SetValue, CreateKind: fltr.CreateKind, diff --git a/api/filters/fsslice/fsslice_test.go b/api/filters/fsslice/fsslice_test.go index 458ad1195b..1dedbedf91 100644 --- a/api/filters/fsslice/fsslice_test.go +++ b/api/filters/fsslice/fsslice_test.go @@ -9,335 +9,77 @@ import ( "testing" "github.com/stretchr/testify/assert" - "sigs.k8s.io/kustomize/api/filters/fsslice" + "sigs.k8s.io/kustomize/api/filters/filtersutil" + . "sigs.k8s.io/kustomize/api/filters/fsslice" "sigs.k8s.io/kustomize/kyaml/kio" "sigs.k8s.io/kustomize/kyaml/yaml" ) type TestCase struct { - name string input string expected string - filter fsslice.Filter + filter Filter fsSlice string error string } -var tests = []TestCase{ - { - name: "update", +var tests = map[string]TestCase{ + "empty": { fsSlice: ` -- path: a/b - group: foo - kind: Bar -`, - input: ` -apiVersion: foo/v1beta1 -kind: Bar -a: - b: c -`, - expected: ` -apiVersion: foo/v1beta1 -kind: Bar -a: - b: e -`, - filter: fsslice.Filter{ - SetValue: fsslice.SetScalar("e"), - }, - }, - - { - name: "update-kind-not-match", - fsSlice: ` -- path: a/b - group: foo - kind: Bar1 -`, - input: ` -apiVersion: foo/v1beta1 -kind: Bar2 -a: - b: c -`, - expected: ` -apiVersion: foo/v1beta1 -kind: Bar2 -a: - b: c -`, - filter: fsslice.Filter{ - SetValue: fsslice.SetScalar("e"), - }, - }, - - { - name: "update-group-not-match", - fsSlice: ` -- path: a/b - group: foo1 - kind: Bar -`, - input: ` -apiVersion: foo2/v1beta1 -kind: Bar -a: - b: c -`, - expected: ` -apiVersion: foo2/v1beta1 -kind: Bar -a: - b: c -`, - filter: fsslice.Filter{ - SetValue: fsslice.SetScalar("e"), - }, - }, - - { - name: "update-version-not-match", - fsSlice: ` -- path: a/b - group: foo - version: v1beta1 - kind: Bar `, input: ` -apiVersion: foo/v1beta2 +apiVersion: foo/v1 kind: Bar -a: - b: c `, expected: ` -apiVersion: foo/v1beta2 +apiVersion: foo/v1 kind: Bar -a: - b: c -`, - filter: fsslice.Filter{ - SetValue: fsslice.SetScalar("e"), - }, - }, - - { - name: "bad-version", - fsSlice: ` -- path: a/b - group: foo - version: v1beta1 - kind: Bar -`, - input: ` -apiVersion: foo/v1beta2/something -kind: Bar -a: - b: c `, - expected: ` -apiVersion: foo/v1beta2/something -kind: Bar -a: - b: c -`, - filter: fsslice.Filter{ - SetValue: fsslice.SetScalar("e"), - }, - }, - - { - name: "bad-meta", - fsSlice: ` -- path: a/b - group: foo - version: v1beta1 - kind: Bar -`, - input: ` -a: - b: c -`, - filter: fsslice.Filter{ - SetValue: fsslice.SetScalar("e"), - }, - error: "missing Resource metadata", - }, - - { - name: "miss-match-type", - fsSlice: ` -- path: a/b/c - kind: Bar -`, - input: ` -kind: Bar -a: - b: a -`, - error: "obj kind: Bar\na:\n b: a\n at path a/b/c: unsupported yaml node", - filter: fsslice.Filter{ - SetValue: fsslice.SetScalar("e"), - }, - }, - - { - name: "add", - fsSlice: ` -- path: a/b/c/d - group: foo - create: true - kind: Bar -`, - input: ` -apiVersion: foo/v1beta1 -kind: Bar -a: {} -`, - expected: ` -apiVersion: foo/v1beta1 -kind: Bar -a: {b: {c: {d: e}}} -`, - filter: fsslice.Filter{ - SetValue: fsslice.SetScalar("e"), + filter: Filter{ + SetValue: filtersutil.SetScalar("e"), CreateKind: yaml.ScalarNode, }, }, - - { - name: "update-in-sequence", + "two": { fsSlice: ` -- path: a/b[]/c/d - group: foo - kind: Bar -`, - input: ` -apiVersion: foo/v1beta1 -kind: Bar -a: - b: - - c: - d: a -`, - expected: ` -apiVersion: foo/v1beta1 -kind: Bar -a: - b: - - c: - d: e -`, - filter: fsslice.Filter{ - SetValue: fsslice.SetScalar("e"), - }, - }, - - // Don't create a sequence - { - name: "empty-sequence-no-create", - fsSlice: ` -- path: a/b[]/c/d +- path: a/b group: foo + version: v1 create: true kind: Bar -`, - input: ` -apiVersion: foo/v1beta1 -kind: Bar -a: {} -`, - expected: ` -apiVersion: foo/v1beta1 -kind: Bar -a: {} -`, - filter: fsslice.Filter{ - SetValue: fsslice.SetScalar("e"), - CreateKind: yaml.ScalarNode, - }, - }, - - // Create a new field for an element in a sequence - { - name: "empty-sequence-create", - fsSlice: ` -- path: a/b[]/c/d +- path: q/r[]/s/t group: foo - create: true - kind: Bar -`, - input: ` -apiVersion: foo/v1beta1 -kind: Bar -a: - b: - - c: {} -`, - expected: ` -apiVersion: foo/v1beta1 -kind: Bar -a: - b: - - c: {d: e} -`, - filter: fsslice.Filter{ - SetValue: fsslice.SetScalar("e"), - CreateKind: yaml.ScalarNode, - }, - }, - - { - name: "group v1", - fsSlice: ` -- path: a/b - group: v1 - create: true - kind: Bar -`, - input: ` -apiVersion: v1 -kind: Bar -`, - expected: ` -apiVersion: v1 -kind: Bar -`, - filter: fsslice.Filter{ - SetValue: fsslice.SetScalar("e"), - CreateKind: yaml.ScalarNode, - }, - }, - - { - name: "version v1", - fsSlice: ` -- path: a/b version: v1 create: true kind: Bar `, input: ` -apiVersion: v1 +apiVersion: foo/v1 kind: Bar +q: + r: + - s: {} `, expected: ` -apiVersion: v1 +apiVersion: foo/v1 kind: Bar +q: + r: + - s: {t: e} a: b: e `, - filter: fsslice.Filter{ - SetValue: fsslice.SetScalar("e"), + filter: Filter{ + SetValue: filtersutil.SetScalar("e"), CreateKind: yaml.ScalarNode, }, }, } -func TestFilter_Filter(t *testing.T) { - for i := range tests { - test := tests[i] - t.Run(test.name, func(t *testing.T) { +func TestFilter(t *testing.T) { + for name := range tests { + test := tests[name] + t.Run(name, func(t *testing.T) { err := yaml.Unmarshal([]byte(test.fsSlice), &test.filter.FsSlice) if !assert.NoError(t, err) { t.FailNow() diff --git a/api/filters/imagetag/imagetag.go b/api/filters/imagetag/imagetag.go index eb78a2af52..f20fe7e075 100644 --- a/api/filters/imagetag/imagetag.go +++ b/api/filters/imagetag/imagetag.go @@ -4,6 +4,7 @@ package imagetag 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" @@ -35,7 +36,7 @@ func (f Filter) filter(node *yaml.RNode) (*yaml.RNode, error) { return node, nil } -func updateImageTagFn(imageTag types.Image) fsslice.SetFn { +func updateImageTagFn(imageTag types.Image) filtersutil.SetFn { return func(node *yaml.RNode) error { return node.PipeE(imageTagUpdater{ ImageTag: imageTag, diff --git a/api/filters/labels/labels.go b/api/filters/labels/labels.go index c3f2bf7251..14ad7e56f2 100644 --- a/api/filters/labels/labels.go +++ b/api/filters/labels/labels.go @@ -31,7 +31,7 @@ func (f Filter) Filter(nodes []*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], yaml.StringTag), + SetValue: filtersutil.SetEntry(k, f.Labels[k], yaml.StringTag), CreateKind: yaml.MappingNode, // Labels are MappingNodes. CreateTag: "!!map", }); err != nil { diff --git a/api/filters/namespace/namespace.go b/api/filters/namespace/namespace.go index 16f5884549..6189529c11 100644 --- a/api/filters/namespace/namespace.go +++ b/api/filters/namespace/namespace.go @@ -4,6 +4,8 @@ package namespace import ( + "sigs.k8s.io/kustomize/api/filters/fieldspec" + "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" @@ -42,7 +44,7 @@ func (ns Filter) run(node *yaml.RNode) (*yaml.RNode, error) { // transformations based on data -- :) err := node.PipeE(fsslice.Filter{ FsSlice: ns.FsSlice, - SetValue: fsslice.SetScalar(ns.Namespace), + SetValue: filtersutil.SetScalar(ns.Namespace), CreateKind: yaml.ScalarNode, // Namespace is a ScalarNode CreateTag: yaml.StringTag, }) @@ -73,7 +75,7 @@ func (ns Filter) hacks(obj *yaml.RNode) error { // 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) + gvk := fieldspec.GetGVK(meta) if !gvk.IsNamespaceableKind() { return nil } @@ -81,7 +83,7 @@ func (ns Filter) metaNamespaceHack(obj *yaml.RNode, meta yaml.ResourceMeta) erro FsSlice: []types.FieldSpec{ {Path: types.MetadataNamespacePath, CreateIfNotPresent: true}, }, - SetValue: fsslice.SetScalar(ns.Namespace), + SetValue: filtersutil.SetScalar(ns.Namespace), CreateKind: yaml.ScalarNode, // Namespace is a ScalarNode } _, err := f.Filter(obj) diff --git a/api/filters/prefixsuffix/example_test.go b/api/filters/prefixsuffix/example_test.go index 67eeecfbf1..1bc698bf5a 100644 --- a/api/filters/prefixsuffix/example_test.go +++ b/api/filters/prefixsuffix/example_test.go @@ -9,12 +9,11 @@ import ( "os" "sigs.k8s.io/kustomize/api/filters/prefixsuffix" - "sigs.k8s.io/kustomize/api/internal/plugins/builtinconfig" + "sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/kustomize/kyaml/kio" ) func ExampleFilter() { - fss := builtinconfig.MakeDefaultConfig().NamePrefix err := kio.Pipeline{ Inputs: []kio.Reader{&kio.ByteReader{Reader: bytes.NewBufferString(` apiVersion: example.com/v1 @@ -27,7 +26,8 @@ kind: Bar metadata: name: instance `)}}, - Filters: []kio.Filter{prefixsuffix.Filter{Prefix: "baz-", FsSlice: fss}}, + Filters: []kio.Filter{prefixsuffix.Filter{ + Prefix: "baz-", FieldSpec: types.FieldSpec{Path: "metadata/name"}}}, Outputs: []kio.Writer{kio.ByteWriter{Writer: os.Stdout}}, }.Execute() if err != nil { diff --git a/api/filters/prefixsuffix/prefixsuffix.go b/api/filters/prefixsuffix/prefixsuffix.go index ae3e277e67..66fead2ba0 100644 --- a/api/filters/prefixsuffix/prefixsuffix.go +++ b/api/filters/prefixsuffix/prefixsuffix.go @@ -6,7 +6,8 @@ package prefixsuffix import ( "fmt" - "sigs.k8s.io/kustomize/api/filters/fsslice" + "sigs.k8s.io/kustomize/api/filters/fieldspec" + "sigs.k8s.io/kustomize/api/filters/filtersutil" "sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/kustomize/kyaml/kio" "sigs.k8s.io/kustomize/kyaml/yaml" @@ -17,28 +18,26 @@ type Filter struct { Prefix string `json:"prefix,omitempty" yaml:"prefix,omitempty"` Suffix string `json:"suffix,omitempty" yaml:"suffix,omitempty"` - FsSlice types.FsSlice `json:"fieldSpecs,omitempty" yaml:"fieldSpecs,omitempty"` + FieldSpec types.FieldSpec `json:"fieldSpec,omitempty" yaml:"fieldSpec,omitempty"` } var _ kio.Filter = Filter{} -func (ns Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) { - return kio.FilterAll(yaml.FilterFunc(ns.run)).Filter(nodes) +func (f Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) { + return kio.FilterAll(yaml.FilterFunc(f.run)).Filter(nodes) } -// Run runs the filter on a single node rather than a slice -func (ns Filter) run(node *yaml.RNode) (*yaml.RNode, error) { - // transformations based on data -- :) - err := node.PipeE(fsslice.Filter{ - FsSlice: ns.FsSlice, - SetValue: ns.set, +func (f Filter) run(node *yaml.RNode) (*yaml.RNode, error) { + err := node.PipeE(fieldspec.Filter{ + FieldSpec: f.FieldSpec, + SetValue: f.evaluateField, CreateKind: yaml.ScalarNode, // Name is a ScalarNode CreateTag: yaml.StringTag, }) return node, err } -func (ns Filter) set(node *yaml.RNode) error { - return fsslice.SetScalar(fmt.Sprintf( - "%s%s%s", ns.Prefix, node.YNode().Value, ns.Suffix))(node) +func (f Filter) evaluateField(node *yaml.RNode) error { + return filtersutil.SetScalar(fmt.Sprintf( + "%s%s%s", f.Prefix, node.YNode().Value, f.Suffix))(node) } diff --git a/api/filters/prefixsuffix/prefixsuffix_test.go b/api/filters/prefixsuffix/prefixsuffix_test.go index 67beeefe92..092a9c3888 100644 --- a/api/filters/prefixsuffix/prefixsuffix_test.go +++ b/api/filters/prefixsuffix/prefixsuffix_test.go @@ -9,14 +9,12 @@ import ( "github.com/stretchr/testify/assert" "sigs.k8s.io/kustomize/api/filters/prefixsuffix" - "sigs.k8s.io/kustomize/api/internal/plugins/builtinconfig" filtertest_test "sigs.k8s.io/kustomize/api/testutils/filtertest" "sigs.k8s.io/kustomize/api/types" ) -var tests = []TestCase{ - { - name: "prefix", +var tests = map[string]TestCase{ + "prefix": { input: ` apiVersion: example.com/v1 kind: Foo @@ -40,10 +38,10 @@ metadata: name: foo-instance `, filter: prefixsuffix.Filter{Prefix: "foo-"}, + fs: types.FieldSpec{Path: "metadata/name"}, }, - { - name: "suffix", + "suffix": { input: ` apiVersion: example.com/v1 kind: Foo @@ -67,10 +65,10 @@ metadata: name: instance-foo `, filter: prefixsuffix.Filter{Suffix: "-foo"}, + fs: types.FieldSpec{Path: "metadata/name"}, }, - { - name: "prefix-suffix", + "prefix-suffix": { input: ` apiVersion: example.com/v1 kind: Foo @@ -94,10 +92,10 @@ metadata: name: bar-instance-foo `, filter: prefixsuffix.Filter{Prefix: "bar-", Suffix: "-foo"}, + fs: types.FieldSpec{Path: "metadata/name"}, }, - { - name: "data-fieldspecs", + "data-fieldspecs": { input: ` apiVersion: example.com/v1 kind: Foo @@ -119,7 +117,7 @@ a: apiVersion: example.com/v1 kind: Foo metadata: - name: foo-instance + name: instance a: b: c: foo-d @@ -127,35 +125,28 @@ a: apiVersion: example.com/v1 kind: Bar metadata: - name: foo-instance + name: instance a: b: c: foo-d `, filter: prefixsuffix.Filter{Prefix: "foo-"}, - fsslice: []types.FieldSpec{ - { - Path: "a/b/c", - }, - }, + fs: types.FieldSpec{Path: "a/b/c"}, }, } type TestCase struct { - name string input string expected string filter prefixsuffix.Filter - fsslice types.FsSlice + fs types.FieldSpec } -var config = builtinconfig.MakeDefaultConfig() - func TestFilter(t *testing.T) { - for i := range tests { - test := tests[i] - t.Run(test.name, func(t *testing.T) { - test.filter.FsSlice = append(config.NamePrefix, test.fsslice...) + for name := range tests { + test := tests[name] + t.Run(name, func(t *testing.T) { + test.filter.FieldSpec = test.fs if !assert.Equal(t, strings.TrimSpace(test.expected), strings.TrimSpace( diff --git a/api/filters/replicacount/replicacount.go b/api/filters/replicacount/replicacount.go index 12d0595292..8efb7e0d14 100644 --- a/api/filters/replicacount/replicacount.go +++ b/api/filters/replicacount/replicacount.go @@ -3,6 +3,7 @@ package replicacount import ( "strconv" + "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" @@ -45,5 +46,5 @@ func (rc Filter) run(node *yaml.RNode) (*yaml.RNode, error) { } func (rc Filter) set(node *yaml.RNode) error { - return fsslice.SetScalar(strconv.FormatInt(rc.Replica.Count, 10))(node) + return filtersutil.SetScalar(strconv.FormatInt(rc.Replica.Count, 10))(node) } diff --git a/plugin/builtin/prefixsuffixtransformer/PrefixSuffixTransformer.go b/plugin/builtin/prefixsuffixtransformer/PrefixSuffixTransformer.go index 4d457d4ed9..356e6e14e1 100644 --- a/plugin/builtin/prefixsuffixtransformer/PrefixSuffixTransformer.go +++ b/plugin/builtin/prefixsuffixtransformer/PrefixSuffixTransformer.go @@ -6,34 +6,30 @@ package main import ( "errors" - "fmt" - - "sigs.k8s.io/kustomize/api/transform" - "sigs.k8s.io/kustomize/api/types" + "sigs.k8s.io/kustomize/api/filters/prefixsuffix" "sigs.k8s.io/kustomize/api/resid" "sigs.k8s.io/kustomize/api/resmap" + "sigs.k8s.io/kustomize/api/types" + "sigs.k8s.io/kustomize/kyaml/filtersutil" "sigs.k8s.io/yaml" ) // Add the given prefix and suffix to the field. type plugin struct { - Prefix string `json:"prefix,omitempty" yaml:"prefix,omitempty"` - Suffix string `json:"suffix,omitempty" yaml:"suffix,omitempty"` - FieldSpecs []types.FieldSpec `json:"fieldSpecs,omitempty" yaml:"fieldSpecs,omitempty"` + Prefix string `json:"prefix,omitempty" yaml:"prefix,omitempty"` + Suffix string `json:"suffix,omitempty" yaml:"suffix,omitempty"` + FieldSpecs types.FsSlice `json:"fieldSpecs,omitempty" yaml:"fieldSpecs,omitempty"` } //noinspection GoUnusedGlobalVariable var KustomizePlugin plugin -// Not placed in a file yet due to lack of demand. -var prefixSuffixFieldSpecsToSkip = []types.FieldSpec{ - { - Gvk: resid.Gvk{Kind: "CustomResourceDefinition"}, - }, - { - Gvk: resid.Gvk{Group: "apiregistration.k8s.io", Kind: "APIService"}, - }, +// A Gvk skip list for prefix/suffix modification. +// hard coded for now - eventually should be part of config. +var prefixSuffixFieldSpecsToSkip = types.FsSlice{ + {Gvk: resid.Gvk{Kind: "CustomResourceDefinition"}}, + {Gvk: resid.Gvk{Group: "apiregistration.k8s.io", Kind: "APIService"}}, } func (p *plugin) Config( @@ -52,29 +48,24 @@ func (p *plugin) Config( } func (p *plugin) Transform(m resmap.ResMap) error { - // Even if both the Prefix and Suffix are empty we want // to proceed with the transformation. This allows to add contextual // information to the resources (AddNamePrefix and AddNameSuffix). - for _, r := range m.Resources() { + // TODO: move this test into the filter (i.e. make a better filter) if p.shouldSkip(r.OrgId()) { - // Don't change the actual definition - // of a CRD. continue } id := r.OrgId() // current default configuration contains // only one entry: "metadata/name" with no GVK - for _, path := range p.FieldSpecs { - if !id.IsSelected(&path.Gvk) { - // With the currrent default configuration, - // because no Gvk is specified, so a wild - // card + for _, fs := range p.FieldSpecs { + // TODO: this is redundant to filter (but needed for now) + if !id.IsSelected(&fs.Gvk) { continue } - - if smellsLikeANameChange(&path) { + // TODO: move this test into the filter. + if smellsLikeANameChange(&fs) { // "metadata/name" is the only field. // this will add a prefix and a suffix // to the resource even if those are @@ -82,15 +73,11 @@ func (p *plugin) Transform(m resmap.ResMap) error { r.AddNamePrefix(p.Prefix) r.AddNameSuffix(p.Suffix) } - - // the addPrefixSuffix method will not - // change the name if both the prefix and suffix - // are empty. - err := transform.MutateField( - r.Map(), - path.PathSlice(), - path.CreateIfNotPresent, - p.addPrefixSuffix) + err := filtersutil.ApplyToJSON(prefixsuffix.Filter{ + Prefix: p.Prefix, + Suffix: p.Suffix, + FieldSpec: fs, + }, r) if err != nil { return err } @@ -103,8 +90,7 @@ func smellsLikeANameChange(fs *types.FieldSpec) bool { return fs.Path == "metadata/name" } -func (p *plugin) shouldSkip( - id resid.ResId) bool { +func (p *plugin) shouldSkip(id resid.ResId) bool { for _, path := range prefixSuffixFieldSpecsToSkip { if id.IsSelected(&path.Gvk) { return true @@ -112,12 +98,3 @@ func (p *plugin) shouldSkip( } return false } - -func (p *plugin) addPrefixSuffix( - in interface{}) (interface{}, error) { - s, ok := in.(string) - if !ok { - return nil, fmt.Errorf("%#v is expected to be %T", in, s) - } - return fmt.Sprintf("%s%s%s", p.Prefix, s, p.Suffix), nil -} diff --git a/plugin/builtin/prefixsuffixtransformer/go.mod b/plugin/builtin/prefixsuffixtransformer/go.mod index 8c48f3bf61..1852fedc17 100644 --- a/plugin/builtin/prefixsuffixtransformer/go.mod +++ b/plugin/builtin/prefixsuffixtransformer/go.mod @@ -4,6 +4,7 @@ go 1.14 require ( sigs.k8s.io/kustomize/api v0.5.1 + sigs.k8s.io/kustomize/kyaml v0.4.1 sigs.k8s.io/yaml v1.2.0 )