diff --git a/cmd/config/internal/commands/merge3.go b/cmd/config/internal/commands/merge3.go index 405638463..6c31186c5 100644 --- a/cmd/config/internal/commands/merge3.go +++ b/cmd/config/internal/commands/merge3.go @@ -25,6 +25,8 @@ func GetMerge3Runner(name string) *Merge3Runner { "Path to updated package") c.Flags().StringVar(&r.toDir, "to", "", "Path to destination package") + c.Flags().BoolVar(&r.path, "path-merge-key", false, + "Use the path as part of the merge key when merging resources") r.Command = c return r @@ -40,6 +42,7 @@ type Merge3Runner struct { ancestor string fromDir string toDir string + path bool } func (r *Merge3Runner) runE(c *cobra.Command, args []string) error { @@ -47,6 +50,7 @@ func (r *Merge3Runner) runE(c *cobra.Command, args []string) error { OriginalPath: r.ancestor, UpdatedPath: r.fromDir, DestPath: r.toDir, + MergeOnPath: r.path, }.Merge() if err != nil { return err diff --git a/kyaml/kio/filters/merge3.go b/kyaml/kio/filters/merge3.go index f302eeada..dfbb47827 100644 --- a/kyaml/kio/filters/merge3.go +++ b/kyaml/kio/filters/merge3.go @@ -7,6 +7,7 @@ import ( "fmt" "sigs.k8s.io/kustomize/kyaml/kio" + "sigs.k8s.io/kustomize/kyaml/kio/kioutil" "sigs.k8s.io/kustomize/kyaml/yaml" "sigs.k8s.io/kustomize/kyaml/yaml/merge3" ) @@ -24,6 +25,11 @@ type Merge3 struct { UpdatedPath string DestPath string MatchFilesGlob []string + + // MergeOnPath will use the relative filepath as part of the merge key. + // This may be necessary if the directory contains multiple copies of + // the same resource, or resources patches. + MergeOnPath bool } func (m Merge3) Merge() error { @@ -61,7 +67,7 @@ func (m Merge3) Merge() error { // Filter combines Resources with the same GVK + N + NS into tuples, and then merges them func (m Merge3) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) { // index the nodes by their identity - tl := tuples{} + tl := tuples{mergeOnPath: m.MergeOnPath} for i := range nodes { if err := tl.add(nodes[i]); err != nil { return nil, err @@ -102,6 +108,37 @@ func (m Merge3) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) { // tuples combines nodes with the same GVK + N + NS type tuples struct { list []*tuple + + // mergeOnPath if set to true will use the resource filepath + // as part of the merge key + mergeOnPath bool +} + +// isSameResource returns true if meta1 and meta2 are for the same logic resource +func (ts *tuples) isSameResource(meta1, meta2 yaml.ResourceMeta) bool { + if meta1.Name != meta2.Name { + return false + } + if meta1.Namespace != meta2.Namespace { + return false + } + if meta1.APIVersion != meta2.APIVersion { + return false + } + if meta1.Kind != meta2.Kind { + return false + } + if ts.mergeOnPath { + // directories may contain multiple copies of a resource with the same + // name, namespace, apiVersion and kind -- e.g. kustomize patches, or + // multiple environments + // mergeOnPath configures the merge logic to use the path as part of the + // resource key + if meta1.Annotations[kioutil.PathAnnotation] != meta2.Annotations[kioutil.PathAnnotation] { + return false + } + } + return true } // add adds a node to the list, combining it with an existing matching Resource if found @@ -112,8 +149,7 @@ func (ts *tuples) add(node *yaml.RNode) error { } for i := range ts.list { t := ts.list[i] - if t.meta.Name == nodeMeta.Name && t.meta.Namespace == nodeMeta.Namespace && - t.meta.APIVersion == nodeMeta.APIVersion && t.meta.Kind == nodeMeta.Kind { + if ts.isSameResource(t.meta, nodeMeta) { return t.add(node) } } diff --git a/kyaml/kio/filters/merge3_test.go b/kyaml/kio/filters/merge3_test.go index d8615b33d..66974c2b5 100644 --- a/kyaml/kio/filters/merge3_test.go +++ b/kyaml/kio/filters/merge3_test.go @@ -54,3 +54,78 @@ func TestMerge3_Merge(t *testing.T) { t.FailNow() } } + +// TestMerge3_Merge_path tests that if the same resource is specified multiple times +// with MergeOnPath, that the resources will be merged by the filepath name. +func TestMerge3_Merge_path(t *testing.T) { + _, datadir, _, ok := runtime.Caller(0) + if !assert.True(t, ok) { + t.FailNow() + } + datadir = filepath.Join(filepath.Dir(datadir), "testdata2") + + // setup the local directory + dir, err := ioutil.TempDir("", "kyaml-test") + if !assert.NoError(t, err) { + t.FailNow() + } + defer os.RemoveAll(dir) + + if !assert.NoError(t, copyutil.CopyDir( + filepath.Join(datadir, "dataset1-localupdates"), + filepath.Join(dir, "dataset1"))) { + t.FailNow() + } + + err = filters.Merge3{ + OriginalPath: filepath.Join(datadir, "dataset1"), + UpdatedPath: filepath.Join(datadir, "dataset1-remoteupdates"), + DestPath: filepath.Join(dir, "dataset1"), + MergeOnPath: true, + }.Merge() + if !assert.NoError(t, err) { + t.FailNow() + } + + diffs, err := copyutil.Diff( + filepath.Join(dir, "dataset1"), + filepath.Join(datadir, "dataset1-expected")) + if !assert.NoError(t, err) { + t.FailNow() + } + if !assert.Empty(t, diffs.List()) { + t.FailNow() + } +} + +// TestMerge3_Merge_fail tests that if the same resource is defined multiple times +// that merge will fail +func TestMerge3_Merge_fail(t *testing.T) { + _, datadir, _, ok := runtime.Caller(0) + if !assert.True(t, ok) { + t.FailNow() + } + datadir = filepath.Join(filepath.Dir(datadir), "testdata2") + + // setup the local directory + dir, err := ioutil.TempDir("", "kyaml-test") + if !assert.NoError(t, err) { + t.FailNow() + } + defer os.RemoveAll(dir) + + if !assert.NoError(t, copyutil.CopyDir( + filepath.Join(datadir, "dataset1-localupdates"), + filepath.Join(dir, "dataset1"))) { + t.FailNow() + } + + err = filters.Merge3{ + OriginalPath: filepath.Join(datadir, "dataset1"), + UpdatedPath: filepath.Join(datadir, "dataset1-remoteupdates"), + DestPath: filepath.Join(dir, "dataset1"), + }.Merge() + if !assert.Error(t, err) { + t.FailNow() + } +} diff --git a/kyaml/kio/filters/testdata2/dataset1-expected/java/java-deployment-1.resource.yaml b/kyaml/kio/filters/testdata2/dataset1-expected/java/java-deployment-1.resource.yaml new file mode 100644 index 000000000..ea1d0f540 --- /dev/null +++ b/kyaml/kio/filters/testdata2/dataset1-expected/java/java-deployment-1.resource.yaml @@ -0,0 +1,38 @@ +# Copyright 2019 The Kubernetes Authors. +# SPDX-License-Identifier: Apache-2.0 +# +apiVersion: apps/v1 +kind: Deployment +metadata: + name: app + labels: + app: java +spec: + replicas: 1 + selector: + matchLabels: + app: java2 + template: + metadata: + labels: + app: java2 + spec: + restartPolicy: Always + containers: + - name: app + image: gcr.io/project/app:version + command: + - java + - -jar + - /app.jar + ports: + - containerPort: 8080 + envFrom: + - configMapRef: + name: app-config + env: + - name: JAVA_OPTS + value: -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap + -Djava.security.egd=file:/dev/./urandom + imagePullPolicy: Always + minReadySeconds: 15 diff --git a/kyaml/kio/filters/testdata2/dataset1-expected/java/java-deployment-2.resource.yaml b/kyaml/kio/filters/testdata2/dataset1-expected/java/java-deployment-2.resource.yaml new file mode 100644 index 000000000..6b13e7872 --- /dev/null +++ b/kyaml/kio/filters/testdata2/dataset1-expected/java/java-deployment-2.resource.yaml @@ -0,0 +1,38 @@ +# Copyright 2019 The Kubernetes Authors. +# SPDX-License-Identifier: Apache-2.0 +# +apiVersion: apps/v1 +kind: Deployment +metadata: + name: app + labels: + app: java3 +spec: + replicas: 4 + selector: + matchLabels: + app: java + template: + metadata: + labels: + app: java + spec: + restartPolicy: Always + containers: + - name: app + image: gcr.io/project/app:version + command: + - java + - -jar + - /app.jar + ports: + - containerPort: 8080 + envFrom: + - configMapRef: + name: app-config + env: + - name: JAVA_OPTS + value: -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap + -Djava.security.egd=file:/dev/./urandom + imagePullPolicy: Always + minReadySeconds: 25 diff --git a/kyaml/kio/filters/testdata2/dataset1-localupdates/java/java-deployment-1.resource.yaml b/kyaml/kio/filters/testdata2/dataset1-localupdates/java/java-deployment-1.resource.yaml new file mode 100644 index 000000000..658a1ec80 --- /dev/null +++ b/kyaml/kio/filters/testdata2/dataset1-localupdates/java/java-deployment-1.resource.yaml @@ -0,0 +1,38 @@ +# Copyright 2019 The Kubernetes Authors. +# SPDX-License-Identifier: Apache-2.0 +# +apiVersion: apps/v1 +kind: Deployment +metadata: + name: app + labels: + app: java +spec: + replicas: 1 + selector: + matchLabels: + app: java + template: + metadata: + labels: + app: java + spec: + restartPolicy: Always + containers: + - name: app + image: gcr.io/project/app:version + command: + - java + - -jar + - /app.jar + ports: + - containerPort: 8080 + envFrom: + - configMapRef: + name: app-config + env: + - name: JAVA_OPTS + value: -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap + -Djava.security.egd=file:/dev/./urandom + imagePullPolicy: Always + minReadySeconds: 15 diff --git a/kyaml/kio/filters/testdata2/dataset1-localupdates/java/java-deployment-2.resource.yaml b/kyaml/kio/filters/testdata2/dataset1-localupdates/java/java-deployment-2.resource.yaml new file mode 100644 index 000000000..64baafe94 --- /dev/null +++ b/kyaml/kio/filters/testdata2/dataset1-localupdates/java/java-deployment-2.resource.yaml @@ -0,0 +1,38 @@ +# Copyright 2019 The Kubernetes Authors. +# SPDX-License-Identifier: Apache-2.0 +# +apiVersion: apps/v1 +kind: Deployment +metadata: + name: app + labels: + app: java +spec: + replicas: 4 + selector: + matchLabels: + app: java + template: + metadata: + labels: + app: java + spec: + restartPolicy: Always + containers: + - name: app + image: gcr.io/project/app:version + command: + - java + - -jar + - /app.jar + ports: + - containerPort: 8080 + envFrom: + - configMapRef: + name: app-config + env: + - name: JAVA_OPTS + value: -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap + -Djava.security.egd=file:/dev/./urandom + imagePullPolicy: Always + minReadySeconds: 25 diff --git a/kyaml/kio/filters/testdata2/dataset1-remoteupdates/java/java-deployment-1.resource.yaml b/kyaml/kio/filters/testdata2/dataset1-remoteupdates/java/java-deployment-1.resource.yaml new file mode 100644 index 000000000..c8174a0b3 --- /dev/null +++ b/kyaml/kio/filters/testdata2/dataset1-remoteupdates/java/java-deployment-1.resource.yaml @@ -0,0 +1,38 @@ +# Copyright 2019 The Kubernetes Authors. +# SPDX-License-Identifier: Apache-2.0 +# +apiVersion: apps/v1 +kind: Deployment +metadata: + name: app + labels: + app: java +spec: + replicas: 1 + selector: + matchLabels: + app: java2 + template: + metadata: + labels: + app: java2 + spec: + restartPolicy: Always + containers: + - name: app + image: gcr.io/project/app:version + command: + - java + - -jar + - /app.jar + ports: + - containerPort: 8080 + envFrom: + - configMapRef: + name: app-config + env: + - name: JAVA_OPTS + value: -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap + -Djava.security.egd=file:/dev/./urandom + imagePullPolicy: Always + minReadySeconds: 5 diff --git a/kyaml/kio/filters/testdata2/dataset1-remoteupdates/java/java-deployment-2.resource.yaml b/kyaml/kio/filters/testdata2/dataset1-remoteupdates/java/java-deployment-2.resource.yaml new file mode 100644 index 000000000..9caeaaea1 --- /dev/null +++ b/kyaml/kio/filters/testdata2/dataset1-remoteupdates/java/java-deployment-2.resource.yaml @@ -0,0 +1,38 @@ +# Copyright 2019 The Kubernetes Authors. +# SPDX-License-Identifier: Apache-2.0 +# +apiVersion: apps/v1 +kind: Deployment +metadata: + name: app + labels: + app: java3 +spec: + replicas: 4 + selector: + matchLabels: + app: java + template: + metadata: + labels: + app: java + spec: + restartPolicy: Always + containers: + - name: app + image: gcr.io/project/app:version + command: + - java + - -jar + - /app.jar + ports: + - containerPort: 8080 + envFrom: + - configMapRef: + name: app-config + env: + - name: JAVA_OPTS + value: -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap + -Djava.security.egd=file:/dev/./urandom + imagePullPolicy: Always + minReadySeconds: 10 diff --git a/kyaml/kio/filters/testdata2/dataset1/java/java-deployment-1.resource.yaml b/kyaml/kio/filters/testdata2/dataset1/java/java-deployment-1.resource.yaml new file mode 100644 index 000000000..53ffa5b4f --- /dev/null +++ b/kyaml/kio/filters/testdata2/dataset1/java/java-deployment-1.resource.yaml @@ -0,0 +1,38 @@ +# Copyright 2019 The Kubernetes Authors. +# SPDX-License-Identifier: Apache-2.0 +# +apiVersion: apps/v1 +kind: Deployment +metadata: + name: app + labels: + app: java +spec: + replicas: 1 + selector: + matchLabels: + app: java + template: + metadata: + labels: + app: java + spec: + restartPolicy: Always + containers: + - name: app + image: gcr.io/project/app:version + command: + - java + - -jar + - /app.jar + ports: + - containerPort: 8080 + envFrom: + - configMapRef: + name: app-config + env: + - name: JAVA_OPTS + value: -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap + -Djava.security.egd=file:/dev/./urandom + imagePullPolicy: Always + minReadySeconds: 5 diff --git a/kyaml/kio/filters/testdata2/dataset1/java/java-deployment-2.resource.yaml b/kyaml/kio/filters/testdata2/dataset1/java/java-deployment-2.resource.yaml new file mode 100644 index 000000000..9c5cdc3aa --- /dev/null +++ b/kyaml/kio/filters/testdata2/dataset1/java/java-deployment-2.resource.yaml @@ -0,0 +1,38 @@ +# Copyright 2019 The Kubernetes Authors. +# SPDX-License-Identifier: Apache-2.0 +# +apiVersion: apps/v1 +kind: Deployment +metadata: + name: app + labels: + app: java +spec: + replicas: 4 + selector: + matchLabels: + app: java + template: + metadata: + labels: + app: java + spec: + restartPolicy: Always + containers: + - name: app + image: gcr.io/project/app:version + command: + - java + - -jar + - /app.jar + ports: + - containerPort: 8080 + envFrom: + - configMapRef: + name: app-config + env: + - name: JAVA_OPTS + value: -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap + -Djava.security.egd=file:/dev/./urandom + imagePullPolicy: Always + minReadySeconds: 10