mirror of
https://github.com/kubernetes-sigs/kustomize.git
synced 2026-06-13 01:50:55 +00:00
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:
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user