diff --git a/kyaml/yaml/fns.go b/kyaml/yaml/fns.go index bec46eeb0..1f75eedb6 100644 --- a/kyaml/yaml/fns.go +++ b/kyaml/yaml/fns.go @@ -63,17 +63,48 @@ type ElementSetter struct { Value string `yaml:"value,omitempty"` } +func (e ElementSetter) Filter(rn *RNode) (*RNode, error) { + return rn.Pipe(ElementSetterList{ + Element: e.Element, + Keys: []string{e.Key}, + Values: []string{e.Value}, + }) +} + +// ElementSetterList sets the value for an Element in an associative list. +// It behaves identically to ElementSetter, except that it uses multiple +// key-value pairs (in the form of a list of keys and a corresponding list +// of values) in order to find a matching element, whereas ElementSetter +// uses only one key-value pair. +type ElementSetterList struct { + Kind string `yaml:"kind,omitempty"` + + // Element is the new value to set -- remove the existing element if nil + Element *Node + + // Key is a list of fields on the elements. It is used to find matching elements to + // update / delete + Keys []string + + // 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 -func (e ElementSetter) isMappingNode(node *RNode) bool { +func (e ElementSetterList) isMappingNode(node *RNode) bool { return ErrorIfInvalid(node, yaml.MappingNode) == nil } // isMappingSetter returns is this setter intended to set a mapping node -func (e ElementSetter) isMappingSetter() bool { - return e.Key != "" && e.Value != "" +func (e ElementSetterList) isMappingSetter() bool { + return len(e.Keys) > 0 && e.Keys[0] != "" && + len(e.Values) > 0 && e.Values[0] != "" } -func (e ElementSetter) Filter(rn *RNode) (*RNode, error) { +// the main difference between this Filter and the ElementSetter Filter +// is that here we must iterate through all the key-value pairs +func (e ElementSetterList) Filter(rn *RNode) (*RNode, error) { if err := ErrorIfInvalid(rn, SequenceNode); err != nil { return nil, err } @@ -96,11 +127,18 @@ 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 + found := true + for j, key := range e.Keys { + val, err := newNode.Pipe(FieldMatcher{Name: key, 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) continue @@ -242,18 +280,54 @@ type ElementMatcher struct { } func (e ElementMatcher) Filter(rn *RNode) (*RNode, error) { + return rn.Pipe(ElementMatcherList{ + Kind: e.Kind, + Keys: []string{e.FieldName}, + Values: []string{e.FieldValue}, + Create: e.Create, + MatchAnyValue: e.MatchAnyValue, + }) +} + +func MatchElementList(keys []string, values []string) ElementMatcherList { + return ElementMatcherList{Keys: keys, Values: values} +} + +// ElementMatcherList returns the first element from a Sequence matching all +// specified key, value pairs (as opposed to ElementMatcher, which matches +// a single key, value pair). If there's no match, and no configuration error, +// the matcher returns nil, nil. +type ElementMatcherList struct { + Kind string `yaml:"kind,omitempty"` + + // Keys are the list of fields upon which to match this element. + Keys []string + + // 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 + // set to true. + MatchAnyValue bool `yaml:"noValue,omitempty"` +} + +func (e ElementMatcherList) Filter(rn *RNode) (*RNode, error) { if err := ErrorIfInvalid(rn, yaml.SequenceNode); err != nil { return nil, err } - if e.MatchAnyValue && e.FieldValue != "" { + if e.MatchAnyValue && len(e.Values) != 0 && e.Values[0] != "" { return nil, fmt.Errorf("FieldValue must be empty when NoValue 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 +342,28 @@ 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)) + matchesElement := true + for i, key := range e.Keys { + if e.MatchAnyValue { + field, err = elem.Pipe(Get(key)) + } else { + field, err = elem.Pipe(MatchField(key, e.Values[i])) + } + if !IsFoundOrError(field, err) { + // this is not the element we are looking for + matchesElement = false + break + } } - if IsFoundOrError(field, err) { + if matchesElement { return elem, err } } @@ -351,7 +433,7 @@ func (f FieldMatcher) Filter(rn *RNode) (*RNode, error) { return rn, nil } return nil, nil - case rn.value.Value == f.Value.YNode().Value: + case f.Value.YNode() != nil && rn.value.Value == f.Value.YNode().Value: return rn, nil default: return nil, nil @@ -404,7 +486,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. diff --git a/kyaml/yaml/fns_test.go b/kyaml/yaml/fns_test.go index 1b8628c97..4a060233a 100644 --- a/kyaml/yaml/fns_test.go +++ b/kyaml/yaml/fns_test.go @@ -217,6 +217,133 @@ func TestElementSetter(t *testing.T) { `, assertNoErrorString(t)(node.String())) } +func TestElementSetterList(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(ElementSetterList{ + 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(ElementSetterList{ + 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(ElementSetterList{ + 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(ElementSetterList{ + 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(ElementSetterList{ + 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(ElementSetterList{ + 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(ElementSetterList{ + 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 @@ -239,6 +366,48 @@ func TestElementMatcherWithNoValue(t *testing.T) { _, err = node.Pipe(ElementMatcher{FieldName: "a", FieldValue: "c", MatchAnyValue: true}) assert.Errorf(t, err, "FieldValue must be empty when NoValue is set to true") } + +func TestElementMatcherList(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)