mirror of
https://github.com/kyverno/kyverno.git
synced 2025-03-06 16:06:56 +00:00
455 lines
14 KiB
Go
455 lines
14 KiB
Go
package engine
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/golang/glog"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
|
|
jsonpatch "github.com/evanphx/json-patch"
|
|
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1"
|
|
"github.com/nirmata/kyverno/pkg/engine/anchor"
|
|
)
|
|
|
|
// processOverlay processes validation patterns on the resource
|
|
func processOverlay(rule kyverno.Rule, resource unstructured.Unstructured) (response RuleResponse, patchedResource unstructured.Unstructured) {
|
|
startTime := time.Now()
|
|
glog.V(4).Infof("started applying overlay rule %q (%v)", rule.Name, startTime)
|
|
response.Name = rule.Name
|
|
response.Type = Mutation.String()
|
|
defer func() {
|
|
response.RuleStats.ProcessingTime = time.Since(startTime)
|
|
glog.V(4).Infof("finished applying overlay rule %q (%v)", response.Name, response.RuleStats.ProcessingTime)
|
|
}()
|
|
|
|
patches, overlayerr := processOverlayPatches(resource.UnstructuredContent(), rule.Mutation.Overlay)
|
|
// resource does not satisfy the overlay pattern, we don't apply this rule
|
|
if !reflect.DeepEqual(overlayerr, overlayError{}) {
|
|
switch overlayerr.statusCode {
|
|
// conditions are not met, don't apply this rule
|
|
// consider as failure
|
|
case conditionFailure:
|
|
glog.Errorf("Resource %s/%s/%s does not meet the conditions in the rule %s with overlay pattern %s", resource.GetKind(), resource.GetNamespace(), resource.GetName(), rule.Name, rule.Mutation.Overlay)
|
|
//TODO: send zero response and not consider this as applied?
|
|
response.Success = false
|
|
response.Message = overlayerr.ErrorMsg()
|
|
return response, resource
|
|
// rule application failed
|
|
case overlayFailure:
|
|
glog.Errorf("Resource %s/%s/%s: failed to process overlay: %v in the rule %s", resource.GetKind(), resource.GetNamespace(), resource.GetName(), overlayerr.ErrorMsg(), rule.Name)
|
|
response.Success = false
|
|
response.Message = fmt.Sprintf("failed to process overlay: %v", overlayerr.ErrorMsg())
|
|
return response, resource
|
|
default:
|
|
glog.Errorf("Resource %s/%s/%s: Unknown type of error: %v", resource.GetKind(), resource.GetNamespace(), resource.GetName(), overlayerr.Error())
|
|
response.Success = false
|
|
response.Message = fmt.Sprintf("Unknown type of error: %v", overlayerr.Error())
|
|
return response, resource
|
|
}
|
|
}
|
|
|
|
if len(patches) == 0 {
|
|
response.Success = true
|
|
return response, resource
|
|
}
|
|
|
|
// convert to RAW
|
|
resourceRaw, err := resource.MarshalJSON()
|
|
if err != nil {
|
|
response.Success = false
|
|
glog.Infof("unable to marshall resource: %v", err)
|
|
response.Message = fmt.Sprintf("failed to process JSON patches: %v", err)
|
|
return response, resource
|
|
}
|
|
|
|
var patchResource []byte
|
|
patchResource, err = ApplyPatches(resourceRaw, patches)
|
|
if err != nil {
|
|
msg := fmt.Sprintf("failed to apply JSON patches: %v", err)
|
|
glog.V(2).Infof("%s, patches=%s", msg, string(JoinPatches(patches)))
|
|
response.Success = false
|
|
response.Message = msg
|
|
return response, resource
|
|
}
|
|
|
|
err = patchedResource.UnmarshalJSON(patchResource)
|
|
if err != nil {
|
|
glog.Infof("failed to unmarshall resource to undstructured: %v", err)
|
|
response.Success = false
|
|
response.Message = fmt.Sprintf("failed to process JSON patches: %v", err)
|
|
return response, resource
|
|
}
|
|
|
|
// rule application succesfuly
|
|
response.Success = true
|
|
response.Message = fmt.Sprintf("successfully processed overlay")
|
|
response.Patches = patches
|
|
// apply the patches to the resource
|
|
return response, patchedResource
|
|
}
|
|
|
|
func processOverlayPatches(resource, overlay interface{}) ([][]byte, overlayError) {
|
|
if path, overlayerr := meetConditions(resource, overlay); !reflect.DeepEqual(overlayerr, overlayError{}) {
|
|
if overlayerr.statusCode == conditionFailure {
|
|
// anchor key is not satisfied in the resource, skip applying policy
|
|
glog.V(4).Infof("Mutate rule: failed to validate condition at %s, err: %v", path, overlayerr)
|
|
return nil, newOverlayError(overlayerr.statusCode, fmt.Sprintf("Conditions are not met at %s, %v", path, overlayerr))
|
|
}
|
|
}
|
|
|
|
patchBytes, err := mutateResourceWithOverlay(resource, overlay)
|
|
if err != nil {
|
|
return patchBytes, newOverlayError(overlayFailure, err.Error())
|
|
}
|
|
|
|
return patchBytes, overlayError{}
|
|
}
|
|
|
|
// mutateResourceWithOverlay is a start of overlaying process
|
|
func mutateResourceWithOverlay(resource, pattern interface{}) ([][]byte, error) {
|
|
// 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) ([][]byte, error) {
|
|
var appliedPatches [][]byte
|
|
// resource item exists but has different type - replace
|
|
// all subtree within this path by overlay
|
|
if reflect.TypeOf(resource) != reflect.TypeOf(overlay) {
|
|
patch, err := replaceSubtree(overlay, path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
appliedPatches = append(appliedPatches, patch)
|
|
//TODO : check if return is needed ?
|
|
}
|
|
return applyOverlayForSameTypes(resource, overlay, path)
|
|
}
|
|
|
|
// applyOverlayForSameTypes is applyOverlay for cases when TypeOf(resource) == TypeOf(overlay)
|
|
func applyOverlayForSameTypes(resource, overlay interface{}, path string) ([][]byte, error) {
|
|
var appliedPatches [][]byte
|
|
|
|
// 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, err := applyOverlayToMap(typedResource, typedOverlay, path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
appliedPatches = append(appliedPatches, patches...)
|
|
// array
|
|
case []interface{}:
|
|
typedResource := resource.([]interface{})
|
|
patches, err := applyOverlayToArray(typedResource, typedOverlay, path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
appliedPatches = append(appliedPatches, patches...)
|
|
// elementary types
|
|
case string, float64, int64, bool:
|
|
patch, err := replaceSubtree(overlay, path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
appliedPatches = append(appliedPatches, patch)
|
|
default:
|
|
return nil, fmt.Errorf("Overlay has unsupported type: %T", overlay)
|
|
}
|
|
|
|
return appliedPatches, nil
|
|
}
|
|
|
|
// for each overlay and resource map elements applies overlay
|
|
func applyOverlayToMap(resourceMap, overlayMap map[string]interface{}, path string) ([][]byte, error) {
|
|
var appliedPatches [][]byte
|
|
|
|
for key, value := range overlayMap {
|
|
// skip anchor element because it has condition, not
|
|
// the value that must replace resource value
|
|
if anchor.IsConditionAnchor(key) {
|
|
continue
|
|
}
|
|
|
|
noAnchorKey := removeAnchor(key)
|
|
currentPath := path + noAnchorKey + "/"
|
|
resourcePart, ok := resourceMap[noAnchorKey]
|
|
|
|
if ok && !anchor.IsAddingAnchor(key) {
|
|
// Key exists - go down through the overlay and resource trees
|
|
patches, err := applyOverlay(resourcePart, value, currentPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
appliedPatches = append(appliedPatches, patches...)
|
|
}
|
|
|
|
if !ok {
|
|
// Key does not exist - insert entire overlay subtree
|
|
patch, err := insertSubtree(value, currentPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
appliedPatches = append(appliedPatches, patch)
|
|
}
|
|
}
|
|
|
|
return appliedPatches, nil
|
|
}
|
|
|
|
// for each overlay and resource array elements applies overlay
|
|
func applyOverlayToArray(resource, overlay []interface{}, path string) ([][]byte, error) {
|
|
var appliedPatches [][]byte
|
|
|
|
if 0 == len(overlay) {
|
|
return nil, errors.New("Empty array detected in the overlay")
|
|
}
|
|
|
|
if 0 == len(resource) {
|
|
// If array resource is empty, insert part from overlay
|
|
patch, err := insertSubtree(overlay, path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
appliedPatches = append(appliedPatches, patch)
|
|
|
|
return appliedPatches, 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])
|
|
}
|
|
|
|
return applyOverlayToArrayOfSameTypes(resource, overlay, path)
|
|
}
|
|
|
|
// applyOverlayToArrayOfSameTypes applies overlay to array elements if they (resource and overlay elements) have same type
|
|
func applyOverlayToArrayOfSameTypes(resource, overlay []interface{}, path string) ([][]byte, error) {
|
|
var appliedPatches [][]byte
|
|
|
|
switch overlay[0].(type) {
|
|
case map[string]interface{}:
|
|
return applyOverlayToArrayOfMaps(resource, overlay, path)
|
|
default:
|
|
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, err := insertSubtree(value, currentPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
appliedPatches = append(appliedPatches, patch)
|
|
}
|
|
}
|
|
|
|
return appliedPatches, nil
|
|
}
|
|
|
|
// Array of maps needs special handling as far as it can have anchors.
|
|
func applyOverlayToArrayOfMaps(resource, overlay []interface{}, path string) ([][]byte, error) {
|
|
var appliedPatches [][]byte
|
|
|
|
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, err := applyOverlayWithAnchors(resource, overlayElement, anchors, path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
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, err := applyOverlay(resourceElement, overlayElement, currentPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
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, err := insertSubtree(overlayElement, currentPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
appliedPatches = append(appliedPatches, patch)
|
|
}
|
|
}
|
|
|
|
return appliedPatches, nil
|
|
}
|
|
|
|
func applyOverlayWithAnchors(resource []interface{}, overlay interface{}, anchors map[string]interface{}, path string) ([][]byte, error) {
|
|
var appliedPatches [][]byte
|
|
|
|
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, err := applyOverlay(resourceElement, overlay, currentPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
appliedPatches = append(appliedPatches, patches...)
|
|
}
|
|
}
|
|
|
|
return appliedPatches, nil
|
|
}
|
|
|
|
func insertSubtree(overlay interface{}, path string) ([]byte, error) {
|
|
return processSubtree(overlay, path, "add")
|
|
}
|
|
|
|
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]
|
|
}
|
|
|
|
path = preparePath(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 {
|
|
glog.V(3).Info(err)
|
|
return nil, fmt.Errorf("Failed to make '%s' patch from an overlay '%s' for path %s", op, value, path)
|
|
}
|
|
|
|
return []byte(patchStr), nil
|
|
}
|
|
|
|
func preparePath(path string) string {
|
|
if path == "" {
|
|
path = "/"
|
|
}
|
|
|
|
annPath := "/metadata/annotations/"
|
|
// escape slash in annotation patch
|
|
if strings.Contains(path, annPath) {
|
|
p := path[len(annPath):]
|
|
path = annPath + strings.ReplaceAll(p, "/", "~1")
|
|
}
|
|
return path
|
|
}
|
|
|
|
// converts overlay to JSON string to be inserted into the JSON Patch
|
|
func prepareJSONValue(overlay interface{}) string {
|
|
var err error
|
|
if err != nil || hasOnlyAnchors(overlay) {
|
|
glog.V(3).Info(err)
|
|
return ""
|
|
}
|
|
// Need to remove anchors from the overlay struct
|
|
overlayWithoutAnchors := removeAnchorFromSubTree(overlay)
|
|
jsonOverlay, err := json.Marshal(overlayWithoutAnchors)
|
|
return string(jsonOverlay)
|
|
}
|
|
|
|
func removeAnchorFromSubTree(overlay interface{}) interface{} {
|
|
var result interface{}
|
|
switch typed := overlay.(type) {
|
|
case map[string]interface{}:
|
|
// assuming only keys have anchors
|
|
result = removeAnchroFromMap(typed)
|
|
case []interface{}:
|
|
arrayResult := make([]interface{}, 0)
|
|
for _, value := range typed {
|
|
arrayResult = append(arrayResult, removeAnchorFromSubTree(value))
|
|
}
|
|
result = arrayResult
|
|
default:
|
|
result = overlay
|
|
}
|
|
return result
|
|
}
|
|
|
|
func removeAnchroFromMap(overlay map[string]interface{}) map[string]interface{} {
|
|
result := make(map[string]interface{}, 0)
|
|
for k, v := range overlay {
|
|
result[getRawKeyIfWrappedWithAttributes(k)] = removeAnchorFromSubTree(v)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// 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{}:
|
|
if anchors := getAnchorsFromMap(typed); len(anchors) == len(typed) {
|
|
return true
|
|
}
|
|
|
|
for _, value := range typed {
|
|
if !hasOnlyAnchors(value) {
|
|
return false
|
|
}
|
|
}
|
|
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{}:
|
|
if anchors := getAnchorsFromMap(typed); len(anchors) > 0 {
|
|
return true
|
|
}
|
|
|
|
for _, value := range typed {
|
|
if hasNestedAnchors(value) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
case []interface{}:
|
|
for _, value := range typed {
|
|
if hasNestedAnchors(value) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
default:
|
|
return false
|
|
}
|
|
}
|