mirror of
https://github.com/kyverno/kyverno.git
synced 2025-04-08 10:04:25 +00:00
Merge pull request #165 from nirmata/Prepare-to-50-and-115
Prepare to 50 and 115
This commit is contained in:
commit
9002a27ddb
5 changed files with 462 additions and 298 deletions
|
@ -24,182 +24,255 @@ func ProcessOverlay(rule kubepolicy.Rule, rawResource []byte, gvk metav1.GroupVe
|
|||
var appliedPatches []PatchBytes
|
||||
json.Unmarshal(rawResource, &resource)
|
||||
|
||||
patch := applyOverlay(resource, *rule.Mutation.Overlay, "/", &overlayApplicationResult)
|
||||
patches, res := mutateResourceWithOverlay(resource, *rule.Mutation.Overlay)
|
||||
overlayApplicationResult.MergeWith(&res)
|
||||
|
||||
if overlayApplicationResult.GetReason() == result.Success {
|
||||
appliedPatches = append(appliedPatches, patch...)
|
||||
appliedPatches = append(appliedPatches, patches...)
|
||||
}
|
||||
|
||||
return appliedPatches, overlayApplicationResult
|
||||
}
|
||||
|
||||
// goes down through overlay and resource trees and applies overlay
|
||||
func applyOverlay(resource, overlay interface{}, path string, res *result.RuleApplicationResult) []PatchBytes {
|
||||
// mutateResourceWithOverlay is a start of overlaying process
|
||||
func mutateResourceWithOverlay(resource, pattern interface{}) ([]PatchBytes, result.RuleApplicationResult) {
|
||||
// It assumes that mutation is started from root, so "/" is passed
|
||||
return applyOverlay(resource, pattern, "/")
|
||||
}
|
||||
|
||||
// applyOverlay detects type of current item and goes down through overlay and resource trees applying overlay
|
||||
func applyOverlay(resource, overlay interface{}, path string) ([]PatchBytes, result.RuleApplicationResult) {
|
||||
var appliedPatches []PatchBytes
|
||||
overlayResult := result.NewRuleApplicationResult("")
|
||||
|
||||
// resource item exists but has different type - replace
|
||||
// all subtree within this path by overlay
|
||||
if reflect.TypeOf(resource) != reflect.TypeOf(overlay) {
|
||||
patch := replaceSubtree(overlay, path, res)
|
||||
if res.Reason == result.Success {
|
||||
patch, res := replaceSubtree(overlay, path)
|
||||
overlayResult.MergeWith(&res)
|
||||
|
||||
if result.Success == overlayResult.GetReason() {
|
||||
appliedPatches = append(appliedPatches, patch)
|
||||
}
|
||||
return appliedPatches
|
||||
return appliedPatches, overlayResult
|
||||
}
|
||||
|
||||
return applyOverlayForSameTypes(resource, overlay, path)
|
||||
}
|
||||
|
||||
// applyOverlayForSameTypes is applyOverlay for cases when TypeOf(resource) == TypeOf(overlay)
|
||||
func applyOverlayForSameTypes(resource, overlay interface{}, path string) ([]PatchBytes, result.RuleApplicationResult) {
|
||||
var appliedPatches []PatchBytes
|
||||
overlayResult := result.NewRuleApplicationResult("")
|
||||
|
||||
// detect the type of resource and overlay and select corresponding handler
|
||||
switch typedOverlay := overlay.(type) {
|
||||
// map
|
||||
case map[string]interface{}:
|
||||
typedResource := resource.(map[string]interface{})
|
||||
patches, res := applyOverlayToMap(typedResource, typedOverlay, path)
|
||||
overlayResult.MergeWith(&res)
|
||||
|
||||
for key, value := range typedOverlay {
|
||||
if wrappedWithParentheses(key) {
|
||||
continue
|
||||
}
|
||||
currentPath := path + key + "/"
|
||||
resourcePart, ok := typedResource[key]
|
||||
|
||||
if ok {
|
||||
patches := applyOverlay(resourcePart, value, currentPath, res)
|
||||
if res.Reason == result.Success {
|
||||
appliedPatches = append(appliedPatches, patches...)
|
||||
}
|
||||
|
||||
} else {
|
||||
patch := insertSubtree(value, currentPath, res)
|
||||
if res.Reason == result.Success {
|
||||
appliedPatches = append(appliedPatches, patch)
|
||||
}
|
||||
|
||||
appliedPatches = append(appliedPatches, patch)
|
||||
}
|
||||
}
|
||||
case []interface{}:
|
||||
typedResource := resource.([]interface{})
|
||||
patches := applyOverlayToArray(typedResource, typedOverlay, path, res)
|
||||
if res.Reason == result.Success {
|
||||
if result.Success == overlayResult.GetReason() {
|
||||
appliedPatches = append(appliedPatches, patches...)
|
||||
}
|
||||
// array
|
||||
case []interface{}:
|
||||
typedResource := resource.([]interface{})
|
||||
patches, res := applyOverlayToArray(typedResource, typedOverlay, path)
|
||||
overlayResult.MergeWith(&res)
|
||||
|
||||
if result.Success == overlayResult.GetReason() {
|
||||
appliedPatches = append(appliedPatches, patches...)
|
||||
}
|
||||
// elementary types
|
||||
case string, float64, int64, bool:
|
||||
patch := replaceSubtree(overlay, path, res)
|
||||
if res.Reason == result.Success {
|
||||
patch, res := replaceSubtree(overlay, path)
|
||||
overlayResult.MergeWith(&res)
|
||||
|
||||
if result.Success == overlayResult.GetReason() {
|
||||
appliedPatches = append(appliedPatches, patch)
|
||||
}
|
||||
default:
|
||||
res.FailWithMessagef("Overlay has unsupported type: %T", overlay)
|
||||
return nil
|
||||
overlayResult.FailWithMessagef("Overlay has unsupported type: %T", overlay)
|
||||
return nil, overlayResult
|
||||
}
|
||||
|
||||
return appliedPatches
|
||||
return appliedPatches, overlayResult
|
||||
}
|
||||
|
||||
// for each overlay and resource array elements and applies overlay
|
||||
func applyOverlayToArray(resource, overlay []interface{}, path string, res *result.RuleApplicationResult) []PatchBytes {
|
||||
// for each overlay and resource map elements applies overlay
|
||||
func applyOverlayToMap(resourceMap, overlayMap map[string]interface{}, path string) ([]PatchBytes, result.RuleApplicationResult) {
|
||||
var appliedPatches []PatchBytes
|
||||
if len(overlay) == 0 {
|
||||
res.FailWithMessagef("Empty array detected in the overlay")
|
||||
return nil
|
||||
overlayResult := result.NewRuleApplicationResult("")
|
||||
|
||||
for key, value := range overlayMap {
|
||||
// skip anchor element because it has condition, not
|
||||
// the value that must replace resource value
|
||||
if wrappedWithParentheses(key) {
|
||||
continue
|
||||
}
|
||||
|
||||
currentPath := path + key + "/"
|
||||
resourcePart, ok := resourceMap[key]
|
||||
|
||||
if ok {
|
||||
// Key exists - go down through the overlay and resource trees
|
||||
patches, res := applyOverlay(resourcePart, value, currentPath)
|
||||
overlayResult.MergeWith(&res)
|
||||
|
||||
if result.Success == overlayResult.GetReason() {
|
||||
appliedPatches = append(appliedPatches, patches...)
|
||||
}
|
||||
} else {
|
||||
// Key does not exist - insert entire overlay subtree
|
||||
patch, res := insertSubtree(value, currentPath)
|
||||
overlayResult.MergeWith(&res)
|
||||
|
||||
if result.Success == overlayResult.GetReason() {
|
||||
appliedPatches = append(appliedPatches, patch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(resource) == 0 {
|
||||
return fillEmptyArray(overlay, path, res)
|
||||
return appliedPatches, overlayResult
|
||||
}
|
||||
|
||||
// for each overlay and resource array elements applies overlay
|
||||
func applyOverlayToArray(resource, overlay []interface{}, path string) ([]PatchBytes, result.RuleApplicationResult) {
|
||||
var appliedPatches []PatchBytes
|
||||
overlayResult := result.NewRuleApplicationResult("")
|
||||
|
||||
if 0 == len(overlay) {
|
||||
overlayResult.FailWithMessagef("Empty array detected in the overlay")
|
||||
return nil, overlayResult
|
||||
}
|
||||
|
||||
if 0 == len(resource) {
|
||||
// If array resource is empty, insert part from overlay
|
||||
patch, res := insertSubtree(overlay, path)
|
||||
overlayResult.MergeWith(&res)
|
||||
|
||||
if result.Success == overlayResult.GetReason() {
|
||||
appliedPatches = append(appliedPatches, patch)
|
||||
}
|
||||
|
||||
return appliedPatches, res
|
||||
}
|
||||
|
||||
if reflect.TypeOf(resource[0]) != reflect.TypeOf(overlay[0]) {
|
||||
res.FailWithMessagef("overlay array and resource array have elements of different types: %T and %T", overlay[0], resource[0])
|
||||
return nil
|
||||
overlayResult.FailWithMessagef("Overlay array and resource array have elements of different types: %T and %T", overlay[0], resource[0])
|
||||
return nil, overlayResult
|
||||
}
|
||||
|
||||
switch overlay[0].(type) {
|
||||
case map[string]interface{}:
|
||||
for _, overlayElement := range overlay {
|
||||
typedOverlay := overlayElement.(map[string]interface{})
|
||||
anchors := getAnchorsFromMap(typedOverlay)
|
||||
if len(anchors) > 0 {
|
||||
for i, resourceElement := range resource {
|
||||
typedResource := resourceElement.(map[string]interface{})
|
||||
|
||||
currentPath := path + strconv.Itoa(i) + "/"
|
||||
if !skipArrayObject(typedResource, anchors) {
|
||||
patches := applyOverlay(resourceElement, overlayElement, currentPath, res)
|
||||
if res.Reason == result.Success {
|
||||
appliedPatches = append(appliedPatches, patches...)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
} else if hasNestedAnchors(overlayElement) {
|
||||
for i, resourceElement := range resource {
|
||||
currentPath := path + strconv.Itoa(i) + "/"
|
||||
patches := applyOverlay(resourceElement, overlayElement, currentPath, res)
|
||||
if res.Reason == result.Success {
|
||||
appliedPatches = append(appliedPatches, patches...)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
currentPath := path + "0/"
|
||||
patch := insertSubtree(overlayElement, currentPath, res)
|
||||
if res.Reason == result.Success {
|
||||
appliedPatches = append(appliedPatches, patch)
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
path += "0/"
|
||||
for _, value := range overlay {
|
||||
patch := insertSubtree(value, path, res)
|
||||
if res.Reason == result.Success {
|
||||
appliedPatches = append(appliedPatches, patch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return appliedPatches
|
||||
return applyOverlayToArrayOfSameTypes(resource, overlay, path)
|
||||
}
|
||||
|
||||
// In case of empty resource array
|
||||
// append all non-anchor items to front
|
||||
func fillEmptyArray(overlay []interface{}, path string, res *result.RuleApplicationResult) []PatchBytes {
|
||||
// applyOverlayToArrayOfSameTypes applies overlay to array elements if they (resource and overlay elements) have same type
|
||||
func applyOverlayToArrayOfSameTypes(resource, overlay []interface{}, path string) ([]PatchBytes, result.RuleApplicationResult) {
|
||||
var appliedPatches []PatchBytes
|
||||
if len(overlay) == 0 {
|
||||
res.FailWithMessagef("Empty array detected in the overlay")
|
||||
return nil
|
||||
}
|
||||
|
||||
path += "0/"
|
||||
overlayResult := result.NewRuleApplicationResult("")
|
||||
|
||||
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 := insertSubtree(overlayElement, path, res)
|
||||
if res.Reason == result.Success {
|
||||
appliedPatches = append(appliedPatches, patch)
|
||||
}
|
||||
}
|
||||
}
|
||||
return applyOverlayToArrayOfMaps(resource, overlay, path)
|
||||
default:
|
||||
for _, overlayElement := range overlay {
|
||||
patch := insertSubtree(overlayElement, path, res)
|
||||
if res.Reason == result.Success {
|
||||
lastElementIdx := len(resource)
|
||||
|
||||
// Add elements to the end
|
||||
for i, value := range overlay {
|
||||
currentPath := path + strconv.Itoa(lastElementIdx+i) + "/"
|
||||
// currentPath example: /spec/template/spec/containers/3/
|
||||
patch, res := insertSubtree(value, currentPath)
|
||||
overlayResult.MergeWith(&res)
|
||||
|
||||
if result.Success == overlayResult.GetReason() {
|
||||
appliedPatches = append(appliedPatches, patch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return appliedPatches
|
||||
return appliedPatches, overlayResult
|
||||
}
|
||||
|
||||
func insertSubtree(overlay interface{}, path string, res *result.RuleApplicationResult) []byte {
|
||||
return processSubtree(overlay, path, "add", res)
|
||||
// Array of maps needs special handling as far as it can have anchors.
|
||||
func applyOverlayToArrayOfMaps(resource, overlay []interface{}, path string) ([]PatchBytes, result.RuleApplicationResult) {
|
||||
var appliedPatches []PatchBytes
|
||||
overlayResult := result.NewRuleApplicationResult("")
|
||||
|
||||
lastElementIdx := len(resource)
|
||||
for i, overlayElement := range overlay {
|
||||
typedOverlay := overlayElement.(map[string]interface{})
|
||||
anchors := getAnchorsFromMap(typedOverlay)
|
||||
|
||||
if len(anchors) > 0 {
|
||||
// If we have anchors - choose corresponding resource element and mutate it
|
||||
patches, res := applyOverlayWithAnchors(resource, overlayElement, anchors, path)
|
||||
overlayResult.MergeWith(&res)
|
||||
|
||||
if result.Success == overlayResult.GetReason() {
|
||||
appliedPatches = append(appliedPatches, patches...)
|
||||
}
|
||||
} else if hasNestedAnchors(overlayElement) {
|
||||
// If we have anchors on the lower level - continue traversing overlay and resource trees
|
||||
for j, resourceElement := range resource {
|
||||
currentPath := path + strconv.Itoa(j) + "/"
|
||||
// currentPath example: /spec/template/spec/containers/3/
|
||||
patches, res := applyOverlay(resourceElement, overlayElement, currentPath)
|
||||
overlayResult.MergeWith(&res)
|
||||
|
||||
if result.Success == overlayResult.GetReason() {
|
||||
appliedPatches = append(appliedPatches, patches...)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Overlay subtree has no anchors - insert new element
|
||||
currentPath := path + strconv.Itoa(lastElementIdx+i) + "/"
|
||||
// currentPath example: /spec/template/spec/containers/3/
|
||||
patch, res := insertSubtree(overlayElement, currentPath)
|
||||
overlayResult.MergeWith(&res)
|
||||
|
||||
if result.Success == overlayResult.GetReason() {
|
||||
appliedPatches = append(appliedPatches, patch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return appliedPatches, overlayResult
|
||||
}
|
||||
|
||||
func replaceSubtree(overlay interface{}, path string, res *result.RuleApplicationResult) []byte {
|
||||
return processSubtree(overlay, path, "replace", res)
|
||||
func applyOverlayWithAnchors(resource []interface{}, overlay interface{}, anchors map[string]interface{}, path string) ([]PatchBytes, result.RuleApplicationResult) {
|
||||
var appliedPatches []PatchBytes
|
||||
overlayResult := result.NewRuleApplicationResult("")
|
||||
|
||||
for i, resourceElement := range resource {
|
||||
typedResource := resourceElement.(map[string]interface{})
|
||||
|
||||
currentPath := path + strconv.Itoa(i) + "/"
|
||||
// currentPath example: /spec/template/spec/containers/3/
|
||||
if !skipArrayObject(typedResource, anchors) {
|
||||
patches, res := applyOverlay(resourceElement, overlay, currentPath)
|
||||
overlayResult.MergeWith(&res)
|
||||
if result.Success == overlayResult.GetReason() {
|
||||
appliedPatches = append(appliedPatches, patches...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return appliedPatches, overlayResult
|
||||
}
|
||||
|
||||
func processSubtree(overlay interface{}, path string, op string, res *result.RuleApplicationResult) PatchBytes {
|
||||
func insertSubtree(overlay interface{}, path string) (PatchBytes, result.RuleApplicationResult) {
|
||||
return processSubtree(overlay, path, "add")
|
||||
}
|
||||
|
||||
func replaceSubtree(overlay interface{}, path string) (PatchBytes, result.RuleApplicationResult) {
|
||||
return processSubtree(overlay, path, "replace")
|
||||
}
|
||||
|
||||
func processSubtree(overlay interface{}, path string, op string) (PatchBytes, result.RuleApplicationResult) {
|
||||
overlayResult := result.NewRuleApplicationResult("")
|
||||
|
||||
if len(path) > 1 && path[len(path)-1] == '/' {
|
||||
path = path[:len(path)-1]
|
||||
}
|
||||
|
@ -214,76 +287,26 @@ func processSubtree(overlay interface{}, path string, op string, res *result.Rul
|
|||
// check the patch
|
||||
_, err := jsonpatch.DecodePatch([]byte("[" + patchStr + "]"))
|
||||
if err != nil {
|
||||
res.FailWithMessagef("Failed to make '%s' patch from an overlay '%s' for path %s", op, value, path)
|
||||
return nil
|
||||
overlayResult.FailWithMessagef("Failed to make '%s' patch from an overlay '%s' for path %s", op, value, path)
|
||||
return nil, overlayResult
|
||||
}
|
||||
|
||||
return PatchBytes(patchStr)
|
||||
return PatchBytes(patchStr), overlayResult
|
||||
}
|
||||
|
||||
// TODO: Overlay is already in JSON, remove this code
|
||||
// converts overlay to JSON string to be inserted into the JSON Patch
|
||||
func prepareJSONValue(overlay interface{}) string {
|
||||
switch typed := overlay.(type) {
|
||||
case map[string]interface{}:
|
||||
if len(typed) == 0 {
|
||||
return ""
|
||||
}
|
||||
jsonOverlay, err := json.Marshal(overlay)
|
||||
|
||||
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)
|
||||
case bool:
|
||||
return fmt.Sprintf("%t", typed)
|
||||
default:
|
||||
if err != nil || hasOnlyAnchors(overlay) {
|
||||
return ""
|
||||
}
|
||||
|
||||
return string(jsonOverlay)
|
||||
}
|
||||
|
||||
// Anchor has pattern value, so resource shouldn't be mutated with it
|
||||
// If entire subtree has only anchor keys - we should skip inserting it
|
||||
func hasOnlyAnchors(overlay interface{}) bool {
|
||||
switch typed := overlay.(type) {
|
||||
case map[string]interface{}:
|
||||
|
@ -296,13 +319,20 @@ func hasOnlyAnchors(overlay interface{}) bool {
|
|||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
case []interface{}:
|
||||
for _, value := range typed {
|
||||
if !hasOnlyAnchors(value) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Checks if subtree has anchors
|
||||
func hasNestedAnchors(overlay interface{}) bool {
|
||||
switch typed := overlay.(type) {
|
||||
case map[string]interface{}:
|
||||
|
|
|
@ -2,7 +2,6 @@ package engine
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/nirmata/kyverno/pkg/result"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
|
@ -66,8 +65,7 @@ func TestApplyOverlay_NestedListWithAnchor(t *testing.T) {
|
|||
json.Unmarshal(resourceRaw, &resource)
|
||||
json.Unmarshal(overlayRaw, &overlay)
|
||||
|
||||
res := result.NewRuleApplicationResult("")
|
||||
patches := applyOverlay(resource, overlay, "/", &res)
|
||||
patches, res := applyOverlay(resource, overlay, "/")
|
||||
assert.NilError(t, res.ToError())
|
||||
assert.Assert(t, patches != nil)
|
||||
|
||||
|
@ -80,7 +78,34 @@ func TestApplyOverlay_NestedListWithAnchor(t *testing.T) {
|
|||
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"}]}]}`)
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -140,8 +165,7 @@ func TestApplyOverlay_InsertIntoArray(t *testing.T) {
|
|||
json.Unmarshal(resourceRaw, &resource)
|
||||
json.Unmarshal(overlayRaw, &overlay)
|
||||
|
||||
res := result.NewRuleApplicationResult("")
|
||||
patches := applyOverlay(resource, overlay, "/", &res)
|
||||
patches, res := applyOverlay(resource, overlay, "/")
|
||||
assert.NilError(t, res.ToError())
|
||||
assert.Assert(t, patches != nil)
|
||||
|
||||
|
@ -155,7 +179,50 @@ func TestApplyOverlay_InsertIntoArray(t *testing.T) {
|
|||
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.172"},{"ip":"192.168.10.173"}],"ports":[{"name":"insecure-connection","port":80.000000,"protocol":"UDP"}]},{"addresses":[{"ip":"192.168.10.171"}],"ports":[{"name":"secure-connection","port":443,"protocol":"TCP"}]}]}`)
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -219,8 +286,7 @@ func TestApplyOverlay_TestInsertToArray(t *testing.T) {
|
|||
json.Unmarshal(resourceRaw, &resource)
|
||||
json.Unmarshal(overlayRaw, &overlay)
|
||||
|
||||
res := result.NewRuleApplicationResult("")
|
||||
patches := applyOverlay(resource, overlay, "/", &res)
|
||||
patches, res := applyOverlay(resource, overlay, "/")
|
||||
assert.NilError(t, res.ToError())
|
||||
assert.Assert(t, patches != nil)
|
||||
|
||||
|
@ -303,13 +369,64 @@ func TestApplyOverlay_ImagePullPolicy(t *testing.T) {
|
|||
json.Unmarshal(resourceRaw, &resource)
|
||||
json.Unmarshal(overlayRaw, &overlay)
|
||||
|
||||
res := result.NewRuleApplicationResult("")
|
||||
patches := applyOverlay(resource, overlay, "/", &res)
|
||||
patches, res := applyOverlay(resource, overlay, "/")
|
||||
assert.NilError(t, res.ToError())
|
||||
assert.Assert(t, len(patches) != 0)
|
||||
|
||||
doc, err := 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":8080.000000},{"containerPort":80}]},{"image":"ghost:latest","imagePullPolicy":"IfNotPresent","name":"ghost","ports":[{"containerPort":8080.000000}]}]}}}}`)
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ package engine
|
|||
import (
|
||||
"math"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/golang/glog"
|
||||
|
@ -58,6 +57,7 @@ func ValidateValueWithPattern(value, pattern interface{}) bool {
|
|||
}
|
||||
}
|
||||
|
||||
// Handler for int values during validation process
|
||||
func validateValueWithIntPattern(value interface{}, pattern int64) bool {
|
||||
switch typedValue := value.(type) {
|
||||
case int:
|
||||
|
@ -78,6 +78,7 @@ func validateValueWithIntPattern(value interface{}, pattern int64) bool {
|
|||
}
|
||||
}
|
||||
|
||||
// Handler for float values during validation process
|
||||
func validateValueWithFloatPattern(value interface{}, pattern float64) bool {
|
||||
switch typedValue := value.(type) {
|
||||
case int:
|
||||
|
@ -96,6 +97,7 @@ func validateValueWithFloatPattern(value interface{}, pattern float64) bool {
|
|||
}
|
||||
}
|
||||
|
||||
// Handler for nil values during validation process
|
||||
func validateValueWithNilPattern(value interface{}) bool {
|
||||
switch typed := value.(type) {
|
||||
case float64:
|
||||
|
@ -119,6 +121,7 @@ func validateValueWithNilPattern(value interface{}) bool {
|
|||
}
|
||||
}
|
||||
|
||||
// Handler for pattern values during validation process
|
||||
func validateValueWithStringPatterns(value interface{}, pattern string) bool {
|
||||
statements := strings.Split(pattern, "|")
|
||||
for _, statement := range statements {
|
||||
|
@ -131,6 +134,8 @@ func validateValueWithStringPatterns(value interface{}, pattern string) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// Handler for single pattern value during validation process
|
||||
// Detects if pattern has a number
|
||||
func validateValueWithStringPattern(value interface{}, pattern string) bool {
|
||||
operator := getOperatorFromStringPattern(pattern)
|
||||
pattern = pattern[len(operator):]
|
||||
|
@ -143,6 +148,7 @@ func validateValueWithStringPattern(value interface{}, pattern string) bool {
|
|||
return validateNumberWithStr(value, number, str, operator)
|
||||
}
|
||||
|
||||
// Handler for string values
|
||||
func validateString(value interface{}, pattern string, operator Operator) bool {
|
||||
if NotEqual == operator || Equal == operator {
|
||||
strValue, ok := value.(string)
|
||||
|
@ -164,6 +170,7 @@ func validateString(value interface{}, pattern string, operator Operator) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// validateNumberWithStr applies wildcard to suffix and operator to numerical part
|
||||
func validateNumberWithStr(value interface{}, patternNumber, patternStr string, operator Operator) bool {
|
||||
// pattern has suffix
|
||||
if "" != patternStr {
|
||||
|
@ -179,51 +186,21 @@ func validateNumberWithStr(value interface{}, patternNumber, patternStr string,
|
|||
return false
|
||||
}
|
||||
|
||||
valueParsedNumber, err := parseNumber(valueNumber)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return validateNumber(valueParsedNumber, patternNumber, operator)
|
||||
return validateNumber(valueNumber, patternNumber, operator)
|
||||
}
|
||||
|
||||
return validateNumber(value, patternNumber, operator)
|
||||
}
|
||||
|
||||
// validateNumber compares two numbers with operator
|
||||
func validateNumber(value, pattern interface{}, operator Operator) bool {
|
||||
var floatPattern, floatValue float64
|
||||
|
||||
switch typed := value.(type) {
|
||||
case string:
|
||||
var err error
|
||||
floatValue, err = strconv.ParseFloat(typed, 64)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
case float64:
|
||||
floatValue = typed
|
||||
case int64:
|
||||
floatValue = float64(typed)
|
||||
case int:
|
||||
floatValue = float64(typed)
|
||||
default:
|
||||
floatPattern, err := convertToFloat(pattern)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
switch typed := pattern.(type) {
|
||||
case string:
|
||||
var err error
|
||||
floatPattern, err = strconv.ParseFloat(typed, 64)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
case float64:
|
||||
floatPattern = typed
|
||||
case int64:
|
||||
floatPattern = float64(typed)
|
||||
case int:
|
||||
floatPattern = float64(typed)
|
||||
default:
|
||||
floatValue, err := convertToFloat(value)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -245,6 +222,7 @@ func validateNumber(value, pattern interface{}, operator Operator) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// getOperatorFromStringPattern parses opeartor from pattern
|
||||
func getOperatorFromStringPattern(pattern string) Operator {
|
||||
if len(pattern) < 2 {
|
||||
return Equal
|
||||
|
@ -273,6 +251,7 @@ func getOperatorFromStringPattern(pattern string) Operator {
|
|||
return Equal
|
||||
}
|
||||
|
||||
// detects numerical and string parts in pattern and returns them
|
||||
func getNumberAndStringPartsFromPattern(pattern string) (number, str string) {
|
||||
regexpStr := `^(\d*(\.\d+)?)(.*)`
|
||||
re := regexp.MustCompile(regexpStr)
|
||||
|
@ -280,17 +259,3 @@ func getNumberAndStringPartsFromPattern(pattern string) (number, str string) {
|
|||
match := matches[0]
|
||||
return match[1], match[3]
|
||||
}
|
||||
|
||||
func parseNumber(number string) (interface{}, error) {
|
||||
var err error
|
||||
|
||||
if floatValue, err := strconv.ParseFloat(number, 64); err == nil {
|
||||
return floatValue, nil
|
||||
}
|
||||
|
||||
if intValue, err := strconv.ParseInt(number, 10, 64); err == nil {
|
||||
return intValue, nil
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@ package engine
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/minio/pkg/wildcard"
|
||||
|
@ -98,7 +100,7 @@ func ParseNamespaceFromObject(bytes []byte) string {
|
|||
return ""
|
||||
}
|
||||
|
||||
// returns true if policyResourceName is a regexp
|
||||
// ParseRegexPolicyResourceName returns true if policyResourceName is a regexp
|
||||
func ParseRegexPolicyResourceName(policyResourceName string) (string, bool) {
|
||||
regex := strings.Split(policyResourceName, "regex:")
|
||||
if len(regex) == 1 {
|
||||
|
@ -153,3 +155,36 @@ func skipArrayObject(object, anchors map[string]interface{}) bool {
|
|||
|
||||
return false
|
||||
}
|
||||
|
||||
// removeAnchor remove special characters around anchored key
|
||||
func removeAnchor(key string) string {
|
||||
if wrappedWithParentheses(key) {
|
||||
return key[1 : len(key)-1]
|
||||
}
|
||||
|
||||
// TODO: Add logic for other anchors here
|
||||
|
||||
return key
|
||||
}
|
||||
|
||||
// convertToFloat converts string and any other value to float64
|
||||
func convertToFloat(value interface{}) (float64, error) {
|
||||
switch typed := value.(type) {
|
||||
case string:
|
||||
var err error
|
||||
floatValue, err := strconv.ParseFloat(typed, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return floatValue, nil
|
||||
case float64:
|
||||
return typed, nil
|
||||
case int64:
|
||||
return float64(typed), nil
|
||||
case int:
|
||||
return float64(typed), nil
|
||||
default:
|
||||
return 0, fmt.Errorf("Could not convert %T to float64", value)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,8 +33,8 @@ func Validate(policy kubepolicy.Policy, rawResource []byte, gvk metav1.GroupVers
|
|||
|
||||
validationResult := validateResourceWithPattern(resource, rule.Validation.Pattern)
|
||||
if result.Success != validationResult.Reason {
|
||||
ruleApplicationResult.MergeWith(&validationResult)
|
||||
ruleApplicationResult.AddMessagef(*rule.Validation.Message)
|
||||
ruleApplicationResult.MergeWith(&validationResult)
|
||||
} else {
|
||||
ruleApplicationResult.AddMessagef("Success")
|
||||
}
|
||||
|
@ -45,68 +45,76 @@ func Validate(policy kubepolicy.Policy, rawResource []byte, gvk metav1.GroupVers
|
|||
return policyResult
|
||||
}
|
||||
|
||||
// validateResourceWithPattern is a start of element-by-element validation process
|
||||
// It assumes that validation is started from root, so "/" is passed
|
||||
func validateResourceWithPattern(resource, pattern interface{}) result.RuleApplicationResult {
|
||||
return validateResourceElement(resource, pattern, "/")
|
||||
}
|
||||
|
||||
func validateResourceElement(value, pattern interface{}, path string) result.RuleApplicationResult {
|
||||
// validateResourceElement detects the element type (map, array, nil, string, int, bool, float)
|
||||
// and calls corresponding handler
|
||||
// Pattern tree and resource tree can have different structure. In this case validation fails
|
||||
func validateResourceElement(resourceElement, patternElement interface{}, path string) result.RuleApplicationResult {
|
||||
res := result.NewRuleApplicationResult("")
|
||||
// TODO: Move similar message templates to message package
|
||||
|
||||
switch typedPattern := pattern.(type) {
|
||||
switch typedPatternElement := patternElement.(type) {
|
||||
// map
|
||||
case map[string]interface{}:
|
||||
typedValue, ok := value.(map[string]interface{})
|
||||
typedResourceElement, ok := resourceElement.(map[string]interface{})
|
||||
if !ok {
|
||||
res.FailWithMessagef("Pattern and resource have different structures. Path: %s. Expected %T, found %T", path, pattern, value)
|
||||
res.FailWithMessagef("Pattern and resource have different structures. Path: %s. Expected %T, found %T", path, patternElement, resourceElement)
|
||||
return res
|
||||
}
|
||||
|
||||
return validateMap(typedValue, typedPattern, path)
|
||||
return validateMap(typedResourceElement, typedPatternElement, path)
|
||||
// array
|
||||
case []interface{}:
|
||||
typedValue, ok := value.([]interface{})
|
||||
typedResourceElement, ok := resourceElement.([]interface{})
|
||||
if !ok {
|
||||
res.FailWithMessagef("Pattern and resource have different structures. Path: %s. Expected %T, found %T", path, pattern, value)
|
||||
res.FailWithMessagef("Pattern and resource have different structures. Path: %s. Expected %T, found %T", path, patternElement, resourceElement)
|
||||
return res
|
||||
}
|
||||
|
||||
return validateArray(typedValue, typedPattern, path)
|
||||
return validateArray(typedResourceElement, typedPatternElement, path)
|
||||
// elementary values
|
||||
case string, float64, int, int64, bool, nil:
|
||||
if !ValidateValueWithPattern(value, pattern) {
|
||||
res.FailWithMessagef("Failed to validate value %v with pattern %v. Path: %s", value, pattern, path)
|
||||
if !ValidateValueWithPattern(resourceElement, patternElement) {
|
||||
res.FailWithMessagef("Failed to validate value %v with pattern %v. Path: %s", resourceElement, patternElement, path)
|
||||
}
|
||||
|
||||
return res
|
||||
default:
|
||||
res.FailWithMessagef("Pattern contains unknown type %T. Path: %s", pattern, path)
|
||||
res.FailWithMessagef("Pattern contains unknown type %T. Path: %s", patternElement, path)
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
func validateMap(valueMap, patternMap map[string]interface{}, path string) result.RuleApplicationResult {
|
||||
// If validateResourceElement detects map element inside resource and pattern trees, it goes to validateMap
|
||||
// For each element of the map we must detect the type again, so we pass these elements to validateResourceElement
|
||||
func validateMap(resourceMap, patternMap map[string]interface{}, path string) result.RuleApplicationResult {
|
||||
res := result.NewRuleApplicationResult("")
|
||||
|
||||
for key, pattern := range patternMap {
|
||||
if wrappedWithParentheses(key) {
|
||||
key = key[1 : len(key)-1]
|
||||
}
|
||||
for key, patternElement := range patternMap {
|
||||
key = removeAnchor(key)
|
||||
|
||||
if pattern == "*" && valueMap[key] != nil {
|
||||
// The '*' pattern means that key exists and has value
|
||||
if patternElement == "*" && resourceMap[key] != nil {
|
||||
continue
|
||||
} else if pattern == "*" && valueMap[key] == nil {
|
||||
} else if patternElement == "*" && resourceMap[key] == nil {
|
||||
res.FailWithMessagef("Field %s is not present", key)
|
||||
} else {
|
||||
elementResult := validateResourceElement(valueMap[key], pattern, path+key+"/")
|
||||
if result.Failed == elementResult.Reason {
|
||||
res.Reason = elementResult.Reason
|
||||
res.Messages = append(res.Messages, elementResult.Messages...)
|
||||
}
|
||||
elementResult := validateResourceElement(resourceMap[key], patternElement, path+key+"/")
|
||||
res.MergeWith(&elementResult)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// If validateResourceElement detects array element inside resource and pattern trees, it goes to validateArray
|
||||
// Unlike the validateMap, we should check the array elements type on-site, because in case of maps, we should
|
||||
// get anchors and check each array element with it.
|
||||
func validateArray(resourceArray, patternArray []interface{}, path string) result.RuleApplicationResult {
|
||||
res := result.NewRuleApplicationResult("")
|
||||
|
||||
|
@ -114,35 +122,44 @@ func validateArray(resourceArray, patternArray []interface{}, path string) resul
|
|||
return res
|
||||
}
|
||||
|
||||
switch pattern := patternArray[0].(type) {
|
||||
switch typedPatternElement := patternArray[0].(type) {
|
||||
case map[string]interface{}:
|
||||
anchors := getAnchorsFromMap(pattern)
|
||||
for i, value := range resourceArray {
|
||||
currentPath := path + strconv.Itoa(i) + "/"
|
||||
resource, ok := value.(map[string]interface{})
|
||||
if !ok {
|
||||
res.FailWithMessagef("Pattern and resource have different structures. Path: %s. Expected %T, found %T", currentPath, pattern, value)
|
||||
return res
|
||||
}
|
||||
|
||||
if skipArrayObject(resource, anchors) {
|
||||
continue
|
||||
}
|
||||
|
||||
mapValidationResult := validateMap(resource, pattern, currentPath)
|
||||
if result.Failed == mapValidationResult.Reason {
|
||||
res.Reason = mapValidationResult.Reason
|
||||
res.Messages = append(res.Messages, mapValidationResult.Messages...)
|
||||
}
|
||||
}
|
||||
case string, float64, int, int64, bool, nil:
|
||||
for _, value := range resourceArray {
|
||||
if !ValidateValueWithPattern(value, pattern) {
|
||||
res.FailWithMessagef("Failed to validate value %v with pattern %v. Path: %s", value, pattern, path)
|
||||
}
|
||||
}
|
||||
// This is special case, because maps in arrays can have anchors that must be
|
||||
// processed with the special way affecting the entire array
|
||||
arrayResult := validateArrayOfMaps(resourceArray, typedPatternElement, path)
|
||||
res.MergeWith(&arrayResult)
|
||||
default:
|
||||
res.FailWithMessagef("Array element pattern of unknown type %T. Path: %s", pattern, path)
|
||||
// In all other cases - detect type and handle each array element with validateResourceElement
|
||||
for i, patternElement := range patternArray {
|
||||
currentPath := path + strconv.Itoa(i) + "/"
|
||||
elementResult := validateResourceElement(resourceArray[i], patternElement, currentPath)
|
||||
res.MergeWith(&elementResult)
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// validateArrayOfMaps gets anchors from pattern array map element, applies anchors logic
|
||||
// and then validates each map due to the pattern
|
||||
func validateArrayOfMaps(resourceMapArray []interface{}, patternMap map[string]interface{}, path string) result.RuleApplicationResult {
|
||||
res := result.NewRuleApplicationResult("")
|
||||
anchors := getAnchorsFromMap(patternMap)
|
||||
|
||||
for i, resourceElement := range resourceMapArray {
|
||||
currentPath := path + strconv.Itoa(i) + "/"
|
||||
typedResourceElement, ok := resourceElement.(map[string]interface{})
|
||||
if !ok {
|
||||
res.FailWithMessagef("Pattern and resource have different structures. Path: %s. Expected %T, found %T", currentPath, patternMap, resourceElement)
|
||||
return res
|
||||
}
|
||||
|
||||
if skipArrayObject(typedResourceElement, anchors) {
|
||||
continue
|
||||
}
|
||||
|
||||
mapValidationResult := validateMap(typedResourceElement, patternMap, currentPath)
|
||||
res.MergeWith(&mapValidationResult)
|
||||
}
|
||||
|
||||
return res
|
||||
|
|
Loading…
Add table
Reference in a new issue