Wildcard support for creation in ReplacementTransformer (#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
This commit is contained in:
Katrina Verey
2022-12-06 15:40:37 -05:00
committed by GitHub
parent 2ed910abb0
commit 903fbb6ed2
5 changed files with 661 additions and 73 deletions

View File

@@ -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))
}
for _, t := range targetFields {
if err := setFieldValue(selector.Options, t, value); err != nil {
return err
}
}
}
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)
func fieldRetrievalError(fieldPath string, isCreate bool) string {
if isCreate {
return fmt.Sprintf("unable to find or create field %q in replacement target", fieldPath)
}
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
}
}
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")
}
}
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 {

View File

@@ -9,7 +9,7 @@ import (
"github.com/stretchr/testify/assert"
filtertest "sigs.k8s.io/kustomize/api/testutils/filtertest"
"sigs.k8s.io/yaml"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
func TestFilter(t *testing.T) {
@@ -1479,6 +1479,85 @@ spec:
value: sample-deploy
- name: foo
value: bar
- image: nginx
name: sidecar
env:
- name: deployment-name
value: sample-deploy`,
},
"one replacements target should create multiple values": {
input: `apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: sample-deploy
name: sample-deploy
spec:
replicas: 1
selector:
matchLabels:
app: sample-deploy
template:
metadata:
labels:
app: sample-deploy
spec:
containers:
- image: other
name: do-not-modify-me
env:
- name: foo
value: bar
- image: nginx
name: main
env:
- name: foo
value: bar
- image: nginx
name: sidecar
`,
replacements: `replacements:
- source:
kind: Deployment
name: sample-deploy
fieldPath: metadata.name
targets:
- select:
kind: Deployment
options:
create: true
fieldPaths:
- spec.template.spec.containers.[image=nginx].env.[name=deployment-name].value
`,
expected: `apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: sample-deploy
name: sample-deploy
spec:
replicas: 1
selector:
matchLabels:
app: sample-deploy
template:
metadata:
labels:
app: sample-deploy
spec:
containers:
- image: other
name: do-not-modify-me
env:
- name: foo
value: bar
- image: nginx
name: main
env:
- name: foo
value: bar
- name: deployment-name
value: sample-deploy
- image: nginx
name: sidecar
env:
@@ -1652,7 +1731,110 @@ spec:
options:
create: true
`,
expectedErr: "cannot support create option in a multi-value target",
expected: `apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: sample-deploy
name: sample-deploy
spec:
replicas: 1
selector:
matchLabels:
app: sample-deploy
template:
metadata:
labels:
app: sample-deploy
spec:
containers:
- image: nginx
name: main
env:
- name: other-env
value: YYYYY
- name: deployment-name
value: sample-deploy
`,
},
"Issue 1493: wildcard to create or replace field in all containers in all workloads": {
input: `apiVersion: v1
kind: ConfigMap
metadata:
name: policy
data:
restart: OnFailure
---
apiVersion: v1
kind: Pod
metadata:
name: pod1
spec:
containers:
- image: nginx
name: main
- image: nginx
name: sidecar
imagePullPolicy: Always
---
apiVersion: v1
kind: Pod
metadata:
name: pod2
spec:
containers:
- image: nginx
name: main
imagePullPolicy: Always
- image: nginx
name: sidecar
`,
replacements: `replacements:
- source:
kind: ConfigMap
name: policy
fieldPath: data.restart
targets:
- select:
kind: Pod
fieldPaths:
- spec.containers.*.imagePullPolicy
options:
create: true
`,
expected: `apiVersion: v1
kind: ConfigMap
metadata:
name: policy
data:
restart: OnFailure
---
apiVersion: v1
kind: Pod
metadata:
name: pod1
spec:
containers:
- image: nginx
name: main
imagePullPolicy: OnFailure
- image: nginx
name: sidecar
imagePullPolicy: OnFailure
---
apiVersion: v1
kind: Pod
metadata:
name: pod2
spec:
containers:
- image: nginx
name: main
imagePullPolicy: OnFailure
- image: nginx
name: sidecar
imagePullPolicy: OnFailure
`,
},
"multiple field paths in target": {
input: `apiVersion: v1
@@ -2435,7 +2617,37 @@ replacements:
options:
create: true
`,
expectedErr: "unable to find or create field spec.configPatches.2.match.proxy.proxyVersion in replacement target",
expected: `
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: request-id
spec:
configPatches:
- applyTo: NETWORK_FILTER
match:
proxy:
proxyVersion: ^1\.14.*
- applyTo: NETWORK_FILTER
match:
proxy:
proxyVersion: ^1\.14.*
- match:
proxy:
proxyVersion: ^1\.14.*
- match:
proxy:
proxyVersion: ^1\.14.*
---
apiVersion: v1
kind: ConfigMap
metadata:
name: istio-version
annotations:
config.kubernetes.io/local-config: true
data:
ISTIO_REGEX: '^1\.14.*'
`,
},
"issue4761 - path not in target with create: false": {
input: `
@@ -2475,7 +2687,148 @@ replacements:
options:
create: false
`,
expectedErr: "unable to find field spec.configPatches.0.match.proxy.proxyVersion in replacement target",
expectedErr: "unable to find field \"spec.configPatches.0.match.proxy.proxyVersion\" in replacement target",
},
"issue4761 - wildcard solution": {
input: `
---
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: request-id
spec:
configPatches:
- applyTo: NETWORK_FILTER
- applyTo: NETWORK_FILTER
---
apiVersion: v1
kind: ConfigMap
metadata:
name: istio-version
annotations:
config.kubernetes.io/local-config: true
data:
ISTIO_REGEX: '^1\.14.*'
`,
replacements: `
replacements:
- source:
kind: ConfigMap
name: istio-version
fieldPath: data.ISTIO_REGEX
targets:
- select:
kind: EnvoyFilter
fieldPaths:
- spec.configPatches.*.match.proxy.proxyVersion
options:
create: true
`,
expected: `
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: request-id
spec:
configPatches:
- applyTo: NETWORK_FILTER
match:
proxy:
proxyVersion: ^1\.14.*
- applyTo: NETWORK_FILTER
match:
proxy:
proxyVersion: ^1\.14.*
---
apiVersion: v1
kind: ConfigMap
metadata:
name: istio-version
annotations:
config.kubernetes.io/local-config: true
data:
ISTIO_REGEX: '^1\.14.*'
`,
},
"append to sequence using index": {
input: `apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: myingress
spec:
rules:
- host: myingress.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-svc
port:
number: 80
`,
replacements: `replacements:
- source:
kind: Ingress
name: myingress
fieldPath: spec.rules.0.host
targets:
- select:
kind: Ingress
name: myingress
fieldPaths:
- spec.tls.0.hosts.0
- spec.tls.0.secretName
options:
create: true
`,
expected: `apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: myingress
spec:
rules:
- host: myingress.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-svc
port:
number: 80
tls:
- hosts:
- myingress.example.com
secretName: myingress.example.com`,
},
"fail to append to sequence using a distant index": {
input: `apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: myingress
spec:
rules:
- host: myingress.example.com
`,
replacements: `replacements:
- source:
kind: Ingress
name: myingress
fieldPath: spec.rules.0.host
targets:
- select:
kind: Ingress
name: myingress
fieldPaths:
- spec.tls.5.hosts.5
- spec.tls.5.secretName
options:
create: true
`,
expectedErr: "unable to find or create field \"spec.tls.5.hosts.5\" in replacement target: index 5 specified but only 0 elements found",
},
}