use merge key tuple instead of single merge key

This commit is contained in:
Natasha Sarkar
2020-11-09 11:29:21 -08:00
parent f4d8ebb1da
commit df0576a270
5 changed files with 258 additions and 121 deletions

View File

@@ -349,6 +349,35 @@ func (rs *ResourceSchema) Field(field string) *ResourceSchema {
return &ResourceSchema{Schema: &s} return &ResourceSchema{Schema: &s}
} }
// PatchStrategyAndKeyList returns the patch strategy and complete merge key list
func (rs *ResourceSchema) PatchStrategyAndKeyList() (string, []string) {
ps, found := rs.Schema.Extensions[kubernetesPatchStrategyExtensionKey]
if !found {
// empty patch strategy
return "", []string{}
}
mkList, found := rs.Schema.Extensions[kubernetesMergeKeyMapList]
if found {
//mkList is []interface, convert to []string
mkListStr := make([]string, len(mkList.([]interface{})))
for i, v := range mkList.([]interface{}) {
mkListStr[i] = v.(string)
}
return ps.(string), mkListStr
}
mk, found := rs.Schema.Extensions[kubernetesMergeKeyExtensionKey]
if !found {
// no mergeKey -- may be a primitive associative list (e.g. finalizers)
return ps.(string), []string{}
}
return ps.(string), []string{mk.(string)}
}
// PatchStrategyAndKey returns the patch strategy and merge key extensions // PatchStrategyAndKey returns the patch strategy and merge key extensions
func (rs *ResourceSchema) PatchStrategyAndKey() (string, string) { func (rs *ResourceSchema) PatchStrategyAndKey() (string, string) {
ps, found := rs.Schema.Extensions[kubernetesPatchStrategyExtensionKey] ps, found := rs.Schema.Extensions[kubernetesPatchStrategyExtensionKey]
@@ -377,13 +406,19 @@ const (
// kubernetesGVKExtensionKey is the key to lookup the kubernetes group version kind extension // kubernetesGVKExtensionKey is the key to lookup the kubernetes group version kind extension
// -- the extension is an array of objects containing a gvk // -- the extension is an array of objects containing a gvk
kubernetesGVKExtensionKey = "x-kubernetes-group-version-kind" kubernetesGVKExtensionKey = "x-kubernetes-group-version-kind"
// kubernetesMergeKeyExtensionKey is the key to lookup the kubernetes merge key extension // kubernetesMergeKeyExtensionKey is the key to lookup the kubernetes merge key extension
// -- the extension is a string // -- the extension is a string
kubernetesMergeKeyExtensionKey = "x-kubernetes-patch-merge-key" kubernetesMergeKeyExtensionKey = "x-kubernetes-patch-merge-key"
// kubernetesPatchStrategyExtensionKey is the key to lookup the kubernetes patch strategy // kubernetesPatchStrategyExtensionKey is the key to lookup the kubernetes patch strategy
// extension -- the extension is a string // extension -- the extension is a string
kubernetesPatchStrategyExtensionKey = "x-kubernetes-patch-strategy" kubernetesPatchStrategyExtensionKey = "x-kubernetes-patch-strategy"
// kubernetesMergeKeyMapList is the list of merge keys when there needs to be multiple
// -- the extension is an array of strings
kubernetesMergeKeyMapList = "x-kubernetes-list-map-keys"
// groupKey is the key to lookup the group from the GVK extension // groupKey is the key to lookup the group from the GVK extension
groupKey = "group" groupKey = "group"
// versionKey is the key to lookup the version from the GVK extension // versionKey is the key to lookup the version from the GVK extension

View File

@@ -1211,8 +1211,8 @@ spec:
ports: ports:
- containerPort: 80 - containerPort: 80
protocol: HTTP protocol: HTTP
- protocol: TCP - containerPort: 8080
containerPort: 8080 protocol: TCP
`}, `},
{ {
@@ -1330,7 +1330,7 @@ spec:
ports: ports:
- containerPort: 8080 - containerPort: 8080
protocol: UDP protocol: UDP
`, // output should have both `,
expected: ` expected: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
@@ -1343,6 +1343,8 @@ spec:
ports: ports:
- containerPort: 8080 - containerPort: 8080
protocol: UDP protocol: UDP
- containerPort: 8080
protocol: TCP
`}, `},
// //
@@ -1378,7 +1380,7 @@ spec:
ports: ports:
- containerPort: 8080 - containerPort: 8080
protocol: TCP protocol: TCP
`, // output should have both `,
expected: ` expected: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
@@ -1389,6 +1391,8 @@ spec:
- image: test-image - image: test-image
name: test-deployment name: test-deployment
ports: ports:
- containerPort: 8080
protocol: TCP
- containerPort: 8080 - containerPort: 8080
protocol: UDP protocol: UDP
`}, `},
@@ -1468,8 +1472,11 @@ spec:
protocol: UDP protocol: UDP
name: original name: original
- containerPort: 8080 - containerPort: 8080
protocol: UDP protocol: HTTP
name: original name: local
- containerPort: 8080
name: updated
protocol: TCP
`}, `},
{ {
@@ -1531,6 +1538,8 @@ spec:
- image: test-image - image: test-image
name: test-deployment name: test-deployment
ports: ports:
- containerPort: 8080
protocol: HTTP
- containerPort: 8080 - containerPort: 8080
protocol: TCP protocol: TCP
`}, `},

View File

@@ -474,6 +474,28 @@ func (rn *RNode) ElementValues(key string) ([]string, error) {
return elements, nil return elements, nil
} }
// ElementValuesList returns a list of lists, where each list is a set of
// values corresponding to each key in keys.
// Returns error for non-SequenceNodes.
func (rn *RNode) ElementValuesList(keys []string) ([][]string, error) {
if err := ErrorIfInvalid(rn, yaml.SequenceNode); err != nil {
return nil, errors.Wrap(err)
}
elements := make([][]string, len(rn.Content()))
for i := 0; i < len(rn.Content()); i++ {
for _, key := range keys {
field := NewRNode(rn.Content()[i]).Field(key)
if field.IsNilOrEmpty() {
elements[i] = append(elements[i], "")
} else {
elements[i] = append(elements[i], field.Value.YNode().Value)
}
}
}
return elements, nil
}
// Element returns the element in the list which contains the field matching the value. // Element returns the element in the list which contains the field matching the value.
// Returns nil for non-SequenceNodes or if no Element matches. // Returns nil for non-SequenceNodes or if no Element matches.
func (rn *RNode) Element(key, value string) *RNode { func (rn *RNode) Element(key, value string) *RNode {
@@ -487,6 +509,20 @@ func (rn *RNode) Element(key, value string) *RNode {
return elem return elem
} }
// ElementList returns the element in the list in which all fields keys[i] matches all
// corresponding values[i].
// Returns nil for non-SequenceNodes or if no Element matches.
func (rn *RNode) ElementList(keys []string, values []string) *RNode {
if rn.YNode().Kind != yaml.SequenceNode {
return nil
}
elem, err := rn.Pipe(MatchElementList(keys, values))
if err != nil {
return nil
}
return elem
}
// VisitElements calls fn for each element in a SequenceNode. // VisitElements calls fn for each element in a SequenceNode.
// Returns an error for non-SequenceNodes // Returns an error for non-SequenceNodes
func (rn *RNode) VisitElements(fn func(node *RNode) error) error { func (rn *RNode) VisitElements(fn func(node *RNode) error) error {

View File

@@ -16,14 +16,15 @@ import (
// src and dst should be both sequence node. key is used to call ElementSetter. // src and dst should be both sequence node. key is used to call ElementSetter.
// ElementSetter will use key-value pair to find and set the element in sequence // ElementSetter will use key-value pair to find and set the element in sequence
// node. // node.
func appendListNode(dst, src *yaml.RNode, key string) (*yaml.RNode, error) { func appendListNode(dst, src *yaml.RNode, keys []string, merge3 bool) (*yaml.RNode, error) {
var err error
for _, elem := range src.Content() { for _, elem := range src.Content() {
// If key is empty, we know this is a scalar value and we can directly set the // If key is empty, we know this is a scalar value and we can directly set the
// node // node
if key == "" { if keys[0] == "" {
_, err := dst.Pipe(yaml.ElementSetter{ _, err = dst.Pipe(yaml.ElementSetter{
Element: elem, Element: elem,
Keys: []string{key}, Keys: []string{""},
Values: []string{elem.Value}, Values: []string{elem.Value},
}) })
if err != nil { if err != nil {
@@ -31,30 +32,40 @@ func appendListNode(dst, src *yaml.RNode, key string) (*yaml.RNode, error) {
} }
continue continue
} }
if len(keys) > 1 && !merge3 {
continue
}
// we need to get the value for key so that we can find the element to set // we need to get the value for key so that we can find the element to set
// in sequence. // in sequence.
tmpNode := yaml.NewRNode(elem) v := []string{}
valueNode, err := tmpNode.Pipe(yaml.Get(key)) for _, key := range keys {
if err != nil { tmpNode := yaml.NewRNode(elem)
return nil, err valueNode, err := tmpNode.Pipe(yaml.Get(key))
}
if valueNode.IsNil() {
// no key found, directly append to dst
err = dst.PipeE(yaml.Append(elem))
if err != nil { if err != nil {
return nil, err return nil, err
} }
continue if valueNode.IsNil() {
// no key found, directly append to dst
err = dst.PipeE(yaml.Append(elem))
if err != nil {
return nil, err
}
continue
}
v = append(v, valueNode.YNode().Value)
} }
v := valueNode.YNode().Value
// We use the key and value from elem to find the corresponding element in dst. // We use the key and value from elem to find the corresponding element in dst.
// Then we will use ElementSetter to replace the element with elem. If we cannot // Then we will use ElementSetter to replace the element with elem. If we cannot
// find the item, the element will be appended. // find the item, the element will be appended.
_, err = dst.Pipe(yaml.ElementSetter{ _, err = dst.Pipe(yaml.ElementSetter{
Element: elem, Element: elem,
Keys: []string{key}, Keys: keys,
Values: []string{v}, Values: v,
}) })
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -62,14 +73,15 @@ func appendListNode(dst, src *yaml.RNode, key string) (*yaml.RNode, error) {
return dst, nil return dst, nil
} }
// setAssociativeSequenceElements recursively set the elements in the list // setPrimitiveSequenceElements sets elements in a primitive list
func (l *Walker) setAssociativeSequenceElements(values []string, key string, dest *yaml.RNode) (*yaml.RNode, error) { func (l *Walker) setPrimitiveSequenceElements(values []string, key string, dest *yaml.RNode) (*yaml.RNode, error) {
// itemsToBeAdded contains the items that will be added to dest // itemsToBeAdded contains the items that will be added to dest
itemsToBeAdded := yaml.NewListRNode() itemsToBeAdded := yaml.NewListRNode()
var schema *openapi.ResourceSchema var schema *openapi.ResourceSchema
if l.Schema != nil { if l.Schema != nil {
schema = l.Schema.Elements() schema = l.Schema.Elements()
} }
for _, value := range values { for _, value := range values {
val, err := Walker{ val, err := Walker{
VisitKeysAsScalars: l.VisitKeysAsScalars, VisitKeysAsScalars: l.VisitKeysAsScalars,
@@ -84,10 +96,7 @@ func (l *Walker) setAssociativeSequenceElements(values []string, key string, des
} }
// delete the node from **dest** if it's null or empty // delete the node from **dest** if it's null or empty
if yaml.IsMissingOrNull(val) || yaml.IsEmptyMap(val) { if yaml.IsMissingOrNull(val) || yaml.IsEmptyMap(val) {
_, err = dest.Pipe(yaml.ElementSetter{ _, err = dest.Pipe(yaml.ElementSetter{Keys: []string{key}, Values: []string{value}})
Keys: []string{key},
Values: []string{value},
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -118,10 +127,115 @@ func (l *Walker) setAssociativeSequenceElements(values []string, key string, des
if l.MergeOptions.ListIncreaseDirection == yaml.MergeOptionsListPrepend { if l.MergeOptions.ListIncreaseDirection == yaml.MergeOptionsListPrepend {
// items from patches are needed to be prepended. so we append the // items from patches are needed to be prepended. so we append the
// dest to itemsToBeAdded // dest to itemsToBeAdded
dest, err = appendListNode(itemsToBeAdded, dest, key) dest, err = appendListNode(itemsToBeAdded, dest, []string{""}, len(l.Sources) > 2)
} else { } else {
// append the items // append the items
dest, err = appendListNode(dest, itemsToBeAdded, key) dest, err = appendListNode(dest, itemsToBeAdded, []string{""}, len(l.Sources) > 2)
}
if err != nil {
return nil, err
}
// sequence is empty
if yaml.IsMissingOrNull(dest) {
return nil, nil
}
return dest, nil
}
// validateKeys returns a list of valid key-value pairs
// if secondary merge key values are missing, use only the available merge keys
func validateKeys(value []string, keys []string) ([]string, []string) {
validKeys := make([]string, 0)
validValues := make([]string, 0)
for i, v := range value {
if v != "" {
validKeys = append(validKeys, keys[i])
validValues = append(validValues, v)
}
}
if len(validKeys) == 0 { // if values missing, fall back to primary keys
validKeys = keys
validValues = value
}
return validKeys, validValues
}
// setAssociativeSequenceElements recursively set the elements in the list
func (l *Walker) setAssociativeSequenceElements(values [][]string, keys []string, dest *yaml.RNode) (*yaml.RNode, error) {
// itemsToBeAdded contains the items that will be added to dest
itemsToBeAdded := yaml.NewListRNode()
var schema *openapi.ResourceSchema
if l.Schema != nil {
schema = l.Schema.Elements()
}
for _, value := range values {
if len(value) == 0 {
continue
}
validKeys, validValues := validateKeys(value, keys)
val, err := Walker{
VisitKeysAsScalars: l.VisitKeysAsScalars,
InferAssociativeLists: l.InferAssociativeLists,
Visitor: l,
Schema: schema,
Sources: l.elementValueList(validKeys, validValues),
MergeOptions: l.MergeOptions,
}.Walk()
if err != nil {
return nil, err
}
exit := false
for i, key := range validKeys {
// delete the node from **dest** if it's null or empty
if yaml.IsMissingOrNull(val) || yaml.IsEmptyMap(val) {
_, err = dest.Pipe(yaml.ElementSetter{
Keys: validKeys,
Values: validValues,
})
if err != nil {
return nil, err
}
exit = true
} else if val.Field(key) == nil {
// make sure the key is set on the field
_, err = val.Pipe(yaml.SetField(key, yaml.NewScalarRNode(validValues[i])))
if err != nil {
return nil, err
}
}
}
if exit {
continue
}
// Add the val to the sequence. val will replace the item in the sequence if
// there is an item that matches all key-value pairs. Otherwise val will be appended
// the the sequence.
_, err = itemsToBeAdded.Pipe(yaml.ElementSetter{
Element: val.YNode(),
Keys: validKeys,
Values: validValues,
})
if err != nil {
return nil, err
}
}
var err error
for _, v := range values {
validKeys, _ := validateKeys(v, keys)
if l.MergeOptions.ListIncreaseDirection == yaml.MergeOptionsListPrepend {
// items from patches are needed to be prepended. so we append the
// dest to itemsToBeAdded
dest, err = appendListNode(itemsToBeAdded, dest, validKeys, len(l.Sources) > 2)
} else {
// append the items
dest, err = appendListNode(dest, itemsToBeAdded, validKeys, len(l.Sources) > 2)
}
} }
if err != nil { if err != nil {
return nil, err return nil, err
@@ -140,26 +254,31 @@ func (l *Walker) walkAssociativeSequence() (*yaml.RNode, error) {
return nil, err return nil, err
} }
// get the merge key from schema // get the merge key(s) from schema
var key, strategy string var strategy string
var keys []string
if l.Schema != nil { if l.Schema != nil {
strategy, key = l.Schema.PatchStrategyAndKey() strategy, keys = l.Schema.PatchStrategyAndKeyList()
} }
if strategy == "" && key == "" { // neither strategy nor not present in the schema -- infer the key if strategy == "" && len(keys) == 0 { // neither strategy nor keys present in the schema -- infer the key
// find the list of elements we need to recursively walk // find the list of elements we need to recursively walk
key, err = l.elementKey() key, err := l.elementKey()
if err != nil { if err != nil {
return nil, err return nil, err
} }
if key != "" {
keys = append(keys, key)
}
} }
if key != "" { // non-primitive associative list -- merge the elements
// non-primitive associative list -- merge the elements values := l.elementValues(keys)
return l.setAssociativeSequenceElements(l.elementValues(key), key, dest) if len(values) != 0 || len(keys) > 0 {
return l.setAssociativeSequenceElements(values, keys, dest)
} }
// primitive associative list -- merge the values // primitive associative list -- merge the values
return l.setAssociativeSequenceElements(l.elementPrimitiveValues(), key, dest) return l.setPrimitiveSequenceElements(l.elementPrimitiveValues(), "", dest)
} }
// elementKey returns the merge key to use for the associative list // elementKey returns the merge key to use for the associative list
@@ -187,10 +306,11 @@ func (l Walker) elementKey() (string, error) {
// from all sources. // from all sources.
// Return value slice is ordered using the original ordering from the elements, where // Return value slice is ordered using the original ordering from the elements, where
// elements missing from earlier sources appear later. // elements missing from earlier sources appear later.
func (l Walker) elementValues(key string) []string { func (l Walker) elementValues(keys []string) [][]string {
// use slice to to keep elements in the original order // use slice to to keep elements in the original order
var returnValues []string var returnValues [][]string
seen := sets.String{} var seen sets.StringList
// if we are doing append, dest node should be the first. // if we are doing append, dest node should be the first.
// otherwise dest node should be the last. // otherwise dest node should be the last.
beginIdx := 0 beginIdx := 0
@@ -205,13 +325,13 @@ func (l Walker) elementValues(key string) []string {
// add the value of the field for each element // add the value of the field for each element
// don't check error, we know this is a list node // don't check error, we know this is a list node
values, _ := src.ElementValues(key) values, _ := src.ElementValuesList(keys)
for _, s := range values { for _, s := range values {
if seen.Has(s) { if len(s) == 0 || seen.Has(s) {
continue continue
} }
returnValues = append(returnValues, s) returnValues = append(returnValues, s)
seen.Insert(s) seen = seen.Insert(s)
} }
} }
return returnValues return returnValues
@@ -259,3 +379,16 @@ func (l Walker) elementValue(key, value string) []*yaml.RNode {
} }
return fields return fields
} }
// fieldValue returns a slice containing each source's value for fieldName
func (l Walker) elementValueList(keys []string, values []string) []*yaml.RNode {
var fields []*yaml.RNode
for i := range l.Sources {
if l.Sources[i] == nil {
fields = append(fields, nil)
continue
}
fields = append(fields, l.Sources[i].ElementList(keys, values))
}
return fields
}

View File

@@ -508,82 +508,6 @@ spec:
`) `)
} }
func TestPatchTransformerWithInlineYamlRegexTarget(t *testing.T) {
th := kusttest_test.MakeEnhancedHarness(t).
PrepBuiltin("PatchTransformer")
defer th.Reset()
th.RunTransformerAndCheckResult(`
apiVersion: builtin
kind: PatchTransformer
metadata:
name: notImportantHere
target:
name: .*Deploy
kind: Deployment|MyKind
group: \w{4}
version: v\d
patch: |-
apiVersion: apps/v1
metadata:
name: myDeploy
kind: Deployment
spec:
replica: 77
`, someDeploymentResources, `
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
old-label: old-value
name: myDeploy
spec:
replica: 77
template:
metadata:
labels:
old-label: old-value
spec:
containers:
- image: nginx
name: nginx
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
new-label: new-value
name: yourDeploy
spec:
replica: 77
template:
metadata:
labels:
new-label: new-value
spec:
containers:
- image: nginx:1.7.9
name: nginx
---
apiVersion: apps/v1
kind: MyKind
metadata:
label:
old-label: old-value
name: myDeploy
spec:
replica: 77
template:
metadata:
labels:
old-label: old-value
spec:
containers:
- image: nginx
name: nginx
`)
}
func TestPatchTransformerWithPatchDelete(t *testing.T) { func TestPatchTransformerWithPatchDelete(t *testing.T) {
th := kusttest_test.MakeEnhancedHarness(t). th := kusttest_test.MakeEnhancedHarness(t).
PrepBuiltin("PatchTransformer") PrepBuiltin("PatchTransformer")