diff --git a/kyaml/yaml/example_test.go b/kyaml/yaml/example_test.go index baa8e8ed9..89da82731 100644 --- a/kyaml/yaml/example_test.go +++ b/kyaml/yaml/example_test.go @@ -228,7 +228,7 @@ func ExampleElementMatcher_Filter() { log.Fatal(err) } elem, err := obj.Pipe(ElementMatcher{ - FieldValue: "c", Create: NewScalarRNode("c"), + Values: []string{"c"}, Create: NewScalarRNode("c"), }) if err != nil { log.Fatal(err) @@ -255,7 +255,9 @@ func ExampleElementMatcher_Filter_primitiveFound() { log.Fatal(err) } elem, err := obj.Pipe(ElementMatcher{ - FieldValue: "c", Create: NewScalarRNode("c"), + Keys: []string{""}, + Values: []string{"c"}, + Create: NewScalarRNode("c"), }) if err != nil { log.Fatal(err) @@ -287,7 +289,7 @@ image: nginx log.Fatal(err) } elem, err := obj.Pipe(ElementMatcher{ - FieldName: "name", FieldValue: "baz", Create: append}) + Keys: []string{"name"}, Values: []string{"baz"}, Create: append}) if err != nil { log.Fatal(err) } @@ -321,7 +323,7 @@ image: nginx log.Fatal(err) } elem, err := obj.Pipe(ElementMatcher{ - FieldName: "name", FieldValue: "baz", Create: append}) + Keys: []string{"name"}, Values: []string{"baz"}, Create: append}) if err != nil { log.Fatal(err) } diff --git a/kyaml/yaml/fns.go b/kyaml/yaml/fns.go index bec46eeb0..e28b79e24 100644 --- a/kyaml/yaml/fns.go +++ b/kyaml/yaml/fns.go @@ -48,19 +48,20 @@ func (a ElementAppender) Filter(rn *RNode) (*RNode, error) { // not designed for this purpose. To append an element, please use ElementAppender. // To replace, set the key-value pair and a non-nil Element. // To delete, set the key-value pair and leave the Element as nil. +// Every key must have a corresponding value. type ElementSetter struct { Kind string `yaml:"kind,omitempty"` // Element is the new value to set -- remove the existing element if nil Element *Node - // Key is a field on the elements. It is used to find the matching element to - // update / delete. - Key string `yaml:"key,omitempty"` + // Key is a list of fields on the elements. It is used to find matching elements to + // update / delete + Keys []string - // Value is a field value on the elements. It is used to find matching elements to - // update / delete. - Value string `yaml:"value,omitempty"` + // Value is a list of field values on the elements corresponding to the keys. It is + // used to find matching elements to update / delete. + Values []string } // isMappingNode returns whether node is a mapping node @@ -70,10 +71,15 @@ func (e ElementSetter) isMappingNode(node *RNode) bool { // isMappingSetter returns is this setter intended to set a mapping node func (e ElementSetter) isMappingSetter() bool { - return e.Key != "" && e.Value != "" + return len(e.Keys) > 0 && e.Keys[0] != "" && + len(e.Values) > 0 && e.Values[0] != "" } func (e ElementSetter) Filter(rn *RNode) (*RNode, error) { + if len(e.Keys) == 0 { + e.Keys = append(e.Keys, "") + } + if err := ErrorIfInvalid(rn, SequenceNode); err != nil { return nil, err } @@ -96,13 +102,28 @@ func (e ElementSetter) Filter(rn *RNode) (*RNode, error) { } // check if this is the element we are matching - val, err := newNode.Pipe(FieldMatcher{Name: e.Key, StringValue: e.Value}) - if err != nil { - return nil, err + var val *RNode + var err error + found := true + for j := range e.Keys { + if j >= len(e.Values) { + val, err = newNode.Pipe(FieldMatcher{Name: e.Keys[j], StringValue: ""}) + } else { + val, err = newNode.Pipe(FieldMatcher{Name: e.Keys[j], StringValue: e.Values[j]}) + } + if err != nil { + return nil, err + } + if val == nil { + found = false + break + } } - if val == nil { + if !found { // not the element we are looking for, keep it in the Content - newContent = append(newContent, elem) + if len(e.Values) > 0 { + newContent = append(newContent, elem) + } continue } matchingElementFound = true @@ -211,49 +232,58 @@ func (c FieldClearer) Filter(rn *RNode) (*RNode, error) { } func MatchElement(field, value string) ElementMatcher { - return ElementMatcher{FieldName: field, FieldValue: value} + return ElementMatcher{Keys: []string{field}, Values: []string{value}} +} + +func MatchElementList(keys []string, values []string) ElementMatcher { + return ElementMatcher{Keys: keys, Values: values} } func GetElementByKey(key string) ElementMatcher { - return ElementMatcher{FieldName: key, MatchAnyValue: true} + return ElementMatcher{Keys: []string{key}, MatchAnyValue: true} } // ElementMatcher returns the first element from a Sequence matching the -// specified field's value. If there's no match, and no configuration error, +// specified key-value pairs. If there's no match, and no configuration error, // the matcher returns nil, nil. type ElementMatcher struct { Kind string `yaml:"kind,omitempty"` - // FieldName will attempt to match this field in each list element. - // Optional. Leave empty for lists of primitives (ScalarNode). - FieldName string `yaml:"name,omitempty"` + // Keys are the list of fields upon which to match this element. + Keys []string - // FieldValue will attempt to match each element field to this value. - // For lists of primitives, this will be used to match the primitive value. - FieldValue string `yaml:"value,omitempty"` + // Values are the list of values upon which to match this element. + Values []string // Create will create the Element if it is not found Create *RNode `yaml:"create,omitempty"` // MatchAnyValue indicates that matcher should only consider the key and ignore - // the actual value in the list. FieldValue must be empty when NoValue is + // the actual value in the list. Values must be empty when MatchAnyValue is // set to true. MatchAnyValue bool `yaml:"noValue,omitempty"` } func (e ElementMatcher) Filter(rn *RNode) (*RNode, error) { + if len(e.Keys) == 0 { + e.Keys = append(e.Keys, "") + } + if len(e.Values) == 0 { + e.Values = append(e.Values, "") + } + if err := ErrorIfInvalid(rn, yaml.SequenceNode); err != nil { return nil, err } - if e.MatchAnyValue && e.FieldValue != "" { - return nil, fmt.Errorf("FieldValue must be empty when NoValue is set to true") + if e.MatchAnyValue && len(e.Values) != 0 && e.Values[0] != "" { + return nil, fmt.Errorf("Values must be empty when MatchAnyValue is set to true") } // SequenceNode Content is a slice of ScalarNodes. Each ScalarNode has a // YNode containing the primitive data. - if len(e.FieldName) == 0 { + if len(e.Keys) == 0 || len(e.Keys[0]) == 0 { for i := range rn.Content() { - if rn.Content()[i].Value == e.FieldValue { + if rn.Content()[i].Value == e.Values[0] { return &RNode{value: rn.Content()[i]}, nil } } @@ -268,20 +298,32 @@ func (e ElementMatcher) Filter(rn *RNode) (*RNode, error) { for i := range rn.Content() { // cast the entry to a RNode so we can operate on it elem := NewRNode(rn.Content()[i]) + var field *RNode + var err error // only check mapping node - if err := ErrorIfInvalid(elem, yaml.MappingNode); err != nil { + if err = ErrorIfInvalid(elem, yaml.MappingNode); err != nil { continue } - var field *RNode - var err error - if e.MatchAnyValue { - field, err = elem.Pipe(Get(e.FieldName)) - } else { - field, err = elem.Pipe(MatchField(e.FieldName, e.FieldValue)) + if !e.MatchAnyValue && len(e.Keys) != len(e.Values) { + return nil, fmt.Errorf("length of keys must equal length of values when MatchAnyValue is false") } - if IsFoundOrError(field, err) { + + matchesElement := true + for i := range e.Keys { + if e.MatchAnyValue { + field, err = elem.Pipe(Get(e.Keys[i])) + } else { + field, err = elem.Pipe(MatchField(e.Keys[i], e.Values[i])) + } + if !IsFoundOrError(field, err) { + // this is not the element we are looking for + matchesElement = false + break + } + } + if matchesElement { return elem, err } } @@ -351,7 +393,7 @@ func (f FieldMatcher) Filter(rn *RNode) (*RNode, error) { return rn, nil } return nil, nil - case rn.value.Value == f.Value.YNode().Value: + case GetValue(rn) == GetValue(f.Value): return rn, nil default: return nil, nil @@ -404,7 +446,7 @@ type PathGetter struct { // See FieldMatcher for more on Fields and Map Keys. // // List Entries can be specified as map entry to match [fieldName=fieldValue] - // or a postional index like 0 to get the element. - (unquoted hyphen) is + // or a positional index like 0 to get the element. - (unquoted hyphen) is // special and means the last element. // // See Elem for more on List Entries. @@ -504,7 +546,7 @@ func (l PathGetter) elemFilter(part string) (Filter, error) { }) } // Append the Node - return ElementMatcher{FieldName: name, FieldValue: value, Create: elem}, nil + return ElementMatcher{Keys: []string{name}, Values: []string{value}, Create: elem}, nil } func (l PathGetter) fieldFilter( diff --git a/kyaml/yaml/fns_test.go b/kyaml/yaml/fns_test.go index 1b8628c97..3331a1fb9 100644 --- a/kyaml/yaml/fns_test.go +++ b/kyaml/yaml/fns_test.go @@ -109,7 +109,7 @@ func TestElementSetter(t *testing.T) { // ElementSetter will update node, so make a copy node := orig.Copy() // Remove an element, because ElementSetter.Element is left nil. - rn, err := node.Pipe(ElementSetter{Key: "a", Value: "b"}) + rn, err := node.Pipe(ElementSetter{Keys: []string{"a"}, Values: []string{"b"}}) assert.NoError(t, err) assert.Nil(t, rn) assert.Equal(t, `- scalarValue @@ -118,7 +118,7 @@ func TestElementSetter(t *testing.T) { node = orig.Copy() // Nothing happens because no element is matched - rn, err = node.Pipe(ElementSetter{Key: "a", Value: "zebra"}) + rn, err = node.Pipe(ElementSetter{Keys: []string{"a"}, Values: []string{"zebra"}}) assert.NoError(t, err) assert.Nil(t, rn) assert.Equal(t, `- a: b @@ -129,19 +129,24 @@ func TestElementSetter(t *testing.T) { node = orig.Copy() // Return error because ElementSetter doesn't support a single key // when there is a scalar value in the list - _, err = node.Pipe(ElementSetter{Key: "a"}) + _, err = node.Pipe(ElementSetter{Keys: []string{"a"}}) assert.EqualError(t, err, "wrong Node Kind for expected: MappingNode was ScalarNode: value: {scalarValue}") + // Return error because ElementSetter will assume all elements are scalar when + // there is only value provided. + _, err = node.Pipe(ElementSetter{Values: []string{"b"}}) + assert.EqualError(t, err, "wrong Node Kind for expected: ScalarNode was MappingNode: value: {a: b}") + node = MustParse(` - a: b - c: d `) - // {a: b} is removed since the value is omitted and only key is used - // to match and no Element specified. - rn, err = node.Pipe(ElementSetter{Key: "a"}) + // If given a key and no values, ElementSetter will + // change node to be an empty list + rn, err = node.Pipe(ElementSetter{Keys: []string{"a"}}) assert.NoError(t, err) assert.Nil(t, rn) - assert.Equal(t, `- c: d + assert.Equal(t, `[] `, assertNoErrorString(t)(node.String())) node = MustParse(` @@ -150,7 +155,7 @@ func TestElementSetter(t *testing.T) { `) // Return error because ElementSetter will assume all elements are scalar when // there is only value provided. - _, err = node.Pipe(ElementSetter{Value: "b"}) + _, err = node.Pipe(ElementSetter{Values: []string{"b"}}) assert.EqualError(t, err, "wrong Node Kind for expected: ScalarNode was MappingNode: value: {a: b}") node = MustParse(` @@ -159,7 +164,7 @@ func TestElementSetter(t *testing.T) { `) // b is removed since ElementSetter use the value "b" to match the // scalar values. - rn, err = node.Pipe(ElementSetter{Value: "b"}) + rn, err = node.Pipe(ElementSetter{Values: []string{"b"}}) assert.NoError(t, err) assert.Nil(t, rn) assert.Equal(t, `- a @@ -171,8 +176,8 @@ func TestElementSetter(t *testing.T) { "e": "f", }) rn, err = node.Pipe(ElementSetter{ - Key: "a", - Value: "b", + Keys: []string{"a"}, + Values: []string{"b"}, Element: newElement.YNode(), }) assert.NoError(t, err) @@ -186,8 +191,8 @@ func TestElementSetter(t *testing.T) { // Set an element with scalar, {"a": "b"} to "foo" newElement = NewScalarRNode("foo") rn, err = node.Pipe(ElementSetter{ - Key: "a", - Value: "b", + Keys: []string{"a"}, + Values: []string{"b"}, Element: newElement.YNode(), }) assert.NoError(t, err) @@ -204,8 +209,8 @@ func TestElementSetter(t *testing.T) { "e": "f", }) rn, err = node.Pipe(ElementSetter{ - Key: "x", - Value: "y", + Keys: []string{"x"}, + Values: []string{"y"}, Element: newElement.YNode(), }) assert.NoError(t, err) @@ -217,6 +222,133 @@ func TestElementSetter(t *testing.T) { `, assertNoErrorString(t)(node.String())) } +func TestElementSetterMultipleKeys(t *testing.T) { + orig := MustParse(` +- a: b + c: d +- scalarValue +- e: f +# null will be removed +- null +`) + + // ElementSetter will update node, so make a copy + node := orig.Copy() + // Remove an element using one key-value pair, + // because ElementSetter.Element is left nil. + rn, err := node.Pipe(ElementSetter{ + Keys: []string{"a"}, + Values: []string{"b"}, + }) + assert.NoError(t, err) + assert.Nil(t, rn) + assert.Equal(t, `- scalarValue +- e: f +`, assertNoErrorString(t)(node.String())) + + node = orig.Copy() + // Remove an element using multiple key-value pairs, + // because ElementSetter.Element is left nil. + rn, err = node.Pipe(ElementSetter{ + Keys: []string{"a", "c"}, + Values: []string{"b", "d"}, + }) + assert.NoError(t, err) + assert.Nil(t, rn) + assert.Equal(t, `- scalarValue +- e: f +`, assertNoErrorString(t)(node.String())) + + node = orig.Copy() + // Should do nothing, because Element is nil + // and there is no element which matches all + // give key-value pairs + rn, err = node.Pipe(ElementSetter{ + Keys: []string{"a", "c"}, + Values: []string{"b", "wrong value"}, + }) + assert.NoError(t, err) + assert.Nil(t, rn) + assert.Equal(t, `- a: b + c: d +- scalarValue +- e: f +`, assertNoErrorString(t)(node.String())) + + node = orig.Copy() + // Set an element, with a single key-value pair + // replacing 'a: b, c: d' with 'g: h' + newElement := NewMapRNode(&map[string]string{ + "g": "h", + }) + rn, err = node.Pipe(ElementSetter{ + Keys: []string{"a"}, + Values: []string{"b"}, + Element: newElement.YNode(), + }) + assert.NoError(t, err) + assert.Equal(t, rn, newElement) + assert.Equal(t, `- g: h +- scalarValue +- e: f +`, assertNoErrorString(t)(node.String())) + + node = orig.Copy() + // Set an element, with multiple key-value pairs + // replacing 'a: b, c: d' with 'g: h' + newElement = NewMapRNode(&map[string]string{ + "g": "h", + }) + rn, err = node.Pipe(ElementSetter{ + Keys: []string{"a", "c"}, + Values: []string{"b", "d"}, + Element: newElement.YNode(), + }) + assert.NoError(t, err) + assert.Equal(t, rn, newElement) + assert.Equal(t, `- g: h +- scalarValue +- e: f +`, assertNoErrorString(t)(node.String())) + + node = orig.Copy() + // Set an element scalar, + // {'a: b, c: d'} to "foo" + newElement = NewScalarRNode("foo") + rn, err = node.Pipe(ElementSetter{ + Keys: []string{"a", "c"}, + Values: []string{"b", "d"}, + Element: newElement.YNode(), + }) + assert.NoError(t, err) + assert.Equal(t, rn, newElement) + assert.Equal(t, `- foo +- scalarValue +- e: f +`, assertNoErrorString(t)(node.String())) + + node = orig.Copy() + // Append an element + // There is no element which matches all given + // key-value pairs, so the element will be appended. + newElement = NewMapRNode(&map[string]string{ + "g": "h", + }) + rn, err = node.Pipe(ElementSetter{ + Keys: []string{"a", "c"}, + Values: []string{"b", "wrong value"}, + Element: newElement.YNode(), + }) + assert.NoError(t, err) + assert.Equal(t, rn, newElement) + assert.Equal(t, `- a: b + c: d +- scalarValue +- e: f +- g: h +`, assertNoErrorString(t)(node.String())) +} + func TestElementMatcherWithNoValue(t *testing.T) { node, err := Parse(` - a: c @@ -224,21 +356,63 @@ func TestElementMatcherWithNoValue(t *testing.T) { `) assert.NoError(t, err) - rn, err := node.Pipe(ElementMatcher{FieldName: "b"}) + rn, err := node.Pipe(ElementMatcher{Keys: []string{"b"}}) assert.NoError(t, err) assert.Equal(t, "b: \"\"\n", assertNoErrorString(t)(rn.String())) - rn, err = node.Pipe(ElementMatcher{FieldName: "a"}) + rn, err = node.Pipe(ElementMatcher{Keys: []string{"a"}}) assert.NoError(t, err) assert.Nil(t, rn) - rn, err = node.Pipe(ElementMatcher{FieldName: "a", MatchAnyValue: true}) + rn, err = node.Pipe(ElementMatcher{Keys: []string{"a"}, MatchAnyValue: true}) assert.NoError(t, err) assert.Equal(t, "a: c\n", assertNoErrorString(t)(rn.String())) - _, err = node.Pipe(ElementMatcher{FieldName: "a", FieldValue: "c", MatchAnyValue: true}) - assert.Errorf(t, err, "FieldValue must be empty when NoValue is set to true") + _, err = node.Pipe(ElementMatcher{Keys: []string{"a"}, Values: []string{"c"}, MatchAnyValue: true}) + assert.Errorf(t, err, "Values must be empty when MatchAnyValue is set to true") } + +func TestElementMatcherMultipleKeys(t *testing.T) { + node, err := Parse(` +- a: b + c: d +- e: f +`) + assert.NoError(t, err) + + // matches all key-value pairs + rn, err := node.Pipe(MatchElementList( + []string{"a", "c"}, // keys + []string{"b", "d"}, // values + )) + assert.NoError(t, err) + assert.NotEmpty(t, rn) + + // matches one key value pair but not the other + rn, err = node.Pipe(MatchElementList( + []string{"a", "c"}, // keys + []string{"b", "f"}, // values + )) + assert.NoError(t, err) + assert.Nil(t, rn) + + // matches single given key value pair + rn, err = node.Pipe(MatchElementList( + []string{"e"}, // keys + []string{"f"}, // values + )) + assert.NoError(t, err) + assert.NotEmpty(t, rn) + + // matching key, but value doesn't match + rn, err = node.Pipe(MatchElementList( + []string{"e"}, // keys + []string{"g"}, // values + )) + assert.NoError(t, err) + assert.Nil(t, rn) +} + func TestClearField_Fn(t *testing.T) { node, err := Parse(NodeSampleData) assert.NoError(t, err) diff --git a/kyaml/yaml/merge2/smpdirective.go b/kyaml/yaml/merge2/smpdirective.go index b5b2fbb7e..f38b188ea 100644 --- a/kyaml/yaml/merge2/smpdirective.go +++ b/kyaml/yaml/merge2/smpdirective.go @@ -95,7 +95,7 @@ func elideMappingPatchDirective(patch *yaml.RNode) error { func elideSequencePatchDirective(patch *yaml.RNode, value string) error { return patch.PipeE(yaml.ElementSetter{ Element: nil, - Key: strategicMergePatchDirectiveKey, - Value: value, + Keys: []string{strategicMergePatchDirectiveKey}, + Values: []string{value}, }) } diff --git a/kyaml/yaml/walk/associative_sequence.go b/kyaml/yaml/walk/associative_sequence.go index 66ead3dcc..6f3e25cd8 100644 --- a/kyaml/yaml/walk/associative_sequence.go +++ b/kyaml/yaml/walk/associative_sequence.go @@ -21,7 +21,11 @@ func appendListNode(dst, src *yaml.RNode, key string) (*yaml.RNode, error) { // If key is empty, we know this is a scalar value and we can directly set the // node if key == "" { - _, err := dst.Pipe(yaml.ElementSetter{Element: elem, Key: key, Value: elem.Value}) + _, err := dst.Pipe(yaml.ElementSetter{ + Element: elem, + Keys: []string{key}, + Values: []string{elem.Value}, + }) if err != nil { return nil, err } @@ -46,7 +50,11 @@ func appendListNode(dst, src *yaml.RNode, key string) (*yaml.RNode, error) { // 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 // find the item, the element will be appended. - _, err = dst.Pipe(yaml.ElementSetter{Element: elem, Key: key, Value: v}) + _, err = dst.Pipe(yaml.ElementSetter{ + Element: elem, + Keys: []string{key}, + Values: []string{v}, + }) if err != nil { return nil, err } @@ -76,7 +84,10 @@ func (l *Walker) setAssociativeSequenceElements(values []string, key string, des } // delete the node from **dest** if it's null or empty if yaml.IsMissingOrNull(val) || yaml.IsEmptyMap(val) { - _, err = dest.Pipe(yaml.ElementSetter{Key: key, Value: value}) + _, err = dest.Pipe(yaml.ElementSetter{ + Keys: []string{key}, + Values: []string{value}, + }) if err != nil { return nil, err } @@ -94,7 +105,11 @@ func (l *Walker) setAssociativeSequenceElements(values []string, key string, des // Add the val to the sequence. val will replace the item in the sequence if // there is an item that matches the key-value pair. Otherwise val will be appended // the the sequence. - _, err = itemsToBeAdded.Pipe(yaml.ElementSetter{Element: val.YNode(), Key: key, Value: value}) + _, err = itemsToBeAdded.Pipe(yaml.ElementSetter{ + Element: val.YNode(), + Keys: []string{key}, + Values: []string{value}, + }) if err != nil { return nil, err }