package mutate import ( "encoding/json" "reflect" "testing" jsonpatch "github.com/evanphx/json-patch/v5" "github.com/kyverno/kyverno/pkg/engine/utils" "gotest.tools/assert" "sigs.k8s.io/controller-runtime/pkg/log" ) func compareJSONAsMap(t *testing.T, expected, actual []byte) { var expectedMap, actualMap map[string]interface{} assert.NilError(t, json.Unmarshal(expected, &expectedMap)) assert.NilError(t, json.Unmarshal(actual, &actualMap)) assert.Assert(t, reflect.DeepEqual(expectedMap, actualMap)) } func TestProcessOverlayPatches_NestedListWithAnchor(t *testing.T) { resourceRaw := []byte(` { "apiVersion":"v1", "kind":"Endpoints", "metadata":{ "name":"test-endpoint", "labels":{ "label":"test" } }, "subsets":[ { "addresses":[ { "ip":"192.168.10.171" } ], "ports":[ { "name":"secure-connection", "port":443, "protocol":"TCP" } ] } ] }`) overlayRaw := []byte(` { "subsets":[ { "ports":[ { "(name)":"secure-connection", "port":444, "protocol":"UDP" } ] } ] }`) var resource, overlay interface{} err := json.Unmarshal(resourceRaw, &resource) assert.NilError(t, err) err = json.Unmarshal(overlayRaw, &overlay) assert.NilError(t, err) patches, overlayerr := processOverlayPatches(log.Log, resource, overlay) assert.Assert(t, reflect.DeepEqual(overlayerr, overlayError{})) assert.Assert(t, patches != nil) patch := utils.JoinPatches(patches) decoded, err := jsonpatch.DecodePatch(patch) assert.NilError(t, err) assert.Assert(t, decoded != nil) patched, err := decoded.Apply(resourceRaw) assert.NilError(t, err) assert.Assert(t, patched != nil) expectedResult := []byte(` { "apiVersion":"v1", "kind":"Endpoints", "metadata":{ "name":"test-endpoint", "labels":{ "label":"test" } }, "subsets":[ { "addresses":[ { "ip":"192.168.10.171" } ], "ports":[ { "name":"secure-connection", "port":444.000000, "protocol":"UDP" } ] } ] }`) compareJSONAsMap(t, expectedResult, patched) } func TestProcessOverlayPatches_InsertIntoArray(t *testing.T) { resourceRaw := []byte(` { "apiVersion":"v1", "kind":"Endpoints", "metadata":{ "name":"test-endpoint", "labels":{ "label":"test" } }, "subsets":[ { "addresses":[ { "ip":"192.168.10.171" } ], "ports":[ { "name":"secure-connection", "port":443, "protocol":"TCP" } ] } ] }`) overlayRaw := []byte(` { "subsets":[ { "addresses":[ { "ip":"192.168.10.172" }, { "ip":"192.168.10.173" } ], "ports":[ { "name":"insecure-connection", "port":80, "protocol":"UDP" } ] } ] }`) var resource, overlay interface{} err := json.Unmarshal(resourceRaw, &resource) assert.NilError(t, err) err = json.Unmarshal(overlayRaw, &overlay) assert.NilError(t, err) patches, overlayerr := processOverlayPatches(log.Log, resource, overlay) assert.Assert(t, reflect.DeepEqual(overlayerr, overlayError{})) assert.Assert(t, patches != nil) patch := utils.JoinPatches(patches) decoded, err := jsonpatch.DecodePatch(patch) assert.NilError(t, err) assert.Assert(t, decoded != nil) patched, err := decoded.Apply(resourceRaw) assert.NilError(t, err) assert.Assert(t, patched != nil) expectedResult := []byte(`{ "apiVersion":"v1", "kind":"Endpoints", "metadata":{ "name":"test-endpoint", "labels":{ "label":"test" } }, "subsets":[ { "addresses":[ { "ip":"192.168.10.171" } ], "ports":[ { "name":"secure-connection", "port":443, "protocol":"TCP" } ] }, { "addresses":[ { "ip":"192.168.10.172" }, { "ip":"192.168.10.173" } ], "ports":[ { "name":"insecure-connection", "port":80, "protocol":"UDP" } ] } ] }`) compareJSONAsMap(t, expectedResult, patched) } func TestProcessOverlayPatches_TestInsertToArray(t *testing.T) { overlayRaw := []byte(` { "spec":{ "template":{ "spec":{ "containers":[ { "name":"pi1", "image":"vasylev.perl" } ] } } } }`) resourceRaw := []byte(`{ "apiVersion":"batch/v1", "kind":"Job", "metadata":{ "name":"pi" }, "spec":{ "template":{ "spec":{ "containers":[ { "name":"piv0", "image":"perl", "command":[ "perl" ] }, { "name":"pi", "image":"perl", "command":[ "perl" ] }, { "name":"piv1", "image":"perl", "command":[ "perl" ] } ], "restartPolicy":"Never" } }, "backoffLimit":4 } }`) var resource, overlay interface{} err := json.Unmarshal(resourceRaw, &resource) assert.NilError(t, err) err = json.Unmarshal(overlayRaw, &overlay) assert.NilError(t, err) patches, overlayerr := processOverlayPatches(log.Log, resource, overlay) assert.Assert(t, reflect.DeepEqual(overlayerr, overlayError{})) assert.Assert(t, patches != nil) patch := utils.JoinPatches(patches) decoded, err := jsonpatch.DecodePatch(patch) assert.NilError(t, err) assert.Assert(t, decoded != nil) patched, err := decoded.Apply(resourceRaw) assert.NilError(t, err) assert.Assert(t, patched != nil) } func TestProcessOverlayPatches_ImagePullPolicy(t *testing.T) { overlayRaw := []byte(`{ "spec": { "template": { "spec": { "containers": [ { "(image)": "*:latest", "imagePullPolicy": "IfNotPresent", "ports": [ { "containerPort": 8080 } ] } ] } } } }`) resourceRaw := []byte(`{ "apiVersion": "apps/v1", "kind": "Deployment", "metadata": { "name": "nginx-deployment", "labels": { "app": "nginx" } }, "spec": { "replicas": 1, "selector": { "matchLabels": { "app": "nginx" } }, "template": { "metadata": { "labels": { "app": "nginx" } }, "spec": { "containers": [ { "name": "nginx", "image": "nginx:latest", "ports": [ { "containerPort": 80 } ] }, { "name": "ghost", "image": "ghost:latest" } ] } } } }`) var resource, overlay interface{} err := json.Unmarshal(resourceRaw, &resource) assert.NilError(t, err) err = json.Unmarshal(overlayRaw, &overlay) assert.NilError(t, err) patches, overlayerr := processOverlayPatches(log.Log, resource, overlay) assert.Assert(t, reflect.DeepEqual(overlayerr, overlayError{})) assert.Assert(t, len(patches) != 0) doc, err := utils.ApplyPatches(resourceRaw, patches) assert.NilError(t, err) expectedResult := []byte(`{ "apiVersion":"apps/v1", "kind":"Deployment", "metadata":{ "name":"nginx-deployment", "labels":{ "app":"nginx" } }, "spec":{ "replicas":1, "selector":{ "matchLabels":{ "app":"nginx" } }, "template":{ "metadata":{ "labels":{ "app":"nginx" } }, "spec":{ "containers":[ { "image":"nginx:latest", "imagePullPolicy":"IfNotPresent", "name":"nginx", "ports":[ { "containerPort":80 }, { "containerPort":8080 } ] }, { "image":"ghost:latest", "imagePullPolicy":"IfNotPresent", "name":"ghost", "ports":[ { "containerPort":8080 } ] } ] } } } }`) compareJSONAsMap(t, expectedResult, doc) overlayRaw = []byte(`{ "spec": { "template": { "metadata": { "labels": { "(app)": "nginx" } }, "spec": { "containers": [ { "(image)": "*:latest", "imagePullPolicy": "IfNotPresent", "ports": [ { "containerPort": 8080 } ] } ] } } } }`) err = json.Unmarshal(overlayRaw, &overlay) assert.NilError(t, err) patches, err = processOverlayPatches(log.Log, resource, overlay) assert.Assert(t, reflect.DeepEqual(err, overlayError{})) assert.Assert(t, len(patches) != 0) doc, err = utils.ApplyPatches(resourceRaw, patches) assert.NilError(t, err) compareJSONAsMap(t, expectedResult, doc) overlayRaw = []byte(`{ "spec": { "template": { "metadata": { "labels": { "(app)": "nginx1" } }, "spec": { "containers": [ { "(image)": "*:latest", "imagePullPolicy": "IfNotPresent", "ports": [ { "containerPort": 8080 } ] } ] } } } }`) err = json.Unmarshal(overlayRaw, &overlay) assert.NilError(t, err) patches, err = processOverlayPatches(log.Log, resource, overlay) assert.Error(t, err, "[overlayError:0] Policy not applied, conditions are not met at /spec/template/metadata/labels/app/, [overlayError:0] Failed validating value nginx with overlay nginx1") assert.Assert(t, len(patches) == 0) } func TestProcessOverlayPatches_AddingAnchor(t *testing.T) { overlayRaw := []byte(`{ "metadata": { "name": "nginx-deployment", "labels": { "+(app)": "should-not-be-here", "+(key1)": "value1" } } }`) resourceRaw := []byte(`{ "metadata": { "name": "nginx-deployment", "labels": { "app": "nginx" } } }`) var resource, overlay interface{} err := json.Unmarshal(resourceRaw, &resource) assert.NilError(t, err) err = json.Unmarshal(overlayRaw, &overlay) assert.NilError(t, err) patches, overlayerr := processOverlayPatches(log.Log, resource, overlay) assert.Assert(t, reflect.DeepEqual(overlayerr, overlayError{})) assert.Assert(t, len(patches) != 0) doc, err := utils.ApplyPatches(resourceRaw, patches) assert.NilError(t, err) expectedResult := []byte(`{ "metadata":{ "labels":{ "app":"nginx", "key1":"value1" }, "name":"nginx-deployment" } }`) compareJSONAsMap(t, expectedResult, doc) } func TestProcessOverlayPatches_AddingAnchorInsideListElement(t *testing.T) { overlayRaw := []byte(` { "spec": { "template": { "spec": { "containers": [ { "(image)": "*:latest", "+(imagePullPolicy)": "IfNotPresent" } ] } } } }`) resourceRaw := []byte(` { "apiVersion":"apps/v1", "kind":"Deployment", "metadata":{ "name":"nginx-deployment", "labels":{ "app":"nginx" } }, "spec":{ "replicas":1, "selector":{ "matchLabels":{ "app":"nginx" } }, "template":{ "metadata":{ "labels":{ "app":"nginx" } }, "spec":{ "containers":[ { "image":"nginx:latest" }, { "image":"ghost:latest", "imagePullPolicy":"Always" }, { "image":"debian:latest" }, { "image":"ubuntu:latest", "imagePullPolicy":"Always" } ] } } } }`) var resource, overlay interface{} err := json.Unmarshal(resourceRaw, &resource) assert.NilError(t, err) err = json.Unmarshal(overlayRaw, &overlay) assert.NilError(t, err) patches, overlayerr := processOverlayPatches(log.Log, resource, overlay) assert.Assert(t, reflect.DeepEqual(overlayerr, overlayError{})) assert.Assert(t, len(patches) != 0) doc, err := utils.ApplyPatches(resourceRaw, patches) assert.NilError(t, err) expectedResult := []byte(` { "apiVersion":"apps/v1", "kind":"Deployment", "metadata":{ "name":"nginx-deployment", "labels":{ "app":"nginx" } }, "spec":{ "replicas":1, "selector":{ "matchLabels":{ "app":"nginx" } }, "template":{ "metadata":{ "labels":{ "app":"nginx" } }, "spec":{ "containers":[ { "image":"nginx:latest", "imagePullPolicy":"IfNotPresent" }, { "image":"ghost:latest", "imagePullPolicy":"Always" }, { "image":"debian:latest", "imagePullPolicy":"IfNotPresent" }, { "image":"ubuntu:latest", "imagePullPolicy":"Always" } ] } } } }`) compareJSONAsMap(t, expectedResult, doc) // multiple anchors overlayRaw = []byte(` { "spec": { "template": { "metadata": { "labels": { "(app)": "nginx" } }, "spec": { "containers": [ { "(image)": "*:latest", "+(imagePullPolicy)": "IfNotPresent" } ] } } } }`) err = json.Unmarshal(overlayRaw, &overlay) assert.NilError(t, err) patches, err = processOverlayPatches(log.Log, resource, overlay) assert.Assert(t, reflect.DeepEqual(err, overlayError{})) assert.Assert(t, len(patches) != 0) doc, err = utils.ApplyPatches(resourceRaw, patches) assert.NilError(t, err) compareJSONAsMap(t, expectedResult, doc) } func TestProcessOverlayPatches_anchorOnPeer(t *testing.T) { resourceRaw := []byte(` { "apiVersion":"v1", "kind":"Endpoints", "metadata":{ "name":"test-endpoint", "labels":{ "label":"test" } }, "subsets":[ { "addresses":[ { "ip":"192.168.10.171" } ], "ports":[ { "name":"secure-connection", "port":443, "protocol":"TCP" } ] } ] }`) overlayRaw := []byte(` { "subsets":[ { "addresses":[ { "(ip)":"192.168.10.171" } ], "ports":[ { "(name)":"secure-connection", "port":444, "protocol":"UDP" } ] } ] }`) var resource, overlay interface{} err := json.Unmarshal(resourceRaw, &resource) assert.NilError(t, err) err = json.Unmarshal(overlayRaw, &overlay) assert.NilError(t, err) patches, overlayerr := processOverlayPatches(log.Log, resource, overlay) assert.Assert(t, reflect.DeepEqual(overlayerr, overlayError{})) assert.Assert(t, len(patches) != 0) doc, err := utils.ApplyPatches(resourceRaw, patches) assert.NilError(t, err) expectedResult := []byte(` { "apiVersion":"v1", "kind":"Endpoints", "metadata":{ "name":"test-endpoint", "labels":{ "label":"test" } }, "subsets":[ { "addresses":[ { "ip":"192.168.10.171" } ], "ports":[ { "name":"secure-connection", "port":444, "protocol":"UDP" } ] } ] }`) compareJSONAsMap(t, expectedResult, doc) overlayRaw = []byte(` { "subsets":[ { "addresses":[ { "ip":"192.168.10.171" } ], "ports":[ { "(name)":"secure-connection", "(port)":444, "protocol":"UDP" } ] } ] }`) err = json.Unmarshal(overlayRaw, &overlay) assert.NilError(t, err) patches, err = processOverlayPatches(log.Log, resource, overlay) assert.Error(t, err, "[overlayError:0] Policy not applied, conditions are not met at /subsets/0/ports/0/port/, [overlayError:0] Failed validating value 443 with overlay 444") assert.Assert(t, len(patches) == 0) } func TestProcessOverlayPatches_insertWithCondition(t *testing.T) { resourceRaw := []byte(`{ "apiVersion": "apps/v1", "kind": "Deployment", "metadata": { "name": "psp-demo-unprivileged", "labels": { "app.type": "prod" } }, "spec": { "replicas": 1, "selector": { "matchLabels": { "app": "psp" } }, "template": { "metadata": { "labels": { "app": "psp" } }, "spec": { "securityContext": { "runAsNonRoot": true }, "containers": [ { "name": "sec-ctx-unprivileged", "image": "nginxinc/nginx-unprivileged", "securityContext": { "runAsNonRoot": true, "allowPrivilegeEscalation": false }, "env": [ { "name": "ENV_KEY", "value": "ENV_VALUE" } ] } ] } } } }`) overlayRaw := []byte(`{ "spec": { "template": { "spec": { "containers": [ { "(image)": "*/nginx-unprivileged", "securityContext": { "(runAsNonRoot)": true, "allowPrivilegeEscalation": true }, "env": [ { "name": "ENV_NEW_KEY", "value": "ENV_NEW_VALUE" } ] } ] } } } }`) var resource, overlay interface{} err := json.Unmarshal(resourceRaw, &resource) assert.NilError(t, err) err = json.Unmarshal(overlayRaw, &overlay) assert.NilError(t, err) patches, overlayerr := processOverlayPatches(log.Log, resource, overlay) assert.Assert(t, reflect.DeepEqual(overlayerr, overlayError{})) assert.Assert(t, len(patches) != 0) doc, err := utils.ApplyPatches(resourceRaw, patches) assert.NilError(t, err) expectedResult := []byte(`{ "apiVersion": "apps/v1", "kind": "Deployment", "metadata": { "name": "psp-demo-unprivileged", "labels": { "app.type": "prod" } }, "spec": { "replicas": 1, "selector": { "matchLabels": { "app": "psp" } }, "template": { "metadata": { "labels": { "app": "psp" } }, "spec": { "securityContext": { "runAsNonRoot": true }, "containers": [ { "name": "sec-ctx-unprivileged", "image": "nginxinc/nginx-unprivileged", "securityContext": { "runAsNonRoot": true, "allowPrivilegeEscalation": true }, "env": [ { "name": "ENV_KEY", "value": "ENV_VALUE" }, { "name": "ENV_NEW_KEY", "value": "ENV_NEW_VALUE" } ] } ] } } } }`) compareJSONAsMap(t, expectedResult, doc) } func TestProcessOverlayPatches_InsertIfNotPresentWithConditions(t *testing.T) { overlayRaw := []byte(` { "metadata": { "annotations": { "+(cluster-autoscaler.kubernetes.io/safe-to-evict)": true } }, "spec": { "volumes": [ { "(emptyDir)": {} } ] } }`) resourceRaw := []byte(` { "apiVersion": "v1", "kind": "Pod", "metadata": { "name": "pod-with-emptydir" }, "spec": { "containers": [ { "image": "k8s.gcr.io/test-webserver", "name": "test-container", "volumeMounts": [ { "mountPath": "/cache", "name": "cache-volume" } ] } ], "volumes": [ { "name": "cache-volume", "emptyDir": {} } ] } }`) var resource, overlay interface{} err := json.Unmarshal(resourceRaw, &resource) assert.NilError(t, err) err = json.Unmarshal(overlayRaw, &overlay) assert.NilError(t, err) patches, overlayerr := processOverlayPatches(log.Log, resource, overlay) assert.Assert(t, reflect.DeepEqual(overlayerr, overlayError{})) assert.Assert(t, len(patches) != 0) doc, err := utils.ApplyPatches(resourceRaw, patches) assert.NilError(t, err) expectedResult := []byte(` { "apiVersion": "v1", "kind": "Pod", "metadata": { "name": "pod-with-emptydir", "annotations": { "cluster-autoscaler.kubernetes.io/safe-to-evict": "true" } }, "spec": { "containers": [ { "image": "k8s.gcr.io/test-webserver", "name": "test-container", "volumeMounts": [ { "mountPath": "/cache", "name": "cache-volume" } ] } ], "volumes": [ { "name": "cache-volume", "emptyDir": {} } ] } }`) t.Log(string(doc)) compareJSONAsMap(t, expectedResult, doc) } func Test_wrapBoolean(t *testing.T) { tests := []struct { test string expected string }{ { test: `{ "op": "add", "path": "/metadata/annotations", "value":{"cluster-autoscaler.kubernetes.io/safe-to-evict":true} }`, expected: `{ "op": "add", "path": "/metadata/annotations", "value":{"cluster-autoscaler.kubernetes.io/safe-to-evict":"true"} }`, }, { test: `{ "op": "add", "path": "/metadata/annotations", "value":{"cluster-autoscaler.kubernetes.io/safe-to-evict": true} }`, expected: `{ "op": "add", "path": "/metadata/annotations", "value":{"cluster-autoscaler.kubernetes.io/safe-to-evict":"true"} }`, }, { test: `{ "op": "add", "path": "/metadata/annotations", "value":{"cluster-autoscaler.kubernetes.io/safe-to-evict": false } }`, expected: `{ "op": "add", "path": "/metadata/annotations", "value":{"cluster-autoscaler.kubernetes.io/safe-to-evict":"false"} }`, }, { test: `{ "op": "add", "path": "/metadata/annotations/cluster-autoscaler.kubernetes.io~1safe-to-evict", "value": false }`, expected: `{ "op": "add", "path": "/metadata/annotations/cluster-autoscaler.kubernetes.io~1safe-to-evict", "value":"false"}`, }, } for _, testcase := range tests { out := wrapBoolean(testcase.test) t.Log(out) assert.Assert(t, testcase.expected == out) } } func TestApplyOverlay_ConditionOnArray(t *testing.T) { resourceRaw := []byte(` { "apiVersion": "v1", "kind": "Pod", "metadata": { "name": "myapp-pod", "labels": { "app": "myapp", "dedicated": "spark" } }, "spec": { "containers": [ { "name": "myapp-container", "image": "busybox", "command": [ "sh", "-c", "echo Hello Kubernetes! && sleep 3600" ] } ], "affinity": { "nodeAffinity": { "a": { "b": [ { "matchExpressions": [ { "key": "dedicated", "operator": "NotIn", "values": [ "spark" ] } ] } ] } } } } } `) overlayRaw := []byte(` { "spec": { "affinity": { "nodeAffinity": { "a": { "b": [ { "matchExpressions": [ { "(key)": "dedicated", "operator": "In", "(values)": [ "spark" ] } ] } ] } } } } } `) var resource, overlay interface{} assert.NilError(t, json.Unmarshal(resourceRaw, &resource)) assert.NilError(t, json.Unmarshal(overlayRaw, &overlay)) expectedPatches := []byte(`[ { "op": "replace", "path": "/spec/affinity/nodeAffinity/a/b/0/matchExpressions/0/operator", "value":"In" } ]`) p, err := applyOverlay(resource, overlay, "/") assert.NilError(t, err) assert.Assert(t, string(utils.JoinPatches(p)) == string(expectedPatches)) }