From ef0498339214d55731ec7cfb65745b83a0376b45 Mon Sep 17 00:00:00 2001 From: jregan Date: Sun, 28 Jun 2020 11:39:14 -0700 Subject: [PATCH] Start supporting strategic merge patch in kyaml merge2. --- kyaml/Makefile | 1 + kyaml/yaml/merge2/list_test.go | 70 ++++++++++++++ kyaml/yaml/merge2/map_test.go | 81 ++++++++++++++++ kyaml/yaml/merge2/merge2.go | 16 +++- kyaml/yaml/merge2/smpdirective.go | 71 ++++++++++++++ kyaml/yaml/merge2/smpdirective_string.go | 26 ++++++ kyaml/yaml/merge2/smpdirective_test.go | 112 +++++++++++++++++++++++ 7 files changed, 375 insertions(+), 2 deletions(-) create mode 100644 kyaml/yaml/merge2/smpdirective.go create mode 100644 kyaml/yaml/merge2/smpdirective_string.go create mode 100644 kyaml/yaml/merge2/smpdirective_test.go diff --git a/kyaml/Makefile b/kyaml/Makefile index 2c06b1306..fa6178496 100644 --- a/kyaml/Makefile +++ b/kyaml/Makefile @@ -14,6 +14,7 @@ fmt: go fmt ./... generate: + (which $(GOPATH)/bin/stringer || go get golang.org/x/tools/cmd/stringer) go generate ./... license: diff --git a/kyaml/yaml/merge2/list_test.go b/kyaml/yaml/merge2/list_test.go index 0ce8ced2d..8e137fa69 100644 --- a/kyaml/yaml/merge2/list_test.go +++ b/kyaml/yaml/merge2/list_test.go @@ -4,6 +4,76 @@ package merge2_test var listTestCases = []testCase{ + {description: `strategic merge patch delete 1`, + source: ` +apiVersion: apps/v1 +kind: Deployment +spec: + template: + spec: + containers: + - name: foo1 + $patch: delete + - name: foo2 + - name: foo3 +`, + dest: ` +apiVersion: apps/v1 +kind: Deployment +spec: + template: + spec: + containers: + - name: foo1 + - name: foo2 + - name: foo3 +`, + expected: ` +apiVersion: apps/v1 +kind: Deployment +spec: + template: + spec: + containers: + - name: foo2 + - name: foo3 +`, + }, + {description: `strategic merge patch delete 2`, + source: ` +apiVersion: apps/v1 +kind: Deployment +spec: + template: + spec: + containers: + - name: foo1 + - name: foo2 + - name: foo3 + $patch: delete +`, + dest: ` +apiVersion: apps/v1 +kind: Deployment +spec: + template: + spec: + containers: + - name: foo1 + - name: foo2 + - name: foo3 +`, + expected: ` +apiVersion: apps/v1 +kind: Deployment +spec: + template: + spec: + containers: + - name: foo1 + - name: foo2 +`, + }, {description: `merge k8s deployment containers`, source: ` apiVersion: apps/v1 diff --git a/kyaml/yaml/merge2/map_test.go b/kyaml/yaml/merge2/map_test.go index 405da742e..513009407 100644 --- a/kyaml/yaml/merge2/map_test.go +++ b/kyaml/yaml/merge2/map_test.go @@ -4,6 +4,87 @@ package merge2_test var mapTestCases = []testCase{ + + {description: `strategic merge patch delete 1`, + source: ` +kind: Deployment +$patch: delete +`, + dest: ` +kind: Deployment +spec: + foo: bar1 +`, + expected: ``, + }, + + {description: `strategic merge patch delete 2`, + source: ` +kind: Deployment +spec: + $patch: delete +`, + dest: ` +kind: Deployment +spec: + foo: bar + color: red +`, + expected: ` +kind: Deployment +`, + }, + + {description: `strategic merge patch delete 3`, + source: ` +kind: Deployment +spec: + metadata: + name: wut + template: + $patch: delete +`, + dest: ` +kind: Deployment +spec: + metadata: + name: wut + template: + spec: + containers: + - name: foo + - name: bar +`, + expected: ` +kind: Deployment +spec: + metadata: + name: wut +`, + }, + + {description: `strategic merge patch replace 1`, + source: ` +kind: Deployment +spec: + metal: heavy + $patch: replace + veggie: carrot +`, + dest: ` +kind: Deployment +spec: + river: nile + color: red +`, + expected: ` +kind: Deployment +spec: + metal: heavy + veggie: carrot +`, + }, + {description: `merge Map -- update field in dest`, source: ` kind: Deployment diff --git a/kyaml/yaml/merge2/merge2.go b/kyaml/yaml/merge2/merge2.go index 973d3e2e9..a2840cd00 100644 --- a/kyaml/yaml/merge2/merge2.go +++ b/kyaml/yaml/merge2/merge2.go @@ -60,8 +60,20 @@ func (m Merger) VisitMap(nodes walk.Sources, s *openapi.ResourceSchema) (*yaml.R // clear the value return walk.ClearNode, nil } - // Recursively Merge dest - return nodes.Dest(), nil + + ps, err := determineMappingNodePatchStrategy(nodes.Origin()) + if err != nil { + return nil, err + } + + switch ps { + case smpDelete: + return walk.ClearNode, nil + case smpReplace: + return nodes.Origin(), nil + default: + return nodes.Dest(), nil + } } func (m Merger) VisitScalar(nodes walk.Sources, s *openapi.ResourceSchema) (*yaml.RNode, error) { diff --git a/kyaml/yaml/merge2/smpdirective.go b/kyaml/yaml/merge2/smpdirective.go new file mode 100644 index 000000000..5d2a973b3 --- /dev/null +++ b/kyaml/yaml/merge2/smpdirective.go @@ -0,0 +1,71 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package merge2 + +import ( + "fmt" + + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +// A strategic merge patch directive. +// See https://github.com/kubernetes/community/blob/master/contributors/devel/sig-api-machinery/strategic-merge-patch.md +// +//go:generate stringer -type=smpDirective -linecomment +type smpDirective int + +const ( + smpUnknown smpDirective = iota // unknown + smpReplace // replace + smpDelete // delete + smpMerge // merge +) + +const strategicMergePatchDirectiveKey = "$patch" + +// Examine patch for a strategic merge patch directive. +// If found, return it, and remove the directive from the patch. +func determineSmpDirective(patch *yaml.RNode) (smpDirective, error) { + if patch == nil { + return smpMerge, nil + } + switch patch.YNode().Kind { + case yaml.SequenceNode: + return determineSequenceNodePatchStrategy(patch) + case yaml.MappingNode: + return determineMappingNodePatchStrategy(patch) + default: + return smpUnknown, fmt.Errorf( + "no implemented strategic merge patch strategy for '%s' ('%s')", + patch.YNode().ShortTag(), patch.MustString()) + } +} + +// TODO: what should this do? +func determineSequenceNodePatchStrategy(_ *yaml.RNode) (smpDirective, error) { + return smpMerge, nil +} + +func determineMappingNodePatchStrategy(patch *yaml.RNode) (smpDirective, error) { + node, err := patch.Pipe(yaml.Get(strategicMergePatchDirectiveKey)) + if err != nil || node == nil || node.YNode() == nil { + return smpMerge, nil + } + v := node.YNode().Value + if v == smpDelete.String() { + return smpDelete, elidePatchDirective(patch) + } + if v == smpReplace.String() { + return smpReplace, elidePatchDirective(patch) + } + if v == smpMerge.String() { + return smpMerge, elidePatchDirective(patch) + } + return smpUnknown, fmt.Errorf( + "unknown patch strategy '%s'", v) +} + +func elidePatchDirective(patch *yaml.RNode) error { + return patch.PipeE(yaml.Clear(strategicMergePatchDirectiveKey)) +} diff --git a/kyaml/yaml/merge2/smpdirective_string.go b/kyaml/yaml/merge2/smpdirective_string.go new file mode 100644 index 000000000..b4f937f0e --- /dev/null +++ b/kyaml/yaml/merge2/smpdirective_string.go @@ -0,0 +1,26 @@ +// Code generated by "stringer -type=smpDirective -linecomment"; DO NOT EDIT. + +package merge2 + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[smpUnknown-0] + _ = x[smpReplace-1] + _ = x[smpDelete-2] + _ = x[smpMerge-3] +} + +const _smpDirective_name = "unknownreplacedeletemerge" + +var _smpDirective_index = [...]uint8{0, 7, 14, 20, 25} + +func (i smpDirective) String() string { + if i < 0 || i >= smpDirective(len(_smpDirective_index)-1) { + return "smpDirective(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _smpDirective_name[_smpDirective_index[i]:_smpDirective_index[i+1]] +} diff --git a/kyaml/yaml/merge2/smpdirective_test.go b/kyaml/yaml/merge2/smpdirective_test.go new file mode 100644 index 000000000..fbb719b9c --- /dev/null +++ b/kyaml/yaml/merge2/smpdirective_test.go @@ -0,0 +1,112 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 +package merge2 + +import ( + "strings" + "testing" + + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +func Test_determineSmpDirective(t *testing.T) { + var cases = map[string]struct { + patch string + elided string + expected smpDirective + errExpected string + }{ + `scalar`: { + patch: "dumb", + expected: smpMerge, + errExpected: "no implemented strategic merge patch strategy", + }, + `list merge`: { + patch: ` +- one +- two +- three +`, + expected: smpMerge, + elided: `- one +- two +- three +`, + }, + `map replace`: { + patch: ` +metal: heavy +$patch: replace +veggie: carrot +`, + expected: smpReplace, + elided: `metal: heavy +veggie: carrot +`, + }, + `map delete`: { + patch: ` +metal: heavy +$patch: delete +veggie: carrot +`, + expected: smpDelete, + elided: `metal: heavy +veggie: carrot +`, + }, + `map merge`: { + patch: ` +metal: heavy +$patch: merge +veggie: carrot +`, + expected: smpMerge, + elided: `metal: heavy +veggie: carrot +`, + }, + `map default`: { + patch: ` +metal: heavy +veggie: carrot +`, + expected: smpMerge, + elided: `metal: heavy +veggie: carrot +`, + }, + } + + for n := range cases { + tc := cases[n] + t.Run(n, func(t *testing.T) { + p, err := yaml.Parse(tc.patch) + if err != nil { + t.Fatalf("unexpected parse err %v", err) + } + unwrapped := yaml.NewRNode(p.YNode()) + actual, err := determineSmpDirective(unwrapped) + if err == nil { + if tc.errExpected != "" { + t.Fatalf("should have seen an error") + } + if tc.expected != actual { + t.Fatalf("expected %s, got %s", tc.expected, actual) + } + if tc.elided != unwrapped.MustString() { + t.Fatalf( + "expected %s, got %s", + tc.elided, unwrapped.MustString()) + } + } else { + if tc.errExpected == "" { + t.Fatalf("unexpected err: %v", err) + } + if !strings.Contains(err.Error(), tc.errExpected) { + t.Fatalf("expected some error other than: %v", err) + } + } + }) + } +}