From 1a89d09f401079e095e68d44ef99518278753e1f Mon Sep 17 00:00:00 2001 From: jregan Date: Sun, 10 May 2020 16:50:40 -0700 Subject: [PATCH] add value add filter --- api/filters/valueadd/valueadd.go | 124 ++++++++++++++++++++++++++ api/filters/valueadd/valueadd_test.go | 109 ++++++++++++++++++++++ 2 files changed, 233 insertions(+) create mode 100644 api/filters/valueadd/valueadd.go create mode 100644 api/filters/valueadd/valueadd_test.go diff --git a/api/filters/valueadd/valueadd.go b/api/filters/valueadd/valueadd.go new file mode 100644 index 000000000..1aa724c2f --- /dev/null +++ b/api/filters/valueadd/valueadd.go @@ -0,0 +1,124 @@ +// Copyright 2020 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package valueadd + +import ( + "strings" + + "sigs.k8s.io/kustomize/api/filesys" + "sigs.k8s.io/kustomize/kyaml/kio" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +// An 'Add' operation aspiring to IETF RFC 6902 JSON. +// +// The filter tries to add a value to a node at a particular field path. +// +// Kinds of target fields: +// +// - Non-existent target field. +// +// The field will be added and the value inserted. +// +// - Existing field, scalar or map. +// +// E.g. 'spec/template/spec/containers/[name:nginx]/image' +// +// This behaves like an IETF RFC 6902 Replace operation would; +// the existing value is replaced without complaint, even though +// this is an Add operation. In contrast, a Replace operation +// must fail (report an error) if the field doesn't exist. +// +// - Existing field, list (array) +// Not supported yet. +// TODO: Honor fields with RFC-6902-style array indices +// TODO: like 'spec/template/spec/containers/2' +// TODO: Modify kyaml/yaml/PathGetter to allow this. +// The value will be inserted into the array at the given position, +// shifting other contents. To instead replace an array entry, use +// an implementation of an IETF RFC 6902 Replace operation. +// +// For the common case of a filepath in the field value, and a desire +// to add the value to the filepath (rather than replace the filepath), +// use a non-zero value of FilePathPosition (see below). +type Filter struct { + // Value is the value to add. + // + // Empty values are disallowed, i.e. this filter isn't intended + // for use in erasing or removing fields. For that, use a filter + // more aligned with the IETF RFC 6902 JSON Remove operation. + // + // At the time of writing, Value's value should be a simple string, + // not a JSON document. This particular filter focuses on easing + // injection of a single-sourced cloud project and/or cluster name + // into various fields, especially namespace and various filepath + // specifications. + Value string + + // FieldPath is a JSON-style path to the field intended to hold the value. + FieldPath string + + // FilePathPosition is a filepath field index. + // + // Call the value of this field _i_. + // + // If _i_ is zero, negative or unspecified, this field has no effect. + // + // If _i_ is > 0, then it's assumed that + // - 'Value' is a string that can work as a directory or file name, + // - the field value intended for replacement holds a filepath. + // + // The filepath is split into a string slice, the value is inserted + // at position [i-1], shifting the rest of the path to the right. + // A value of i==1 puts the new value at the start of the path. + // This change never converts an absolute path to a relative path, + // meaning adding a new field at position i==1 will preserve a + // leading slash. E.g. if Value == 'PEACH' + // + // OLD : NEW : FilePathPosition + // -------------------------------------------------------- + // {empty} : PEACH : irrelevant + // / : /PEACH : irrelevant + // pie : PEACH/pie : 1 (or less to prefix) + // /pie : /PEACH/pie : 1 (or less to prefix) + // raw : raw/PEACH : 2 (or more to postfix) + // /raw : /raw/PEACH : 2 (or more to postfix) + // a/nice/warm/pie : a/nice/warm/PEACH/pie : 4 + // /a/nice/warm/pie : /a/nice/warm/PEACH/pie : 4 + // + // For robustness (liberal input, conservative output) FilePathPosition + // values that that are too large to index the split filepath result in a + // postfix rather than an error. So use 1 to prefix, 9999 to postfix. + FilePathPosition int `json:"filePathPosition,omitempty" yaml:"filePathPosition,omitempty"` +} + +var _ kio.Filter = Filter{} + +func (f Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) { + _, err := kio.FilterAll(yaml.FilterFunc( + func(node *yaml.RNode) (*yaml.RNode, error) { + fields := strings.Split(f.FieldPath, "/") + // TODO: support SequenceNode. + // Presumably here one could look for array indices (digits) at + // the end of the field path (as described in IETF RFC 6902 JSON), + // and if found, take it as a signal that this should be a + // SequenceNode instead of a ScalarNode, and insert the value + // into the proper slot, shifting every over. + n, err := node.Pipe(yaml.LookupCreate(yaml.ScalarNode, fields...)) + if err != nil { + return node, err + } + // TODO: allow more kinds + if err := yaml.ErrorIfInvalid(n, yaml.ScalarNode); err != nil { + return nil, err + } + newValue := f.Value + if f.FilePathPosition > 0 { + newValue = filesys.InsertPathPart( + n.YNode().Value, f.FilePathPosition-1, newValue) + } + return n.Pipe(yaml.FieldSetter{StringValue: newValue}) + })).Filter(nodes) + return nodes, err +} diff --git a/api/filters/valueadd/valueadd_test.go b/api/filters/valueadd/valueadd_test.go new file mode 100644 index 000000000..66edbd249 --- /dev/null +++ b/api/filters/valueadd/valueadd_test.go @@ -0,0 +1,109 @@ +// Copyright 2020 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package valueadd + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + filtertest_test "sigs.k8s.io/kustomize/api/testutils/filtertest" +) + +const someResource = ` +kind: SomeKind +spec: + resourceRef: + external: projects/whatever +` + +func TestValueAddFilter(t *testing.T) { + testCases := map[string]struct { + input string + expectedOutput string + filter Filter + }{ + "simpleAdd": { + input: ` +kind: SomeKind +`, + expectedOutput: ` +kind: SomeKind +spec: + resourceRef: + external: valueAdded +`, + filter: Filter{ + Value: "valueAdded", + FieldPath: "spec/resourceRef/external", + }, + }, + "replaceExisting": { + input: someResource, + expectedOutput: ` +kind: SomeKind +spec: + resourceRef: + external: valueAdded +`, + filter: Filter{ + Value: "valueAdded", + FieldPath: "spec/resourceRef/external", + }, + }, + "prefixExisting": { + input: someResource, + expectedOutput: ` +kind: SomeKind +spec: + resourceRef: + external: valueAdded/projects/whatever +`, + filter: Filter{ + Value: "valueAdded", + FieldPath: "spec/resourceRef/external", + FilePathPosition: 1, + }, + }, + "postfixExisting": { + input: someResource, + expectedOutput: ` +kind: SomeKind +spec: + resourceRef: + external: projects/whatever/valueAdded +`, + filter: Filter{ + Value: "valueAdded", + FieldPath: "spec/resourceRef/external", + FilePathPosition: 99, + }, + }, + "placeInMiddleOfExisting": { + input: someResource, + expectedOutput: ` +kind: SomeKind +spec: + resourceRef: + external: projects/valueAdded/whatever +`, + filter: Filter{ + Value: "valueAdded", + FieldPath: "spec/resourceRef/external", + FilePathPosition: 2, + }, + }, + } + + for tn, tc := range testCases { + t.Run(tn, func(t *testing.T) { + filter := tc.filter + if !assert.Equal(t, + strings.TrimSpace(tc.expectedOutput), + strings.TrimSpace(filtertest_test.RunFilter(t, tc.input, filter))) { + t.FailNow() + } + }) + } +}