// Copyright 2019 The Kubernetes Authors. // SPDX-License-Identifier: Apache-2.0 package yaml import ( "fmt" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" yaml "go.yaml.in/yaml/v3" ) func TestPathMatcher_Filter(t *testing.T) { node := MustParse(`apiVersion: apps/v1 kind: Deployment metadata: name: nginx-deployment labels: app: nginx spec: replicas: 3 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.7.9 args: [-c, conf.yaml] ports: - containerPort: 80 - name: sidecar image: sidecar:1.0.0 ports: - containerPort: 8081 - containerPort: 9090 `) updates := []struct { path []string value string }{ {[]string{ "spec", "template", "spec", "containers", "[name=.*]"}, "- name: nginx\n image: nginx:1.7.9\n args: [-c, conf.yaml]\n ports:\n - containerPort: 80\n" + "- name: sidecar\n image: sidecar:1.0.0\n ports:\n - containerPort: 8081\n - containerPort: 9090\n"}, {[]string{ "spec", "template", "spec", "containers", "[name=.*]", "image"}, "- nginx:1.7.9\n- sidecar:1.0.0\n"}, {[]string{ "spec", "template", "spec", "containers", "[name=n.*]", "image"}, "- nginx:1.7.9\n"}, {[]string{ "spec", "template", "spec", "containers", "[name=s.*]", "image"}, "- sidecar:1.0.0\n"}, {[]string{ "spec", "template", "spec", "containers", "[name=.*x]", "image"}, "- nginx:1.7.9\n"}, {[]string{ "spec", "template", "spec", "containers", "[name=.*]", "ports"}, "- - containerPort: 80\n- - containerPort: 8081\n - containerPort: 9090\n"}, {[]string{ "spec", "template", "spec", "containers", "[name=.*]", "ports", "[containerPort=8.*]"}, "- containerPort: 80\n- containerPort: 8081\n"}, {[]string{ "spec", "template", "spec", "containers", "[name=.*]", "ports", "[containerPort=.*1]"}, "- containerPort: 8081\n"}, {[]string{ "spec", "template", "spec", "containers", "[name=.*]", "ports", "[containerPort=9.*]"}, "- containerPort: 9090\n"}, {[]string{ "spec", "template", "spec", "containers", "[name=s.*]", "ports", "[containerPort=8.*]"}, "- containerPort: 8081\n"}, {[]string{ "spec", "template", "spec", "containers", "[name=s.*]", "ports", "[containerPort=.*2]"}, ""}, {[]string{ "spec", "template", "spec", "containers", "*", "image"}, "- nginx:1.7.9\n- sidecar:1.0.0\n"}, {[]string{ "spec", "template", "spec", "containers", "*", "ports", "*"}, "- containerPort: 80\n- containerPort: 8081\n- containerPort: 9090\n"}, {[]string{ "spec", "template", "spec", "containers", "[name=.*]", "args", "1"}, "- conf.yaml\n"}, } for i, u := range updates { result, err := node.Pipe(&PathMatcher{Path: u.path}) if !assert.NoError(t, err) { return } assert.Equal(t, u.value, result.MustString(), fmt.Sprintf("%d", i)) } } func TestPathMatcher_Filter_Create(t *testing.T) { testCases := map[string]struct { path []string matches []string modifiedNodeMustContain string create yaml.Kind expectErr string }{ "create non-primitive sequence item that does not exist": { path: []string{"spec", "template", "spec", "containers", "[name=please-create-me]"}, matches: []string{ "name: please-create-me\n", }, modifiedNodeMustContain: "- name: please-create-me", create: yaml.MappingNode, }, "create non-primitive item in empty sequence by index": { path: []string{"spec", "template", "spec", "containers", "[name=nginx]", "envFrom", "0"}, matches: []string{"{}\n"}, modifiedNodeMustContain: "envFrom:\n - {}\n", create: yaml.MappingNode, }, "create primitive item in empty sequence by index": { path: []string{"spec", "template", "spec", "containers", "[name=sidecar]", "args", "0"}, matches: []string{"\n"}, modifiedNodeMustContain: "args:\n -\n", create: yaml.ScalarNode, }, "append primitive item to sequence by index": { path: []string{"spec", "template", "spec", "containers", "[name=nginx]", "args", "2"}, matches: []string{"\n"}, create: yaml.ScalarNode, modifiedNodeMustContain: "args: [-c, conf.yaml, '']", }, "append non-primitive item to sequence by index": { path: []string{"spec", "template", "spec", "containers", "[name=nginx]", "ports", "1"}, matches: []string{"{}\n"}, modifiedNodeMustContain: "ports: [{containerPort: 80}, {}]", create: yaml.MappingNode, }, "appending non-primitive element in middle of sequence": { path: []string{"spec", "template", "spec", "containers", "2", "imagePullPolicy"}, matches: []string{"\n"}, create: yaml.ScalarNode, modifiedNodeMustContain: "\n - imagePullPolicy:\n", }, "fail to create non-primitive item by non-zero index in created sequence": { path: []string{"spec", "template", "spec", "containers", "[name=nginx]", "envFrom", "1"}, matches: []string{}, create: yaml.MappingNode, expectErr: "index 1 specified but only 0 elements found", }, "fail to create primitive item by non-zero index in created sequence": { path: []string{"spec", "template", "spec", "containers", "[name=sidecar]", "args", "1"}, matches: []string{}, create: yaml.ScalarNode, expectErr: "index 1 specified but only 0 elements found", }, "fail to create non-primitive item by distant index in existing sequence": { path: []string{"spec", "template", "spec", "containers", "3"}, matches: []string{}, create: yaml.MappingNode, expectErr: "index 3 specified but only 2 elements found", }, "fail to create primitive item by distant index in existing sequence": { path: []string{"spec", "template", "spec", "containers", "[name=nginx]", "args", "3"}, matches: []string{}, create: yaml.ScalarNode, expectErr: "index 3 specified but only 2 elements found", }, "create primitive sequence item that does not exist": { path: []string{"metadata", "finalizers", "[=create-me]"}, matches: []string{ "create-me\n", }, modifiedNodeMustContain: "finalizers:\n - create-me\n", create: yaml.ScalarNode, }, "create series of maps that do not exist": { path: []string{"spec", "selector", "matchLabels", "does-not-exist"}, matches: []string{ "{}\n", }, modifiedNodeMustContain: "selector:\n matchLabels:\n app: nginx\n does-not-exist: {}\n", create: yaml.MappingNode, }, "create scalar below series of maps and sequences that do not exist": { path: []string{"spec", "template", "spec", "containers", "[name=please-create-me]", "env", "[key=please-create-me]", "value"}, matches: []string{ "\n", }, modifiedNodeMustContain: "- name: please-create-me\n env:\n - key: please-create-me\n value:\n", create: yaml.ScalarNode, }, "find sequence items that already exist": { path: []string{"spec", "template", "spec", "containers", "[name=.*]"}, matches: []string{ "name: nginx\nimage: nginx:1.7.9\nargs: [-c, conf.yaml]\nports: [{containerPort: 80}]\nenv:\n- key: CONTAINER_NAME\n value: nginx\n", "name: sidecar\nimage: sidecar:1.0.0\nports:\n- containerPort: 8081\n- containerPort: 9090\n", }, create: yaml.MappingNode, }, "find and create sequence below wildcard that exists on some sequence items": { path: []string{"spec", "template", "spec", "containers", "[name=.*]", "env"}, matches: []string{ "- key: CONTAINER_NAME\n value: nginx\n", "[]\n", }, create: yaml.SequenceNode, }, "find field below wildcard that exists on all sequence items": { path: []string{"spec", "template", "spec", "containers", "[name=.*]", "ports"}, matches: []string{ "[{containerPort: 80}]\n", "- containerPort: 8081\n- containerPort: 9090\n", }, create: yaml.SequenceNode, }, "find field below query that targets a specific item": { path: []string{"spec", "template", "spec", "containers", "[name=nginx]", "env"}, matches: []string{ "- key: CONTAINER_NAME\n value: nginx\n", }, create: yaml.SequenceNode, }, "create field below query that targets any value of a field that does not exist": { path: []string{"spec", "template", "spec", "containers", "[foo=.*]", "env"}, matches: []string{ "[]\n", }, // This is kinda weird. The query doesn't match anything, and we can't tell that it is a // wildcard rather than a literal, so we use the value to create the field. modifiedNodeMustContain: "- foo: .*\n env: []\n", create: yaml.SequenceNode, }, } nodeStr := `apiVersion: apps/v1 kind: Deployment metadata: name: nginx-deployment labels: app: nginx spec: replicas: 3 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.7.9 args: [-c, conf.yaml] ports: [{containerPort: 80}] env: - key: CONTAINER_NAME value: nginx - name: sidecar image: sidecar:1.0.0 ports: - containerPort: 8081 - containerPort: 9090 ` for name, tc := range testCases { t.Run(name, func(t *testing.T) { node := MustParse(nodeStr) result, err := node.Pipe(&PathMatcher{Path: tc.path, Create: tc.create}) if tc.expectErr != "" { require.EqualError(t, err, tc.expectErr) return } require.NoError(t, err) matches, err := result.Elements() require.NoError(t, err) require.Equalf(t, len(tc.matches), len(matches), "Full sequence wrapper of result:\n%s", result.MustString()) modifiedNode := node.MustString() for i, expected := range tc.matches { assert.Equal(t, tc.create, matches[i].YNode().Kind) assert.Equal(t, expected, matches[i].MustString()) assert.Contains(t, modifiedNode, tc.modifiedNodeMustContain) } }) } } 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()) }) } }