Skip to content

Commit

Permalink
Wildcard support for creation in ReplacementTransformer (kubernetes-s…
Browse files Browse the repository at this point in the history
…igs#4886)

* Ahead-of-time wildcard path expansion solution

* Wrapped PathGetter solution

This approach doesn't work when multiple existing sequence elements
should match, i.e. because the sequence contains maps and we're
searching on a key they all contain (target all containers with a certain
image would be one use case for this). PathGetter just takes the first
match in that case, which is not what we want.

* Add creation support to PathMatcher

* Regression test for existing bug when creation is enabled and sequence query should match multiple elements

* PathMatcher Create tests and support for sequence appending

* revert hyphen append support

PathGetter treats it as meaning 'last' not 'append' and does not have test coverage for its handling of this when create is set. Semantics are dubious given that multiple Replacement fieldPaths may be specified, which would cause successive appends.

* This also provides a solution to issue 1493

* Review feedback
  • Loading branch information
KnVerey authored and Cailyn Edwards committed Jan 11, 2023
1 parent 93fe3a7 commit 1ef5edf
Show file tree
Hide file tree
Showing 5 changed files with 659 additions and 71 deletions.
69 changes: 22 additions & 47 deletions api/filters/replacement/replacement.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,63 +179,38 @@ func rejectId(rejects []*types.Selector, id *resid.ResId) bool {

func copyValueToTarget(target *yaml.RNode, value *yaml.RNode, selector *types.TargetSelector) error {
for _, fp := range selector.FieldPaths {
mutator := updateMatchingFields
createKind := yaml.Kind(0) // do not create
if selector.Options != nil && selector.Options.Create {
mutator = createOrUpdateOneField
createKind = value.YNode().Kind
}
if err := mutator(target, value, selector.Options, fp); err != nil {
return err
targetFieldList, err := target.Pipe(&yaml.PathMatcher{
Path: kyaml_utils.SmarterPathSplitter(fp, "."),
Create: createKind})
if err != nil {
return errors.WrapPrefixf(err, fieldRetrievalError(fp, createKind != 0))
}
targetFields, err := targetFieldList.Elements()
if err != nil {
return errors.WrapPrefixf(err, fieldRetrievalError(fp, createKind != 0))
}
if len(targetFields) == 0 {
return errors.Errorf(fieldRetrievalError(fp, createKind != 0))
}
}
return nil
}

// updateMatchingFields updates all fields in target that match the given field path.
// If the field path does not already exist in the target, an error is returned.
func updateMatchingFields(target *yaml.RNode, value *yaml.RNode, opts *types.FieldOptions, fp string) error {
fieldPath := kyaml_utils.SmarterPathSplitter(fp, ".")
// may return multiple fields, always wrapped in a sequence node
foundFieldSequence, lookupErr := target.Pipe(&yaml.PathMatcher{Path: fieldPath})
if lookupErr != nil {
return fmt.Errorf("error finding field in replacement target: %w", lookupErr)
}
targetFields, err := foundFieldSequence.Elements()
if err != nil {
return fmt.Errorf("error fetching elements in replacement target: %w", err)
}
if len(targetFields) == 0 {
return errors.Errorf("unable to find field %s in replacement target", fp)
}

for _, t := range targetFields {
if err := setFieldValue(opts, t, value); err != nil {
return err
for _, t := range targetFields {
if err := setFieldValue(selector.Options, t, value); err != nil {
return err
}
}
}
return nil
}

// createOrUpdateOneField updates the field in the target that matches the given field path.
// If the field path does not already exist in the target, it is created.
// Wildcard matching is not supported, nor is creating intermediate list nodes.
func createOrUpdateOneField(target *yaml.RNode, value *yaml.RNode, opts *types.FieldOptions, fp string) error {
fieldPath := kyaml_utils.SmarterPathSplitter(fp, ".")
for _, f := range fieldPath {
if f == "*" {
return fmt.Errorf("cannot support create option in a multi-value target")
}
func fieldRetrievalError(fieldPath string, isCreate bool) string {
if isCreate {
return fmt.Sprintf("unable to find or create field %q in replacement target", fieldPath)
}
createdField, createErr := target.Pipe(yaml.LookupCreate(value.YNode().Kind, fieldPath...))
if createErr != nil {
return fmt.Errorf("error creating replacement node: %w", createErr)
}
if createdField == nil {
return errors.Errorf("unable to find or create field %s in replacement target", fp)
}
if err := setFieldValue(opts, createdField, value); err != nil {
return errors.WrapPrefixf(err, "unable to set field %s in replacement target", fp)
}
return nil
return fmt.Sprintf("unable to find field %q in replacement target", fieldPath)
}

func setFieldValue(options *types.FieldOptions, targetField *yaml.RNode, value *yaml.RNode) error {
Expand Down

0 comments on commit 1ef5edf

Please sign in to comment.