mirror of
https://github.com/kubernetes-sigs/kustomize.git
synced 2026-06-14 02:20:53 +00:00
implements nested structure replacements
This commit is contained in:
@@ -164,6 +164,11 @@ func (p *PathMatcher) doField(rn *RNode) (*RNode, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the field is a scalar and there are remaining path segments
|
||||
if field != nil && field.YNode().Kind == yaml.ScalarNode && len(p.Path) > 1 {
|
||||
return p.handleStructuredDataInScalar(field)
|
||||
}
|
||||
|
||||
// recurse on the field, removing the first element of the path
|
||||
pm := &PathMatcher{Path: p.Path[1:], Create: p.Create}
|
||||
p.val, err = pm.filter(field)
|
||||
@@ -263,12 +268,12 @@ func (p *PathMatcher) doSeq(rn *RNode) (*RNode, error) {
|
||||
func (p *PathMatcher) visitPrimitiveElem(elem *RNode) error {
|
||||
r, err := regexp.Compile(p.matchRegex)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("%w", err)
|
||||
}
|
||||
|
||||
str, err := elem.String()
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("%w", err)
|
||||
}
|
||||
str = strings.TrimSpace(str)
|
||||
if !r.MatchString(str) {
|
||||
@@ -282,7 +287,7 @@ func (p *PathMatcher) visitPrimitiveElem(elem *RNode) error {
|
||||
func (p *PathMatcher) visitElem(elem *RNode) error {
|
||||
r, err := regexp.Compile(p.matchRegex)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("%w", err)
|
||||
}
|
||||
|
||||
// check if this elements field matches the regex
|
||||
@@ -292,7 +297,7 @@ func (p *PathMatcher) visitElem(elem *RNode) error {
|
||||
}
|
||||
str, err := val.Value.String()
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("%w", err)
|
||||
}
|
||||
str = strings.TrimSpace(str)
|
||||
if !r.MatchString(str) {
|
||||
@@ -341,3 +346,69 @@ func cleanPath(path []string) []string {
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// structuredDataNode is a wrapper around RNode that handles serialization back to scalar fields
|
||||
// when the underlying structured data is modified
|
||||
type structuredDataNode struct {
|
||||
RNode
|
||||
structuredRoot *RNode // The root of the structured data
|
||||
scalarField *RNode // The original scalar field containing the structured data
|
||||
}
|
||||
|
||||
// Override SetYNode to handle structured data serialization
|
||||
func (s *structuredDataNode) SetYNode(node *yaml.Node) {
|
||||
// Call the original SetYNode
|
||||
s.RNode.SetYNode(node)
|
||||
|
||||
// Serialize the modified structured data back to the scalar field
|
||||
if modifiedData, err := s.structuredRoot.String(); err == nil {
|
||||
s.scalarField.YNode().Value = strings.TrimSpace(modifiedData)
|
||||
}
|
||||
}
|
||||
|
||||
// handleStructuredDataInScalar processes a scalar field that contains structured data (JSON/YAML)
|
||||
// and allows path navigation within that structured data
|
||||
func (p *PathMatcher) handleStructuredDataInScalar(scalarField *RNode) (*RNode, error) {
|
||||
scalarValue := scalarField.YNode().Value
|
||||
var parsedNode yaml.Node
|
||||
if err := yaml.Unmarshal([]byte(scalarValue), &parsedNode); err != nil {
|
||||
return nil, fmt.Errorf("%w", err)
|
||||
}
|
||||
|
||||
// Create a structured field from the parsed data
|
||||
structuredField := NewRNode(&parsedNode)
|
||||
|
||||
// Process the remaining path on the structured data
|
||||
pm := &PathMatcher{Path: p.Path[1:], Create: p.Create}
|
||||
result, err := pm.filter(structuredField)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.Matches = pm.Matches
|
||||
|
||||
// For structured data, we need to create a special result node that knows how to
|
||||
// serialize back to the original scalar when modified
|
||||
if result != nil && len(result.YNode().Content) > 0 {
|
||||
// Wrap the result with structured data context
|
||||
for _, node := range result.YNode().Content {
|
||||
wrappedNode := NewRNode(node)
|
||||
// Add a custom method to handle setting values back to structured data
|
||||
wrappedNode = p.wrapWithStructuredDataHandler(wrappedNode, structuredField, scalarField)
|
||||
result.YNode().Content[0] = wrappedNode.YNode()
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// wrapWithStructuredDataHandler creates a wrapper that will serialize structured data back
|
||||
// to the scalar field when the node is modified
|
||||
func (p *PathMatcher) wrapWithStructuredDataHandler(targetNode, structuredRoot, scalarField *RNode) *RNode {
|
||||
// Create a custom node that overrides SetYNode to handle structured data serialization
|
||||
wrapper := &structuredDataNode{
|
||||
RNode: *targetNode,
|
||||
structuredRoot: structuredRoot,
|
||||
scalarField: scalarField,
|
||||
}
|
||||
return &wrapper.RNode
|
||||
}
|
||||
|
||||
@@ -287,3 +287,236 @@ spec:
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathMatcher_StructuredDataInScalar(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
input string
|
||||
path []string
|
||||
expected string
|
||||
expectError bool
|
||||
}{
|
||||
"json field access": {
|
||||
input: `apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: test-config
|
||||
data:
|
||||
config.json: |-
|
||||
{
|
||||
"database": {
|
||||
"host": "localhost",
|
||||
"port": 5432
|
||||
},
|
||||
"app": {
|
||||
"name": "myapp",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
}`,
|
||||
path: []string{"data", "config.json", "database", "host"},
|
||||
expected: "- \"localhost\"\n",
|
||||
},
|
||||
"json nested field access": {
|
||||
input: `apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: test-config
|
||||
data:
|
||||
config.json: |-
|
||||
{
|
||||
"database": {
|
||||
"host": "localhost",
|
||||
"port": 5432
|
||||
},
|
||||
"app": {
|
||||
"name": "myapp",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
}`,
|
||||
path: []string{"data", "config.json", "app", "name"},
|
||||
expected: "- \"myapp\"\n",
|
||||
},
|
||||
"yaml field access": {
|
||||
input: `apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: prometheus-config
|
||||
data:
|
||||
prometheus.yml: |-
|
||||
global:
|
||||
external_labels:
|
||||
prometheus_env: dev
|
||||
cluster: local
|
||||
scrape_configs:
|
||||
- job_name: "prometheus"
|
||||
static_configs:
|
||||
- targets: ["localhost:9090"]`,
|
||||
path: []string{"data", "prometheus.yml", "global", "external_labels", "prometheus_env"},
|
||||
expected: "- dev\n",
|
||||
},
|
||||
"yaml array access": {
|
||||
input: `apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: deployment-config
|
||||
data:
|
||||
deployment.yaml: |-
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: my-app
|
||||
spec:
|
||||
replicas: 3
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: web
|
||||
image: nginx:1.18
|
||||
- name: sidecar
|
||||
image: busybox:latest`,
|
||||
path: []string{"data", "deployment.yaml", "spec", "replicas"},
|
||||
expected: "- 3\n",
|
||||
},
|
||||
"yaml container array with selector": {
|
||||
input: `apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: deployment-config
|
||||
data:
|
||||
deployment.yaml: |-
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: my-app
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: web
|
||||
image: nginx:1.18
|
||||
- name: sidecar
|
||||
image: busybox:latest`,
|
||||
path: []string{"data", "deployment.yaml", "spec", "template", "spec", "containers", "[name=web]", "image"},
|
||||
expected: "- nginx:1.18\n",
|
||||
},
|
||||
"json complex nested structure": {
|
||||
input: `apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: complex-config
|
||||
data:
|
||||
config.json: |-
|
||||
{
|
||||
"database": {
|
||||
"connections": {
|
||||
"primary": {
|
||||
"host": "primary-db.example.com",
|
||||
"port": 5432,
|
||||
"ssl": true
|
||||
},
|
||||
"secondary": {
|
||||
"host": "secondary-db.example.com",
|
||||
"port": 5433
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
path: []string{"data", "config.json", "database", "connections", "primary", "host"},
|
||||
expected: "- \"primary-db.example.com\"\n",
|
||||
},
|
||||
"yaml flow style (kyaml) field access": {
|
||||
input: `apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: test-config
|
||||
data:
|
||||
config.yaml: |-
|
||||
labels: {
|
||||
app: "foobar",
|
||||
foo: "bar",
|
||||
something: "12345",
|
||||
}
|
||||
spec: {
|
||||
replicas: 3,
|
||||
selector: {
|
||||
matchLabels: {
|
||||
app: "foobar"
|
||||
}
|
||||
}
|
||||
}`,
|
||||
path: []string{"data", "config.yaml", "labels", "app"},
|
||||
expected: "- \"foobar\"\n",
|
||||
},
|
||||
"yaml flow style nested field access": {
|
||||
input: `apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: test-config
|
||||
data:
|
||||
config.yaml: |-
|
||||
labels: {
|
||||
app: "foobar",
|
||||
foo: "bar",
|
||||
something: "12345",
|
||||
}
|
||||
spec: {
|
||||
replicas: 3,
|
||||
selector: {
|
||||
matchLabels: {
|
||||
app: "foobar"
|
||||
}
|
||||
}
|
||||
}`,
|
||||
path: []string{"data", "config.yaml", "spec", "selector", "matchLabels", "app"},
|
||||
expected: "- \"foobar\"\n",
|
||||
},
|
||||
"invalid json returns field value as-is": {
|
||||
input: `apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: bad-config
|
||||
data:
|
||||
config.json: |-
|
||||
{
|
||||
"invalid": json
|
||||
}`,
|
||||
path: []string{"data", "config.json"},
|
||||
expected: "- |-\n {\n \"invalid\": json\n }\n",
|
||||
},
|
||||
"access non-existent field": {
|
||||
input: `apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: test-config
|
||||
data:
|
||||
config.json: |-
|
||||
{
|
||||
"existing": "value"
|
||||
}`,
|
||||
path: []string{"data", "config.json", "nonexistent"},
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
node := MustParse(tc.input)
|
||||
result, err := node.Pipe(&PathMatcher{Path: tc.path})
|
||||
|
||||
if tc.expectError {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
if tc.expected == "" {
|
||||
assert.True(t, result == nil || result.IsNil() || result.MustString() == "")
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, tc.expected, result.MustString())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user