From 8f75c39e9de8214b21ddd2e8cd9466e2533dc4b0 Mon Sep 17 00:00:00 2001 From: Samir Saada Date: Sun, 1 Dec 2024 21:06:47 +0100 Subject: [PATCH 1/4] feat: Add regex support for Replacement selectors --- api/filters/replacement/replacement.go | 21 +- api/filters/replacement/replacement_test.go | 297 ++++++++++++++++++++ api/types/replacement.go | 47 ++++ 3 files changed, 349 insertions(+), 16 deletions(-) diff --git a/api/filters/replacement/replacement.go b/api/filters/replacement/replacement.go index a988b60e8d..14795c4ec5 100644 --- a/api/filters/replacement/replacement.go +++ b/api/filters/replacement/replacement.go @@ -11,7 +11,6 @@ import ( "sigs.k8s.io/kustomize/api/resource" "sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/kustomize/kyaml/errors" - "sigs.k8s.io/kustomize/kyaml/resid" kyaml_utils "sigs.k8s.io/kustomize/kyaml/utils" "sigs.k8s.io/kustomize/kyaml/yaml" ) @@ -110,6 +109,10 @@ func applyReplacement(nodes []*yaml.RNode, value *yaml.RNode, targetSelectors [] if len(selector.FieldPaths) == 0 { selector.FieldPaths = []string{types.DefaultReplacementFieldPath} } + tsr, err := types.NewTargetSelectorRegex(selector) + if err != nil { + return nil, err + } for _, possibleTarget := range nodes { ids, err := utils.MakeResIds(possibleTarget) if err != nil { @@ -127,7 +130,7 @@ func applyReplacement(nodes []*yaml.RNode, value *yaml.RNode, targetSelectors [] // filter targets by matching resource IDs for _, id := range ids { - if id.IsSelectedBy(selector.Select.ResId) && !containsRejectId(selector.Reject, ids) { + if tsr.Selects(id) && !tsr.RejectsAny(ids) { err := copyValueToTarget(possibleTarget, value, selector) if err != nil { return nil, err @@ -168,20 +171,6 @@ func matchesAnnoAndLabelSelector(n *yaml.RNode, selector *types.Selector) (bool, 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 { for _, fp := range selector.FieldPaths { createKind := yaml.Kind(0) // do not create diff --git a/api/filters/replacement/replacement_test.go b/api/filters/replacement/replacement_test.go index 7619e5fc9d..acec37372c 100644 --- a/api/filters/replacement/replacement_test.go +++ b/api/filters/replacement/replacement_test.go @@ -2830,6 +2830,303 @@ spec: `, expectedErr: "unable to find or create field \"spec.tls.5.hosts.5\" in replacement target: index 5 specified but only 0 elements found", }, + +"reject 1 with regex": { + input: `apiVersion: v1 +kind: Deployment +metadata: + name: deploy1 +spec: + template: + spec: + containers: + - image: nginx:1.7.9 + name: nginx-tagged + - image: postgres:1.8.0 + name: postgresdb +--- +apiVersion: v1 +kind: Deployment +metadata: + name: deploy2 +spec: + template: + spec: + containers: + - image: nginx:1.7.9 + name: nginx-tagged + - image: postgres:1.8.0 + name: postgresdb +--- +apiVersion: v1 +kind: Deployment +metadata: + name: deploy3 +spec: + template: + spec: + containers: + - image: nginx:1.7.9 + name: nginx-tagged + - image: postgres:1.8.0 + name: postgresdb +`, + replacements: `replacements: +- source: + kind: Deployment + name: deploy2 + fieldPath: spec.template.spec.containers.0.image + targets: + - select: + kind: Deploy.* + reject: + - name: .*ploy2|deploy3 + fieldPaths: + - spec.template.spec.containers.1.image +`, + expected: `apiVersion: v1 +kind: Deployment +metadata: + name: deploy1 +spec: + template: + spec: + containers: + - image: nginx:1.7.9 + name: nginx-tagged + - image: nginx:1.7.9 + name: postgresdb +--- +apiVersion: v1 +kind: Deployment +metadata: + name: deploy2 +spec: + template: + spec: + containers: + - image: nginx:1.7.9 + name: nginx-tagged + - image: postgres:1.8.0 + name: postgresdb +--- +apiVersion: v1 +kind: Deployment +metadata: + name: deploy3 +spec: + template: + spec: + containers: + - image: nginx:1.7.9 + name: nginx-tagged + - image: postgres:1.8.0 + name: postgresdb +`, + }, + "reject 2 with regex": { + input: `apiVersion: v1 +kind: Deployment +metadata: + name: my-name +spec: + template: + spec: + containers: + - image: nginx:1.7.9 + name: nginx-tagged + - image: postgres:1.8.0 + name: postgresdb +--- +apiVersion: v1 +kind: StatefulSet +metadata: + name: my-name +spec: + template: + spec: + containers: + - image: nginx:1.7.9 + name: nginx-tagged + - image: postgres:1.8.0 + name: postgresdb +`, + replacements: `replacements: +- source: + kind: Deployment + fieldPath: spec.template.spec.containers.0.image + targets: + - select: + version: v1 + reject: + - kind: Deplo.* + name: my-name + fieldPaths: + - spec.template.spec.containers.1.image +`, + expected: `apiVersion: v1 +kind: Deployment +metadata: + name: my-name +spec: + template: + spec: + containers: + - image: nginx:1.7.9 + name: nginx-tagged + - image: postgres:1.8.0 + name: postgresdb +--- +apiVersion: v1 +kind: StatefulSet +metadata: + name: my-name +spec: + template: + spec: + containers: + - image: nginx:1.7.9 + name: nginx-tagged + - image: nginx:1.7.9 + name: postgresdb +`, + }, + // the only difference in the inputs between this and the previous test + // is the dash before `name: my-name` + "reject 3 with regex": { + input: `apiVersion: v1 +kind: Deployment +metadata: + name: my-name +spec: + template: + spec: + containers: + - image: nginx:1.7.9 + name: nginx-tagged + - image: postgres:1.8.0 + name: postgresdb +--- +apiVersion: v1 +kind: StatefulSet +metadata: + name: my-name +spec: + template: + spec: + containers: + - image: nginx:1.7.9 + name: nginx-tagged + - image: postgres:1.8.0 + name: postgresdb +`, + replacements: `replacements: +- source: + kind: Deployment + fieldPath: spec.template.spec.containers.0.image + targets: + - select: + version: v1 + reject: + - kind: Deployment + - name: my.n.me + fieldPaths: + - spec.template.spec.containers.1.image +`, + expected: `apiVersion: v1 +kind: Deployment +metadata: + name: my-name +spec: + template: + spec: + containers: + - image: nginx:1.7.9 + name: nginx-tagged + - image: postgres:1.8.0 + name: postgresdb +--- +apiVersion: v1 +kind: StatefulSet +metadata: + name: my-name +spec: + template: + spec: + containers: + - image: nginx:1.7.9 + name: nginx-tagged + - image: postgres:1.8.0 + name: postgresdb +`, + }, + "simple with regex": { + input: `apiVersion: v1 +kind: Deployment +metadata: + name: deploy +spec: + template: + spec: + containers: + - image: nginx:1.7.9 + name: nginx-tagged + - image: postgres:1.8.0 + name: postgresdb +--- +kind: StatefulSet +metadata: + name: deploy +spec: + template: + spec: + containers: + - image: nginx:1.7.9 + name: nginx-tagged + - image: postgres:1.8.0 + name: postgresdb + +`, + replacements: `replacements: +- source: + kind: Deployment + name: deploy + fieldPath: spec.template.spec.containers.0.image + targets: + - select: + kind: Deployment|StatefulSet + name: deploy + fieldPaths: + - spec.template.spec.containers.1.image +`, + expected: `apiVersion: v1 +kind: Deployment +metadata: + name: deploy +spec: + template: + spec: + containers: + - image: nginx:1.7.9 + name: nginx-tagged + - image: nginx:1.7.9 + name: postgresdb +--- +kind: StatefulSet +metadata: + name: deploy +spec: + template: + spec: + containers: + - image: nginx:1.7.9 + name: nginx-tagged + - image: nginx:1.7.9 + name: postgresdb + +`, + }, + } for tn, tc := range testCases { diff --git a/api/types/replacement.go b/api/types/replacement.go index cb4163429a..016770dfab 100644 --- a/api/types/replacement.go +++ b/api/types/replacement.go @@ -63,6 +63,53 @@ type TargetSelector struct { 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. type FieldOptions struct { // Used to split/join the field. From f1af7ad13e594a8c7c997f91eb85d3f0063a51ac Mon Sep 17 00:00:00 2001 From: Samir Saada Date: Fri, 4 Apr 2025 19:58:52 +0200 Subject: [PATCH 2/4] Add new tests for regex support --- api/filters/replacement/replacement_test.go | 1284 ++++++++++++++++++- api/krusty/replacementtransformer_test.go | 78 ++ 2 files changed, 1361 insertions(+), 1 deletion(-) diff --git a/api/filters/replacement/replacement_test.go b/api/filters/replacement/replacement_test.go index acec37372c..2b12c7c5ac 100644 --- a/api/filters/replacement/replacement_test.go +++ b/api/filters/replacement/replacement_test.go @@ -2831,7 +2831,1070 @@ spec: expectedErr: "unable to find or create field \"spec.tls.5.hosts.5\" in replacement target: index 5 specified but only 0 elements found", }, -"reject 1 with regex": { + // -- regex -- + "select 1 from many using regex": { + input: ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: cm +data: + PORT: 8080 +--- +kind: Deployment +metadata: + name: deploy1 +spec: + template: + spec: + containers: + - image: nginx:1 + name: nginx1 + args: + - PORT +--- +kind: Deployment +metadata: + name: deploy2 +spec: + template: + spec: + containers: + - image: nginx:2 + name: nginx1 + args: + - PORT +--- +kind: Deployment +metadata: + name: deploy3 +spec: + template: + spec: + containers: + - image: nginx:3 + name: nginx1 + args: + - PORT +`, + replacements: `replacements: +- source: + kind: ConfigMap + name: cm + fieldPath: data.PORT + targets: + - select: + name: d.*2 + fieldPaths: + - spec.template.spec.containers.0.args.0 +`, + expected: ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: cm +data: + PORT: 8080 +--- +kind: Deployment +metadata: + name: deploy1 +spec: + template: + spec: + containers: + - image: nginx:1 + name: nginx1 + args: + - PORT +--- +kind: Deployment +metadata: + name: deploy2 +spec: + template: + spec: + containers: + - image: nginx:2 + name: nginx1 + args: + - "8080" +--- +kind: Deployment +metadata: + name: deploy3 +spec: + template: + spec: + containers: + - image: nginx:3 + name: nginx1 + args: + - PORT +`, + }, + + "select 2 among 3 using regex": { + input: ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: cm +data: + PORT: 8080 +--- +kind: Deployment +metadata: + name: deploy1 +spec: + template: + spec: + containers: + - image: nginx:1 + name: nginx1 + args: + - PORT +--- +kind: Deployment +metadata: + name: deploy2 +spec: + template: + spec: + containers: + - image: nginx:2 + name: nginx1 + args: + - PORT +--- +kind: Deployment +metadata: + name: deploy3 +spec: + template: + spec: + containers: + - image: nginx:3 + name: nginx1 + args: + - PORT +`, + replacements: `replacements: +- source: + kind: ConfigMap + name: cm + fieldPath: data.PORT + targets: + - select: + name: d.*[1-2] + fieldPaths: + - spec.template.spec.containers.0.args.0 +`, + expected: ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: cm +data: + PORT: 8080 +--- +kind: Deployment +metadata: + name: deploy1 +spec: + template: + spec: + containers: + - image: nginx:1 + name: nginx1 + args: + - "8080" +--- +kind: Deployment +metadata: + name: deploy2 +spec: + template: + spec: + containers: + - image: nginx:2 + name: nginx1 + args: + - "8080" +--- +kind: Deployment +metadata: + name: deploy3 +spec: + template: + spec: + containers: + - image: nginx:3 + name: nginx1 + args: + - PORT +`, + }, + + "select 2 among 3 and create one field using regex": { + input: ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: cm +data: + HOST: "server.svc" +--- +kind: Deployment +metadata: + name: deploy1 +spec: + template: + spec: + containers: + - image: nginx:1 + name: nginx1 + args: + - HOST +--- +kind: Deployment +metadata: + name: deploy2 +spec: + template: + spec: + containers: + - image: nginx:2 + name: nginx1 + args: + - HOST +--- +kind: Deployment +metadata: + name: deploy3 +spec: + template: + spec: + containers: + - image: nginx:3 + name: nginx1 +`, + replacements: `replacements: +- source: + kind: ConfigMap + name: cm + fieldPath: data.HOST + targets: + - select: + name: d.*[2-3] + fieldPaths: + - spec.template.spec.containers.0.args.0 + options: + create: true +`, + expected: ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: cm +data: + HOST: "server.svc" +--- +kind: Deployment +metadata: + name: deploy1 +spec: + template: + spec: + containers: + - image: nginx:1 + name: nginx1 + args: + - HOST +--- +kind: Deployment +metadata: + name: deploy2 +spec: + template: + spec: + containers: + - image: nginx:2 + name: nginx1 + args: + - server.svc +--- +kind: Deployment +metadata: + name: deploy3 +spec: + template: + spec: + containers: + - image: nginx:3 + name: nginx1 + args: + - server.svc +`, + }, + + "select 2 of 3 and create all the fields using regex": { + input: ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: cm +data: + HOST: "server.svc" +--- +kind: Deployment +metadata: + name: deploy1 +spec: + template: + spec: + containers: + - image: nginx:1 + name: nginx1 +--- +kind: Deployment +metadata: + name: deploy2 +spec: + template: + spec: + containers: + - image: nginx:2 + name: nginx1 +--- +kind: Deployment +metadata: + name: deploy3 +spec: + template: + spec: + containers: + - image: nginx:3 + name: nginx1 +`, + replacements: `replacements: +- source: + kind: ConfigMap + name: cm + fieldPath: data.HOST + targets: + - select: + name: d.*[2-3] + fieldPaths: + - spec.template.spec.containers.0.args.0 + options: + create: true +`, + expected: ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: cm +data: + HOST: "server.svc" +--- +kind: Deployment +metadata: + name: deploy1 +spec: + template: + spec: + containers: + - image: nginx:1 + name: nginx1 +--- +kind: Deployment +metadata: + name: deploy2 +spec: + template: + spec: + containers: + - image: nginx:2 + name: nginx1 + args: + - server.svc +--- +kind: Deployment +metadata: + name: deploy3 +spec: + template: + spec: + containers: + - image: nginx:3 + name: nginx1 + args: + - server.svc +`, + }, + + "select multiple targets and create multiple fields using regex": { + input: ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: cm +data: + HOST: "server.svc" + OPT: "--debug=true" +--- +kind: Deployment +metadata: + name: deploy1 +spec: + template: + spec: + containers: + - image: tomcat:1 + name: tomcat1 +--- +kind: Deployment +metadata: + name: deploy2 +spec: + template: + spec: + containers: + - image: tomcat:2 + name: tomcat2 + args: + - tomcat.svc +--- +kind: StatefulSet +metadata: + name: statefulset1 +spec: + template: + spec: + containers: + - image: pg:1 + name: pg1 +--- +kind: State +metadata: + name: state2 +spec: + template: + spec: + containers: + - image: pg:2 + name: pg2 +`, + replacements: `replacements: +- source: + kind: ConfigMap + name: cm + fieldPath: data.HOST + targets: + # create multiple fields, multiple targets, in multiple places + - select: + # should match deploy1 deploy2 + name: d.*[1-9] + kind: Deployment|State + fieldPaths: + - spec.template.spec.containers.0.args.0 + - spec.template.spec.containers.1.name + options: + create: true + - select: + # should match state2, should not match statefulset1 + name: s.*[1-9] + kind: Deployment|State + fieldPaths: + - metadata.annotations.service + - spec.template.spec.containers.0.args.0 + options: + create: true + +- source: + kind: ConfigMap + name: cm + fieldPath: data.OPT + targets: + - select: + # should match State/state2, should not match other kinds + name: s.*[0-9] + kind: State|fulSet|Set|ful|Dep.* + fieldPaths: + - spec.template.spec.containers.*.args.1 + options: + create: true +`, + expected: ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: cm +data: + HOST: "server.svc" + OPT: "--debug=true" +--- +kind: Deployment +metadata: + name: deploy1 +spec: + template: + spec: + containers: + - image: tomcat:1 + name: tomcat1 + args: + - server.svc + - name: server.svc +--- +kind: Deployment +metadata: + name: deploy2 +spec: + template: + spec: + containers: + - image: tomcat:2 + name: tomcat2 + args: + - server.svc + - name: server.svc +--- +kind: StatefulSet +metadata: + name: statefulset1 +spec: + template: + spec: + containers: + - image: pg:1 + name: pg1 +--- +kind: State +metadata: + name: state2 + annotations: + service: server.svc +spec: + template: + spec: + containers: + - image: pg:2 + name: pg2 + args: + - server.svc + - --debug=true +`, + }, + + "name not matching suffixed and prefixed names using regex": { + input: ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: cm +data: + HOST: "tomcat.svc" +--- +kind: Deployment +metadata: + name: deploy1 + annotations: + service: SERVICE +--- +kind: Deployment +metadata: + name: prefix-deploy1 + annotations: + service: SERVICE +--- +kind: Deployment +metadata: + name: deploy1-suffix + annotations: + service: SERVICE +`, + replacements: `replacements: +- source: + kind: ConfigMap + name: cm + fieldPath: data.HOST + targets: + - select: + # should match deploy1-suffix + name: dep.*[1-9]-suffix + kind: Deployment|State + fieldPaths: + - metadata.annotations.service +- source: + kind: ConfigMap + name: cm + fieldPath: data.HOST + options: + delimiter: '.' + targets: + - select: + # should match prefix-deploy1 + name: pref.*-dep.* + fieldPaths: + - metadata.annotations.service + options: + delimiter: '.' + index: -1 + create: true +`, + expected: ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: cm +data: + HOST: "tomcat.svc" +--- +kind: Deployment +metadata: + name: deploy1 + annotations: + service: SERVICE +--- +kind: Deployment +metadata: + name: prefix-deploy1 + annotations: + service: tomcat.SERVICE +--- +kind: Deployment +metadata: + name: deploy1-suffix + annotations: + service: tomcat.svc +`, + }, + + "not matching source error using regex": { + input: ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: cm +data: + HOST: "tomcat.svc" +--- +kind: Deployment +metadata: + name: deploy1 + annotations: + service: SERVICE +--- +kind: Deployment +metadata: + name: prefix-deploy1 + annotations: + service: SERVICE +--- +kind: Deployment +metadata: + name: deploy1-suffix + annotations: + service: SERVICE +`, + replacements: `replacements: +- source: + kind: ConfigMap + name: cm.* + fieldPath: data.HOST + targets: + - select: + kind: Deployment + fieldPaths: + - metadata.annotations.service +`, + expectedErr: `nothing selected by ConfigMap.[noVer].[noGrp]/cm.*.[noNs]:data.HOST`, + }, + + "not matching target no error using regex": { + input: ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: cm +data: + HOST: "tomcat.svc" +--- +kind: Deployment +metadata: + name: deploy1 + annotations: + service: SERVICE +--- +kind: Deployment +metadata: + name: prefix-deploy1 + annotations: + service: SERVICE +--- +kind: Deployment +metadata: + name: deploy1-suffix + annotations: + service: SERVICE +`, + replacements: `replacements: +- source: + kind: ConfigMap + name: cm + fieldPath: data.HOST + targets: + - select: + name: .*z.* + kind: Deployment|State + fieldPaths: + - metadata.annotations.service +`, + expected: `apiVersion: v1 +kind: ConfigMap +metadata: + name: cm +data: + HOST: "tomcat.svc" +--- +kind: Deployment +metadata: + name: deploy1 + annotations: + service: SERVICE +--- +kind: Deployment +metadata: + name: prefix-deploy1 + annotations: + service: SERVICE +--- +kind: Deployment +metadata: + name: deploy1-suffix + annotations: + service: SERVICE +`, + }, + + "reduce selection with matching label using regex": { + input: ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: cm +data: + HOST: "tomcat.svc" +--- +kind: Deployment +metadata: + name: deploy1 + labels: + target: selectme +--- +kind: Deployment +metadata: + name: deploy2 +--- +kind: Deployment +metadata: + name: deploy3 +`, + replacements: `replacements: +- source: + kind: ConfigMap + name: cm + fieldPath: data.HOST + targets: + - select: + # shoudl match deploy1 + name: .*depl.* + labelSelector: + target=selectme + fieldPaths: + - metadata.annotations.service + options: + create: true +`, + expected: `apiVersion: v1 +kind: ConfigMap +metadata: + name: cm +data: + HOST: "tomcat.svc" +--- +kind: Deployment +metadata: + name: deploy1 + labels: + target: selectme + annotations: + service: tomcat.svc +--- +kind: Deployment +metadata: + name: deploy2 +--- +kind: Deployment +metadata: + name: deploy3 +`, + }, + + "select label not matching name using regex": { + input: ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: cm +data: + HOST: "tomcat.svc" +--- +kind: Deployment +metadata: + name: deploy1 + labels: + target: selectme +--- +kind: Deployment +metadata: + name: deploy2 +--- +kind: Deployment +metadata: + name: deploy3 +`, + replacements: `replacements: +- source: + kind: ConfigMap + name: cm + fieldPath: data.HOST + targets: + - select: + name: .*notmatching.* + labelSelector: + target=selectme + fieldPaths: + - metadata.annotations.service + options: + create: true +`, + expected: `apiVersion: v1 +kind: ConfigMap +metadata: + name: cm +data: + HOST: "tomcat.svc" +--- +kind: Deployment +metadata: + name: deploy1 + labels: + target: selectme +--- +kind: Deployment +metadata: + name: deploy2 +--- +kind: Deployment +metadata: + name: deploy3 +`, + }, + + "not matching substring using regex": { + input: ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: cm +data: + HOST: "tomcat.svc" +--- +kind: Deployment +metadata: + name: deploy1 +--- +kind: Deployment +metadata: + name: deploy2 +--- +kind: Deployment +metadata: + name: mydeploy3 +`, + replacements: `replacements: +- source: + kind: ConfigMap + name: cm + fieldPath: data.HOST + targets: + - select: + # should not match deploy1 deploy2 mydeploy3 + name: deploy + fieldPaths: + - metadata.annotations.service + options: + create: true +`, + expected: `apiVersion: v1 +kind: ConfigMap +metadata: + name: cm +data: + HOST: "tomcat.svc" +--- +kind: Deployment +metadata: + name: deploy1 +--- +kind: Deployment +metadata: + name: deploy2 +--- +kind: Deployment +metadata: + name: mydeploy3 +`, + }, + + "select by group using regex": { + input: ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: cm +data: + HOST: "tomcat.svc" +--- +apiVersion: g1/v1 +kind: Deployment +metadata: + name: deploy1 +--- +apiVersion: g2/v1 +kind: Deployment +metadata: + name: deploy2 +--- +apiVersion: g3/v1 +kind: Deployment +metadata: + name: deploy3 +`, + replacements: `replacements: +- source: + kind: ConfigMap + name: cm + fieldPath: data.HOST + targets: + - select: + # shoudl match deploy1 and deploy2 + name: .*depl.* + group: g[13] + fieldPaths: + - metadata.annotations.service + options: + create: true +`, + expected: `apiVersion: v1 +kind: ConfigMap +metadata: + name: cm +data: + HOST: "tomcat.svc" +--- +apiVersion: g1/v1 +kind: Deployment +metadata: + name: deploy1 + annotations: + service: tomcat.svc +--- +apiVersion: g2/v1 +kind: Deployment +metadata: + name: deploy2 +--- +apiVersion: g3/v1 +kind: Deployment +metadata: + name: deploy3 + annotations: + service: tomcat.svc +`, + }, + + "select by version using regex": { + input: ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: cm +data: + HOST: "tomcat.svc" +--- +apiVersion: g1/v1 +kind: Deployment +metadata: + name: deploy1 +--- +apiVersion: g1/v2 +kind: Deployment +metadata: + name: deploy2 +--- +apiVersion: g1/v3 +kind: Deployment +metadata: + name: deploy3 +`, + replacements: `replacements: +- source: + kind: ConfigMap + name: cm + fieldPath: data.HOST + targets: + - select: + # should match deploy1 and deploy2 + name: .*depl.* + version: v[13] + fieldPaths: + - metadata.annotations.service + options: + create: true +`, + expected: `apiVersion: v1 +kind: ConfigMap +metadata: + name: cm +data: + HOST: "tomcat.svc" +--- +apiVersion: g1/v1 +kind: Deployment +metadata: + name: deploy1 + annotations: + service: tomcat.svc +--- +apiVersion: g1/v2 +kind: Deployment +metadata: + name: deploy2 +--- +apiVersion: g1/v3 +kind: Deployment +metadata: + name: deploy3 + annotations: + service: tomcat.svc +`, + }, + + "reject 1 with regex": { input: `apiVersion: v1 kind: Deployment metadata: @@ -3126,7 +4189,226 @@ spec: `, }, + "create using regex": { + input: `apiVersion: v1 +kind: Pod +metadata: + name: pod +spec: + containers: + - image: busybox + name: myapp-container +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deploy1 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deploy2 +--- +apiVersion: apps/v1 +kind: NotDeployment +metadata: + name: notdeploy +`, + replacements: `replacements: +- source: + kind: Pod + name: pod + fieldPath: spec.containers + targets: + - select: + name: depl.* + fieldPaths: + - spec.template.spec.containers + options: + create: true +`, + expected: `apiVersion: v1 +kind: Pod +metadata: + name: pod +spec: + containers: + - image: busybox + name: myapp-container +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deploy1 +spec: + template: + spec: + containers: + - image: busybox + name: myapp-container +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deploy2 +spec: + template: + spec: + containers: + - image: busybox + name: myapp-container +--- +apiVersion: apps/v1 +kind: NotDeployment +metadata: + name: notdeploy + +`, + }, + "create in source using regex": { + input: `apiVersion: v1 +kind: Pod +metadata: + name: pod +spec: + containers: + - image: busybox + name: myapp-container +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deploy1 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deploy2 +--- +apiVersion: apps/v1 +kind: NotDeployment +metadata: + name: notdeploy +`, + replacements: `replacements: +- source: + kind: Pod + name: pod + fieldPath: spec.containers + targets: + - select: + name: depl.* + fieldPaths: + - spec.template.spec.containers + options: + create: true +`, + expected: `apiVersion: v1 +kind: Pod +metadata: + name: pod +spec: + containers: + - image: busybox + name: myapp-container +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deploy1 +spec: + template: + spec: + containers: + - image: busybox + name: myapp-container +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deploy2 +spec: + template: + spec: + containers: + - image: busybox + name: myapp-container +--- +apiVersion: apps/v1 +kind: NotDeployment +metadata: + name: notdeploy +`, + }, + "delimiter using regex": { + input: `apiVersion: v1 +kind: Pod +metadata: + name: pod + labels: + number: zero-one +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deploy1 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deploy2 + labels: + number: one +--- +apiVersion: apps/v1 +kind: NotDeployment +metadata: + name: notdeploy +`, + replacements: `replacements: +- source: + kind: Pod + name: pod + fieldPath: metadata.labels.number + options: + delimiter: "-" + index: 1 + targets: + - select: + name: depl.* + fieldPaths: + - metadata.labels.number + options: + create: true +`, + expected: `apiVersion: v1 +kind: Pod +metadata: + name: pod + labels: + number: zero-one +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deploy1 + labels: + number: one +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deploy2 + labels: + number: one +--- +apiVersion: apps/v1 +kind: NotDeployment +metadata: + name: notdeploy +`, + }, } for tn, tc := range testCases { diff --git a/api/krusty/replacementtransformer_test.go b/api/krusty/replacementtransformer_test.go index 273246756b..7397890e1a 100644 --- a/api/krusty/replacementtransformer_test.go +++ b/api/krusty/replacementtransformer_test.go @@ -614,3 +614,81 @@ metadata: name: app-config-dev-97544dk6t8 `) } + + +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: + - fieldPaths: + - spec.template.spec.containers.0.name + select: + kind: Deployment + reject: + - name: .*original.* + - fieldPaths: + - data.name-copy + select: + kind: ConfigMap + name: app-config + 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 +`) +} From dc0344bf04e5aa00d1123eb85a6a02141423ef2a Mon Sep 17 00:00:00 2001 From: Samir Saada Date: Wed, 30 Apr 2025 22:09:38 +0200 Subject: [PATCH 3/4] Earlier exit with rejectAny, and fix linting --- api/filters/replacement/replacement.go | 8 ++++++-- api/filters/replacement/replacement_test.go | 4 ++-- api/krusty/replacementtransformer_test.go | 1 - 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/api/filters/replacement/replacement.go b/api/filters/replacement/replacement.go index 14795c4ec5..e7c34d4c32 100644 --- a/api/filters/replacement/replacement.go +++ b/api/filters/replacement/replacement.go @@ -111,7 +111,7 @@ func applyReplacement(nodes []*yaml.RNode, value *yaml.RNode, targetSelectors [] } tsr, err := types.NewTargetSelectorRegex(selector) if err != nil { - return nil, err + return nil, fmt.Errorf("error creating target selector: %w", err) } for _, possibleTarget := range nodes { ids, err := utils.MakeResIds(possibleTarget) @@ -128,9 +128,13 @@ func applyReplacement(nodes []*yaml.RNode, value *yaml.RNode, targetSelectors [] continue } + if tsr.RejectsAny(ids) { + continue + } + // filter targets by matching resource IDs for _, id := range ids { - if tsr.Selects(id) && !tsr.RejectsAny(ids) { + if tsr.Selects(id) { err := copyValueToTarget(possibleTarget, value, selector) if err != nil { return nil, err diff --git a/api/filters/replacement/replacement_test.go b/api/filters/replacement/replacement_test.go index 2b12c7c5ac..b40e990c50 100644 --- a/api/filters/replacement/replacement_test.go +++ b/api/filters/replacement/replacement_test.go @@ -3609,7 +3609,7 @@ metadata: fieldPath: data.HOST targets: - select: - # shoudl match deploy1 + # should match deploy1 name: .*depl.* labelSelector: target=selectme @@ -3791,7 +3791,7 @@ metadata: fieldPath: data.HOST targets: - select: - # shoudl match deploy1 and deploy2 + # should match deploy1 and deploy2 name: .*depl.* group: g[13] fieldPaths: diff --git a/api/krusty/replacementtransformer_test.go b/api/krusty/replacementtransformer_test.go index 7397890e1a..8fd13f5508 100644 --- a/api/krusty/replacementtransformer_test.go +++ b/api/krusty/replacementtransformer_test.go @@ -615,7 +615,6 @@ metadata: `) } - func TestReplacementTransformerWithSuffixTransformerAndRejectUsingRegex(t *testing.T) { th := kusttest_test.MakeEnhancedHarness(t) defer th.Reset() From 2a6481b237abf9f8b7001693172eb7fc35bf077c Mon Sep 17 00:00:00 2001 From: Samir Saada Date: Wed, 28 May 2025 09:20:47 +0200 Subject: [PATCH 4/4] Add example Use cases using regex --- api/krusty/replacementtransformer_test.go | 347 ++++++++++++++++++++-- 1 file changed, 329 insertions(+), 18 deletions(-) diff --git a/api/krusty/replacementtransformer_test.go b/api/krusty/replacementtransformer_test.go index 8fd13f5508..0e2411d26c 100644 --- a/api/krusty/replacementtransformer_test.go +++ b/api/krusty/replacementtransformer_test.go @@ -615,6 +615,317 @@ metadata: `) } +// 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() @@ -650,24 +961,24 @@ configMapGenerator: - name=something-else replacements: - - source: - kind: ConfigMap - name: app-config - fieldPath: data.name - targets: - - fieldPaths: - - spec.template.spec.containers.0.name - select: - kind: Deployment - reject: - - name: .*original.* - - fieldPaths: - - data.name-copy - select: - kind: ConfigMap - name: app-config - options: - create: true +- 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, `