Use OpenAPI when merging (3way) resources

- When merging (3way) resources use the patch strategy from the openAPI if the definition exists for the field
- Allow disabling of guessing patch strategy merge keys when no definition exists
- Support defining strategy and key directly on configuration fields through line and header coments
- Support attaching schema to parent fields of lists, and propagating -- e.g. that a field is a PodTemplate
This commit is contained in:
Phillip Wittrock
2020-02-27 10:08:40 -08:00
parent 8991b193c6
commit 5d1a0346b5
24 changed files with 991 additions and 507 deletions

View File

@@ -5,6 +5,7 @@ package fieldmeta
import ( import (
"encoding/json" "encoding/json"
"reflect"
"strconv" "strconv"
"strings" "strings"
@@ -36,16 +37,29 @@ type PartialFieldSetter struct {
Value string `yaml:"value" json:"value"` Value string `yaml:"value" json:"value"`
} }
// IsEmpty returns true if the FieldMeta has any empty Schema
func (fm *FieldMeta) IsEmpty() bool {
if fm == nil {
return true
}
return reflect.DeepEqual(fm.Schema, spec.Schema{})
}
// Read reads the FieldMeta from a node // Read reads the FieldMeta from a node
func (fm *FieldMeta) Read(n *yaml.RNode) error { func (fm *FieldMeta) Read(n *yaml.RNode) error {
if n.YNode().LineComment != "" { // check for metadata on head and line comments
v := strings.TrimLeft(n.YNode().LineComment, "#") comments := []string{n.YNode().LineComment, n.YNode().HeadComment}
for _, c := range comments {
if c == "" {
continue
}
c := strings.TrimLeft(c, "#")
// if it doesn't Unmarshal that is fine, it means there is no metadata // if it doesn't Unmarshal that is fine, it means there is no metadata
// other comments are valid, they just don't parse // other comments are valid, they just don't parse
// TODO: consider most sophisticated parsing techniques similar to what is used // TODO: consider more sophisticated parsing techniques similar to what is used
// for go struct tags. // for go struct tags.
if err := fm.Schema.UnmarshalJSON([]byte(v)); err != nil { if err := fm.Schema.UnmarshalJSON([]byte(c)); err != nil {
// note: don't return an error if the comment isn't a fieldmeta struct // note: don't return an error if the comment isn't a fieldmeta struct
return nil return nil
} }

View File

@@ -7,6 +7,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"reflect"
"sync" "sync"
"github.com/go-openapi/spec" "github.com/go-openapi/spec"
@@ -32,6 +33,14 @@ type ResourceSchema struct {
Schema *spec.Schema Schema *spec.Schema
} }
// IsEmpty returns true if the ResourceSchema is empty
func (rs *ResourceSchema) IsEmpty() bool {
if rs == nil || rs.Schema == nil {
return true
}
return reflect.DeepEqual(*rs.Schema, spec.Schema{})
}
// SchemaForResourceType returns the Schema for the given Resource // SchemaForResourceType returns the Schema for the given Resource
// TODO(pwittrock): create a version of this function that will return a schema // TODO(pwittrock): create a version of this function that will return a schema
// which can be used for duck-typed Resources -- e.g. contains common fields such // which can be used for duck-typed Resources -- e.g. contains common fields such
@@ -193,15 +202,15 @@ func SuppressBuiltInSchemaUse() {
} }
// Elements returns the Schema for the elements of an array. // Elements returns the Schema for the elements of an array.
func (r *ResourceSchema) Elements() *ResourceSchema { func (rs *ResourceSchema) Elements() *ResourceSchema {
// load the schema from swagger.json // load the schema from swagger.json
initSchema() initSchema()
if len(r.Schema.Type) != 1 || r.Schema.Type[0] != "array" { if len(rs.Schema.Type) != 1 || rs.Schema.Type[0] != "array" {
// either not an array, or array has multiple types // either not an array, or array has multiple types
return nil return nil
} }
s := *r.Schema.Items.Schema s := *rs.Schema.Items.Schema
for s.Ref.String() != "" { for s.Ref.String() != "" {
sc, e := Resolve(&s.Ref) sc, e := Resolve(&s.Ref)
if e != nil { if e != nil {
@@ -219,8 +228,8 @@ const Elements = "[]"
// Field is called. // Field is called.
// If any Field or Elements call returns nil, then Lookup returns // If any Field or Elements call returns nil, then Lookup returns
// nil immediately. // nil immediately.
func (r *ResourceSchema) Lookup(path ...string) *ResourceSchema { func (rs *ResourceSchema) Lookup(path ...string) *ResourceSchema {
s := r s := rs
for _, p := range path { for _, p := range path {
if s == nil { if s == nil {
break break
@@ -235,19 +244,19 @@ func (r *ResourceSchema) Lookup(path ...string) *ResourceSchema {
} }
// Field returns the Schema for a field. // Field returns the Schema for a field.
func (r *ResourceSchema) Field(field string) *ResourceSchema { func (rs *ResourceSchema) Field(field string) *ResourceSchema {
// load the schema from swagger.json // load the schema from swagger.json
initSchema() initSchema()
// locate the Schema // locate the Schema
s, found := r.Schema.Properties[field] s, found := rs.Schema.Properties[field]
switch { switch {
case found: case found:
// no-op, continue with s as the schema // no-op, continue with s as the schema
case r.Schema.AdditionalProperties != nil && r.Schema.AdditionalProperties.Schema != nil: case rs.Schema.AdditionalProperties != nil && rs.Schema.AdditionalProperties.Schema != nil:
// map field type -- use Schema of the value // map field type -- use Schema of the value
// (the key doesn't matter, they all have the same value type) // (the key doesn't matter, they all have the same value type)
s = *r.Schema.AdditionalProperties.Schema s = *rs.Schema.AdditionalProperties.Schema
default: default:
// no Schema found from either swagger.json or line comments // no Schema found from either swagger.json or line comments
return nil return nil
@@ -267,14 +276,14 @@ func (r *ResourceSchema) Field(field string) *ResourceSchema {
} }
// PatchStrategyAndKey returns the patch strategy and merge key extensions // PatchStrategyAndKey returns the patch strategy and merge key extensions
func (r *ResourceSchema) PatchStrategyAndKey() (string, string) { func (rs *ResourceSchema) PatchStrategyAndKey() (string, string) {
ps, found := r.Schema.Extensions[kubernetesPatchStrategyExtensionKey] ps, found := rs.Schema.Extensions[kubernetesPatchStrategyExtensionKey]
if !found { if !found {
// merge key and patch strategy must appear together // merge key and patch strategy must appear together
return "", "" return "", ""
} }
mk, found := r.Schema.Extensions[kubernetesMergeKeyExtensionKey] mk, found := rs.Schema.Extensions[kubernetesMergeKeyExtensionKey]
if !found { if !found {
// merge key and patch strategy must appear together // merge key and patch strategy must appear together
return "", "" return "", ""

View File

@@ -4,21 +4,21 @@
package merge2_test package merge2_test
var elementTestCases = []testCase{ var elementTestCases = []testCase{
{`merge Element -- keep field in dest`, {description: `merge Element -- keep field in dest`,
` source: `
kind: Deployment kind: Deployment
items: items:
- name: foo - name: foo
image: foo:v1 image: foo:v1
`, `,
` dest: `
kind: Deployment kind: Deployment
items: items:
- name: foo - name: foo
image: foo:v0 image: foo:v0
command: ['run.sh'] command: ['run.sh']
`, `,
` expected: `
kind: Deployment kind: Deployment
items: items:
- name: foo - name: foo
@@ -27,21 +27,21 @@ items:
`, `,
}, },
{`merge Element -- add field to dest`, {description: `merge Element -- add field to dest`,
` source: `
kind: Deployment kind: Deployment
items: items:
- name: foo - name: foo
image: foo:v1 image: foo:v1
command: ['run.sh'] command: ['run.sh']
`, `,
` dest: `
kind: Deployment kind: Deployment
items: items:
- name: foo - name: foo
image: foo:v0 image: foo:v0
`, `,
` expected: `
kind: Deployment kind: Deployment
items: items:
- name: foo - name: foo
@@ -50,19 +50,19 @@ items:
`, `,
}, },
{`merge Element -- add list, empty in dest`, {description: `merge Element -- add list, empty in dest`,
` source: `
kind: Deployment kind: Deployment
items: items:
- name: foo - name: foo
image: foo:v1 image: foo:v1
command: ['run.sh'] command: ['run.sh']
`, `,
` dest: `
kind: Deployment kind: Deployment
items: [] items: []
`, `,
` expected: `
kind: Deployment kind: Deployment
items: items:
- name: foo - name: foo
@@ -71,18 +71,18 @@ items:
`, `,
}, },
{`merge Element -- add list, missing from dest`, {description: `merge Element -- add list, missing from dest`,
` source: `
kind: Deployment kind: Deployment
items: items:
- name: foo - name: foo
image: foo:v1 image: foo:v1
command: ['run.sh'] command: ['run.sh']
`, `,
` dest: `
kind: Deployment kind: Deployment
`, `,
` expected: `
kind: Deployment kind: Deployment
items: items:
- name: foo - name: foo
@@ -91,8 +91,8 @@ items:
`, `,
}, },
{`merge Element -- add Element first`, {description: `merge Element -- add Element first`,
` source: `
kind: Deployment kind: Deployment
items: items:
- name: bar - name: bar
@@ -101,13 +101,13 @@ items:
- name: foo - name: foo
image: foo:v1 image: foo:v1
`, `,
` dest: `
kind: Deployment kind: Deployment
items: items:
- name: foo - name: foo
image: foo:v0 image: foo:v0
`, `,
` expected: `
kind: Deployment kind: Deployment
items: items:
- name: foo - name: foo
@@ -118,8 +118,8 @@ items:
`, `,
}, },
{`merge Element -- add Element second`, {description: `merge Element -- add Element second`,
` source: `
kind: Deployment kind: Deployment
items: items:
- name: foo - name: foo
@@ -128,13 +128,13 @@ items:
image: bar:v1 image: bar:v1
command: ['run2.sh'] command: ['run2.sh']
`, `,
` dest: `
kind: Deployment kind: Deployment
items: items:
- name: foo - name: foo
image: foo:v0 image: foo:v0
`, `,
` expected: `
kind: Deployment kind: Deployment
items: items:
- name: foo - name: foo
@@ -148,11 +148,11 @@ items:
// //
// Test Case // Test Case
// //
{`keep list -- list missing from src`, {description: `keep list -- list missing from src`,
` source: `
kind: Deployment kind: Deployment
`, `,
` dest: `
kind: Deployment kind: Deployment
items: items:
- name: foo - name: foo
@@ -161,7 +161,7 @@ items:
image: bar:v1 image: bar:v1
command: ['run2.sh'] command: ['run2.sh']
`, `,
` expected: `
kind: Deployment kind: Deployment
items: items:
- name: foo - name: foo
@@ -175,14 +175,14 @@ items:
// //
// Test Case // Test Case
// //
{`keep Element -- element missing in src`, {description: `keep Element -- element missing in src`,
` source: `
kind: Deployment kind: Deployment
items: items:
- name: foo - name: foo
image: foo:v1 image: foo:v1
`, `,
` dest: `
kind: Deployment kind: Deployment
items: items:
- name: foo - name: foo
@@ -191,7 +191,7 @@ items:
image: bar:v1 image: bar:v1
command: ['run2.sh'] command: ['run2.sh']
`, `,
` expected: `
kind: Deployment kind: Deployment
items: items:
- name: foo - name: foo
@@ -205,12 +205,12 @@ items:
// //
// Test Case // Test Case
// //
{`keep element -- empty list in src`, {description: `keep element -- empty list in src`,
` source: `
kind: Deployment kind: Deployment
items: {} items: {}
`, `,
` dest: `
kind: Deployment kind: Deployment
items: items:
- name: foo - name: foo
@@ -219,7 +219,7 @@ items:
image: bar:v1 image: bar:v1
command: ['run2.sh'] command: ['run2.sh']
`, `,
` expected: `
kind: Deployment kind: Deployment
items: items:
- name: foo - name: foo
@@ -233,12 +233,12 @@ items:
// //
// Test Case // Test Case
// //
{`remove Element -- null in src`, {description: `remove Element -- null in src`,
` source: `
kind: Deployment kind: Deployment
items: null items: null
`, `,
` dest: `
kind: Deployment kind: Deployment
items: items:
- name: foo - name: foo
@@ -247,8 +247,97 @@ items:
image: bar:v1 image: bar:v1
command: ['run2.sh'] command: ['run2.sh']
`, `,
` expected: `
kind: Deployment kind: Deployment
`, `,
}, },
//
// Test Case
//
{description: `no infer merge keys no merge'`,
source: `
kind: Deployment
containers:
- name: foo
command: ['run2.sh']
`,
dest: `
kind: Deployment
containers:
- name: foo
image: foo:bar
`,
expected: `
kind: Deployment
containers:
- name: foo
command: ['run2.sh']
`,
noInfer: true,
},
//
// Test Case
//
{description: `no infer merge keys merge using schema`,
source: `
kind: Deployment
apiVersion: apps/v1
spec:
template:
spec:
containers:
- name: foo
command: ['run2.sh']
`,
dest: `
kind: Deployment
apiVersion: apps/v1
spec:
template:
spec:
containers:
- name: foo
image: foo:bar
`,
expected: `
kind: Deployment
apiVersion: apps/v1
spec:
template:
spec:
containers:
- name: foo
image: foo:bar
command: ['run2.sh']
`,
noInfer: true,
},
//
// Test Case
//
{description: `no infer merge keys merge using explicit schema as line comment'`,
source: `
kind: Deployment
containers:
- name: foo
command: ['run2.sh']
`,
dest: `
kind: Deployment
containers: # {"items":{"$ref": "#/definitions/io.k8s.api.core.v1.Container"},"type":"array","x-kubernetes-patch-merge-key":"name","x-kubernetes-patch-strategy": "merge"}
- name: foo # hell ow
image: foo:bar
`,
expected: `
kind: Deployment
containers: # {"items":{"$ref": "#/definitions/io.k8s.api.core.v1.Container"},"type":"array","x-kubernetes-patch-merge-key":"name","x-kubernetes-patch-strategy": "merge"}
- name: foo
image: foo:bar
command: ['run2.sh']
`,
noInfer: true,
},
} }

View File

@@ -4,21 +4,21 @@
package merge2_test package merge2_test
var listTestCases = []testCase{ var listTestCases = []testCase{
{`replace List -- different value in dest`, {description: `replace List -- different value in dest`,
` source: `
kind: Deployment kind: Deployment
items: items:
- 1 - 1
- 2 - 2
- 3 - 3
`, `,
` dest: `
kind: Deployment kind: Deployment
items: items:
- 0 - 0
- 1 - 1
`, `,
` expected: `
kind: Deployment kind: Deployment
items: items:
- 1 - 1
@@ -27,18 +27,18 @@ items:
`, `,
}, },
{`replace List -- missing from dest`, {description: `replace List -- missing from dest`,
` source: `
kind: Deployment kind: Deployment
items: items:
- 1 - 1
- 2 - 2
- 3 - 3
`, `,
` dest: `
kind: Deployment kind: Deployment
`, `,
` expected: `
kind: Deployment kind: Deployment
items: items:
- 1 - 1
@@ -50,22 +50,22 @@ items:
// //
// Test Case // Test Case
// //
{`keep List -- same value in src and dest`, {description: `keep List -- same value in src and dest`,
` source: `
kind: Deployment kind: Deployment
items: items:
- 1 - 1
- 2 - 2
- 3 - 3
`, `,
` dest: `
kind: Deployment kind: Deployment
items: items:
- 1 - 1
- 2 - 2
- 3 - 3
`, `,
` expected: `
kind: Deployment kind: Deployment
items: items:
- 1 - 1
@@ -77,18 +77,18 @@ items:
// //
// Test Case // Test Case
// //
{`keep List -- unspecified in src`, {description: `keep List -- unspecified in src`,
` source: `
kind: Deployment kind: Deployment
`, `,
` dest: `
kind: Deployment kind: Deployment
items: items:
- 1 - 1
- 2 - 2
- 3 - 3
`, `,
` expected: `
kind: Deployment kind: Deployment
items: items:
- 1 - 1
@@ -100,19 +100,19 @@ items:
// //
// Test Case // Test Case
// //
{`remove List -- null in src`, {description: `remove List -- null in src`,
` source: `
kind: Deployment kind: Deployment
items: null items: null
`, `,
` dest: `
kind: Deployment kind: Deployment
items: items:
- 1 - 1
- 2 - 2
- 3 - 3
`, `,
` expected: `
kind: Deployment kind: Deployment
`, `,
}, },
@@ -120,19 +120,19 @@ kind: Deployment
// //
// Test Case // Test Case
// //
{`remove list -- empty in src`, {description: `remove list -- empty in src`,
` source: `
kind: Deployment kind: Deployment
items: {} items: {}
`, `,
` dest: `
kind: Deployment kind: Deployment
items: items:
- 1 - 1
- 2 - 2
- 3 - 3
`, `,
` expected: `
kind: Deployment kind: Deployment
items: {} items: {}
`, `,

View File

@@ -4,19 +4,19 @@
package merge2_test package merge2_test
var mapTestCases = []testCase{ var mapTestCases = []testCase{
{`merge Map -- update field in dest`, {description: `merge Map -- update field in dest`,
` source: `
kind: Deployment kind: Deployment
spec: spec:
foo: bar1 foo: bar1
`, `,
` dest: `
kind: Deployment kind: Deployment
spec: spec:
foo: bar0 foo: bar0
baz: buz baz: buz
`, `,
` expected: `
kind: Deployment kind: Deployment
spec: spec:
foo: bar1 foo: bar1
@@ -24,19 +24,19 @@ spec:
`, `,
}, },
{`merge Map -- add field to dest`, {description: `merge Map -- add field to dest`,
` source: `
kind: Deployment kind: Deployment
spec: spec:
foo: bar1 foo: bar1
baz: buz baz: buz
`, `,
` dest: `
kind: Deployment kind: Deployment
spec: spec:
foo: bar0 foo: bar0
`, `,
` expected: `
kind: Deployment kind: Deployment
spec: spec:
foo: bar1 foo: bar1
@@ -44,18 +44,18 @@ spec:
`, `,
}, },
{`merge Map -- add list, empty in dest`, {description: `merge Map -- add list, empty in dest`,
` source: `
kind: Deployment kind: Deployment
spec: spec:
foo: bar1 foo: bar1
baz: buz baz: buz
`, `,
` dest: `
kind: Deployment kind: Deployment
spec: {} spec: {}
`, `,
` expected: `
kind: Deployment kind: Deployment
spec: spec:
foo: bar1 foo: bar1
@@ -63,17 +63,17 @@ spec:
`, `,
}, },
{`merge Map -- add list, missing from dest`, {description: `merge Map -- add list, missing from dest`,
` source: `
kind: Deployment kind: Deployment
spec: spec:
foo: bar1 foo: bar1
baz: buz baz: buz
`, `,
` dest: `
kind: Deployment kind: Deployment
`, `,
` expected: `
kind: Deployment kind: Deployment
spec: spec:
foo: bar1 foo: bar1
@@ -81,19 +81,19 @@ spec:
`, `,
}, },
{`merge Map -- add Map first`, {description: `merge Map -- add Map first`,
` source: `
kind: Deployment kind: Deployment
spec: spec:
foo: bar1 foo: bar1
baz: buz baz: buz
`, `,
` dest: `
kind: Deployment kind: Deployment
spec: spec:
foo: bar1 foo: bar1
`, `,
` expected: `
kind: Deployment kind: Deployment
spec: spec:
foo: bar1 foo: bar1
@@ -101,19 +101,19 @@ spec:
`, `,
}, },
{`merge Map -- add Map second`, {description: `merge Map -- add Map second`,
` source: `
kind: Deployment kind: Deployment
spec: spec:
baz: buz baz: buz
foo: bar1 foo: bar1
`, `,
` dest: `
kind: Deployment kind: Deployment
spec: spec:
foo: bar1 foo: bar1
`, `,
` expected: `
kind: Deployment kind: Deployment
spec: spec:
foo: bar1 foo: bar1
@@ -124,17 +124,17 @@ spec:
// //
// Test Case // Test Case
// //
{`keep map -- map missing from src`, {description: `keep map -- map missing from src`,
` source: `
kind: Deployment kind: Deployment
`, `,
` dest: `
kind: Deployment kind: Deployment
spec: spec:
foo: bar1 foo: bar1
baz: buz baz: buz
`, `,
` expected: `
kind: Deployment kind: Deployment
spec: spec:
foo: bar1 foo: bar1
@@ -145,18 +145,18 @@ spec:
// //
// Test Case // Test Case
// //
{`keep map -- empty list in src`, {description: `keep map -- empty list in src`,
` source: `
kind: Deployment kind: Deployment
items: {} items: {}
`, `,
` dest: `
kind: Deployment kind: Deployment
spec: spec:
foo: bar1 foo: bar1
baz: buz baz: buz
`, `,
` expected: `
kind: Deployment kind: Deployment
spec: spec:
foo: bar1 foo: bar1
@@ -168,18 +168,18 @@ items: {}
// //
// Test Case // Test Case
// //
{`remove Map -- null in src`, {description: `remove Map -- null in src`,
` source: `
kind: Deployment kind: Deployment
spec: null spec: null
`, `,
` dest: `
kind: Deployment kind: Deployment
spec: spec:
foo: bar1 foo: bar1
baz: buz baz: buz
`, `,
` expected: `
kind: Deployment kind: Deployment
`, `,
}, },

View File

@@ -6,6 +6,7 @@
package merge2 package merge2
import ( import (
"sigs.k8s.io/kustomize/kyaml/openapi"
"sigs.k8s.io/kustomize/kyaml/yaml" "sigs.k8s.io/kustomize/kyaml/yaml"
"sigs.k8s.io/kustomize/kyaml/yaml/walk" "sigs.k8s.io/kustomize/kyaml/yaml/walk"
) )
@@ -16,7 +17,7 @@ func Merge(src, dest *yaml.RNode) (*yaml.RNode, error) {
} }
// Merge parses the arguments, and merges fields from srcStr into destStr. // Merge parses the arguments, and merges fields from srcStr into destStr.
func MergeStrings(srcStr, destStr string) (string, error) { func MergeStrings(srcStr, destStr string, infer bool) (string, error) {
src, err := yaml.Parse(srcStr) src, err := yaml.Parse(srcStr)
if err != nil { if err != nil {
return "", err return "", err
@@ -26,10 +27,15 @@ func MergeStrings(srcStr, destStr string) (string, error) {
return "", err return "", err
} }
result, err := Merge(src, dest) result, err := walk.Walker{
Sources: []*yaml.RNode{dest, src},
Visitor: Merger{},
InferAssociativeLists: infer,
}.Walk()
if err != nil { if err != nil {
return "", err return "", err
} }
return result.String() return result.String()
} }
@@ -39,7 +45,7 @@ type Merger struct {
var _ walk.Visitor = Merger{} var _ walk.Visitor = Merger{}
func (m Merger) VisitMap(nodes walk.Sources) (*yaml.RNode, error) { func (m Merger) VisitMap(nodes walk.Sources, s *openapi.ResourceSchema) (*yaml.RNode, error) {
if err := m.SetComments(nodes); err != nil { if err := m.SetComments(nodes); err != nil {
return nil, err return nil, err
} }
@@ -58,7 +64,7 @@ func (m Merger) VisitMap(nodes walk.Sources) (*yaml.RNode, error) {
return nodes.Dest(), nil return nodes.Dest(), nil
} }
func (m Merger) VisitScalar(nodes walk.Sources) (*yaml.RNode, error) { func (m Merger) VisitScalar(nodes walk.Sources, s *openapi.ResourceSchema) (*yaml.RNode, error) {
if err := m.SetComments(nodes); err != nil { if err := m.SetComments(nodes); err != nil {
return nil, err return nil, err
} }
@@ -73,7 +79,7 @@ func (m Merger) VisitScalar(nodes walk.Sources) (*yaml.RNode, error) {
return nodes.Dest(), nil return nodes.Dest(), nil
} }
func (m Merger) VisitList(nodes walk.Sources, kind walk.ListKind) (*yaml.RNode, error) { func (m Merger) VisitList(nodes walk.Sources, s *openapi.ResourceSchema, kind walk.ListKind) (*yaml.RNode, error) {
if err := m.SetComments(nodes); err != nil { if err := m.SetComments(nodes); err != nil {
return nil, err return nil, err
} }

View File

@@ -365,7 +365,7 @@ a:
b: b:
# header comment # header comment
c: d c: d
`) `, true)
if !assert.NoError(t, err) { if !assert.NoError(t, err) {
return return
} }
@@ -385,7 +385,7 @@ a:
b: b:
c: d c: d
# footer comment # footer comment
`) `, true)
if !assert.NoError(t, err) { if !assert.NoError(t, err) {
return return
} }
@@ -404,7 +404,7 @@ a:
a: a:
b: b:
c: d # line comment c: d # line comment
`) `, true)
if !assert.NoError(t, err) { if !assert.NoError(t, err) {
return return
} }
@@ -426,7 +426,7 @@ a:
b: b:
# replace comment # replace comment
c: d c: d
`) `, true)
if !assert.NoError(t, err) { if !assert.NoError(t, err) {
return return
} }
@@ -447,7 +447,7 @@ a:
b: b:
c: d c: d
# replace comment # replace comment
`) `, true)
if !assert.NoError(t, err) { if !assert.NoError(t, err) {
return return
} }
@@ -466,7 +466,7 @@ a:
a: a:
b: b:
c: d # replace comment c: d # replace comment
`) `, true)
if !assert.NoError(t, err) { if !assert.NoError(t, err) {
return return
} }
@@ -484,7 +484,7 @@ a:
a: a:
b: b:
c: d # replace comment c: d # replace comment
`) `, true)
if !assert.NoError(t, err) { if !assert.NoError(t, err) {
return return
} }

View File

@@ -17,24 +17,27 @@ var testCases = [][]testCase{scalarTestCases, listTestCases, elementTestCases, m
func TestMerge(t *testing.T) { func TestMerge(t *testing.T) {
for i := range testCases { for i := range testCases {
for _, tc := range testCases[i] { for j := range testCases[i] {
actual, err := MergeStrings(tc.source, tc.dest) tc := testCases[i][j]
if !assert.NoError(t, err, tc.description) { t.Run(tc.description, func(t *testing.T) {
t.FailNow() actual, err := MergeStrings(tc.source, tc.dest, !tc.noInfer)
} if !assert.NoError(t, err, tc.description) {
e, err := filters.FormatInput(bytes.NewBufferString(tc.expected)) t.FailNow()
if !assert.NoError(t, err) { }
t.FailNow() e, err := filters.FormatInput(bytes.NewBufferString(tc.expected))
} if !assert.NoError(t, err) {
estr := strings.TrimSpace(e.String()) t.FailNow()
a, err := filters.FormatInput(bytes.NewBufferString(actual)) }
if !assert.NoError(t, err) { estr := strings.TrimSpace(e.String())
t.FailNow() a, err := filters.FormatInput(bytes.NewBufferString(actual))
} if !assert.NoError(t, err) {
astr := strings.TrimSpace(a.String()) t.FailNow()
if !assert.Equal(t, estr, astr, tc.description) { }
t.FailNow() astr := strings.TrimSpace(a.String())
} if !assert.Equal(t, estr, astr, tc.description) {
t.FailNow()
}
})
} }
} }
} }
@@ -44,4 +47,5 @@ type testCase struct {
source string source string
dest string dest string
expected string expected string
noInfer bool
} }

View File

@@ -4,30 +4,30 @@
package merge2_test package merge2_test
var scalarTestCases = []testCase{ var scalarTestCases = []testCase{
{`replace scalar -- different value in dest`, {description: `replace scalar -- different value in dest`,
` source: `
kind: Deployment kind: Deployment
field: value1 field: value1
`, `,
` dest: `
kind: Deployment kind: Deployment
field: value0 field: value0
`, `,
` expected: `
kind: Deployment kind: Deployment
field: value1 field: value1
`, `,
}, },
{`replace scalar -- missing from dest`, {description: `replace scalar -- missing from dest`,
` source: `
kind: Deployment kind: Deployment
field: value1 field: value1
`, `,
` dest: `
kind: Deployment kind: Deployment
`, `,
` expected: `
kind: Deployment kind: Deployment
field: value1 field: value1
`, `,
@@ -36,16 +36,16 @@ field: value1
// //
// Test Case // Test Case
// //
{`keep scalar -- same value in src and dest`, {description: `keep scalar -- same value in src and dest`,
` source: `
kind: Deployment kind: Deployment
field: value1 field: value1
`, `,
` dest: `
kind: Deployment kind: Deployment
field: value1 field: value1
`, `,
` expected: `
kind: Deployment kind: Deployment
field: value1 field: value1
`, `,
@@ -54,15 +54,15 @@ field: value1
// //
// Test Case // Test Case
// //
{`keep scalar -- unspecified in src`, {description: `keep scalar -- unspecified in src`,
` source: `
kind: Deployment kind: Deployment
`, `,
` dest: `
kind: Deployment kind: Deployment
field: value1 field: value1
`, `,
` expected: `
kind: Deployment kind: Deployment
field: value1 field: value1
`, `,
@@ -71,16 +71,16 @@ field: value1
// //
// Test Case // Test Case
// //
{`remove scalar -- null in src`, {description: `remove scalar -- null in src`,
` source: `
kind: Deployment kind: Deployment
field: null field: null
`, `,
` dest: `
kind: Deployment kind: Deployment
field: value1 field: value1
`, `,
` expected: `
kind: Deployment kind: Deployment
`, `,
}, },
@@ -88,16 +88,16 @@ kind: Deployment
// //
// Test Case // Test Case
// //
{`remove scalar -- empty in src`, {description: `remove scalar -- empty in src`,
` source: `
kind: Deployment kind: Deployment
field: {} field: {}
`, `,
` dest: `
kind: Deployment kind: Deployment
field: value1 field: value1
`, `,
` expected: `
kind: Deployment kind: Deployment
field: {} field: {}
`, `,
@@ -106,15 +106,15 @@ field: {}
// //
// Test Case // Test Case
// //
{`remove scalar -- null in src, missing in dest`, {description: `remove scalar -- null in src, missing in dest`,
` source: `
kind: Deployment kind: Deployment
field: null field: null
`, `,
` dest: `
kind: Deployment kind: Deployment
`, `,
` expected: `
kind: Deployment kind: Deployment
`, `,
}, },
@@ -122,15 +122,15 @@ kind: Deployment
// //
// Test Case // Test Case
// //
{`merge an empty value`, {description: `merge an empty value`,
` source: `
kind: Deployment kind: Deployment
field: {} field: {}
`, `,
` dest: `
kind: Deployment kind: Deployment
`, `,
` expected: `
kind: Deployment kind: Deployment
field: {} field: {}
`, `,

View File

@@ -7,14 +7,14 @@ var elementTestCases = []testCase{
// //
// Test Case // Test Case
// //
{`Add an element to an existing list`, {description: `Add an element to an existing list`,
` origin: `
kind: Deployment kind: Deployment
containers: containers:
- name: foo - name: foo
image: foo:1 image: foo:1
`, `,
` update: `
kind: Deployment kind: Deployment
containers: containers:
- name: foo - name: foo
@@ -22,13 +22,13 @@ containers:
- name: baz - name: baz
image: baz:2 image: baz:2
`, `,
` local: `
kind: Deployment kind: Deployment
containers: containers:
- name: foo - name: foo
image: foo:1 image: foo:1
`, `,
` expected: `
kind: Deployment kind: Deployment
containers: containers:
- name: foo - name: foo
@@ -36,65 +36,65 @@ containers:
- image: baz:2 - image: baz:2
name: baz name: baz
`, nil}, `},
// //
// Test Case // Test Case
// //
{`Add an element to a non-existing list`, {description: `Add an element to a non-existing list`,
` origin: `
kind: Deployment`, kind: Deployment`,
` update: `
kind: Deployment kind: Deployment
containers: containers:
- name: foo - name: foo
image: foo:bar image: foo:bar
`, `,
` local: `
kind: Deployment kind: Deployment
`, `,
` expected: `
kind: Deployment kind: Deployment
containers: containers:
- image: foo:bar - image: foo:bar
name: foo name: foo
`, nil}, `},
{`Add an element to a non-existing list, existing in dest`, {description: `Add an element to a non-existing list, existing in dest`,
` origin: `
kind: Deployment`, kind: Deployment`,
` update: `
kind: Deployment kind: Deployment
containers: containers:
- name: foo - name: foo
image: foo:bar image: foo:bar
`, `,
` local: `
kind: Deployment kind: Deployment
containers: containers:
- name: baz - name: baz
image: baz:bar image: baz:bar
`, `,
` expected: `
kind: Deployment kind: Deployment
containers: containers:
- name: baz - name: baz
image: baz:bar image: baz:bar
- image: foo:bar - image: foo:bar
name: foo name: foo
`, nil}, `},
// //
// Test Case // Test Case
// TODO(pwittrock): Figure out if there is something better we can do here // TODO(pwittrock): Figure out if there is something better we can do here
// This element is missing from the destination -- only the new fields are added // This element is missing from the destination -- only the new fields are added
{`Add a field to the element, element missing from dest`, {description: `Add a field to the element, element missing from dest`,
` origin: `
kind: Deployment kind: Deployment
containers: containers:
- name: foo - name: foo
image: foo:bar`, image: foo:bar`,
` update: `
kind: Deployment kind: Deployment
containers: containers:
- name: foo - name: foo
@@ -102,22 +102,22 @@ containers:
command: command:
- run.sh - run.sh
`, `,
` local: `
kind: Deployment kind: Deployment
`, `,
` expected: `
kind: Deployment kind: Deployment
containers: containers:
- command: - command:
- run.sh - run.sh
name: foo name: foo
`, nil}, `},
// //
// Test Case // Test Case
// //
{`Update a field on the elem, element missing from the dest`, {description: `Update a field on the elem, element missing from the dest`,
` origin: `
kind: Deployment kind: Deployment
containers: containers:
- name: foo - name: foo
@@ -125,7 +125,7 @@ containers:
command: command:
- run.sh - run.sh
`, `,
` update: `
kind: Deployment kind: Deployment
containers: containers:
- name: foo - name: foo
@@ -133,210 +133,210 @@ containers:
command: command:
- run2.sh - run2.sh
`, `,
` local: `
kind: Deployment kind: Deployment
`, `,
` expected: `
kind: Deployment kind: Deployment
containers: containers:
- command: - command:
- run2.sh - run2.sh
name: foo name: foo
`, nil}, `},
// //
// Test Case // Test Case
// //
{`Update a field on the elem, element present in the dest`, {description: `Update a field on the elem, element present in the dest`,
` origin: `
kind: Deployment kind: Deployment
containers: containers:
- name: foo - name: foo
image: foo:bar image: foo:bar
command: ['run.sh'] command: ['run.sh']
`, `,
` update: `
kind: Deployment kind: Deployment
containers: containers:
- name: foo - name: foo
image: foo:bar image: foo:bar
command: ['run2.sh'] command: ['run2.sh']
`, `,
` local: `
kind: Deployment kind: Deployment
containers: containers:
- name: foo - name: foo
image: foo:bar image: foo:bar
command: ['run.sh'] command: ['run.sh']
`, `,
` expected: `
kind: Deployment kind: Deployment
containers: containers:
- name: foo - name: foo
image: foo:bar image: foo:bar
command: ['run2.sh'] command: ['run2.sh']
`, nil}, `},
// //
// Test Case // Test Case
// //
{`Add a field on the elem, element present in the dest`, {description: `Add a field on the elem, element present in the dest`,
` origin: `
kind: Deployment kind: Deployment
containers: containers:
- name: foo - name: foo
image: foo:bar image: foo:bar
`, `,
` update: `
kind: Deployment kind: Deployment
containers: containers:
- name: foo - name: foo
image: foo:bar image: foo:bar
command: ['run2.sh'] command: ['run2.sh']
`, `,
` local: `
kind: Deployment kind: Deployment
containers: containers:
- name: foo - name: foo
image: foo:bar image: foo:bar
`, `,
` expected: `
kind: Deployment kind: Deployment
containers: containers:
- name: foo - name: foo
image: foo:bar image: foo:bar
command: ['run2.sh'] command: ['run2.sh']
`, nil}, `},
// //
// Test Case // Test Case
// //
{`Add a field on the elem, element and field present in the dest`, {description: `Add a field on the elem, element and field present in the dest`,
` origin: `
kind: Deployment kind: Deployment
containers: containers:
- name: foo - name: foo
image: foo:bar image: foo:bar
`, `,
` update: `
kind: Deployment kind: Deployment
containers: containers:
- name: foo - name: foo
image: foo:bar image: foo:bar
command: ['run2.sh'] command: ['run2.sh']
`, `,
` local: `
kind: Deployment kind: Deployment
containers: containers:
- name: foo - name: foo
image: foo:bar image: foo:bar
command: ['run.sh'] command: ['run.sh']
`, `,
` expected: `
kind: Deployment kind: Deployment
containers: containers:
- name: foo - name: foo
image: foo:bar image: foo:bar
command: ['run2.sh'] command: ['run2.sh']
`, nil}, `},
// //
// Test Case // Test Case
// //
{`Ignore an element`, {description: `Ignore an element`,
` origin: `
kind: Deployment kind: Deployment
containers: {} containers: {}
`, `,
` update: `
kind: Deployment kind: Deployment
containers: {} containers: {}
`, `,
` local: `
kind: Deployment kind: Deployment
containers: containers:
- name: foo - name: foo
image: foo:bar image: foo:bar
`, `,
` expected: `
kind: Deployment kind: Deployment
containers: containers:
- name: foo - name: foo
image: foo:bar image: foo:bar
`, nil}, `},
// //
// Test Case // Test Case
// //
{`Leave deleted`, {description: `Leave deleted`,
` origin: `
kind: Deployment kind: Deployment
containers: containers:
- name: foo - name: foo
image: foo:bar image: foo:bar
`, `,
` update: `
kind: Deployment kind: Deployment
`, `,
` local: `
kind: Deployment kind: Deployment
`, `,
` expected: `
kind: Deployment kind: Deployment
`, nil}, `},
// //
// Test Case // Test Case
// //
{`Remove an element -- matching`, {description: `Remove an element -- matching`,
` origin: `
kind: Deployment kind: Deployment
containers: containers:
- name: foo - name: foo
image: foo:bar image: foo:bar
`, `,
` update: `
kind: Deployment kind: Deployment
`, `,
` local: `
kind: Deployment kind: Deployment
containers: containers:
- name: foo - name: foo
image: foo:bar image: foo:bar
`, `,
` expected: `
kind: Deployment kind: Deployment
`, nil}, `},
// //
// Test Case // Test Case
// //
{`Remove an element -- field missing from update`, {description: `Remove an element -- field missing from update`,
` origin: `
kind: Deployment kind: Deployment
containers: containers:
- name: foo - name: foo
image: foo:bar image: foo:bar
`, `,
` update: `
kind: Deployment kind: Deployment
`, `,
` local: `
kind: Deployment kind: Deployment
containers: containers:
- name: foo - name: foo
image: foo:bar image: foo:bar
command: ['run.sh'] command: ['run.sh']
`, `,
` expected: `
kind: Deployment kind: Deployment
`, nil}, `},
// //
// Test Case // Test Case
// //
{`Remove an element -- element missing`, {description: `Remove an element -- element missing`,
` origin: `
kind: Deployment kind: Deployment
containers: containers:
- name: foo - name: foo
@@ -344,13 +344,13 @@ containers:
- name: baz - name: baz
image: baz:bar image: baz:bar
`, `,
` update: `
kind: Deployment kind: Deployment
containers: containers:
- name: foo - name: foo
image: foo:bar image: foo:bar
`, `,
` local: `
kind: Deployment kind: Deployment
containers: containers:
- name: foo - name: foo
@@ -359,60 +359,273 @@ containers:
- name: baz - name: baz
image: baz:bar image: baz:bar
`, `,
` expected: `
kind: Deployment kind: Deployment
containers: containers:
- name: foo - name: foo
image: foo:bar image: foo:bar
command: ['run.sh'] command: ['run.sh']
`, nil}, `},
// //
// Test Case // Test Case
// //
{`Remove an element -- empty containers`, {description: `Remove an element -- empty containers`,
` origin: `
kind: Deployment kind: Deployment
containers: containers:
- name: foo - name: foo
image: foo:bar image: foo:bar
`, `,
` update: `
kind: Deployment kind: Deployment
containers: {} containers: {}
`, `,
` local: `
kind: Deployment kind: Deployment
containers: containers:
- name: foo - name: foo
image: foo:bar image: foo:bar
command: ['run.sh'] command: ['run.sh']
`, `,
` expected: `
kind: Deployment kind: Deployment
`, nil}, `},
// //
// Test Case // Test Case
// //
{`Remove an element -- missing list field`, {description: `Remove an element -- missing list field`,
` origin: `
kind: Deployment kind: Deployment
containers: containers:
- name: foo - name: foo
image: foo:bar image: foo:bar
`, `,
` update: `
kind: Deployment kind: Deployment
`, `,
` local: `
kind: Deployment kind: Deployment
containers: containers:
- name: foo - name: foo
image: foo:bar image: foo:bar
command: ['run.sh'] command: ['run.sh']
`, `,
` expected: `
kind: Deployment kind: Deployment
`, nil}, `},
//
// Test Case
//
{description: `no infer merge keys no merge'`,
origin: `
kind: Deployment
containers:
- name: foo
`,
update: `
kind: Deployment
containers:
- name: foo
command: ['run2.sh']
`,
local: `
kind: Deployment
containers:
- name: foo
image: foo:bar
`,
expected: `
kind: Deployment
containers:
- name: foo
command: ['run2.sh']
`,
noInfer: true,
},
//
// Test Case
//
{description: `no infer merge keys merge using schema`,
origin: `
kind: Deployment
apiVersion: apps/v1
spec:
template:
spec:
containers:
- name: foo
`,
update: `
kind: Deployment
apiVersion: apps/v1
spec:
template:
spec:
containers:
- name: foo
command: ['run2.sh']
`,
local: `
kind: Deployment
apiVersion: apps/v1
spec:
template:
spec:
containers:
- name: foo
image: foo:bar
`,
expected: `
kind: Deployment
apiVersion: apps/v1
spec:
template:
spec:
containers:
- name: foo
image: foo:bar
command: ['run2.sh']
`,
noInfer: true,
},
//
// Test Case
//
{description: `no infer merge keys merge using explicit schema as line comment'`,
origin: `
kind: Deployment
containers:
- name: foo
`,
update: `
kind: Deployment
containers:
- name: foo
command: ['run2.sh']
`,
local: `
kind: Deployment
containers: # {"items":{"$ref": "#/definitions/io.k8s.api.core.v1.Container"},"type":"array","x-kubernetes-patch-merge-key":"name","x-kubernetes-patch-strategy": "merge"}
- name: foo # hell ow
image: foo:bar
`,
expected: `
kind: Deployment
containers: # {"items":{"$ref": "#/definitions/io.k8s.api.core.v1.Container"},"type":"array","x-kubernetes-patch-merge-key":"name","x-kubernetes-patch-strategy": "merge"}
- name: foo # hell ow
image: foo:bar
command: ['run2.sh']
`,
noInfer: true,
},
//
// Test Case
//
{description: `no infer merge keys merge using explicit schema as head comment'`,
origin: `
kind: Deployment
containers:
- name: foo
`,
update: `
kind: Deployment
containers:
- name: foo
command: ['run2.sh']
`,
local: `
kind: Deployment
# {"items":{"$ref": "#/definitions/io.k8s.api.core.v1.Container"},"type":"array","x-kubernetes-patch-merge-key":"name","x-kubernetes-patch-strategy": "merge"}
containers:
- name: foo # hell ow
image: foo:bar
`,
expected: `
kind: Deployment
# {"items":{"$ref": "#/definitions/io.k8s.api.core.v1.Container"},"type":"array","x-kubernetes-patch-merge-key":"name","x-kubernetes-patch-strategy": "merge"}
containers:
- name: foo # hell ow
image: foo:bar
command: ['run2.sh']
`,
noInfer: true,
},
//
// Test Case
//
{description: `no infer merge keys merge using explicit schema to parent field'`,
origin: `
kind: Deployment
spec:
containers:
- name: foo
`,
update: `
kind: Deployment
spec:
containers:
- name: foo
command: ['run2.sh']
`,
local: `
kind: Deployment
spec: # {"$ref":"#/definitions/io.k8s.api.core.v1.PodSpec"}
containers:
- name: foo # hell ow
image: foo:bar
`,
expected: `
kind: Deployment
spec: # {"$ref":"#/definitions/io.k8s.api.core.v1.PodSpec"}
containers:
- name: foo # hell ow
image: foo:bar
command: ['run2.sh']
`,
noInfer: true,
},
//
// Test Case
//
{description: `no infer merge keys merge using explicit schema to parent field header'`,
origin: `
kind: Deployment
spec:
containers:
- name: foo
`,
update: `
kind: Deployment
spec:
containers:
- name: foo
command: ['run2.sh']
`,
local: `
kind: Deployment
# {"$ref":"#/definitions/io.k8s.api.core.v1.PodSpec"}
spec:
containers:
- name: foo # hell ow
image: foo:bar
`,
expected: `
kind: Deployment
# {"$ref":"#/definitions/io.k8s.api.core.v1.PodSpec"}
spec:
containers:
- name: foo # hell ow
image: foo:bar
command: ['run2.sh']
`,
noInfer: true,
},
} }

View File

@@ -9,224 +9,226 @@ var listTestCases = []testCase{
// //
// Test Case // Test Case
// //
{`Replace list`, {description: `Replace list`,
` origin: `
list: list:
- 1 - 1
- 2 - 2
- 3`, - 3`,
` update: `
list: list:
- 2 - 2
- 3 - 3
- 4`, - 4`,
` local: `
list: list:
- 1 - 1
- 2 - 2
- 3`, - 3`,
` expected: `
list: list:
- 2 - 2
- 3 - 3
- 4`, nil}, - 4`},
// //
// Test Case // Test Case
// //
{`Add an updated list`, {description: `Add an updated list`,
` origin: `
apiVersion: apps/v1 apiVersion: apps/v1
list: # old value list: # old value
- 1 - 1
- 2 - 2
- 3 - 3
`, `,
` update: `
apiVersion: apps/v1 apiVersion: apps/v1
list: # new value list: # new value
- 2 - 2
- 3 - 3
- 4 - 4
`, `,
` local: `
apiVersion: apps/v1`, apiVersion: apps/v1`,
` expected: `
apiVersion: apps/v1 apiVersion: apps/v1
list: list:
- 2 - 2
- 3 - 3
- 4 - 4
`, nil}, `},
// //
// Test Case // Test Case
// //
{`Add keep an omitted field`, {description: `Add keep an omitted field`,
` origin: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment`, kind: Deployment`,
` update: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: StatefulSet`, kind: StatefulSet`,
` local: `
apiVersion: apps/v1 apiVersion: apps/v1
list: # not present in sources list: # not present in sources
- 2 - 2
- 3 - 3
- 4 - 4
`, `,
` expected: `
apiVersion: apps/v1 apiVersion: apps/v1
list: # not present in sources list: # not present in sources
- 2 - 2
- 3 - 3
- 4 - 4
kind: StatefulSet kind: StatefulSet
`, nil}, `},
// //
// Test Case // Test Case
// //
// TODO(#36): consider making this an error // TODO(#36): consider making this an error
{`Change an updated field`, {description: `Change an updated field`,
` origin: `
apiVersion: apps/v1 apiVersion: apps/v1
list: # old value list: # old value
- 1 - 1
- 2 - 2
- 3`, - 3`,
` update: `
apiVersion: apps/v1 apiVersion: apps/v1
list: # new value list: # new value
- 2 - 2
- 3 - 3
- 4`, - 4`,
` local: `
apiVersion: apps/v1 apiVersion: apps/v1
list: # conflicting value list: # conflicting value
- a - a
- b - b
- c`, - c`,
` expected: `
apiVersion: apps/v1 apiVersion: apps/v1
list: # conflicting value list: # conflicting value
- 2 - 2
- 3 - 3
- 4 - 4
`, nil}, `},
// //
// Test Case // Test Case
// //
{`Ignore a field -- set`, {description: `Ignore a field -- set`,
` origin: `
apiVersion: apps/v1 apiVersion: apps/v1
list: # ignore value list: # ignore value
- 1 - 1
- 2 - 2
- 3 - 3
`, `,
` update: `
apiVersion: apps/v1 apiVersion: apps/v1
list: # ignore value list: # ignore value
- 1 - 1
- 2 - 2
- 3`, ` - 3`,
local: `
apiVersion: apps/v1 apiVersion: apps/v1
list: list:
- 2 - 2
- 3 - 3
- 4 - 4
`, ` `,
expected: `
apiVersion: apps/v1 apiVersion: apps/v1
list: list:
- 2 - 2
- 3 - 3
- 4 - 4
`, nil}, `},
// //
// Test Case // Test Case
// //
{`Ignore a field -- empty`, {description: `Ignore a field -- empty`,
` origin: `
apiVersion: apps/v1 apiVersion: apps/v1
list: # ignore value list: # ignore value
- 1 - 1
- 2 - 2
- 3`, - 3`,
` update: `
apiVersion: apps/v1 apiVersion: apps/v1
list: # ignore value list: # ignore value
- 1 - 1
- 2 - 2
- 3`, - 3`,
` local: `
apiVersion: apps/v1 apiVersion: apps/v1
`, `,
` expected: `
apiVersion: apps/v1 apiVersion: apps/v1
`, nil}, `},
// //
// Test Case // Test Case
// //
{`Explicitly clear a field`, {description: `Explicitly clear a field`,
` origin: `
apiVersion: apps/v1`, apiVersion: apps/v1`,
` update: `
apiVersion: apps/v1 apiVersion: apps/v1
list: null # clear`, list: null # clear`,
` local: `
apiVersion: apps/v1 apiVersion: apps/v1
list: # value to clear list: # value to clear
- 1 - 1
- 2 - 2
- 3`, - 3`,
` expected: `
apiVersion: apps/v1`, nil}, apiVersion: apps/v1`},
// //
// Test Case // Test Case
// //
{`Implicitly clear a field`, {description: `Implicitly clear a field`,
` origin: `
apiVersion: apps/v1 apiVersion: apps/v1
list: # clear value list: # clear value
- 1 - 1
- 2 - 2
- 3`, - 3`,
` update: `
apiVersion: apps/v1`, apiVersion: apps/v1`,
` local: `
apiVersion: apps/v1 apiVersion: apps/v1
list: # old value list: # old value
- 1 - 1
- 2 - 2
- 3`, - 3`,
` expected: `
apiVersion: apps/v1`, nil}, apiVersion: apps/v1`},
// //
// Test Case // Test Case
// //
// TODO(#36): consider making this an error // TODO(#36): consider making this an error
{`Implicitly clear a changed field`, {description: `Implicitly clear a changed field`,
` origin: `
apiVersion: apps/v1 apiVersion: apps/v1
list: # old value list: # old value
- 1 - 1
- 2 - 2
- 3`, - 3`,
` update: `
apiVersion: apps/v1`, apiVersion: apps/v1`,
` local: `
apiVersion: apps/v1 apiVersion: apps/v1
list: # old value list: # old value
- a - a
- b - b
- c`, - c`,
` expected: `
apiVersion: apps/v1`, nil}, apiVersion: apps/v1`},
} }

View File

@@ -7,267 +7,267 @@ var mapTestCases = []testCase{
// //
// Test Case // Test Case
// //
{`Add the annotations map field`, {description: `Add the annotations map field`,
` origin: `
kind: Deployment`, kind: Deployment`,
` update: `
kind: Deployment kind: Deployment
metadata: metadata:
annotations: annotations:
d: e # add these annotations d: e # add these annotations
`, `,
` local: `
kind: Deployment`, kind: Deployment`,
` expected: `
kind: Deployment kind: Deployment
metadata: metadata:
annotations: annotations:
d: e # add these annotations`, nil}, d: e # add these annotations`},
// //
// Test Case // Test Case
// //
{`Add an annotation to the field`, {description: `Add an annotation to the field`,
` origin: `
kind: Deployment kind: Deployment
metadata: metadata:
annotations: annotations:
a: b`, a: b`,
` update: `
kind: Deployment kind: Deployment
metadata: metadata:
annotations: annotations:
a: b a: b
d: e # add these annotations`, d: e # add these annotations`,
` local: `
kind: Deployment kind: Deployment
metadata: metadata:
annotations: annotations:
g: h # keep these annotations`, g: h # keep these annotations`,
` expected: `
kind: Deployment kind: Deployment
metadata: metadata:
annotations: annotations:
g: h # keep these annotations g: h # keep these annotations
d: e # add these annotations`, nil}, d: e # add these annotations`},
// //
// Test Case // Test Case
// //
{`Add an annotation to the field, field missing from dest`, {description: `Add an annotation to the field, field missing from dest`,
` origin: `
kind: Deployment kind: Deployment
metadata: metadata:
annotations: annotations:
a: b # ignored because unchanged`, a: b # ignored because unchanged`,
` update: `
kind: Deployment kind: Deployment
metadata: metadata:
annotations: annotations:
a: b # ignore because unchanged a: b # ignore because unchanged
d: e`, d: e`,
` local: `
kind: Deployment`, kind: Deployment`,
` expected: `
kind: Deployment kind: Deployment
metadata: metadata:
annotations: annotations:
d: e`, nil}, d: e`},
// //
// Test Case // Test Case
// //
{`Update an annotation on the field, field messing rom the dest`, {description: `Update an annotation on the field, field messing rom the dest`,
` origin: `
kind: Deployment kind: Deployment
metadata: metadata:
annotations: annotations:
a: b a: b
d: c`, d: c`,
` update: `
kind: Deployment kind: Deployment
metadata: metadata:
annotations: annotations:
a: b a: b
d: e # set these annotations`, d: e # set these annotations`,
` local: `
kind: Deployment kind: Deployment
metadata: metadata:
annotations: annotations:
g: h # keep these annotations`, g: h # keep these annotations`,
` expected: `
kind: Deployment kind: Deployment
metadata: metadata:
annotations: annotations:
g: h # keep these annotations g: h # keep these annotations
d: e # set these annotations`, nil}, d: e # set these annotations`},
// //
// Test Case // Test Case
// //
{`Add an annotation to the field, field missing from dest`, {description: `Add an annotation to the field, field missing from dest`,
` origin: `
kind: Deployment kind: Deployment
metadata: metadata:
annotations: annotations:
a: b # ignored because unchanged`, a: b # ignored because unchanged`,
` update: `
kind: Deployment kind: Deployment
metadata: metadata:
annotations: annotations:
a: b # ignore because unchanged a: b # ignore because unchanged
d: e`, d: e`,
` local: `
kind: Deployment`, kind: Deployment`,
` expected: `
kind: Deployment kind: Deployment
metadata: metadata:
annotations: annotations:
d: e`, nil}, d: e`},
// //
// Test Case // Test Case
// //
{`Remove an annotation`, {description: `Remove an annotation`,
` origin: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
annotations: annotations:
a: b`, a: b`,
` update: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
annotations: {}`, annotations: {}`,
` local: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
annotations: annotations:
c: d c: d
a: b`, a: b`,
` expected: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
annotations: annotations:
c: d`, nil}, c: d`},
// //
// Test Case // Test Case
// //
// TODO(#36) support ~annotations~: {} deletion // TODO(#36) support ~annotations~: {} deletion
{`Specify a field as empty that isn't present in the source`, {description: `Specify a field as empty that isn't present in the source`,
` origin: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
name: foo`, name: foo`,
` update: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
name: foo name: foo
annotations: null`, annotations: null`,
` local: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
name: foo name: foo
annotations: annotations:
a: b`, a: b`,
` expected: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
name: foo`, nil}, name: foo`},
// //
// Test Case // Test Case
// //
{`Remove an annotation`, {description: `Remove an annotation`,
` origin: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
annotations: annotations:
a: b`, a: b`,
` update: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment`, kind: Deployment`,
` local: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
annotations: annotations:
c: d c: d
a: b`, a: b`,
` expected: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
annotations: annotations:
c: d`, nil}, c: d`},
// //
// Test Case // Test Case
// //
{`Remove annotations field`, {description: `Remove annotations field`,
` origin: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
annotations: annotations:
a: b`, a: b`,
` update: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment`, kind: Deployment`,
` local: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
name: foo`, name: foo`,
` expected: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
name: foo name: foo
`, nil}, `},
// //
// Test Case // Test Case
// //
{`Remove annotations field, but keep in dest`, {description: `Remove annotations field, but keep in dest`,
` origin: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
annotations: annotations:
a: b`, a: b`,
` update: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment`, kind: Deployment`,
` local: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
name: foo name: foo
annotations: annotations:
foo: bar # keep this annotation even though the parent field was removed`, foo: bar # keep this annotation even though the parent field was removed`,
` expected: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
name: foo name: foo
annotations: annotations:
foo: bar # keep this annotation even though the parent field was removed`, nil}, foo: bar # keep this annotation even though the parent field was removed`},
// //
// Test Case // Test Case
// //
{`Remove annotations, but they are already empty`, {description: `Remove annotations, but they are already empty`,
` origin: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
@@ -275,24 +275,24 @@ metadata:
annotations: annotations:
a: b a: b
`, `,
` update: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
name: foo name: foo
`, `,
` local: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
name: foo name: foo
annotations: {} annotations: {}
`, `,
` expected: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
name: foo name: foo
annotations: {} annotations: {}
`, nil}, `},
} }

View File

@@ -13,11 +13,12 @@ import (
func Merge(dest, original, update *yaml.RNode) (*yaml.RNode, error) { func Merge(dest, original, update *yaml.RNode) (*yaml.RNode, error) {
// if update == nil && original != nil => declarative deletion // if update == nil && original != nil => declarative deletion
return walk.Walker{Visitor: Visitor{}, return walk.Walker{
Visitor: Visitor{},
Sources: []*yaml.RNode{dest, original, update}}.Walk() Sources: []*yaml.RNode{dest, original, update}}.Walk()
} }
func MergeStrings(dest, original, update string) (string, error) { func MergeStrings(dest, original, update string, infer bool) (string, error) {
srcOriginal, err := yaml.Parse(original) srcOriginal, err := yaml.Parse(original)
if err != nil { if err != nil {
return "", err return "", err
@@ -31,7 +32,10 @@ func MergeStrings(dest, original, update string) (string, error) {
return "", err return "", err
} }
result, err := Merge(d, srcOriginal, srcUpdated) result, err := walk.Walker{
InferAssociativeLists: infer,
Visitor: Visitor{},
Sources: []*yaml.RNode{d, srcOriginal, srcUpdated}}.Walk()
if err != nil { if err != nil {
return "", err return "", err
} }

View File

@@ -15,24 +15,27 @@ var testCases = [][]testCase{scalarTestCases, listTestCases, mapTestCases, eleme
func TestMerge(t *testing.T) { func TestMerge(t *testing.T) {
for i := range testCases { for i := range testCases {
for _, tc := range testCases[i] { for j := range testCases[i] {
actual, err := MergeStrings(tc.local, tc.origin, tc.update) tc := testCases[i][j]
if tc.err == nil { t.Run(tc.description, func(t *testing.T) {
if !assert.NoError(t, err, tc.description) { actual, err := MergeStrings(tc.local, tc.origin, tc.update, !tc.noInfer)
t.FailNow() if tc.err == nil {
if !assert.NoError(t, err, tc.description) {
t.FailNow()
}
if !assert.Equal(t,
strings.TrimSpace(tc.expected), strings.TrimSpace(actual), tc.description) {
t.FailNow()
}
} else {
if !assert.Errorf(t, err, tc.description) {
t.FailNow()
}
if !assert.Contains(t, tc.err.Error(), err.Error()) {
t.FailNow()
}
} }
if !assert.Equal(t, })
strings.TrimSpace(tc.expected), strings.TrimSpace(actual), tc.description) {
t.FailNow()
}
} else {
if !assert.Errorf(t, err, tc.description) {
t.FailNow()
}
if !assert.Contains(t, tc.err.Error(), err.Error()) {
t.FailNow()
}
}
} }
} }
} }
@@ -44,4 +47,5 @@ type testCase struct {
local string local string
expected string expected string
err error err error
noInfer bool
} }

View File

@@ -8,128 +8,128 @@ var scalarTestCases = []testCase{
// //
// Test Case // Test Case
// //
{`Set and updated a field`, {description: `Set and updated a field`,
`kind: Deployment`, origin: `kind: Deployment`,
`kind: StatefulSet`, update: `kind: StatefulSet`,
`kind: Deployment`, local: `kind: Deployment`,
`kind: StatefulSet`, nil}, expected: `kind: StatefulSet`},
{`Add an updated field`, {description: `Add an updated field`,
` origin: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment # old value`, kind: Deployment # old value`,
` update: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: StatefulSet # new value`, kind: StatefulSet # new value`,
` local: `
apiVersion: apps/v1`, apiVersion: apps/v1`,
` expected: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: StatefulSet # new value`, nil}, kind: StatefulSet # new value`},
{`Add keep an omitted field`, {description: `Add keep an omitted field`,
` origin: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment`, kind: Deployment`,
` update: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: StatefulSet`, kind: StatefulSet`,
` local: `
apiVersion: apps/v1 apiVersion: apps/v1
spec: foo # field not present in source spec: foo # field not present in source
`, `,
` expected: `
apiVersion: apps/v1 apiVersion: apps/v1
spec: foo # field not present in source spec: foo # field not present in source
kind: StatefulSet kind: StatefulSet
`, nil}, `},
// //
// Test Case // Test Case
// //
// TODO(#36): consider making this an error // TODO(#36): consider making this an error
{`Change an updated field`, {description: `Change an updated field`,
` origin: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment # old value`, kind: Deployment # old value`,
` update: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: StatefulSet # new value`, kind: StatefulSet # new value`,
` local: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Service # conflicting value`, kind: Service # conflicting value`,
` expected: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: StatefulSet # new value`, nil}, kind: StatefulSet # new value`},
{`Ignore a field`, {description: `Ignore a field`,
` origin: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment # ignore this field`, kind: Deployment # ignore this field`,
` update: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment # ignore this field`, kind: Deployment # ignore this field`,
` local: `
apiVersion: apps/v1`, apiVersion: apps/v1`,
` expected: `
apiVersion: apps/v1`, nil}, apiVersion: apps/v1`},
{`Explicitly clear a field`, {description: `Explicitly clear a field`,
` origin: `
apiVersion: apps/v1`, apiVersion: apps/v1`,
` update: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: null # clear this value`, kind: null # clear this value`,
` local: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment # value to be cleared`, kind: Deployment # value to be cleared`,
` expected: `
apiVersion: apps/v1`, nil}, apiVersion: apps/v1`},
{`Implicitly clear a field`, {description: `Implicitly clear a field`,
` origin: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment # clear this field`, kind: Deployment # clear this field`,
` update: `
apiVersion: apps/v1`, apiVersion: apps/v1`,
` local: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment # clear this field`, kind: Deployment # clear this field`,
` expected: `
apiVersion: apps/v1`, nil}, apiVersion: apps/v1`},
// //
// Test Case // Test Case
// //
// TODO(#36): consider making this an error // TODO(#36): consider making this an error
{`Implicitly clear a changed field`, {description: `Implicitly clear a changed field`,
` origin: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment`, kind: Deployment`,
` update: `
apiVersion: apps/v1`, apiVersion: apps/v1`,
` local: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: StatefulSet`, kind: StatefulSet`,
` expected: `
apiVersion: apps/v1`, nil}, apiVersion: apps/v1`},
// //
// Test Case // Test Case
// //
{`Merge an empty scalar value`, {description: `Merge an empty scalar value`,
` origin: `
apiVersion: apps/v1 apiVersion: apps/v1
`, `,
` update: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: {} kind: {}
`, `,
` local: `
apiVersion: apps/v1 apiVersion: apps/v1
`, `,
` expected: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: {} kind: {}
`, nil}, `},
} }

View File

@@ -4,6 +4,7 @@
package merge3 package merge3
import ( import (
"sigs.k8s.io/kustomize/kyaml/openapi"
"sigs.k8s.io/kustomize/kyaml/yaml" "sigs.k8s.io/kustomize/kyaml/yaml"
"sigs.k8s.io/kustomize/kyaml/yaml/walk" "sigs.k8s.io/kustomize/kyaml/yaml/walk"
) )
@@ -17,7 +18,7 @@ const (
type Visitor struct{} type Visitor struct{}
func (m Visitor) VisitMap(nodes walk.Sources) (*yaml.RNode, error) { func (m Visitor) VisitMap(nodes walk.Sources, s *openapi.ResourceSchema) (*yaml.RNode, error) {
if yaml.IsNull(nodes.Updated()) || yaml.IsNull(nodes.Dest()) { if yaml.IsNull(nodes.Updated()) || yaml.IsNull(nodes.Dest()) {
// explicitly cleared from either dest or update // explicitly cleared from either dest or update
return walk.ClearNode, nil return walk.ClearNode, nil
@@ -36,7 +37,7 @@ func (m Visitor) VisitMap(nodes walk.Sources) (*yaml.RNode, error) {
return nodes.Dest(), nil return nodes.Dest(), nil
} }
func (m Visitor) visitAList(nodes walk.Sources) (*yaml.RNode, error) { func (m Visitor) visitAList(nodes walk.Sources, _ *openapi.ResourceSchema) (*yaml.RNode, error) {
if yaml.IsEmpty(nodes.Updated()) && !yaml.IsEmpty(nodes.Origin()) { if yaml.IsEmpty(nodes.Updated()) && !yaml.IsEmpty(nodes.Origin()) {
// implicitly cleared from update -- element was deleted // implicitly cleared from update -- element was deleted
return walk.ClearNode, nil return walk.ClearNode, nil
@@ -51,7 +52,7 @@ func (m Visitor) visitAList(nodes walk.Sources) (*yaml.RNode, error) {
return nodes.Dest(), nil return nodes.Dest(), nil
} }
func (m Visitor) VisitScalar(nodes walk.Sources) (*yaml.RNode, error) { func (m Visitor) VisitScalar(nodes walk.Sources, s *openapi.ResourceSchema) (*yaml.RNode, error) {
if yaml.IsNull(nodes.Updated()) || yaml.IsNull(nodes.Dest()) { if yaml.IsNull(nodes.Updated()) || yaml.IsNull(nodes.Dest()) {
// explicitly cleared from either dest or update // explicitly cleared from either dest or update
return nil, nil return nil, nil
@@ -103,9 +104,9 @@ func (m Visitor) visitNAList(nodes walk.Sources) (*yaml.RNode, error) {
return nodes.Dest(), nil return nodes.Dest(), nil
} }
func (m Visitor) VisitList(nodes walk.Sources, kind walk.ListKind) (*yaml.RNode, error) { func (m Visitor) VisitList(nodes walk.Sources, s *openapi.ResourceSchema, kind walk.ListKind) (*yaml.RNode, error) {
if kind == walk.AssociativeList { if kind == walk.AssociativeList {
return m.visitAList(nodes) return m.visitAList(nodes, s)
} }
// non-associative list // non-associative list
return m.visitNAList(nodes) return m.visitNAList(nodes)

View File

@@ -0,0 +1,34 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
// Package schema contains libraries for working with the yaml and openapi packages.
package schema
import (
"sigs.k8s.io/kustomize/kyaml/openapi"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
// IsAssociative returns true if all elements in the list contain an AssociativeSequenceKey
// as a field.
func IsAssociative(schema *openapi.ResourceSchema, nodes []*yaml.RNode, infer bool) bool {
if schema != nil {
// use the schema to identify if the list is associative
s, _ := schema.PatchStrategyAndKey()
return s == "merge"
}
if !infer {
return false
}
for i := range nodes {
node := nodes[i]
if yaml.IsEmpty(node) {
continue
}
if node.IsAssociative() {
return true
}
}
return false
}

View File

@@ -651,24 +651,8 @@ func (rn *RNode) VisitElements(fn func(node *RNode) error) error {
// The order sets the precedence of the merge keys -- if multiple keys are present // The order sets the precedence of the merge keys -- if multiple keys are present
// in Resources in a list, then the FIRST key which ALL elements in the list have is used as the // in Resources in a list, then the FIRST key which ALL elements in the list have is used as the
// associative key for merging that list. // associative key for merging that list.
var AssociativeSequenceKeys = []string{ // Only infer name as a merge key.
"mountPath", "devicePath", "ip", "type", "topologyKey", "name", "containerPort", var AssociativeSequenceKeys = []string{"name"}
}
// IsAssociative returns true if all elements in the list contain an AssociativeSequenceKey
// as a field.
func IsAssociative(nodes []*RNode) bool {
for i := range nodes {
node := nodes[i]
if IsEmpty(node) {
continue
}
if node.IsAssociative() {
return true
}
}
return false
}
// IsAssociative returns true if the RNode contains an AssociativeSequenceKey as a field. // IsAssociative returns true if the RNode contains an AssociativeSequenceKey as a field.
func (rn *RNode) IsAssociative() bool { func (rn *RNode) IsAssociative() bool {

View File

@@ -7,28 +7,44 @@ import (
"strings" "strings"
"github.com/go-errors/errors" "github.com/go-errors/errors"
"sigs.k8s.io/kustomize/kyaml/openapi"
"sigs.k8s.io/kustomize/kyaml/sets" "sigs.k8s.io/kustomize/kyaml/sets"
"sigs.k8s.io/kustomize/kyaml/yaml" "sigs.k8s.io/kustomize/kyaml/yaml"
) )
func (l *Walker) walkAssociativeSequence() (*yaml.RNode, error) { func (l *Walker) walkAssociativeSequence() (*yaml.RNode, error) {
// may require initializing the dest node // may require initializing the dest node
dest, err := l.Sources.setDestNode(l.VisitList(l.Sources, AssociativeList)) dest, err := l.Sources.setDestNode(l.VisitList(l.Sources, l.Schema, AssociativeList))
if dest == nil || err != nil { if dest == nil || err != nil {
return nil, err return nil, err
} }
// find the list of elements we need to recursively walk var key string
key, err := l.elementKey() if l.Schema != nil {
if err != nil { _, key = l.Schema.PatchStrategyAndKey()
return nil, err
} }
if key == "" { // no key from the schema, try to infer one
// find the list of elements we need to recursively walk
key, err = l.elementKey()
if err != nil {
return nil, err
}
}
values := l.elementValues(key) values := l.elementValues(key)
// recursively set the elements in the list // recursively set the elements in the list
var s *openapi.ResourceSchema
if l.Schema != nil {
s = l.Schema.Elements()
}
for _, value := range values { for _, value := range values {
val, err := Walker{Visitor: l, val, err := Walker{
Sources: l.elementValue(key, value)}.Walk() InferAssociativeLists: l.InferAssociativeLists,
Visitor: l,
Schema: s,
Sources: l.elementValue(key, value),
}.Walk()
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -6,6 +6,8 @@ package walk
import ( import (
"sort" "sort"
"sigs.k8s.io/kustomize/kyaml/fieldmeta"
"sigs.k8s.io/kustomize/kyaml/openapi"
"sigs.k8s.io/kustomize/kyaml/sets" "sigs.k8s.io/kustomize/kyaml/sets"
"sigs.k8s.io/kustomize/kyaml/yaml" "sigs.k8s.io/kustomize/kyaml/yaml"
) )
@@ -18,15 +20,27 @@ import (
// - set each source field value on l.Dest // - set each source field value on l.Dest
func (l Walker) walkMap() (*yaml.RNode, error) { func (l Walker) walkMap() (*yaml.RNode, error) {
// get the new map value // get the new map value
dest, err := l.Sources.setDestNode(l.VisitMap(l.Sources)) dest, err := l.Sources.setDestNode(l.VisitMap(l.Sources, l.Schema))
if dest == nil || err != nil { if dest == nil || err != nil {
return nil, err return nil, err
} }
// recursively set the field values on the map // recursively set the field values on the map
for _, key := range l.fieldNames() { for _, key := range l.fieldNames() {
val, err := Walker{Visitor: l, var s *openapi.ResourceSchema
Sources: l.fieldValue(key), Path: append(l.Path, key)}.Walk() if l.Schema != nil {
s = l.Schema.Field(key)
}
fv, commentSch := l.fieldValue(key)
if commentSch != nil {
s = commentSch
}
val, err := Walker{
InferAssociativeLists: l.InferAssociativeLists,
Visitor: l,
Schema: s,
Sources: fv,
Path: append(l.Path, key)}.Walk()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -42,11 +56,40 @@ func (l Walker) walkMap() (*yaml.RNode, error) {
} }
// valueIfPresent returns node.Value if node is non-nil, otherwise returns nil // valueIfPresent returns node.Value if node is non-nil, otherwise returns nil
func (l Walker) valueIfPresent(node *yaml.MapNode) *yaml.RNode { func (l Walker) valueIfPresent(node *yaml.MapNode) (*yaml.RNode, *openapi.ResourceSchema) {
if node == nil { if node == nil {
return nil return nil, nil
} }
return node.Value
// parse the schema for the field if present
var s *openapi.ResourceSchema
fm := fieldmeta.FieldMeta{}
var err error
// check the value for a schema
if err = fm.Read(node.Value); err == nil {
s = &openapi.ResourceSchema{Schema: &fm.Schema}
if fm.Schema.Ref.String() != "" {
r, err := openapi.Resolve(&fm.Schema.Ref)
if err == nil && r != nil {
s.Schema = r
}
}
}
// check the key for a schema -- this will be used
// when the value is a Sequence (comments are attached)
// to the key
if fm.IsEmpty() {
if err = fm.Read(node.Key); err == nil {
s = &openapi.ResourceSchema{Schema: &fm.Schema}
}
if fm.Schema.Ref.String() != "" {
r, err := openapi.Resolve(&fm.Schema.Ref)
if err == nil && r != nil {
s.Schema = r
}
}
}
return node.Value, s
} }
// fieldNames returns a sorted slice containing the names of all fields that appear in any of // fieldNames returns a sorted slice containing the names of all fields that appear in any of
@@ -67,15 +110,20 @@ func (l Walker) fieldNames() []string {
} }
// fieldValue returns a slice containing each source's value for fieldName // fieldValue returns a slice containing each source's value for fieldName
func (l Walker) fieldValue(fieldName string) []*yaml.RNode { func (l Walker) fieldValue(fieldName string) ([]*yaml.RNode, *openapi.ResourceSchema) {
var fields []*yaml.RNode var fields []*yaml.RNode
var sch *openapi.ResourceSchema
for i := range l.Sources { for i := range l.Sources {
if l.Sources[i] == nil { if l.Sources[i] == nil {
fields = append(fields, nil) fields = append(fields, nil)
continue continue
} }
field := l.Sources[i].Field(fieldName) field := l.Sources[i].Field(fieldName)
fields = append(fields, l.valueIfPresent(field)) f, s := l.valueIfPresent(field)
fields = append(fields, f)
if sch == nil && !s.IsEmpty() {
sch = s
}
} }
return fields return fields, sch
} }

View File

@@ -9,5 +9,5 @@ import (
// walkNonAssociativeSequence returns the value of VisitList // walkNonAssociativeSequence returns the value of VisitList
func (l Walker) walkNonAssociativeSequence() (*yaml.RNode, error) { func (l Walker) walkNonAssociativeSequence() (*yaml.RNode, error) {
return l.VisitList(l.Sources, NonAssociateList) return l.VisitList(l.Sources, l.Schema, NonAssociateList)
} }

View File

@@ -7,5 +7,5 @@ import "sigs.k8s.io/kustomize/kyaml/yaml"
// walkScalar returns the value of VisitScalar // walkScalar returns the value of VisitScalar
func (l Walker) walkScalar() (*yaml.RNode, error) { func (l Walker) walkScalar() (*yaml.RNode, error) {
return l.VisitScalar(l.Sources) return l.VisitScalar(l.Sources, l.Schema)
} }

View File

@@ -4,6 +4,7 @@
package walk package walk
import ( import (
"sigs.k8s.io/kustomize/kyaml/openapi"
"sigs.k8s.io/kustomize/kyaml/yaml" "sigs.k8s.io/kustomize/kyaml/yaml"
) )
@@ -16,11 +17,11 @@ const (
// Visitor is invoked by walk with source and destination node pairs // Visitor is invoked by walk with source and destination node pairs
type Visitor interface { type Visitor interface {
VisitMap(nodes Sources) (*yaml.RNode, error) VisitMap(Sources, *openapi.ResourceSchema) (*yaml.RNode, error)
VisitScalar(nodes Sources) (*yaml.RNode, error) VisitScalar(Sources, *openapi.ResourceSchema) (*yaml.RNode, error)
VisitList(nodes Sources, kind ListKind) (*yaml.RNode, error) VisitList(Sources, *openapi.ResourceSchema, ListKind) (*yaml.RNode, error)
} }
// ClearNode is returned if GrepFilter should do nothing after calling Set // ClearNode is returned if GrepFilter should do nothing after calling Set

View File

@@ -8,20 +8,30 @@ import (
"os" "os"
"strings" "strings"
"sigs.k8s.io/kustomize/kyaml/fieldmeta"
"sigs.k8s.io/kustomize/kyaml/openapi"
"sigs.k8s.io/kustomize/kyaml/yaml" "sigs.k8s.io/kustomize/kyaml/yaml"
"sigs.k8s.io/kustomize/kyaml/yaml/schema"
) )
// Filter walks the Source RNode and modifies the RNode provided to GrepFilter. // Walker walks the Source RNode and modifies the RNode provided to GrepFilter.
type Walker struct { type Walker struct {
// Visitor is invoked by GrepFilter // Visitor is invoked by GrepFilter
Visitor Visitor
Schema *openapi.ResourceSchema
// Source is the RNode to walk. All Source fields and associative list elements // Source is the RNode to walk. All Source fields and associative list elements
// will be visited. // will be visited.
Sources Sources Sources Sources
// Path is the field path to the current Source Node. // Path is the field path to the current Source Node.
Path []string Path []string
// InferAssociativeLists if set to true will infer merge strategies for
// fields which it doesn't have the schema based on the fields in the
// list elements.
InferAssociativeLists bool
} }
func (l Walker) Kind() yaml.Kind { func (l Walker) Kind() yaml.Kind {
@@ -35,6 +45,8 @@ func (l Walker) Kind() yaml.Kind {
// GrepFilter implements yaml.GrepFilter // GrepFilter implements yaml.GrepFilter
func (l Walker) Walk() (*yaml.RNode, error) { func (l Walker) Walk() (*yaml.RNode, error) {
l.Schema = l.GetSchema()
// invoke the handler for the corresponding node type // invoke the handler for the corresponding node type
switch l.Kind() { switch l.Kind() {
case yaml.MappingNode: case yaml.MappingNode:
@@ -46,7 +58,7 @@ func (l Walker) Walk() (*yaml.RNode, error) {
if err := yaml.ErrorIfAnyInvalidAndNonNull(yaml.SequenceNode, l.Sources...); err != nil { if err := yaml.ErrorIfAnyInvalidAndNonNull(yaml.SequenceNode, l.Sources...); err != nil {
return nil, err return nil, err
} }
if yaml.IsAssociative(l.Sources) { if schema.IsAssociative(l.Schema, l.Sources, l.InferAssociativeLists) {
return l.walkAssociativeSequence() return l.walkAssociativeSequence()
} }
return l.walkNonAssociativeSequence() return l.walkNonAssociativeSequence()
@@ -64,6 +76,49 @@ func (l Walker) Walk() (*yaml.RNode, error) {
} }
} }
func (l Walker) GetSchema() *openapi.ResourceSchema {
for i := range l.Sources {
r := l.Sources[i]
if yaml.IsEmpty(r) {
continue
}
fm := fieldmeta.FieldMeta{}
if err := fm.Read(r); err == nil && !fm.IsEmpty() {
// per-field schema, this is fine
if fm.Schema.Ref.String() != "" {
// resolve the reference
s, err := openapi.Resolve(&fm.Schema.Ref)
if err == nil && s != nil {
fm.Schema = *s
}
}
return &openapi.ResourceSchema{Schema: &fm.Schema}
}
}
if l.Schema != nil {
return l.Schema
}
for i := range l.Sources {
r := l.Sources[i]
if yaml.IsEmpty(r) {
continue
}
m, _ := r.GetMeta()
if m.Kind == "" || m.APIVersion == "" {
continue
}
s := openapi.SchemaForResourceType(yaml.TypeMeta{Kind: m.Kind, APIVersion: m.APIVersion})
if s != nil {
return s
}
}
return nil
}
const ( const (
DestIndex = iota DestIndex = iota
OriginIndex OriginIndex