mirror of
https://github.com/kyverno/kyverno.git
synced 2024-12-15 17:51:20 +00:00
Finished mutating overlay. Added several tests
This commit is contained in:
parent
afb6b068e4
commit
09e0065d4c
2 changed files with 329 additions and 40 deletions
|
@ -2,9 +2,11 @@ package engine
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"reflect"
|
||||
"strconv"
|
||||
|
||||
jsonpatch "github.com/evanphx/json-patch"
|
||||
|
||||
kubepolicy "github.com/nirmata/kyverno/pkg/apis/policy/v1alpha1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
@ -37,10 +39,18 @@ func ProcessOverlay(policy kubepolicy.Policy, rawResource []byte, gvk metav1.Gro
|
|||
}
|
||||
|
||||
func applyOverlay(resource, overlay interface{}, path string) ([]PatchBytes, error) {
|
||||
var appliedPatches []PatchBytes
|
||||
|
||||
// resource item exists but has different type - replace
|
||||
// all subtree within this path by overlay
|
||||
if reflect.TypeOf(resource) != reflect.TypeOf(overlay) {
|
||||
replaceResource(resource, overlay, path)
|
||||
patch, err := replaceSubtree(overlay, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
appliedPatches = append(appliedPatches, patch)
|
||||
return appliedPatches, nil
|
||||
}
|
||||
|
||||
switch typedOverlay := overlay.(type) {
|
||||
|
@ -48,57 +58,169 @@ func applyOverlay(resource, overlay interface{}, path string) ([]PatchBytes, err
|
|||
typedResource := resource.(map[string]interface{})
|
||||
|
||||
for key, value := range typedOverlay {
|
||||
path += "/" + key
|
||||
if wrappedWithParentheses(key) {
|
||||
key = key[1 : len(key)-1]
|
||||
}
|
||||
currentPath := path + key + "/"
|
||||
resourcePart, ok := typedResource[key]
|
||||
|
||||
if ok {
|
||||
applyOverlay(resourcePart, value, path)
|
||||
patches, err := applyOverlay(resourcePart, value, currentPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
appliedPatches = append(appliedPatches, patches...)
|
||||
|
||||
} else {
|
||||
createSubtree(value, path)
|
||||
patch, err := insertSubtree(value, currentPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
appliedPatches = append(appliedPatches, patch)
|
||||
}
|
||||
}
|
||||
case []interface{}:
|
||||
typedResource := resource.([]interface{})
|
||||
applyOverlayToArray(typedResource, typedOverlay, path)
|
||||
patches, err := applyOverlayToArray(typedResource, typedOverlay, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
appliedPatches = append(appliedPatches, patches...)
|
||||
case string:
|
||||
replaceResource(resource, overlay, path)
|
||||
patch, err := replaceSubtree(overlay, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
appliedPatches = append(appliedPatches, patch)
|
||||
case float64:
|
||||
replaceResource(resource, overlay, path)
|
||||
patch, err := replaceSubtree(overlay, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
appliedPatches = append(appliedPatches, patch)
|
||||
case int64:
|
||||
replaceResource(resource, overlay, path)
|
||||
patch, err := replaceSubtree(overlay, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
appliedPatches = append(appliedPatches, patch)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
return appliedPatches, nil
|
||||
}
|
||||
|
||||
func applyOverlayToArray(resource, overlay []interface{}, path string) {
|
||||
func applyOverlayToArray(resource, overlay []interface{}, path string) ([]PatchBytes, error) {
|
||||
var appliedPatches []PatchBytes
|
||||
if len(overlay) == 0 {
|
||||
return nil, fmt.Errorf("overlay does not support empty arrays")
|
||||
}
|
||||
|
||||
if len(resource) == 0 {
|
||||
patches, err := fillEmptyArray(overlay, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return patches, nil
|
||||
}
|
||||
|
||||
if reflect.TypeOf(resource[0]) != reflect.TypeOf(overlay[0]) {
|
||||
return nil, fmt.Errorf("overlay array and resource array have elements of different types: %T and %T", overlay[0], resource[0])
|
||||
}
|
||||
|
||||
switch overlay[0].(type) {
|
||||
case map[string]interface{}:
|
||||
for _, overlayElement := range overlay {
|
||||
typedOverlay := overlayElement.(map[string]interface{})
|
||||
anchors := GetAnchorsFromMap(typedOverlay)
|
||||
|
||||
if len(anchors) > 0 {
|
||||
// Try to replace
|
||||
for i, resourceElement := range resource {
|
||||
path += "/" + strconv.Itoa(i)
|
||||
typedResource := resourceElement.(map[string]interface{})
|
||||
currentPath := path + "0/"
|
||||
for _, resourceElement := range resource {
|
||||
typedResource := resourceElement.(map[string]interface{})
|
||||
if len(anchors) > 0 {
|
||||
if !skipArrayObject(typedResource, anchors) {
|
||||
replaceResource(typedResource, typedOverlay, path)
|
||||
patches, err := applyOverlay(resourceElement, overlayElement, currentPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
appliedPatches = append(appliedPatches, patches...)
|
||||
}
|
||||
} else {
|
||||
if hasNestedAnchors(overlayElement) {
|
||||
patches, err := applyOverlay(resourceElement, overlayElement, currentPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
appliedPatches = append(appliedPatches, patches...)
|
||||
} else {
|
||||
patch, err := insertSubtree(overlayElement, currentPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
appliedPatches = append(appliedPatches, patch)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Add new item to the front
|
||||
path += "/0"
|
||||
createSubtree(typedOverlay, path)
|
||||
}
|
||||
|
||||
}
|
||||
default:
|
||||
path += "0/"
|
||||
for _, value := range overlay {
|
||||
patch, err := insertSubtree(value, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
appliedPatches = append(appliedPatches, patch)
|
||||
}
|
||||
}
|
||||
|
||||
return appliedPatches, nil
|
||||
}
|
||||
|
||||
// In case of empty resource array
|
||||
// append all non-anchor items to front
|
||||
func fillEmptyArray(overlay []interface{}, path string) ([]PatchBytes, error) {
|
||||
var appliedPatches []PatchBytes
|
||||
if len(overlay) == 0 {
|
||||
return nil, fmt.Errorf("overlay does not support empty arrays")
|
||||
}
|
||||
|
||||
path += "0/"
|
||||
|
||||
switch overlay[0].(type) {
|
||||
case map[string]interface{}:
|
||||
for _, overlayElement := range overlay {
|
||||
typedOverlay := overlayElement.(map[string]interface{})
|
||||
anchors := GetAnchorsFromMap(typedOverlay)
|
||||
|
||||
if len(anchors) == 0 {
|
||||
patch, err := insertSubtree(overlayElement, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
appliedPatches = append(appliedPatches, patch)
|
||||
}
|
||||
}
|
||||
default:
|
||||
path += "/0"
|
||||
for _, value := range overlay {
|
||||
createSubtree(value, path)
|
||||
for _, overlayElement := range overlay {
|
||||
patch, err := insertSubtree(overlayElement, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
appliedPatches = append(appliedPatches, patch)
|
||||
}
|
||||
}
|
||||
|
||||
return appliedPatches, nil
|
||||
}
|
||||
|
||||
func skipArrayObject(object, anchors map[string]interface{}) bool {
|
||||
|
@ -118,11 +240,140 @@ func skipArrayObject(object, anchors map[string]interface{}) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func replaceResource(resource, overlay interface{}, path string) {
|
||||
|
||||
func insertSubtree(overlay interface{}, path string) ([]byte, error) {
|
||||
return processSubtree(overlay, path, "add")
|
||||
}
|
||||
|
||||
func createSubtree(overlayPart interface{}, path string) []PatchBytes {
|
||||
|
||||
return nil
|
||||
func replaceSubtree(overlay interface{}, path string) ([]byte, error) {
|
||||
return processSubtree(overlay, path, "replace")
|
||||
}
|
||||
|
||||
func processSubtree(overlay interface{}, path string, op string) ([]byte, error) {
|
||||
if len(path) > 1 && path[len(path)-1] == '/' {
|
||||
path = path[:len(path)-1]
|
||||
}
|
||||
|
||||
if path == "" {
|
||||
path = "/"
|
||||
}
|
||||
|
||||
value := prepareJSONValue(overlay)
|
||||
patchStr := fmt.Sprintf(`{ "op": "%s", "path": "%s", "value": %s }`, op, path, value)
|
||||
|
||||
// check the patch
|
||||
_, err := jsonpatch.DecodePatch([]byte("[" + patchStr + "]"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return []byte(patchStr), nil
|
||||
}
|
||||
|
||||
func prepareJSONValue(overlay interface{}) string {
|
||||
switch typed := overlay.(type) {
|
||||
case map[string]interface{}:
|
||||
if len(typed) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
if hasOnlyAnchors(overlay) {
|
||||
return ""
|
||||
}
|
||||
|
||||
result := ""
|
||||
for key, value := range typed {
|
||||
jsonValue := prepareJSONValue(value)
|
||||
|
||||
pair := fmt.Sprintf(`"%s":%s`, key, jsonValue)
|
||||
|
||||
if result != "" {
|
||||
result += ", "
|
||||
}
|
||||
|
||||
result += pair
|
||||
}
|
||||
|
||||
result = fmt.Sprintf(`{ %s }`, result)
|
||||
return result
|
||||
case []interface{}:
|
||||
if len(typed) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
if hasOnlyAnchors(overlay) {
|
||||
return ""
|
||||
}
|
||||
|
||||
result := ""
|
||||
for _, value := range typed {
|
||||
jsonValue := prepareJSONValue(value)
|
||||
|
||||
if result != "" {
|
||||
result += ", "
|
||||
}
|
||||
|
||||
result += jsonValue
|
||||
}
|
||||
|
||||
result = fmt.Sprintf(`[ %s ]`, result)
|
||||
return result
|
||||
case string:
|
||||
return fmt.Sprintf(`"%s"`, typed)
|
||||
case float64:
|
||||
return fmt.Sprintf("%f", typed)
|
||||
case int64:
|
||||
return fmt.Sprintf("%d", typed)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func hasOnlyAnchors(overlay interface{}) bool {
|
||||
switch typed := overlay.(type) {
|
||||
case map[string]interface{}:
|
||||
if anchors := GetAnchorsFromMap(typed); len(anchors) == len(typed) {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, value := range typed {
|
||||
if !hasOnlyAnchors(value) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
case string:
|
||||
return false
|
||||
case float64:
|
||||
return false
|
||||
case int64:
|
||||
return false
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func hasNestedAnchors(overlay interface{}) bool {
|
||||
switch typed := overlay.(type) {
|
||||
case map[string]interface{}:
|
||||
if anchors := GetAnchorsFromMap(typed); len(anchors) > 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, value := range typed {
|
||||
if hasNestedAnchors(value) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
case string:
|
||||
return false
|
||||
case float64:
|
||||
return false
|
||||
case int64:
|
||||
return false
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,24 +2,62 @@ package engine
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"log"
|
||||
"testing"
|
||||
|
||||
jsonpatch "github.com/evanphx/json-patch"
|
||||
"gotest.tools/assert"
|
||||
)
|
||||
|
||||
func TestApplyOverlay_BaseCase(t *testing.T) {
|
||||
resource1Raw := []byte(`{ "dictionary": { "key1": "val1", "key2": "val2", "array": [ 1, 2 ] } }`)
|
||||
resource2Raw := []byte(`{ "dictionary": "somestring" }`)
|
||||
func TestApplyOverlay_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 resource1, resource2 interface{}
|
||||
var resource, overlay interface{}
|
||||
|
||||
json.Unmarshal(resource1Raw, &resource1)
|
||||
json.Unmarshal(resource2Raw, &resource2)
|
||||
json.Unmarshal(resourceRaw, &resource)
|
||||
json.Unmarshal(overlayRaw, &overlay)
|
||||
|
||||
fmt.Printf("First resource type: %v", reflect.TypeOf(resource1))
|
||||
fmt.Printf("Second resource type: %v", reflect.TypeOf(resource2))
|
||||
patches, err := applyOverlay(resource, overlay, "/")
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, patches != nil)
|
||||
|
||||
assert.Assert(t, reflect.TypeOf(resource1) == reflect.TypeOf(resource2))
|
||||
patch := 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"}]}]}`)
|
||||
assert.Equal(t, string(expectedResult), string(patched))
|
||||
}
|
||||
|
||||
func TestApplyOverlay_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{}
|
||||
|
||||
json.Unmarshal(resourceRaw, &resource)
|
||||
json.Unmarshal(overlayRaw, &overlay)
|
||||
|
||||
patches, err := applyOverlay(resource, overlay, "/")
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, patches != nil)
|
||||
|
||||
patch := 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)
|
||||
|
||||
log.Fatalf("%s", patched)
|
||||
//assert.Equal(t, string(expectedResult), string(patched))
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue