diff --git a/kyaml/yaml/fns.go b/kyaml/yaml/fns.go index 5289997dd..43e54ce36 100644 --- a/kyaml/yaml/fns.go +++ b/kyaml/yaml/fns.go @@ -424,12 +424,46 @@ func Lookup(path ...string) PathGetter { return PathGetter{Path: path} } -// Lookup returns a PathGetter to lookup a field by its path and create it if it doesn't already +// LookupCreate returns a PathGetter to lookup a field by its path and create it if it doesn't already // exist. func LookupCreate(kind yaml.Kind, path ...string) PathGetter { return PathGetter{Path: path, Create: kind} } +// ConventionalContainerPaths is a list of paths at which containers typically appear in workload APIs. +// It is intended for use with LookupFirstMatch. +var ConventionalContainerPaths = [][]string{ + // e.g. Deployment, ReplicaSet, DaemonSet, Job, StatefulSet + {"spec", "template", "spec", "containers"}, + // e.g. CronJob + {"spec", "jobTemplate", "spec", "template", "spec", "containers"}, + // e.g. Pod + {"spec", "containers"}, + // e.g. PodTemplate + {"template", "spec", "containers"}, +} + +// LookupFirstMatch returns a Filter for locating a value that may exist at one of several possible paths. +// For example, it can be used with ConventionalContainerPaths to find the containers field in a standard workload resource. +// If more than one of the paths exists in the resource, the first will be returned. If none exist, +// nil will be returned. If an error is encountered during lookup, it will be returned. +func LookupFirstMatch(paths [][]string) Filter { + return FilterFunc(func(object *RNode) (*RNode, error) { + var result *RNode + var err error + for _, path := range paths { + result, err = object.Pipe(PathGetter{Path: path}) + if err != nil { + return nil, errors.Wrap(err) + } + if result != nil { + return result, nil + } + } + return nil, nil + }) +} + // PathGetter returns the RNode under Path. type PathGetter struct { Kind string `yaml:"kind,omitempty"` diff --git a/kyaml/yaml/fns_test.go b/kyaml/yaml/fns_test.go index 3c7377877..b6e0c0cf8 100644 --- a/kyaml/yaml/fns_test.go +++ b/kyaml/yaml/fns_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "sigs.k8s.io/kustomize/kyaml/internal/forked/github.com/go-yaml/yaml" . "sigs.k8s.io/kustomize/kyaml/yaml" ) @@ -724,6 +725,72 @@ j: k assert.Nil(t, rn) } +func TestLookupFirstMatch(t *testing.T) { + tests := []struct { + name string + paths [][]string + wantPath []string + }{ + { + name: "finds path that exists", + paths: [][]string{{"spec", "jobTemplate", "spec", "template", "spec", "containers"}}, + wantPath: []string{"spec", "jobTemplate", "spec", "template", "spec", "containers"}, + }, + { + name: "chooses first path when multiple exist: containers example", + paths: ConventionalContainerPaths, + wantPath: []string{"spec", "template", "spec", "containers"}, + }, + { + name: "chooses first path when multiple exist: annotations example", + paths: [][]string{ + {"metadata", "annotations", "example.kustomize.io/new"}, + {"metadata", "annotations", "example.kustomize.io/deprecated"}, + }, + wantPath: []string{"metadata", "annotations", "example.kustomize.io/new"}, + }, + { + name: "returns nil when path does not exist", + paths: [][]string{ + {"metadata", "annotations", "example.kustomize.io/does-not-exist"}, + {"metadata", "annotations", "example.kustomize.io/also-not-exist"}, + }, + wantPath: nil, + }, + } + for _, tt := range tests { + s := ` +apiVersion: example.kustomize.io/v1 +kind: Custom +metadata: + annotations: + example.kustomize.io/deprecated: foo + example.kustomize.io/new: foo +spec: + template: + spec: + containers: + - name: foo + jobTemplate: + spec: + template: + spec: + containers: + - name: foo +` + resource := MustParse(s) + t.Run(tt.name, func(t *testing.T) { + result, err := LookupFirstMatch(tt.paths).Filter(resource) + require.NoError(t, err) + if tt.wantPath != nil { + assert.Equal(t, tt.wantPath, result.FieldPath()) + } else { + assert.Nil(t, result) + } + }) + } +} + func TestFieldSetter(t *testing.T) { // Change field node, err := Parse(`