Files
kubemirror/pkg/transformer/transformer_test.go
T

1404 lines
35 KiB
Go

package transformer
import (
"fmt"
"testing"
"github.com/lukaszraczylo/kubemirror/pkg/constants"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
)
func TestTransformer_Transform(t *testing.T) {
tests := []struct {
name string
source runtime.Object
ctx TransformContext
options TransformOptions
wantErr bool
errMsg string
validate func(t *testing.T, result runtime.Object)
}{
// Good cases - Value rules
{
name: "value rule - simple data field",
source: &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-config",
Namespace: "default",
Annotations: map[string]string{
constants.AnnotationTransform: `
rules:
- path: data.LOG_LEVEL
value: "error"
`,
},
},
Data: map[string]string{
"LOG_LEVEL": "debug",
},
},
ctx: TransformContext{
TargetNamespace: "prod",
},
options: DefaultTransformOptions(),
wantErr: false,
validate: func(t *testing.T, result runtime.Object) {
u := result.(*unstructured.Unstructured)
value, found, err := unstructured.NestedString(u.Object, "data", "LOG_LEVEL")
require.NoError(t, err)
assert.True(t, found)
assert.Equal(t, "error", value)
},
},
// Good cases - Template rules
{
name: "template rule - namespace substitution",
source: &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-config",
Namespace: "default",
Annotations: map[string]string{
constants.AnnotationTransform: `
rules:
- path: data.API_URL
template: "https://{{.TargetNamespace}}.api.example.com"
`,
},
},
Data: map[string]string{},
},
ctx: TransformContext{
TargetNamespace: "prod-app",
SourceNamespace: "default",
},
options: DefaultTransformOptions(),
wantErr: false,
validate: func(t *testing.T, result runtime.Object) {
u := result.(*unstructured.Unstructured)
value, found, err := unstructured.NestedString(u.Object, "data", "API_URL")
require.NoError(t, err)
assert.True(t, found)
assert.Equal(t, "https://prod-app.api.example.com", value)
},
},
{
name: "template rule - with template functions",
source: &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-config",
Namespace: "default",
Annotations: map[string]string{
constants.AnnotationTransform: `
rules:
- path: data.NAMESPACE_UPPER
template: "{{upper .TargetNamespace}}"
- path: data.SOURCE_LOWER
template: "{{lower .SourceName}}"
`,
},
},
Data: map[string]string{},
},
ctx: TransformContext{
TargetNamespace: "prod-app",
SourceName: "TEST-CONFIG",
},
options: DefaultTransformOptions(),
wantErr: false,
validate: func(t *testing.T, result runtime.Object) {
u := result.(*unstructured.Unstructured)
upperValue, found, err := unstructured.NestedString(u.Object, "data", "NAMESPACE_UPPER")
require.NoError(t, err)
assert.True(t, found)
assert.Equal(t, "PROD-APP", upperValue)
lowerValue, found, err := unstructured.NestedString(u.Object, "data", "SOURCE_LOWER")
require.NoError(t, err)
assert.True(t, found)
assert.Equal(t, "test-config", lowerValue)
},
},
// Good cases - Merge rules
{
name: "merge rule - add labels",
source: &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-config",
Namespace: "default",
Labels: map[string]string{
"app": "myapp",
},
Annotations: map[string]string{
constants.AnnotationTransform: `
rules:
- path: metadata.labels
merge:
environment: "production"
tier: "frontend"
`,
},
},
Data: map[string]string{},
},
ctx: TransformContext{},
options: DefaultTransformOptions(),
wantErr: false,
validate: func(t *testing.T, result runtime.Object) {
u := result.(*unstructured.Unstructured)
labels := u.GetLabels()
assert.Equal(t, "myapp", labels["app"], "original label should be preserved")
assert.Equal(t, "production", labels["environment"])
assert.Equal(t, "frontend", labels["tier"])
},
},
{
name: "merge rule - create new map",
source: &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-config",
Namespace: "default",
Annotations: map[string]string{
constants.AnnotationTransform: `
rules:
- path: metadata.labels
merge:
new-label: "new-value"
`,
},
},
Data: map[string]string{},
},
ctx: TransformContext{},
options: DefaultTransformOptions(),
wantErr: false,
validate: func(t *testing.T, result runtime.Object) {
u := result.(*unstructured.Unstructured)
labels := u.GetLabels()
assert.Equal(t, "new-value", labels["new-label"])
},
},
// Good cases - Delete rules
{
name: "delete rule - remove data field",
source: &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-config",
Namespace: "default",
Annotations: map[string]string{
constants.AnnotationTransform: `
rules:
- path: data.DEBUG_MODE
delete: true
`,
},
},
Data: map[string]string{
"DEBUG_MODE": "true",
"LOG_LEVEL": "info",
},
},
ctx: TransformContext{},
options: DefaultTransformOptions(),
wantErr: false,
validate: func(t *testing.T, result runtime.Object) {
u := result.(*unstructured.Unstructured)
data, found, err := unstructured.NestedMap(u.Object, "data")
require.NoError(t, err)
assert.True(t, found)
_, exists := data["DEBUG_MODE"]
assert.False(t, exists, "DEBUG_MODE should be deleted")
assert.Equal(t, "info", data["LOG_LEVEL"], "LOG_LEVEL should remain")
},
},
// Bad cases - Invalid YAML
{
name: "invalid YAML in transform annotation",
source: &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-config",
Namespace: "default",
Annotations: map[string]string{
constants.AnnotationTransform: "invalid: yaml: [[[",
constants.AnnotationTransformStrict: "true",
},
},
Data: map[string]string{},
},
ctx: TransformContext{},
options: DefaultTransformOptions(),
wantErr: true,
errMsg: "failed to parse",
},
// Bad cases - Invalid rules
{
name: "empty path in strict mode",
source: &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-config",
Namespace: "default",
Annotations: map[string]string{
constants.AnnotationTransform: `
rules:
- value: "something"
`,
constants.AnnotationTransformStrict: "true",
},
},
Data: map[string]string{},
},
ctx: TransformContext{},
options: DefaultTransformOptions(),
wantErr: true,
errMsg: "invalid transformation rules",
},
{
name: "too many rules in strict mode",
source: func() runtime.Object {
rules := "rules:\n"
for i := 0; i < 100; i++ {
rules += fmt.Sprintf(" - path: data.KEY%d\n value: \"val\"\n", i)
}
return &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-config",
Namespace: "default",
Annotations: map[string]string{
constants.AnnotationTransform: rules,
constants.AnnotationTransformStrict: "true",
},
},
Data: map[string]string{},
}
}(),
ctx: TransformContext{},
options: DefaultTransformOptions(),
wantErr: true,
errMsg: "too many rules",
},
// Edge cases
{
name: "no transformation rules",
source: &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-config",
Namespace: "default",
},
Data: map[string]string{
"KEY": "value",
},
},
ctx: TransformContext{},
options: DefaultTransformOptions(),
wantErr: false,
validate: func(t *testing.T, result runtime.Object) {
// When no rules, returns original typed object - convert to unstructured for checking
unstructuredObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(result)
require.NoError(t, err)
u := &unstructured.Unstructured{Object: unstructuredObj}
value, found, err := unstructured.NestedString(u.Object, "data", "KEY")
require.NoError(t, err)
assert.True(t, found)
assert.Equal(t, "value", value, "original value should be unchanged")
},
},
{
name: "non-strict mode ignores errors",
source: &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-config",
Namespace: "default",
Annotations: map[string]string{
constants.AnnotationTransform: "invalid yaml [[[",
},
},
Data: map[string]string{
"KEY": "value",
},
},
ctx: TransformContext{},
options: DefaultTransformOptions(),
wantErr: false,
validate: func(t *testing.T, result runtime.Object) {
// Should return original unchanged - check via unstructured conversion
unstructuredObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(result)
require.NoError(t, err)
u := &unstructured.Unstructured{Object: unstructuredObj}
value, found, err := unstructured.NestedString(u.Object, "data", "KEY")
require.NoError(t, err)
assert.True(t, found)
assert.Equal(t, "value", value)
},
},
{
name: "multiple rules applied in order",
source: &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-config",
Namespace: "default",
Annotations: map[string]string{
constants.AnnotationTransform: `
rules:
- path: data.KEY1
value: "first"
- path: data.KEY2
template: "{{.TargetNamespace}}-value"
- path: metadata.labels
merge:
env: "prod"
- path: data.TO_DELETE
delete: true
`,
},
},
Data: map[string]string{
"TO_DELETE": "remove-me",
},
},
ctx: TransformContext{
TargetNamespace: "production",
},
options: DefaultTransformOptions(),
wantErr: false,
validate: func(t *testing.T, result runtime.Object) {
u := result.(*unstructured.Unstructured)
key1, found, _ := unstructured.NestedString(u.Object, "data", "KEY1")
assert.True(t, found)
assert.Equal(t, "first", key1)
key2, found, _ := unstructured.NestedString(u.Object, "data", "KEY2")
assert.True(t, found)
assert.Equal(t, "production-value", key2)
labels := u.GetLabels()
assert.Equal(t, "prod", labels["env"])
data, found, _ := unstructured.NestedMap(u.Object, "data")
assert.True(t, found)
_, exists := data["TO_DELETE"]
assert.False(t, exists)
},
},
// Array indexing cases
{
name: "array indexing - modify container image",
source: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Pod",
"metadata": map[string]interface{}{
"name": "test-pod",
"namespace": "default",
"annotations": map[string]interface{}{
constants.AnnotationTransform: `
rules:
- path: spec.containers[0].image
template: "registry.{{.TargetNamespace}}.example.com/app:v1"
- path: spec.containers[0].env[1].value
value: "production"
`,
},
},
"spec": map[string]interface{}{
"containers": []interface{}{
map[string]interface{}{
"name": "app",
"image": "app:latest",
"env": []interface{}{
map[string]interface{}{"name": "VAR1", "value": "val1"},
map[string]interface{}{"name": "VAR2", "value": "val2"},
},
},
},
},
},
},
ctx: TransformContext{
TargetNamespace: "prod-app",
},
options: DefaultTransformOptions(),
wantErr: false,
validate: func(t *testing.T, result runtime.Object) {
u := result.(*unstructured.Unstructured)
// Check container image was transformed
containers, found, _ := unstructured.NestedSlice(u.Object, "spec", "containers")
assert.True(t, found)
assert.Len(t, containers, 1)
container := containers[0].(map[string]interface{})
assert.Equal(t, "registry.prod-app.example.com/app:v1", container["image"])
// Check env var was transformed
env := container["env"].([]interface{})
assert.Len(t, env, 2)
envVar := env[1].(map[string]interface{})
assert.Equal(t, "production", envVar["value"])
},
},
// Awkward cases
{
name: "template with missing context variable",
source: &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-config",
Namespace: "default",
Annotations: map[string]string{
constants.AnnotationTransform: `
rules:
- path: data.VALUE
template: "{{.TargetNamespace}}-empty"
`,
},
},
Data: map[string]string{},
},
ctx: TransformContext{
TargetNamespace: "",
},
options: DefaultTransformOptions(),
wantErr: false, // Non-strict mode
validate: func(t *testing.T, result runtime.Object) {
u := result.(*unstructured.Unstructured)
value, found, _ := unstructured.NestedString(u.Object, "data", "VALUE")
// Template with empty context variable produces "-empty"
assert.True(t, found)
assert.Equal(t, "-empty", value)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
transformer := NewTransformer(tt.options)
result, err := transformer.Transform(tt.source, tt.ctx)
if tt.wantErr {
assert.Error(t, err)
if tt.errMsg != "" {
assert.Contains(t, err.Error(), tt.errMsg)
}
} else {
require.NoError(t, err)
if tt.validate != nil {
tt.validate(t, result)
}
}
})
}
}
func TestParsePath(t *testing.T) {
tests := []struct {
name string
path string
want []string
}{
{
name: "simple path",
path: "data.KEY",
want: []string{"data", "KEY"},
},
{
name: "nested path",
path: "metadata.labels.app",
want: []string{"metadata", "labels", "app"},
},
{
name: "empty path",
path: "",
want: nil,
},
{
name: "single segment",
path: "data",
want: []string{"data"},
},
// Array indexing tests
{
name: "array index - single",
path: "containers[0]",
want: []string{"containers", "[0]"},
},
{
name: "array index - with nested field",
path: "spec.containers[0].image",
want: []string{"spec", "containers", "[0]", "image"},
},
{
name: "array index - multiple levels",
path: "spec.template.spec.containers[0].env[2].value",
want: []string{"spec", "template", "spec", "containers", "[0]", "env", "[2]", "value"},
},
{
name: "array index - at end",
path: "data.items[5]",
want: []string{"data", "items", "[5]"},
},
{
name: "array index - large number",
path: "list[999].field",
want: []string{"list", "[999]", "field"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := parsePath(tt.path)
assert.Equal(t, tt.want, got)
})
}
}
func TestSetNestedField(t *testing.T) {
tests := []struct {
name string
obj map[string]interface{}
path []string
value interface{}
wantErr bool
want map[string]interface{}
}{
{
name: "set top-level field",
obj: map[string]interface{}{},
path: []string{"key"},
value: "value",
wantErr: false,
want: map[string]interface{}{
"key": "value",
},
},
{
name: "set nested field - creates intermediate maps",
obj: map[string]interface{}{},
path: []string{"data", "key"},
value: "value",
wantErr: false,
want: map[string]interface{}{
"data": map[string]interface{}{
"key": "value",
},
},
},
{
name: "set deeply nested field",
obj: map[string]interface{}{},
path: []string{"a", "b", "c", "d"},
value: "deep",
wantErr: false,
want: map[string]interface{}{
"a": map[string]interface{}{
"b": map[string]interface{}{
"c": map[string]interface{}{
"d": "deep",
},
},
},
},
},
{
name: "empty path",
obj: map[string]interface{}{},
path: []string{},
value: "value",
wantErr: true,
},
{
name: "path segment is not a map",
obj: map[string]interface{}{
"key": "string-value",
},
path: []string{"key", "nested"},
value: "value",
wantErr: true,
},
// Array indexing tests
{
name: "set array element value",
obj: map[string]interface{}{
"items": []interface{}{"a", "b", "c"},
},
path: []string{"items", "[1]"},
value: "modified",
wantErr: false,
want: map[string]interface{}{
"items": []interface{}{"a", "modified", "c"},
},
},
{
name: "set nested field in array element",
obj: map[string]interface{}{
"containers": []interface{}{
map[string]interface{}{
"name": "app",
"image": "old-image",
},
},
},
path: []string{"containers", "[0]", "image"},
value: "new-image",
wantErr: false,
want: map[string]interface{}{
"containers": []interface{}{
map[string]interface{}{
"name": "app",
"image": "new-image",
},
},
},
},
{
name: "set deeply nested array access",
obj: map[string]interface{}{
"spec": map[string]interface{}{
"containers": []interface{}{
map[string]interface{}{
"env": []interface{}{
map[string]interface{}{"name": "VAR1", "value": "val1"},
map[string]interface{}{"name": "VAR2", "value": "val2"},
},
},
},
},
},
path: []string{"spec", "containers", "[0]", "env", "[1]", "value"},
value: "new-val2",
wantErr: false,
want: map[string]interface{}{
"spec": map[string]interface{}{
"containers": []interface{}{
map[string]interface{}{
"env": []interface{}{
map[string]interface{}{"name": "VAR1", "value": "val1"},
map[string]interface{}{"name": "VAR2", "value": "new-val2"},
},
},
},
},
},
},
{
name: "array index out of bounds",
obj: map[string]interface{}{
"items": []interface{}{"a", "b"},
},
path: []string{"items", "[5]"},
value: "value",
wantErr: true,
},
{
name: "array index on non-array",
obj: map[string]interface{}{
"items": "not-an-array",
},
path: []string{"items", "[0]"},
value: "value",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := setNestedField(tt.obj, tt.path, tt.value)
if tt.wantErr {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, tt.want, tt.obj)
}
})
}
}
func TestTransformer_TemplateTimeout(t *testing.T) {
// Test that template execution times out
// Since we can't easily create a template that times out reliably in tests,
// we'll skip this test or use a very aggressive timeout
// The timeout mechanism is implemented via context in the transformer
t.Skip("Template timeout testing is unreliable in unit tests - covered by integration tests")
}
func TestMatchGlob(t *testing.T) {
tests := []struct {
name string
pattern string
text string
expected bool
}{
// Exact matches
{
name: "exact match",
pattern: "production",
text: "production",
expected: true,
},
{
name: "exact match - no match",
pattern: "production",
text: "staging",
expected: false,
},
// Wildcard * patterns
{
name: "wildcard all",
pattern: "*",
text: "anything",
expected: true,
},
{
name: "prefix wildcard",
pattern: "prod-*",
text: "prod-app-1",
expected: true,
},
{
name: "prefix wildcard - no match",
pattern: "prod-*",
text: "staging-app-1",
expected: false,
},
{
name: "suffix wildcard",
pattern: "*-staging",
text: "app-staging",
expected: true,
},
{
name: "suffix wildcard - no match",
pattern: "*-staging",
text: "app-production",
expected: false,
},
{
name: "middle wildcard",
pattern: "app-*-db",
text: "app-prod-db",
expected: true,
},
{
name: "middle wildcard - no match",
pattern: "app-*-db",
text: "app-prod-cache",
expected: false,
},
{
name: "multiple wildcards",
pattern: "*-prod-*",
text: "service-prod-v1",
expected: true,
},
{
name: "wildcard matches empty",
pattern: "app-*",
text: "app-",
expected: true,
},
// Single character wildcard ?
{
name: "single char wildcard",
pattern: "app-?",
text: "app-1",
expected: true,
},
{
name: "single char wildcard - no match (too long)",
pattern: "app-?",
text: "app-12",
expected: false,
},
{
name: "single char wildcard - no match (too short)",
pattern: "app-?",
text: "app-",
expected: false,
},
{
name: "multiple single char wildcards",
pattern: "app-??",
text: "app-12",
expected: true,
},
{
name: "mixed wildcards",
pattern: "app-?-*",
text: "app-1-prod",
expected: true,
},
// Edge cases
{
name: "empty pattern and text",
pattern: "",
text: "",
expected: true,
},
{
name: "empty pattern non-empty text",
pattern: "",
text: "text",
expected: false,
},
{
name: "pattern longer than text",
pattern: "production",
text: "prod",
expected: false,
},
{
name: "text longer than pattern",
pattern: "prod",
text: "production",
expected: false,
},
// Real-world examples
{
name: "preprod namespaces",
pattern: "preprod-*",
text: "preprod-api",
expected: true,
},
{
name: "staging environments",
pattern: "*-staging",
text: "app-staging",
expected: true,
},
{
name: "numbered namespaces",
pattern: "namespace-?",
text: "namespace-1",
expected: true,
},
{
name: "versioned services",
pattern: "service-v*",
text: "service-v1.2.3",
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := matchGlob(tt.pattern, tt.text)
assert.Equal(t, tt.expected, result, "matchGlob(%q, %q)", tt.pattern, tt.text)
})
}
}
func TestMatchesNamespacePattern(t *testing.T) {
tests := []struct {
name string
pattern *string
targetNamespace string
expected bool
}{
{
name: "no pattern - matches all",
pattern: nil,
targetNamespace: "any-namespace",
expected: true,
},
{
name: "empty pattern - matches all",
pattern: stringPtr(""),
targetNamespace: "any-namespace",
expected: true,
},
{
name: "exact match",
pattern: stringPtr("production"),
targetNamespace: "production",
expected: true,
},
{
name: "exact match - no match",
pattern: stringPtr("production"),
targetNamespace: "staging",
expected: false,
},
{
name: "prefix pattern match",
pattern: stringPtr("preprod-*"),
targetNamespace: "preprod-api",
expected: true,
},
{
name: "prefix pattern no match",
pattern: stringPtr("preprod-*"),
targetNamespace: "prod-api",
expected: false,
},
{
name: "suffix pattern match",
pattern: stringPtr("*-staging"),
targetNamespace: "app-staging",
expected: true,
},
{
name: "suffix pattern no match",
pattern: stringPtr("*-staging"),
targetNamespace: "app-prod",
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
rule := Rule{
Path: "data.test",
Value: stringPtr("value"),
NamespacePattern: tt.pattern,
}
result := matchesNamespacePattern(rule, tt.targetNamespace)
assert.Equal(t, tt.expected, result)
})
}
}
func TestTransformer_NamespacePatternFiltering(t *testing.T) {
tests := []struct {
name string
sourceData map[string]interface{}
rules string
targetNamespace string
expectedData map[string]interface{}
description string
}{
{
name: "rule applies to matching namespace",
sourceData: map[string]interface{}{
"data": map[string]interface{}{
"HOST": "default.example.com",
},
},
rules: `
rules:
- path: data.HOST
value: "preprod.example.com"
namespacePattern: "preprod-*"
`,
targetNamespace: "preprod-api",
expectedData: map[string]interface{}{
"data": map[string]interface{}{
"HOST": "preprod.example.com",
},
},
description: "Rule with pattern 'preprod-*' should apply to 'preprod-api'",
},
{
name: "rule skipped for non-matching namespace",
sourceData: map[string]interface{}{
"data": map[string]interface{}{
"HOST": "default.example.com",
},
},
rules: `
rules:
- path: data.HOST
value: "preprod.example.com"
namespacePattern: "preprod-*"
`,
targetNamespace: "production",
expectedData: map[string]interface{}{
"data": map[string]interface{}{
"HOST": "default.example.com",
},
},
description: "Rule with pattern 'preprod-*' should NOT apply to 'production'",
},
{
name: "multiple rules with different patterns",
sourceData: map[string]interface{}{
"data": map[string]interface{}{
"HOST": "default.example.com",
"LOG_LEVEL": "info",
},
},
rules: `
rules:
- path: data.HOST
value: "preprod.example.com"
namespacePattern: "preprod-*"
- path: data.HOST
value: "prod.example.com"
namespacePattern: "prod-*"
- path: data.LOG_LEVEL
value: "debug"
namespacePattern: "preprod-*"
`,
targetNamespace: "preprod-api",
expectedData: map[string]interface{}{
"data": map[string]interface{}{
"HOST": "preprod.example.com",
"LOG_LEVEL": "debug",
},
},
description: "Only rules matching 'preprod-*' should apply to 'preprod-api'",
},
{
name: "rule without pattern applies to all namespaces",
sourceData: map[string]interface{}{
"data": map[string]interface{}{
"GLOBAL": "old",
"SCOPED": "old",
},
},
rules: `
rules:
- path: data.GLOBAL
value: "applied-to-all"
- path: data.SCOPED
value: "applied-to-prod"
namespacePattern: "prod-*"
`,
targetNamespace: "staging",
expectedData: map[string]interface{}{
"data": map[string]interface{}{
"GLOBAL": "applied-to-all",
"SCOPED": "old",
},
},
description: "Rule without pattern should apply, rule with non-matching pattern should not",
},
{
name: "template rule with namespace pattern",
sourceData: map[string]interface{}{
"data": map[string]interface{}{
"API_URL": "default.api.com",
},
},
rules: `
rules:
- path: data.API_URL
template: "https://{{.TargetNamespace}}.api.com"
namespacePattern: "preprod-*"
`,
targetNamespace: "preprod-service",
expectedData: map[string]interface{}{
"data": map[string]interface{}{
"API_URL": "https://preprod-service.api.com",
},
},
description: "Template rule with namespace pattern should apply when pattern matches",
},
{
name: "suffix pattern matching (*-staging)",
sourceData: map[string]interface{}{
"data": map[string]interface{}{
"LOG_LEVEL": "info",
"GRAPHQL_HOST": "default.example.com",
},
},
rules: `
rules:
- path: data.LOG_LEVEL
value: "warn"
namespacePattern: "*-staging"
- path: data.GRAPHQL_HOST
value: "https://staging.example.com/v1/graphql"
namespacePattern: "*-staging"
`,
targetNamespace: "app-staging",
expectedData: map[string]interface{}{
"data": map[string]interface{}{
"LOG_LEVEL": "warn",
"GRAPHQL_HOST": "https://staging.example.com/v1/graphql",
},
},
description: "Suffix pattern *-staging should match app-staging",
},
{
name: "suffix pattern non-matching",
sourceData: map[string]interface{}{
"data": map[string]interface{}{
"LOG_LEVEL": "info",
},
},
rules: `
rules:
- path: data.LOG_LEVEL
value: "warn"
namespacePattern: "*-staging"
`,
targetNamespace: "production",
expectedData: map[string]interface{}{
"data": map[string]interface{}{
"LOG_LEVEL": "info",
},
},
description: "Suffix pattern *-staging should NOT match production",
},
{
name: "single character wildcard (namespace-?)",
sourceData: map[string]interface{}{
"data": map[string]interface{}{
"ENVIRONMENT": "unknown",
},
},
rules: `
rules:
- path: data.ENVIRONMENT
value: "development"
namespacePattern: "namespace-?"
`,
targetNamespace: "namespace-2",
expectedData: map[string]interface{}{
"data": map[string]interface{}{
"ENVIRONMENT": "development",
},
},
description: "Single char wildcard namespace-? should match namespace-2",
},
{
name: "single character wildcard non-matching (too long)",
sourceData: map[string]interface{}{
"data": map[string]interface{}{
"ENVIRONMENT": "unknown",
},
},
rules: `
rules:
- path: data.ENVIRONMENT
value: "development"
namespacePattern: "namespace-?"
`,
targetNamespace: "namespace-10",
expectedData: map[string]interface{}{
"data": map[string]interface{}{
"ENVIRONMENT": "unknown",
},
},
description: "Single char wildcard namespace-? should NOT match namespace-10 (too many chars)",
},
{
name: "prod-* pattern with value rule",
sourceData: map[string]interface{}{
"data": map[string]interface{}{
"LOG_LEVEL": "info",
"GRAPHQL_HOST": "default.example.com",
},
},
rules: `
rules:
- path: data.LOG_LEVEL
value: "error"
namespacePattern: "prod-*"
- path: data.GRAPHQL_HOST
value: "https://api.example.com/v1/graphql"
namespacePattern: "prod-*"
`,
targetNamespace: "prod-api",
expectedData: map[string]interface{}{
"data": map[string]interface{}{
"LOG_LEVEL": "error",
"GRAPHQL_HOST": "https://api.example.com/v1/graphql",
},
},
description: "Pattern prod-* should match prod-api",
},
{
name: "merge rule with namespace pattern",
sourceData: map[string]interface{}{
"metadata": map[string]interface{}{
"labels": map[string]interface{}{
"app": "myapp",
},
},
"data": map[string]interface{}{
"config": "value",
},
},
rules: `
rules:
- path: metadata.labels
merge:
security-tier: "high"
compliance: "required"
namespacePattern: "prod-*"
`,
targetNamespace: "prod-api",
expectedData: map[string]interface{}{
"data": map[string]interface{}{
"config": "value",
},
"metadata": map[string]interface{}{
"labels": map[string]interface{}{
"app": "myapp",
"security-tier": "high",
"compliance": "required",
},
},
},
description: "Merge rule with prod-* pattern should add labels to prod-api",
},
{
name: "delete rule with namespace pattern",
sourceData: map[string]interface{}{
"data": map[string]interface{}{
"DEBUG_MODE": "true",
"ADMIN_KEY": "secret",
"PUBLIC_VALUE": "safe",
},
},
rules: `
rules:
- path: data.DEBUG_MODE
delete: true
namespacePattern: "prod-*"
- path: data.ADMIN_KEY
delete: true
namespacePattern: "prod-*"
`,
targetNamespace: "prod-api",
expectedData: map[string]interface{}{
"data": map[string]interface{}{
"PUBLIC_VALUE": "safe",
},
},
description: "Delete rules with prod-* pattern should remove debug fields from prod-api",
},
{
name: "complex multi-environment pattern (like example 10)",
sourceData: map[string]interface{}{
"data": map[string]interface{}{
"APP_NAME": "default-app",
"ENVIRONMENT": "unknown",
"LOG_LEVEL": "info",
},
},
rules: `
rules:
# Global rule - no pattern
- path: data.APP_NAME
value: "universal-app"
# Numbered namespaces
- path: data.ENVIRONMENT
value: "development"
namespacePattern: "namespace-?"
# Preprod
- path: data.ENVIRONMENT
value: "preproduction"
namespacePattern: "preprod-*"
- path: data.LOG_LEVEL
value: "debug"
namespacePattern: "preprod-*"
# Production
- path: data.ENVIRONMENT
value: "production"
namespacePattern: "prod-*"
- path: data.LOG_LEVEL
value: "error"
namespacePattern: "prod-*"
`,
targetNamespace: "preprod-api",
expectedData: map[string]interface{}{
"data": map[string]interface{}{
"APP_NAME": "universal-app",
"ENVIRONMENT": "preproduction",
"LOG_LEVEL": "debug",
},
},
description: "Complex multi-pattern rules should apply global + preprod-specific rules to preprod-api",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create source object with transformation rules
source := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "test-config",
"namespace": "source-namespace",
"annotations": map[string]interface{}{
constants.AnnotationTransform: tt.rules,
},
},
},
}
// Merge source data
for k, v := range tt.sourceData {
if k == "metadata" {
// Merge metadata instead of replacing it
existingMeta := source.Object["metadata"].(map[string]interface{})
newMeta := v.(map[string]interface{})
for mk, mv := range newMeta {
existingMeta[mk] = mv
}
} else {
source.Object[k] = v
}
}
// Transform
transformer := NewDefaultTransformer()
ctx := TransformContext{
TargetNamespace: tt.targetNamespace,
SourceNamespace: "source-namespace",
SourceName: "test-config",
TargetName: "test-config",
}
result, err := transformer.Transform(source, ctx)
require.NoError(t, err, tt.description)
resultU, ok := result.(*unstructured.Unstructured)
require.True(t, ok)
// Check that the expected fields have the expected values
for key, expectedValue := range tt.expectedData {
if key == "data" {
dataMap, found, err := unstructured.NestedMap(resultU.Object, "data")
require.NoError(t, err)
require.True(t, found)
expectedDataMap := expectedValue.(map[string]interface{})
for dataKey, dataValue := range expectedDataMap {
assert.Equal(t, dataValue, dataMap[dataKey], "%s: data.%s should be %v", tt.description, dataKey, dataValue)
}
}
if key == "metadata" {
metadataMap := expectedValue.(map[string]interface{})
if labelsExpected, ok := metadataMap["labels"]; ok {
labelsMap, found, err := unstructured.NestedMap(resultU.Object, "metadata", "labels")
require.NoError(t, err)
require.True(t, found)
expectedLabels := labelsExpected.(map[string]interface{})
for labelKey, labelValue := range expectedLabels {
assert.Equal(t, labelValue, labelsMap[labelKey], "%s: metadata.labels.%s should be %v", tt.description, labelKey, labelValue)
}
}
}
}
})
}
}