diff --git a/api/filters/replacement/replacement.go b/api/filters/replacement/replacement.go index 279ac7b787..8afc646d26 100644 --- a/api/filters/replacement/replacement.go +++ b/api/filters/replacement/replacement.go @@ -4,6 +4,7 @@ package replacement import ( + "encoding/json" "fmt" "strings" @@ -188,6 +189,14 @@ func copyValueToTarget(target *yaml.RNode, value *yaml.RNode, selector *types.Ta if selector.Options != nil && selector.Options.Create { createKind = value.YNode().Kind } + + // Check if this fieldPath contains structured data access + if err := setValueInStructuredData(target, value, fp, createKind); err == nil { + // Successfully handled as structured data + continue + } + + // Fall back to normal path handling targetFieldList, err := target.Pipe(&yaml.PathMatcher{ Path: kyaml_utils.SmarterPathSplitter(fp, "."), Create: createKind}) @@ -204,7 +213,7 @@ func copyValueToTarget(target *yaml.RNode, value *yaml.RNode, selector *types.Ta for _, t := range targetFields { if err := setFieldValue(selector.Options, t, value); err != nil { - return err + return fmt.Errorf("%w", err) } } } @@ -247,3 +256,146 @@ func setFieldValue(options *types.FieldOptions, targetField *yaml.RNode, value * return nil } + +// 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, ".") + if len(pathParts) < 2 { + return fmt.Errorf("not a structured data path") + } + + // Find the potential scalar field that might contain structured data + var scalarFieldPath []string + var structuredDataPath []string + var foundScalar = false + + // Try to find where the scalar field ends and structured data begins + for i := 1; i <= len(pathParts); i++ { + potentialScalarPath := pathParts[:i] + scalarField, err := target.Pipe(yaml.Lookup(potentialScalarPath...)) + if err != nil { + continue + } + if scalarField != nil && scalarField.YNode().Kind == yaml.ScalarNode && i < len(pathParts) { + // Try to parse the scalar value as structured data + scalarValue := scalarField.YNode().Value + var parsedNode yaml.Node + if err := yaml.Unmarshal([]byte(scalarValue), &parsedNode); err == nil { + // Successfully parsed - this is structured data + scalarFieldPath = potentialScalarPath + structuredDataPath = pathParts[i:] + foundScalar = true + break + } + } + } + + if !foundScalar { + return fmt.Errorf("no structured data found in path") + } + + // Get the scalar field containing structured data + scalarField, err := target.Pipe(yaml.Lookup(scalarFieldPath...)) + if err != nil { + return fmt.Errorf("%w", err) + } + + // Parse the structured data + scalarValue := scalarField.YNode().Value + var parsedNode yaml.Node + if err := yaml.Unmarshal([]byte(scalarValue), &parsedNode); err != nil { + return fmt.Errorf("%w", err) + } + + structuredData := yaml.NewRNode(&parsedNode) + + // Navigate to the target location within the structured data + targetInStructured, err := structuredData.Pipe(&yaml.PathMatcher{ + Path: structuredDataPath, + Create: createKind, + }) + if err != nil { + return fmt.Errorf("%w", err) + } + + targetFields, err := targetInStructured.Elements() + if err != nil { + return fmt.Errorf("%w", err) + } + + if len(targetFields) == 0 { + return fmt.Errorf("unable to find field in structured data") + } + + // Set the value in the structured data + for _, t := range targetFields { + if t.YNode().Kind == yaml.ScalarNode { + t.YNode().Value = value.YNode().Value + } else { + t.SetYNode(value.YNode()) + } + } + + // Serialize the modified structured data back to the scalar field + // Try to detect if original was JSON or YAML and preserve formatting + serializedData, err := serializeStructuredData(structuredData, scalarValue) + if err != nil { + return fmt.Errorf("%w", err) + } + + // Update the original scalar field + scalarField.YNode().Value = serializedData + + return nil +} + +// serializeStructuredData handles the serialization of structured data back to string format +// preserving the original format (JSON vs YAML) and style (pretty vs compact) +func serializeStructuredData(structuredData *yaml.RNode, originalValue string) (string, error) { + firstChar := rune(strings.TrimSpace(originalValue)[0]) + if firstChar == '{' || firstChar == '[' { + return serializeAsJSON(structuredData, originalValue) + } + + // Fallback to YAML format + return serializeAsYAML(structuredData) +} + +// serializeAsJSON converts structured data back to JSON format +func serializeAsJSON(structuredData *yaml.RNode, originalValue string) (string, error) { + modifiedData, err := structuredData.String() + if err != nil { + return "", fmt.Errorf("failed to serialize structured data: %w", err) + } + + // Parse the YAML output as JSON + var jsonData interface{} + if err := yaml.Unmarshal([]byte(modifiedData), &jsonData); err != nil { + return "", fmt.Errorf("failed to unmarshal YAML data: %w", err) + } + + // Check if original was pretty-printed by looking for newlines and indentation + if strings.Contains(originalValue, "\n") && strings.Contains(originalValue, " ") { + // Pretty-print the JSON to match original formatting + if prettyJSON, err := json.MarshalIndent(jsonData, "", " "); err == nil { + return string(prettyJSON), nil + } + } + + // Compact JSON + if compactJSON, err := json.Marshal(jsonData); err == nil { + return string(compactJSON), nil + } + + return "", fmt.Errorf("failed to marshal JSON data") +} + +// serializeAsYAML converts structured data back to YAML format +func serializeAsYAML(structuredData *yaml.RNode) (string, error) { + modifiedData, err := structuredData.String() + if err != nil { + return "", fmt.Errorf("failed to serialize YAML data: %w", err) + } + + return strings.TrimSpace(modifiedData), nil +} diff --git a/api/filters/replacement/replacement_test.go b/api/filters/replacement/replacement_test.go index 7c37726274..67d6a1e763 100644 --- a/api/filters/replacement/replacement_test.go +++ b/api/filters/replacement/replacement_test.go @@ -2779,7 +2779,7 @@ spec: name: myingress fieldPaths: - spec.tls.0.hosts.0 - - spec.tls.0.secretName + - spec.tls.0.secretName options: create: true `, @@ -4498,3 +4498,536 @@ metadata: }) } } + +func TestValueInlineStructuredData(t *testing.T) { + testCases := map[string]struct { + input string + replacements string + expected string + expectedErr string + }{ + "replacement contain jsonfield": { + input: `apiVersion: v1 +kind: ConfigMap +metadata: + name: target-configmap + annotations: + hostname: www.example.com +data: + config.json: |- + { + "config": { + "id": "42", + "hostname": "REPLACE_TARGET_HOSTNAME" + } + } +`, + replacements: `replacements: +- source: + kind: ConfigMap + name: target-configmap + fieldPath: metadata.annotations.hostname + targets: + - select: + kind: ConfigMap + fieldPaths: + - data.config\.json.config.hostname +`, + expected: `apiVersion: v1 +kind: ConfigMap +metadata: + name: target-configmap + annotations: + hostname: www.example.com +data: + config.json: |- + { + "config": { + "hostname": "www.example.com", + "id": "42" + } + }`, + }, + "replacement json field with source from separate configmap": { + input: `apiVersion: v1 +kind: ConfigMap +metadata: + name: source-configmap +data: + HOSTNAME: www.example.com +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: target-configmap +data: + config.json: |- + { + "config": { + "id": "42", + "hostname": "REPLACE_TARGET_HOSTNAME" + } + } +`, + replacements: `replacements: +- source: + kind: ConfigMap + name: source-configmap + fieldPath: data.HOSTNAME + targets: + - select: + kind: ConfigMap + name: target-configmap + fieldPaths: + - data.config\.json.config.hostname +`, + expected: `apiVersion: v1 +kind: ConfigMap +metadata: + name: source-configmap +data: + HOSTNAME: www.example.com +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: target-configmap +data: + config.json: |- + { + "config": { + "hostname": "www.example.com", + "id": "42" + } + } +`, + }, + "replacement yaml field in configmap": { + input: `apiVersion: v1 +kind: ConfigMap +metadata: + name: environment-config +data: + env: dev +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: prometheus-config +data: + prometheus.yml: |- + global: + external_labels: + prometheus_env: TARGET_ENVIRONMENT + scrape_configs: + - job_name: "prometheus" + static_configs: + - targets: ["localhost:9090"] +`, + replacements: `replacements: +- source: + kind: ConfigMap + name: environment-config + fieldPath: data.env + targets: + - select: + kind: ConfigMap + name: prometheus-config + fieldPaths: + - data.prometheus\.yml.global.external_labels.prometheus_env +`, + expected: `apiVersion: v1 +kind: ConfigMap +metadata: + name: environment-config +data: + env: dev +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: prometheus-config +data: + prometheus.yml: |- + global: + external_labels: + prometheus_env: dev + scrape_configs: + - job_name: "prometheus" + static_configs: + - targets: ["localhost:9090"]`, + }, + "replacement json field in annotations": { + input: `apiVersion: cloud.google.com/v1 +kind: BackendConfig +metadata: + name: debug-backend-config +spec: + securityPolicy: + name: "debug-security-policy" +--- +apiVersion: v1 +kind: Service +metadata: + name: appA-svc + annotations: + cloud.google.com/backend-config: '{"ports": {"appA":"gke-default-backend-config"}}' +spec: + ports: + - name: appA + port: 1234 + protocol: TCP + targetPort: 8080 +`, + replacements: `replacements: +- source: + kind: BackendConfig + name: debug-backend-config + fieldPath: metadata.name + targets: + - select: + kind: Service + name: appA-svc + fieldPaths: + - metadata.annotations.cloud\.google\.com/backend-config.ports.appA +`, + expected: `apiVersion: cloud.google.com/v1 +kind: BackendConfig +metadata: + name: debug-backend-config +spec: + securityPolicy: + name: "debug-security-policy" +--- +apiVersion: v1 +kind: Service +metadata: + name: appA-svc + annotations: + cloud.google.com/backend-config: '{"ports":{"appA":"debug-backend-config"}}' +spec: + ports: + - name: appA + port: 1234 + protocol: TCP + targetPort: 8080`, + }, + "replacement yaml nested value": { + input: `apiVersion: v1 +kind: ConfigMap +metadata: + name: app-config +data: + replicas: "3" +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: deployment-template +data: + deployment.yaml: |- + apiVersion: apps/v1 + kind: Deployment + metadata: + name: my-app + spec: + replicas: 1 + selector: + matchLabels: + app: my-app + template: + metadata: + labels: + app: my-app + spec: + containers: + - name: app + image: nginx:latest +`, + replacements: `replacements: +- source: + kind: ConfigMap + name: app-config + fieldPath: data.replicas + targets: + - select: + kind: ConfigMap + name: deployment-template + fieldPaths: + - data.deployment\.yaml.spec.replicas +`, + expected: `apiVersion: v1 +kind: ConfigMap +metadata: + name: app-config +data: + replicas: "3" +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: deployment-template +data: + deployment.yaml: |- + apiVersion: apps/v1 + kind: Deployment + metadata: + name: my-app + spec: + replicas: 3 + selector: + matchLabels: + app: my-app + template: + metadata: + labels: + app: my-app + spec: + containers: + - name: app + image: nginx:latest`, + }, + "replacement json complex nested structure": { + input: `apiVersion: v1 +kind: ConfigMap +metadata: + name: source-config +data: + database_host: prod-db.example.com + database_port: "5432" +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: app-config +data: + config.json: |- + { + "database": { + "connections": { + "primary": { + "host": "localhost", + "port": 3306, + "ssl": true + }, + "secondary": { + "host": "backup-host", + "port": 3307 + } + }, + "pool": { + "min": 5, + "max": 10 + } + }, + "logging": { + "level": "info" + } + } +`, + replacements: `replacements: +- source: + kind: ConfigMap + name: source-config + fieldPath: data.database_host + targets: + - select: + kind: ConfigMap + name: app-config + fieldPaths: + - data.config\.json.database.connections.primary.host +- source: + kind: ConfigMap + name: source-config + fieldPath: data.database_port + targets: + - select: + kind: ConfigMap + name: app-config + fieldPaths: + - data.config\.json.database.connections.primary.port +`, + expected: `apiVersion: v1 +kind: ConfigMap +metadata: + name: source-config +data: + database_host: prod-db.example.com + database_port: "5432" +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: app-config +data: + config.json: |- + { + "database": { + "connections": { + "primary": { + "host": "prod-db.example.com", + "port": 5432, + "ssl": true + }, + "secondary": { + "host": "backup-host", + "port": 3307 + } + }, + "pool": { + "max": 10, + "min": 5 + } + }, + "logging": { + "level": "info" + } + }`, + }, + "replacement yaml array element": { + input: `apiVersion: v1 +kind: ConfigMap +metadata: + name: image-config +data: + new_image: nginx:1.20 +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: k8s-manifest +data: + deployment.yaml: |- + apiVersion: apps/v1 + kind: Deployment + metadata: + name: web-app + spec: + template: + spec: + containers: + - name: web + image: nginx:1.18 + - name: sidecar + image: busybox:latest +`, + replacements: `replacements: +- source: + kind: ConfigMap + name: image-config + fieldPath: data.new_image + targets: + - select: + kind: ConfigMap + name: k8s-manifest + fieldPaths: + - data.deployment\.yaml.spec.template.spec.containers.[name=web].image +`, + expected: `apiVersion: v1 +kind: ConfigMap +metadata: + name: image-config +data: + new_image: nginx:1.20 +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: k8s-manifest +data: + deployment.yaml: |- + apiVersion: apps/v1 + kind: Deployment + metadata: + name: web-app + spec: + template: + spec: + containers: + - name: web + image: nginx:1.20 + - name: sidecar + image: busybox:latest`, + }, + "replacement yaml flow style (kyaml) field": { + input: `apiVersion: v1 +kind: ConfigMap +metadata: + name: source-values +data: + app_name: "my-awesome-app" +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: target-config +data: + config.yaml: |- + labels: { + app: "REPLACE_APP_NAME", + version: "1.0.0", + env: "production", + } + spec: { + replicas: 3, + selector: { + matchLabels: { + app: "REPLACE_APP_NAME" + } + } + }`, + replacements: `replacements: +- source: + kind: ConfigMap + name: source-values + fieldPath: data.app_name + targets: + - select: + kind: ConfigMap + name: target-config + fieldPaths: + - data.config\.yaml.labels.app + - data.config\.yaml.spec.selector.matchLabels.app +`, + expected: `apiVersion: v1 +kind: ConfigMap +metadata: + name: source-values +data: + app_name: "my-awesome-app" +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: target-config +data: + config.yaml: |- + labels: {app: "my-awesome-app", version: "1.0.0", env: "production"} + spec: {replicas: 3, selector: {matchLabels: {app: "my-awesome-app"}}}`, + }, + } + + for tn := range testCases { + t.Run(tn, func(t *testing.T) { + // Test one case to see if structured data replacement works + f := Filter{} + err := yaml.Unmarshal([]byte(testCases[tn].replacements), &f) + if !assert.NoError(t, err) { + t.FailNow() + } + actual, err := filtertest.RunFilterE(t, testCases[tn].input, f) + if err != nil { + if testCases[tn].expectedErr == "" { + t.Errorf("unexpected error: %s\n", err.Error()) + t.FailNow() + } + if !assert.Contains(t, err.Error(), testCases[tn].expectedErr) { + t.FailNow() + } + } + if !assert.Equal(t, strings.TrimSpace(testCases[tn].expected), strings.TrimSpace(actual)) { + t.FailNow() + } + }) + } +} diff --git a/kyaml/yaml/fns.go b/kyaml/yaml/fns.go index 740a28ed02..9c26055ed7 100644 --- a/kyaml/yaml/fns.go +++ b/kyaml/yaml/fns.go @@ -830,6 +830,10 @@ func (e *InvalidNodeKindError) Error() string { return msg } +func (e *InvalidNodeKindError) Unwrap() error { + return errors.Errorf("InvalidNodeKindError") +} + func (e *InvalidNodeKindError) ActualNodeKind() Kind { return e.node.YNode().Kind } diff --git a/kyaml/yaml/match.go b/kyaml/yaml/match.go index 28ea03ca6f..1e70de3c94 100644 --- a/kyaml/yaml/match.go +++ b/kyaml/yaml/match.go @@ -14,7 +14,7 @@ import ( ) // PathMatcher returns all RNodes matching the path wrapped in a SequenceNode. -// Lists may have multiple elements matching the path, and each matching element +// Lists may have multiple elements matching the pafunc cleanPath(path []string) []string {g element // is added to the return result. // If Path points to a SequenceNode, the SequenceNode is wrapped in another SequenceNode // If Path does not contain any lists, the result is still wrapped in a SequenceNode of len == 1 @@ -137,10 +137,14 @@ func (p *PathMatcher) visitEveryElem(elem *RNode) error { func (p *PathMatcher) doField(rn *RNode) (*RNode, error) { // lookup the field field, err := rn.Pipe(Get(p.Path[0])) - if err != nil || (!IsCreate(p.Create) && field == nil) { + if err != nil { return nil, err } + if !IsCreate(p.Create) && field == nil { + return nil, nil + } + if IsCreate(p.Create) && field == nil { var nextPart string if len(p.Path) > 1 { @@ -154,6 +158,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) @@ -253,12 +262,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) { @@ -272,7 +281,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 @@ -282,7 +291,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) { @@ -331,3 +340,26 @@ func cleanPath(path []string) []string { } return p } + +// 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 + + return result, nil +} diff --git a/kyaml/yaml/match_test.go b/kyaml/yaml/match_test.go index 70692b8401..2d4bd71e3e 100644 --- a/kyaml/yaml/match_test.go +++ b/kyaml/yaml/match_test.go @@ -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()) + }) + } +} diff --git a/proposals/22-03-value-in-the-structured-data.md b/proposals/22-03-value-in-the-structured-data.md index eed7fe6ff6..d56af7335f 100644 --- a/proposals/22-03-value-in-the-structured-data.md +++ b/proposals/22-03-value-in-the-structured-data.md @@ -386,7 +386,7 @@ replacements: kind: ConfigMap name: prometheus-config fieldPaths: - - data.prometheus\.yml.global.external_labels?prometheus_env + - data.prometheus\.yml.global.external_labels.prometheus_env ``` ```yaml