diff --git a/kyaml/fn/framework/patch.go b/kyaml/fn/framework/patch.go index 5b0b3b098..157080d1a 100644 --- a/kyaml/fn/framework/patch.go +++ b/kyaml/fn/framework/patch.go @@ -168,8 +168,7 @@ func (cpt ContainerPatchTemplate) apply(matches []*yaml.RNode) error { } for i := range matches { - // TODO(knverey): Make this work for more Kinds and expose the helper for doing so. - containers, err := matches[i].Pipe(yaml.Lookup("spec", "template", "spec", "containers")) + containers, err := matches[i].Pipe(yaml.LookupFirstMatch(yaml.ConventionalContainerPaths)) if err != nil { return errors.Wrap(err) } diff --git a/kyaml/fn/framework/processors_test.go b/kyaml/fn/framework/processors_test.go index 554a15e47..0b3e9f9d3 100644 --- a/kyaml/fn/framework/processors_test.go +++ b/kyaml/fn/framework/processors_test.go @@ -5,6 +5,7 @@ package framework_test import ( "bytes" + "regexp" "strings" "testing" @@ -276,6 +277,57 @@ functionConfig: `), strings.TrimSpace(out.String())) } +func TestTemplateProcessor_ContainerPatchTemplates_MultipleWorkloadKinds(t *testing.T) { + type API struct { + Spec struct { + Key string `json:"key" yaml:"key"` + Value string `json:"value" yaml:"value"` + A string `json:"a" yaml:"a"` + } + } + config := &API{} + p := framework.TemplateProcessor{ + TemplateData: config, + ResourceTemplates: []framework.ResourceTemplate{{ + Templates: parser.TemplateFiles("testdata/template-processor/templates/container-sources"), + }}, + PatchTemplates: []framework.PatchTemplate{ + &framework.ContainerPatchTemplate{ + Templates: parser.TemplateFiles("testdata/template-processor/container-patches"), + }, + }, + } + + out := new(bytes.Buffer) + rw := &kio.ByteReadWriter{Writer: out, Reader: bytes.NewBufferString(` +apiVersion: config.kubernetes.io/v1alpha1 +kind: ResourceList +items: [] +functionConfig: + spec: + key: Hello + value: World + a: bar +`)} + + require.NoError(t, framework.Execute(p, rw)) + resources, err := (&kio.ByteReader{Reader: out}).Read() + require.NoError(t, err) + envRegex := regexp.MustCompile(strings.TrimSpace(` +\s+ env: +\s+ - name: EXISTING +\s+ value: variable +\s+ - name: Hello +\s+ value: World +`)) + require.Equal(t, 9, len(resources)) + for i, r := range resources { + t.Run(r.GetKind(), func(t *testing.T) { + assert.Regexp(t, envRegex, resources[i].MustString()) + }) + } +} + func TestSimpleProcessor_Process_loads_config(t *testing.T) { cfg := new(struct { Value string `yaml:"value"` diff --git a/kyaml/fn/framework/testdata/template-processor/templates/container-sources/cron_job.template.yaml b/kyaml/fn/framework/testdata/template-processor/templates/container-sources/cron_job.template.yaml new file mode 100644 index 000000000..d6a47caed --- /dev/null +++ b/kyaml/fn/framework/testdata/template-processor/templates/container-sources/cron_job.template.yaml @@ -0,0 +1,19 @@ +# Copyright 2021 The Kubernetes Authors. +# SPDX-License-Identifier: Apache-2.0 + +apiVersion: batch/v1 +kind: CronJob +metadata: + name: hello +spec: + schedule: "*/1 * * * *" + jobTemplate: + spec: + template: + spec: + containers: + - name: hello + image: nginx + env: + - name: EXISTING + value: variable diff --git a/kyaml/fn/framework/testdata/template-processor/templates/container-sources/daemon_set.template.yaml b/kyaml/fn/framework/testdata/template-processor/templates/container-sources/daemon_set.template.yaml new file mode 100644 index 000000000..1ef61dc2a --- /dev/null +++ b/kyaml/fn/framework/testdata/template-processor/templates/container-sources/daemon_set.template.yaml @@ -0,0 +1,23 @@ +# Copyright 2021 The Kubernetes Authors. +# SPDX-License-Identifier: Apache-2.0 + +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: hello +spec: + selector: + matchLabels: + name: hello + template: + metadata: + labels: + name: hello + spec: + containers: + - name: hello + image: nginx + env: + - name: EXISTING + value: variable + diff --git a/kyaml/fn/framework/testdata/template-processor/templates/container-sources/deployment.template.yaml b/kyaml/fn/framework/testdata/template-processor/templates/container-sources/deployment.template.yaml new file mode 100644 index 000000000..a9e8882a6 --- /dev/null +++ b/kyaml/fn/framework/testdata/template-processor/templates/container-sources/deployment.template.yaml @@ -0,0 +1,22 @@ +# Copyright 2021 The Kubernetes Authors. +# SPDX-License-Identifier: Apache-2.0 + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: hello +spec: + selector: + matchLabels: + app: hello + template: + metadata: + labels: + app: hello + spec: + containers: + - name: hello + image: nginx + env: + - name: EXISTING + value: variable diff --git a/kyaml/fn/framework/testdata/template-processor/templates/container-sources/job.template.yaml b/kyaml/fn/framework/testdata/template-processor/templates/container-sources/job.template.yaml new file mode 100644 index 000000000..8cbd7e46f --- /dev/null +++ b/kyaml/fn/framework/testdata/template-processor/templates/container-sources/job.template.yaml @@ -0,0 +1,16 @@ +# Copyright 2021 The Kubernetes Authors. +# SPDX-License-Identifier: Apache-2.0 + +apiVersion: batch/v1 +kind: Job +metadata: + name: hello +spec: + template: + spec: + containers: + - name: hello + image: nginx + env: + - name: EXISTING + value: variable diff --git a/kyaml/fn/framework/testdata/template-processor/templates/container-sources/pod.template.yaml b/kyaml/fn/framework/testdata/template-processor/templates/container-sources/pod.template.yaml new file mode 100644 index 000000000..0ff1a656e --- /dev/null +++ b/kyaml/fn/framework/testdata/template-processor/templates/container-sources/pod.template.yaml @@ -0,0 +1,14 @@ +# Copyright 2021 The Kubernetes Authors. +# SPDX-License-Identifier: Apache-2.0 + +apiVersion: v1 +kind: Pod +metadata: + name: hello +spec: + containers: + - name: hello + image: nginx + env: + - name: EXISTING + value: variable diff --git a/kyaml/fn/framework/testdata/template-processor/templates/container-sources/pod_template.template.yaml b/kyaml/fn/framework/testdata/template-processor/templates/container-sources/pod_template.template.yaml new file mode 100644 index 000000000..a3069a080 --- /dev/null +++ b/kyaml/fn/framework/testdata/template-processor/templates/container-sources/pod_template.template.yaml @@ -0,0 +1,17 @@ +# Copyright 2021 The Kubernetes Authors. +# SPDX-License-Identifier: Apache-2.0 + +apiVersion: v1 +kind: PodTemplate +metadata: + name: hello + labels: + tier: hello +template: + spec: + containers: + - name: hello + image: nginx + env: + - name: EXISTING + value: variable diff --git a/kyaml/fn/framework/testdata/template-processor/templates/container-sources/replica_set.template.yaml b/kyaml/fn/framework/testdata/template-processor/templates/container-sources/replica_set.template.yaml new file mode 100644 index 000000000..d0c0f9b72 --- /dev/null +++ b/kyaml/fn/framework/testdata/template-processor/templates/container-sources/replica_set.template.yaml @@ -0,0 +1,22 @@ +# Copyright 2021 The Kubernetes Authors. +# SPDX-License-Identifier: Apache-2.0 + +apiVersion: apps/v1 +kind: ReplicaSet +metadata: + name: hello +spec: + selector: + matchLabels: + app: hello + template: + metadata: + labels: + app: hello + spec: + containers: + - name: hello + image: nginx + env: + - name: EXISTING + value: variable diff --git a/kyaml/fn/framework/testdata/template-processor/templates/container-sources/replication_controller.template.yaml b/kyaml/fn/framework/testdata/template-processor/templates/container-sources/replication_controller.template.yaml new file mode 100644 index 000000000..3c8e386c8 --- /dev/null +++ b/kyaml/fn/framework/testdata/template-processor/templates/container-sources/replication_controller.template.yaml @@ -0,0 +1,22 @@ +# Copyright 2021 The Kubernetes Authors. +# SPDX-License-Identifier: Apache-2.0 + +apiVersion: v1 +kind: ReplicationController +metadata: + name: hello +spec: + selector: + app: hello + template: + metadata: + name: hello + labels: + app: hello + spec: + containers: + - name: hello + image: nginx + env: + - name: EXISTING + value: variable diff --git a/kyaml/fn/framework/testdata/template-processor/templates/container-sources/stateful_set.template.yaml b/kyaml/fn/framework/testdata/template-processor/templates/container-sources/stateful_set.template.yaml new file mode 100644 index 000000000..1aa312fce --- /dev/null +++ b/kyaml/fn/framework/testdata/template-processor/templates/container-sources/stateful_set.template.yaml @@ -0,0 +1,24 @@ +# Copyright 2021 The Kubernetes Authors. +# SPDX-License-Identifier: Apache-2.0 + +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: hello +spec: + serviceName: "nginx" + selector: + matchLabels: + app: hello + template: + metadata: + labels: + app: hello + spec: + containers: + - name: hello + image: nginx + env: + - name: EXISTING + value: variable + diff --git a/kyaml/openapi/kubernetesapi/openapiinfo.go b/kyaml/openapi/kubernetesapi/openapiinfo.go index 0f6baba83..c4e1c287a 100644 --- a/kyaml/openapi/kubernetesapi/openapiinfo.go +++ b/kyaml/openapi/kubernetesapi/openapiinfo.go @@ -6,13 +6,13 @@ package kubernetesapi import ( - "sigs.k8s.io/kustomize/kyaml/openapi/kubernetesapi/v1212" + "sigs.k8s.io/kustomize/kyaml/openapi/kubernetesapi/v1212" ) const Info = "{title:Kubernetes,version:v1.21.2}" -var OpenAPIMustAsset = map[string]func(string)[]byte{ - "v1212": v1212.MustAsset, +var OpenAPIMustAsset = map[string]func(string) []byte{ + "v1212": v1212.MustAsset, } const DefaultOpenAPI = "v1212" diff --git a/kyaml/openapi/kubernetesapi/v1212/swagger.go b/kyaml/openapi/kubernetesapi/v1212/swagger.go index 3c0b542f1..79c7e06c8 100644 --- a/kyaml/openapi/kubernetesapi/v1212/swagger.go +++ b/kyaml/openapi/kubernetesapi/v1212/swagger.go @@ -1,6 +1,6 @@ // Code generated by go-bindata. (@generated) DO NOT EDIT. - //Package v1212 generated by go-bindata.// sources: +//Package v1212 generated by go-bindata.// sources: // kubernetesapi/v1212/swagger.json package v1212 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(`