mirror of
https://github.com/kubernetes-sigs/kustomize.git
synced 2026-05-17 18:25:26 +00:00
Add regex support for Replacement selectors (#5863)
* feat: Add regex support for Replacement selectors * Add new tests for regex support * Earlier exit with rejectAny, and fix linting * Add example Use cases using regex
This commit is contained in:
@@ -11,7 +11,6 @@ import (
|
|||||||
"sigs.k8s.io/kustomize/api/resource"
|
"sigs.k8s.io/kustomize/api/resource"
|
||||||
"sigs.k8s.io/kustomize/api/types"
|
"sigs.k8s.io/kustomize/api/types"
|
||||||
"sigs.k8s.io/kustomize/kyaml/errors"
|
"sigs.k8s.io/kustomize/kyaml/errors"
|
||||||
"sigs.k8s.io/kustomize/kyaml/resid"
|
|
||||||
kyaml_utils "sigs.k8s.io/kustomize/kyaml/utils"
|
kyaml_utils "sigs.k8s.io/kustomize/kyaml/utils"
|
||||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||||
)
|
)
|
||||||
@@ -117,6 +116,10 @@ func applyReplacement(nodes []*yaml.RNode, value *yaml.RNode, targetSelectors []
|
|||||||
if len(selector.FieldPaths) == 0 {
|
if len(selector.FieldPaths) == 0 {
|
||||||
selector.FieldPaths = []string{types.DefaultReplacementFieldPath}
|
selector.FieldPaths = []string{types.DefaultReplacementFieldPath}
|
||||||
}
|
}
|
||||||
|
tsr, err := types.NewTargetSelectorRegex(selector)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error creating target selector: %w", err)
|
||||||
|
}
|
||||||
for _, possibleTarget := range nodes {
|
for _, possibleTarget := range nodes {
|
||||||
ids, err := utils.MakeResIds(possibleTarget)
|
ids, err := utils.MakeResIds(possibleTarget)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -132,9 +135,13 @@ func applyReplacement(nodes []*yaml.RNode, value *yaml.RNode, targetSelectors []
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if tsr.RejectsAny(ids) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// filter targets by matching resource IDs
|
// filter targets by matching resource IDs
|
||||||
for _, id := range ids {
|
for _, id := range ids {
|
||||||
if id.IsSelectedBy(selector.Select.ResId) && !containsRejectId(selector.Reject, ids) {
|
if tsr.Selects(id) {
|
||||||
err := copyValueToTarget(possibleTarget, value, selector)
|
err := copyValueToTarget(possibleTarget, value, selector)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -175,20 +182,6 @@ func matchesAnnoAndLabelSelector(n *yaml.RNode, selector *types.Selector) (bool,
|
|||||||
return annoMatch && labelMatch, nil
|
return annoMatch && labelMatch, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func containsRejectId(rejects []*types.Selector, ids []resid.ResId) bool {
|
|
||||||
for _, r := range rejects {
|
|
||||||
if r.ResId.IsEmpty() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for _, id := range ids {
|
|
||||||
if id.IsSelectedBy(r.ResId) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func copyValueToTarget(target *yaml.RNode, value *yaml.RNode, selector *types.TargetSelector) error {
|
func copyValueToTarget(target *yaml.RNode, value *yaml.RNode, selector *types.TargetSelector) error {
|
||||||
for _, fp := range selector.FieldPaths {
|
for _, fp := range selector.FieldPaths {
|
||||||
createKind := yaml.Kind(0) // do not create
|
createKind := yaml.Kind(0) // do not create
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -614,3 +614,391 @@ metadata:
|
|||||||
name: app-config-dev-97544dk6t8
|
name: app-config-dev-97544dk6t8
|
||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// regex selector: append in annotation by visitor name
|
||||||
|
func TestReplacementTransformerAppendToAnnotationUsingRegex(t *testing.T) {
|
||||||
|
th := kusttest_test.MakeEnhancedHarness(t)
|
||||||
|
defer th.Reset()
|
||||||
|
|
||||||
|
th.WriteF("base/app1.yaml", `
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: d1
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- image: app1:1.0
|
||||||
|
name: app
|
||||||
|
`)
|
||||||
|
th.WriteF("base/app2.yaml", `
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: d2
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- image: app2:1.0
|
||||||
|
name: app
|
||||||
|
`)
|
||||||
|
th.WriteF("base/cm1.yaml", `
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: cm1
|
||||||
|
`)
|
||||||
|
th.WriteF("base/cm2.yaml", `
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: cm2
|
||||||
|
`)
|
||||||
|
th.WriteF("base/pg1.yaml", `
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: postgresql
|
||||||
|
metadata:
|
||||||
|
name: pg1
|
||||||
|
`)
|
||||||
|
th.WriteK("base", `
|
||||||
|
resources:
|
||||||
|
- app1.yaml
|
||||||
|
- app2.yaml
|
||||||
|
- cm1.yaml
|
||||||
|
- cm2.yaml
|
||||||
|
- pg1.yaml
|
||||||
|
|
||||||
|
replacements:
|
||||||
|
- source:
|
||||||
|
kind: ConfigMap
|
||||||
|
name: cm1
|
||||||
|
targets:
|
||||||
|
- reject:
|
||||||
|
- kind: ConfigMap
|
||||||
|
name: c.1
|
||||||
|
select:
|
||||||
|
kind: Deployment|ConfigMap|postgresql
|
||||||
|
fieldPaths:
|
||||||
|
- metadata.annotations.visitedby
|
||||||
|
options:
|
||||||
|
index: -1
|
||||||
|
delimiter: ","
|
||||||
|
create: true
|
||||||
|
- source:
|
||||||
|
kind: ConfigMap
|
||||||
|
name: cm2
|
||||||
|
targets:
|
||||||
|
- reject:
|
||||||
|
- kind: ConfigMap
|
||||||
|
name: .*2
|
||||||
|
select:
|
||||||
|
kind: Deployment|ConfigMap|postgresql
|
||||||
|
fieldPaths:
|
||||||
|
- metadata.annotations.visitedby
|
||||||
|
options:
|
||||||
|
index: -1
|
||||||
|
delimiter: ","
|
||||||
|
create: true
|
||||||
|
`)
|
||||||
|
m := th.Run("base", th.MakeDefaultOptions())
|
||||||
|
th.AssertActualEqualsExpected(m, `
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
visitedby: cm2,cm1,
|
||||||
|
name: d1
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- image: app1:1.0
|
||||||
|
name: app
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
visitedby: cm2,cm1,
|
||||||
|
name: d2
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- image: app2:1.0
|
||||||
|
name: app
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
visitedby: cm2,
|
||||||
|
name: cm1
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
visitedby: cm1,
|
||||||
|
name: cm2
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: postgresql
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
visitedby: cm2,cm1,
|
||||||
|
name: pg1
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// selector regex: construct service url
|
||||||
|
func TestReplacementTransformerServiceNamespaceUrlUsingRegex(t *testing.T) {
|
||||||
|
th := kusttest_test.MakeEnhancedHarness(t)
|
||||||
|
defer th.Reset()
|
||||||
|
|
||||||
|
th.WriteF("base/d1.yaml", `
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: d1
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- image: app1:1.0
|
||||||
|
name: app
|
||||||
|
env:
|
||||||
|
- name: APP1_SERVICE
|
||||||
|
value: "d1.app1"
|
||||||
|
`)
|
||||||
|
th.WriteF("base/d2.yaml", `
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: DaemonSet
|
||||||
|
metadata:
|
||||||
|
name: d2
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- image: app1:1.0
|
||||||
|
name: app
|
||||||
|
env:
|
||||||
|
- name: APP1_SERVICE
|
||||||
|
value: "d2.app1"
|
||||||
|
`)
|
||||||
|
th.WriteF("base/sts1.yaml", `
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: StatefulSet
|
||||||
|
metadata:
|
||||||
|
name: sts1
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- image: app1:1.0
|
||||||
|
name: app
|
||||||
|
env:
|
||||||
|
- name: APP1_SERVICE
|
||||||
|
value: "app1"
|
||||||
|
`)
|
||||||
|
th.WriteF("base/cm1.yaml", `
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: cm1
|
||||||
|
data:
|
||||||
|
APP1_SERVICE_PORT: "8080"
|
||||||
|
`)
|
||||||
|
th.WriteF("base/svc1.yaml", `
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: svc1
|
||||||
|
namespace: svc1-namespace
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app.kubernetes.io/name: app1
|
||||||
|
ports:
|
||||||
|
- protocol: TCP
|
||||||
|
port: 80
|
||||||
|
targetPort: 9376
|
||||||
|
|
||||||
|
`)
|
||||||
|
th.WriteK("base", `
|
||||||
|
resources:
|
||||||
|
- d1.yaml
|
||||||
|
- d2.yaml
|
||||||
|
- sts1.yaml
|
||||||
|
- cm1.yaml
|
||||||
|
- svc1.yaml
|
||||||
|
|
||||||
|
replacements:
|
||||||
|
- source:
|
||||||
|
kind: Service
|
||||||
|
name: svc1
|
||||||
|
fieldPath: metadata.namespace
|
||||||
|
targets:
|
||||||
|
- select:
|
||||||
|
kind: Deployment|.*Set
|
||||||
|
fieldPaths:
|
||||||
|
- spec.template.spec.containers.*.env.[name=APP1_SERVICE].value
|
||||||
|
options:
|
||||||
|
index: 99
|
||||||
|
delimiter: "."
|
||||||
|
- source:
|
||||||
|
kind: ConfigMap
|
||||||
|
name: cm1
|
||||||
|
fieldPath: data.APP1_SERVICE_PORT
|
||||||
|
targets:
|
||||||
|
- select:
|
||||||
|
kind: Deployment|.*Set
|
||||||
|
fieldPaths:
|
||||||
|
- spec.template.spec.containers.*.env.[name=APP1_SERVICE].value
|
||||||
|
options:
|
||||||
|
index: 99
|
||||||
|
delimiter: ":"
|
||||||
|
`)
|
||||||
|
m := th.Run("base", th.MakeDefaultOptions())
|
||||||
|
th.AssertActualEqualsExpected(m, `
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: d1
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- env:
|
||||||
|
- name: APP1_SERVICE
|
||||||
|
value: d1.app1.svc1-namespace:8080
|
||||||
|
image: app1:1.0
|
||||||
|
name: app
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: DaemonSet
|
||||||
|
metadata:
|
||||||
|
name: d2
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- env:
|
||||||
|
- name: APP1_SERVICE
|
||||||
|
value: d2.app1.svc1-namespace:8080
|
||||||
|
image: app1:1.0
|
||||||
|
name: app
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: StatefulSet
|
||||||
|
metadata:
|
||||||
|
name: sts1
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- env:
|
||||||
|
- name: APP1_SERVICE
|
||||||
|
value: app1.svc1-namespace:8080
|
||||||
|
image: app1:1.0
|
||||||
|
name: app
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
data:
|
||||||
|
APP1_SERVICE_PORT: "8080"
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: cm1
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: svc1
|
||||||
|
namespace: svc1-namespace
|
||||||
|
spec:
|
||||||
|
ports:
|
||||||
|
- port: 80
|
||||||
|
protocol: TCP
|
||||||
|
targetPort: 9376
|
||||||
|
selector:
|
||||||
|
app.kubernetes.io/name: app1
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReplacementTransformerWithSuffixTransformerAndRejectUsingRegex(t *testing.T) {
|
||||||
|
th := kusttest_test.MakeEnhancedHarness(t)
|
||||||
|
defer th.Reset()
|
||||||
|
|
||||||
|
th.WriteF("base/app.yaml", `
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: original-name
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- image: app1:1.0
|
||||||
|
name: app
|
||||||
|
`)
|
||||||
|
th.WriteK("base", `
|
||||||
|
resources:
|
||||||
|
- app.yaml
|
||||||
|
`)
|
||||||
|
th.WriteK("overlay", `
|
||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
|
||||||
|
nameSuffix: -dev
|
||||||
|
namePrefix: pre-
|
||||||
|
resources:
|
||||||
|
- ../base
|
||||||
|
|
||||||
|
configMapGenerator:
|
||||||
|
- name: app-config
|
||||||
|
literals:
|
||||||
|
- name=something-else
|
||||||
|
|
||||||
|
replacements:
|
||||||
|
- source:
|
||||||
|
kind: ConfigMap
|
||||||
|
name: app-config
|
||||||
|
fieldPath: data.name
|
||||||
|
targets:
|
||||||
|
- reject:
|
||||||
|
- name: .*original.*
|
||||||
|
select:
|
||||||
|
kind: Deployment
|
||||||
|
fieldPaths:
|
||||||
|
- spec.template.spec.containers.0.name
|
||||||
|
- select:
|
||||||
|
kind: ConfigMap
|
||||||
|
name: app-config
|
||||||
|
fieldPaths:
|
||||||
|
- data.name-copy
|
||||||
|
options:
|
||||||
|
create: true
|
||||||
|
`)
|
||||||
|
m := th.Run("overlay", th.MakeDefaultOptions())
|
||||||
|
th.AssertActualEqualsExpected(m, `
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: pre-original-name-dev
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- image: app1:1.0
|
||||||
|
name: app
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
data:
|
||||||
|
name: something-else
|
||||||
|
name-copy: something-else
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: pre-app-config-dev-7266b7f2m9
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|||||||
@@ -66,6 +66,53 @@ type TargetSelector struct {
|
|||||||
Options *FieldOptions `json:"options,omitempty" yaml:"options,omitempty"`
|
Options *FieldOptions `json:"options,omitempty" yaml:"options,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TargetSelectorRegex struct {
|
||||||
|
targetSelector *TargetSelector
|
||||||
|
selectRegex *SelectorRegex
|
||||||
|
rejectRegex []*SelectorRegex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTargetSelectorRegex(ts *TargetSelector) (*TargetSelectorRegex, error) {
|
||||||
|
tsr := new(TargetSelectorRegex)
|
||||||
|
tsr.targetSelector = ts
|
||||||
|
var err error
|
||||||
|
|
||||||
|
tsr.selectRegex, err = NewSelectorRegex(ts.Select)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rej := []*SelectorRegex{}
|
||||||
|
for _, r := range ts.Reject {
|
||||||
|
rr, err := NewSelectorRegex(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rej = append(rej, rr)
|
||||||
|
}
|
||||||
|
tsr.rejectRegex = rej
|
||||||
|
|
||||||
|
return tsr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tsr *TargetSelectorRegex) Selects(id resid.ResId) bool {
|
||||||
|
return tsr.selectRegex.MatchGvk(id.Gvk) && tsr.selectRegex.MatchName(id.Name) && tsr.selectRegex.MatchNamespace(id.Namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tsr *TargetSelectorRegex) RejectsAny(ids []resid.ResId) bool {
|
||||||
|
for _, r := range tsr.rejectRegex {
|
||||||
|
if r.selector.ResId.IsEmpty() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, id := range ids {
|
||||||
|
if r.MatchGvk(id.Gvk) && r.MatchName(id.Name) && r.MatchNamespace(id.Namespace) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// FieldOptions refine the interpretation of FieldPaths.
|
// FieldOptions refine the interpretation of FieldPaths.
|
||||||
type FieldOptions struct {
|
type FieldOptions struct {
|
||||||
// Used to split/join the field.
|
// Used to split/join the field.
|
||||||
|
|||||||
Reference in New Issue
Block a user