Start supporting strategic merge patch in kyaml merge2.

This commit is contained in:
jregan
2020-06-28 11:39:14 -07:00
parent 2153863355
commit ef04983392
7 changed files with 375 additions and 2 deletions

View File

@@ -14,6 +14,7 @@ fmt:
go fmt ./...
generate:
(which $(GOPATH)/bin/stringer || go get golang.org/x/tools/cmd/stringer)
go generate ./...
license:

View File

@@ -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

View File

@@ -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

View File

@@ -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) {

View File

@@ -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))
}

View File

@@ -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]]
}

View File

@@ -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)
}
}
})
}
}