diff --git a/pkg/generate/validate.go b/pkg/generate/validate.go new file mode 100644 index 000000000000..0bc4345f229e --- /dev/null +++ b/pkg/generate/validate.go @@ -0,0 +1,156 @@ +package generate + +import ( + "container/list" + "fmt" + "strconv" + + "github.com/go-logr/logr" + "github.com/kyverno/kyverno/pkg/engine/validate" + "github.com/kyverno/kyverno/pkg/engine/wildcards" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +type Handler struct { + element string + pattern interface{} + path string +} + +type resourceElementHandler = func(log logr.Logger, resourceElement, patternElement, originPattern interface{}, path string) (string, error) + +// ValidateResourceWithPattern is a start of element-by-element validation process +// It assumes that validation is started from root, so "/" is passed +// Anchors are not expected in the pattern +func ValidateResourceWithPattern(log logr.Logger, resource, pattern interface{}) (string, error) { + elemPath, err := validateResourceElement(log, resource, pattern, pattern, "/") + if err != nil { + return elemPath, err + } + return "", nil +} + +// 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(log logr.Logger, resourceElement, patternElement, originPattern interface{}, path string) (string, error) { + // var err error + switch typedPatternElement := patternElement.(type) { + // map + case map[string]interface{}: + typedResourceElement, ok := resourceElement.(map[string]interface{}) + if !ok { + log.V(4).Info("Pattern and resource have different structures.", "path", path, "expected", fmt.Sprintf("%T", patternElement), "current", fmt.Sprintf("%T", resourceElement)) + return path, fmt.Errorf("Pattern and resource have different structures. Path: %s. Expected %T, found %T", path, patternElement, resourceElement) + } + return validateMap(log, typedResourceElement, typedPatternElement, originPattern, path) + // array + case []interface{}: + typedResourceElement, ok := resourceElement.([]interface{}) + if !ok { + log.V(4).Info("Pattern and resource have different structures.", "path", path, "expected", fmt.Sprintf("%T", patternElement), "current", fmt.Sprintf("%T", resourceElement)) + return path, fmt.Errorf("Validation rule Failed at path %s, resource does not satisfy the expected overlay pattern", path) + } + return validateArray(log, typedResourceElement, typedPatternElement, originPattern, path) + // elementary values + case string, float64, int, int64, bool, nil: + if !validate.ValidateValueWithPattern(log, resourceElement, patternElement) { + return path, fmt.Errorf("Validation rule failed at '%s' to validate value '%v' with pattern '%v'", path, resourceElement, patternElement) + } + + default: + log.V(4).Info("Pattern contains unknown type", "path", path, "current", fmt.Sprintf("%T", patternElement)) + return path, fmt.Errorf("Validation rule failed at '%s', pattern contains unknown type", path) + } + return "", nil +} + +// 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(log logr.Logger, resourceMap, patternMap map[string]interface{}, origPattern interface{}, path string) (string, error) { + patternMap = wildcards.ExpandInMetadata(patternMap, resourceMap) + sortedResourceKeys := list.New() + for k := range patternMap { + sortedResourceKeys.PushBack(k) + } + + for e := sortedResourceKeys.Front(); e != nil; e = e.Next() { + key := e.Value.(string) + handler := NewHandler(key, patternMap[key], path) + handlerPath, err := handler.Handle(validateResourceElement, resourceMap, origPattern) + if err != nil { + return handlerPath, err + } + } + return "", nil +} + +// If validateResourceElement detects array element inside resource and pattern trees, it goes to validateArray +func validateArray(log logr.Logger, resourceArray, patternArray []interface{}, originPattern interface{}, path string) (string, error) { + if 0 == len(patternArray) { + return path, fmt.Errorf("Pattern Array empty") + } + + switch patternArray[0].(type) { + case map[string]interface{}: + for _, patternElement := range patternArray { + elemPath, err := validateArrayOfMaps(log, resourceArray, patternElement.(map[string]interface{}), originPattern, path) + if err != nil { + return elemPath, err + } + } + default: + if len(resourceArray) >= len(patternArray) { + for i, patternElement := range patternArray { + currentPath := path + strconv.Itoa(i) + "/" + elemPath, err := validateResourceElement(log, resourceArray[i], patternElement, originPattern, currentPath) + if err != nil { + return elemPath, err + } + } + } else { + return "", fmt.Errorf("Validate Array failed, array length mismatch, resource Array len is %d and pattern Array len is %d", len(resourceArray), len(patternArray)) + } + } + return "", nil +} + +// Matches all the elements in resource with the pattern +func validateArrayOfMaps(log logr.Logger, resourceMapArray []interface{}, patternMap map[string]interface{}, originPattern interface{}, path string) (string, error) { + lengthOflenResourceMapArray := len(resourceMapArray) - 1 + for i, resourceElement := range resourceMapArray { + currentPath := path + strconv.Itoa(i) + "/" + returnpath, err := validateResourceElement(log, resourceElement, patternMap, originPattern, currentPath) + if err != nil { + if i < lengthOflenResourceMapArray { + continue + } + return returnpath, err + } + break + } + return "", nil +} + +func NewHandler(element string, pattern interface{}, path string) Handler { + return Handler{ + element: element, + pattern: pattern, + path: path, + } +} + +func (dh Handler) Handle(handler resourceElementHandler, resourceMap map[string]interface{}, originPattern interface{}) (string, error) { + currentPath := dh.path + dh.element + "/" + if dh.pattern == "*" && resourceMap[dh.element] != nil { + return "", nil + } else if dh.pattern == "*" && resourceMap[dh.element] == nil { + return dh.path, fmt.Errorf("Validation rule failed at %s, Field %s is not present", dh.path, dh.element) + } else { + path, err := handler(log.Log, resourceMap[dh.element], dh.pattern, originPattern, currentPath) + if err != nil { + return path, err + } + } + return "", nil +} diff --git a/pkg/generate/validate_test.go b/pkg/generate/validate_test.go new file mode 100644 index 000000000000..6c883c1d4c70 --- /dev/null +++ b/pkg/generate/validate_test.go @@ -0,0 +1,82 @@ +package generate + +import ( + "testing" + + "github.com/go-logr/logr" + "gotest.tools/assert" +) + +func TestValidatePass(t *testing.T) { + resource := map[string]interface{}{ + "spec": map[string]interface{}{ + "egress": map[string]interface{}{ + "port": []interface{}{ + map[string]interface{}{ + "port": 5353, + "protocol": "UDP", + }, + map[string]interface{}{ + "port": 5353, + "protocol": "TCP", + }, + }, + }, + }, + } + pattern := map[string]interface{}{ + "spec": map[string]interface{}{ + "egress": map[string]interface{}{ + "port": []interface{}{ + map[string]interface{}{ + "port": 5353, + "protocol": "UDP", + }, + map[string]interface{}{ + "port": 5353, + "protocol": "TCP", + }, + }, + }, + }, + } + + var log logr.Logger + _, err := ValidateResourceWithPattern(log, resource, pattern) + assert.NilError(t, err) +} + +func TestValidateFail(t *testing.T) { + resource := map[string]interface{}{ + "spec": map[string]interface{}{ + "egress": map[string]interface{}{ + "port": []interface{}{ + map[string]interface{}{ + "port": 5353, + "protocol": "TCP", + }, + }, + }, + }, + } + pattern := map[string]interface{}{ + "spec": map[string]interface{}{ + "egress": map[string]interface{}{ + "port": []interface{}{ + map[string]interface{}{ + "port": 5353, + "protocol": "UDP", + }, + map[string]interface{}{ + "port": 5353, + "protocol": "TCP", + }, + }, + }, + }, + } + + var log logr.Logger + _, err := ValidateResourceWithPattern(log, resource, pattern) + assert.Assert(t, err != nil) +} diff --git a/pkg/webhooks/generation.go b/pkg/webhooks/generation.go index bb405e50fa5a..debd713ffa85 100644 --- a/pkg/webhooks/generation.go +++ b/pkg/webhooks/generation.go @@ -20,6 +20,7 @@ import ( enginutils "github.com/kyverno/kyverno/pkg/engine/utils" "github.com/kyverno/kyverno/pkg/engine/validate" "github.com/kyverno/kyverno/pkg/event" + gen "github.com/kyverno/kyverno/pkg/generate" kyvernoutils "github.com/kyverno/kyverno/pkg/utils" "github.com/kyverno/kyverno/pkg/webhooks/generate" v1beta1 "k8s.io/api/admission/v1beta1" @@ -149,7 +150,7 @@ func (ws *WebhookServer) handleUpdateTargetResource(request *v1beta1.AdmissionRe if rule.Generation.Kind == targetSourceKind && rule.Generation.Name == targetSourceName { data := rule.Generation.DeepCopy().Data if data != nil { - if _, err := validate.ValidateResourceWithPattern(logger, newRes.Object, data); err != nil { + if _, err := gen.ValidateResourceWithPattern(logger, newRes.Object, data); err != nil { enqueueBool = true break }