Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions api/filters/replacement/replacement.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,15 @@ func fieldRetrievalError(fieldPath string, isCreate bool) string {

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

// Handle prefix/suffix matching for sequence nodes (e.g., container args)
// Only apply prefix/suffix matching if the target field is a sequence and
// prefix/suffix options are specified
if targetField.YNode().Kind == yaml.SequenceNode && options != nil && (options.Prefix != "" || options.Suffix != "") {
return setFieldValueInSequence(options, targetField, value)
}

// Handle delimiter option for scalar nodes
if options != nil && options.Delimiter != "" {
if targetField.YNode().Kind != yaml.ScalarNode {
return fmt.Errorf("delimiter option can only be used with scalar nodes")
Expand All @@ -247,6 +256,24 @@ func setFieldValue(options *types.FieldOptions, targetField *yaml.RNode, value *
value.YNode().Value = strings.Join(tv, options.Delimiter)
}

// Handle prefix/suffix matching for scalar nodes
if options != nil && (options.Prefix != "" || options.Suffix != "") {
if targetField.YNode().Kind != yaml.ScalarNode {
return fmt.Errorf("prefix/suffix options can only be used with scalar or sequence nodes")
}
targetValue := targetField.YNode().Value
newValue := yaml.GetValue(value)

// Only modify if the target matches both prefix and suffix
if !matchesPrefixAndSuffix(targetValue, options.Prefix, options.Suffix) {
return nil
}

// Preserve prefix and suffix in the result
result := options.Prefix + newValue + options.Suffix
value.YNode().Value = result
}

if targetField.YNode().Kind == yaml.ScalarNode {
// For scalar, only copy the value (leave any type intact to auto-convert int->string or string->int)
targetField.YNode().Value = value.YNode().Value
Expand All @@ -257,6 +284,43 @@ func setFieldValue(options *types.FieldOptions, targetField *yaml.RNode, value *
return nil
}

// setFieldValueInSequence handles setting values in sequence nodes (string arrays)
// when prefix and/or suffix options are specified
func setFieldValueInSequence(options *types.FieldOptions, targetField *yaml.RNode, value *yaml.RNode) error {
elements, err := targetField.Elements()
if err != nil {
return err
}

newValue := yaml.GetValue(value)

for _, elem := range elements {
if elem.YNode().Kind != yaml.ScalarNode {
continue
}

elemValue := elem.YNode().Value
if matchesPrefixAndSuffix(elemValue, options.Prefix, options.Suffix) {
// Replace the value portion while preserving prefix and suffix
result := options.Prefix + newValue + options.Suffix
elem.YNode().Value = result
}
}

return nil
}

// matchesPrefixAndSuffix checks if a string has the specified prefix and suffix
func matchesPrefixAndSuffix(s, prefix, suffix string) bool {
if prefix != "" && !strings.HasPrefix(s, prefix) {
return false
}
if suffix != "" && !strings.HasSuffix(s, suffix) {
return false
}
return true
}

// setValueInStructuredData handles setting values within structured data (JSON/YAML) in scalar fields
func setValueInStructuredData(target *yaml.RNode, value *yaml.RNode, fieldPath string, createKind yaml.Kind) error {
pathParts := kyaml_utils.SmarterPathSplitter(fieldPath, ".")
Expand Down
283 changes: 283 additions & 0 deletions api/filters/replacement/replacement_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5005,6 +5005,289 @@ data:
labels: {app: "my-awesome-app", version: "1.0.0", env: "production"}
spec: {replicas: 3, selector: {matchLabels: {app: "my-awesome-app"}}}`,
},
"prefix option on scalar field": {
input: `apiVersion: v1
kind: ConfigMap
metadata:
name: source
namespace: production
---
apiVersion: v1
kind: ConfigMap
metadata:
name: target
data:
config: "--namespace=default"
`,
replacements: `replacements:
- source:
kind: ConfigMap
name: source
fieldPath: metadata.namespace
targets:
- select:
kind: ConfigMap
name: target
fieldPaths:
- data.config
options:
prefix: "--namespace="
`,
expected: `apiVersion: v1
kind: ConfigMap
metadata:
name: source
namespace: production
---
apiVersion: v1
kind: ConfigMap
metadata:
name: target
data:
config: "--namespace=production"
`,
},
"prefix option on sequence field (container args)": {
input: `apiVersion: v1
kind: Namespace
metadata:
name: my-namespace
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: cert-manager
spec:
template:
spec:
containers:
- name: cert-manager
image: quay.io/jetstack/cert-manager-controller:v1.2.0
args:
- --v=2
- --default-issuer-kind=ClusterIssuer
- --leader-election-namespace=kube-system
`,
replacements: `replacements:
- source:
kind: Namespace
name: my-namespace
fieldPath: metadata.name
targets:
- select:
kind: Deployment
fieldPaths:
- spec.template.spec.containers.[name=cert-manager].args
options:
prefix: "--leader-election-namespace="
`,
expected: `apiVersion: v1
kind: Namespace
metadata:
name: my-namespace
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: cert-manager
spec:
template:
spec:
containers:
- name: cert-manager
image: quay.io/jetstack/cert-manager-controller:v1.2.0
args:
- --v=2
- --default-issuer-kind=ClusterIssuer
- --leader-election-namespace=my-namespace
`,
},
"prefix option on sequence field with multiple matches": {
input: `apiVersion: v1
kind: ConfigMap
metadata:
name: source
namespace: staging
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
spec:
template:
spec:
containers:
- name: app
args:
- --namespace=default
- --other-namespace=default
- --namespace=other
`,
replacements: `replacements:
- source:
kind: ConfigMap
name: source
fieldPath: metadata.namespace
targets:
- select:
kind: Deployment
fieldPaths:
- spec.template.spec.containers.[name=app].args
options:
prefix: "--namespace="
`,
expected: `apiVersion: v1
kind: ConfigMap
metadata:
name: source
namespace: staging
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
spec:
template:
spec:
containers:
- name: app
args:
- --namespace=staging
- --other-namespace=default
- --namespace=staging
`,
},
"suffix option on scalar field": {
input: `apiVersion: v1
kind: ConfigMap
metadata:
name: source
data:
port: "8080"
---
apiVersion: v1
kind: ConfigMap
metadata:
name: target
data:
url: "3000/api"
`,
replacements: `replacements:
- source:
kind: ConfigMap
name: source
fieldPath: data.port
targets:
- select:
kind: ConfigMap
name: target
fieldPaths:
- data.url
options:
suffix: "/api"
`,
expected: `apiVersion: v1
kind: ConfigMap
metadata:
name: source
data:
port: "8080"
---
apiVersion: v1
kind: ConfigMap
metadata:
name: target
data:
url: "8080/api"
`,
},
"prefix and suffix options combined": {
input: `apiVersion: v1
kind: ConfigMap
metadata:
name: source
data:
value: "myvalue"
---
apiVersion: v1
kind: ConfigMap
metadata:
name: target
data:
config: "prefix_oldvalue_suffix"
`,
replacements: `replacements:
- source:
kind: ConfigMap
name: source
fieldPath: data.value
targets:
- select:
kind: ConfigMap
name: target
fieldPaths:
- data.config
options:
prefix: "prefix_"
suffix: "_suffix"
`,
expected: `apiVersion: v1
kind: ConfigMap
metadata:
name: source
data:
value: "myvalue"
---
apiVersion: v1
kind: ConfigMap
metadata:
name: target
data:
config: "prefix_myvalue_suffix"
`,
},
"prefix option no match on scalar field": {
input: `apiVersion: v1
kind: ConfigMap
metadata:
name: source
namespace: production
---
apiVersion: v1
kind: ConfigMap
metadata:
name: target
data:
config: "--other=value"
`,
replacements: `replacements:
- source:
kind: ConfigMap
name: source
fieldPath: metadata.namespace
targets:
- select:
kind: ConfigMap
name: target
fieldPaths:
- data.config
options:
prefix: "--namespace="
`,
expected: `apiVersion: v1
kind: ConfigMap
metadata:
name: source
namespace: production
---
apiVersion: v1
kind: ConfigMap
metadata:
name: target
data:
config: "--other=value"
`,
},
}

for tn := range testCases {
Expand Down
14 changes: 12 additions & 2 deletions api/types/replacement.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,21 @@ type FieldOptions struct {

// If field missing, add it.
Create bool `json:"create,omitempty" yaml:"create,omitempty"`

// Prefix is used to select elements in a list that start with this prefix.
// When set, only elements starting with this prefix will be modified.
// The prefix is preserved in the resulting value.
Prefix string `json:"prefix,omitempty" yaml:"prefix,omitempty"`

// Suffix is used to select elements in a list that end with this suffix.
// When set, only elements ending with this suffix will be modified.
// The suffix is preserved in the resulting value.
Suffix string `json:"suffix,omitempty" yaml:"suffix,omitempty"`
}

func (fo *FieldOptions) String() string {
if fo == nil || (fo.Delimiter == "" && !fo.Create) {
if fo == nil || (fo.Delimiter == "" && !fo.Create && fo.Prefix == "" && fo.Suffix == "") {
return ""
}
return fmt.Sprintf("%s(%d), create=%t", fo.Delimiter, fo.Index, fo.Create)
return fmt.Sprintf("%s(%d), create=%t, prefix=%q, suffix=%q", fo.Delimiter, fo.Index, fo.Create, fo.Prefix, fo.Suffix)
}