Skip to content

Commit

Permalink
wildcard label and annotation keys validate patterns
Browse files Browse the repository at this point in the history
  • Loading branch information
JimBugwadia committed Dec 4, 2020
1 parent 630a9cc commit 44afdf2
Show file tree
Hide file tree
Showing 6 changed files with 209 additions and 56 deletions.
45 changes: 2 additions & 43 deletions pkg/engine/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/go-logr/logr"
kyverno "github.com/kyverno/kyverno/pkg/api/kyverno/v1"
"github.com/kyverno/kyverno/pkg/engine/context"
"github.com/kyverno/kyverno/pkg/engine/wildcards"
"github.com/kyverno/kyverno/pkg/resourcecache"
"github.com/kyverno/kyverno/pkg/utils"
"github.com/minio/minio/pkg/wildcard"
Expand Down Expand Up @@ -76,7 +77,7 @@ func checkAnnotations(annotations map[string]string, resourceAnnotations map[str
}

func checkSelector(labelSelector *metav1.LabelSelector, resourceLabels map[string]string) (bool, error) {
replaceWildcardsInSelector(labelSelector, resourceLabels)
wildcards.ReplaceInSelector(labelSelector, resourceLabels)
selector, err := metav1.LabelSelectorAsSelector(labelSelector)
if err != nil {
log.Log.Error(err, "failed to build label selector")
Expand All @@ -90,48 +91,6 @@ func checkSelector(labelSelector *metav1.LabelSelector, resourceLabels map[strin
return false, nil
}

// replaceWildcardsInSelector replaces label selector keys and values containing
// wildcard characters with matching keys and values from the resource labels.
func replaceWildcardsInSelector(labelSelector *metav1.LabelSelector, resourceLabels map[string]string) {
result := map[string]string{}
for k, v := range labelSelector.MatchLabels {
if containsWildcards(k) || containsWildcards(v) {
matchK, matchV := expandWildcards(k, v, resourceLabels)
result[matchK] = matchV
} else {
result[k] = v
}
}

labelSelector.MatchLabels = result
}

func containsWildcards(s string) bool {
return strings.Contains(s, "*") || strings.Contains(s, "?")
}

func expandWildcards(k, v string, labels map[string]string) (key string, val string) {
for k1, v1 := range labels {
if wildcard.Match(k, k1) {
if wildcard.Match(v, v1) {
return k1, v1
}
}
}

k = replaceWildCardChars(k)
v = replaceWildCardChars(v)
return k, v
}

// replaceWildCardChars will replace '*' and '?' characters which are not
// supported by Kubernetes with a '0'.
func replaceWildCardChars(s string) string {
s = strings.Replace(s, "*", "0", -1)
s = strings.Replace(s, "?", "0", -1)
return s
}

// doesResourceMatchConditionBlock filters the resource with defined conditions
// for a match / exclude block, it has the following attributes:
// ResourceDescription:
Expand Down
2 changes: 1 addition & 1 deletion pkg/engine/validate/pattern.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func ValidateValueWithPattern(log logr.Logger, value, pattern interface{}) bool
return validateValueWithMapPattern(log, value, typedPattern)
case []interface{}:
// TODO: check if this is ever called?
log.Info("arrays as patterns is not supported")
log.Info("arrays are not supported as patterns")
return false
default:
log.Info("Unknown type", "type", fmt.Sprintf("%T", typedPattern), "value", typedPattern)
Expand Down
28 changes: 18 additions & 10 deletions pkg/engine/validate/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package validate
import (
"errors"
"fmt"
"github.com/kyverno/kyverno/pkg/engine/wildcards"
"path"
"reflect"
"strconv"
Expand All @@ -20,10 +21,10 @@ import (
func ValidateResourceWithPattern(log logr.Logger, resource, pattern interface{}) (string, error) {
// newAnchorMap - to check anchor key has values
ac := common.NewAnchorMap()
path, err := validateResourceElement(log, resource, pattern, pattern, "/", ac)
elemPath, err := validateResourceElement(log, resource, pattern, pattern, "/", ac)
if err != nil {
if !ac.IsAnchorError() {
return path, err
return elemPath, err
}
}

Expand Down Expand Up @@ -65,6 +66,7 @@ func validateResourceElement(log logr.Logger, resourceElement, patternElement, o
}
}
}

if !ValidateValueWithPattern(log, resourceElement, patternElement) {
return path, fmt.Errorf("Validation rule failed at '%s' to validate value '%v' with pattern '%v'", path, resourceElement, patternElement)
}
Expand All @@ -79,13 +81,18 @@ func validateResourceElement(log logr.Logger, resourceElement, patternElement, o
// 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, ac *common.AnchorKey) (string, error) {

//
patternMap = wildcards.ExpandInMetadata(patternMap, resourceMap)

// check if there is anchor in pattern
// Phase 1 : Evaluate all the anchors
// Phase 2 : Evaluate non-anchors
anchors, resources := anchor.GetAnchorsResourcesFromMap(patternMap)

// Evaluate anchors
for key, patternElement := range anchors {

// get handler for each pattern in the pattern
// - Conditional
// - Existence
Expand All @@ -104,6 +111,7 @@ func validateMap(log logr.Logger, resourceMap, patternMap map[string]interface{}
return handlerPath, err
}
}

// If anchor fails then succeed validate and skip further validation of recursion
if ac.AnchorError != nil {
return "", nil
Expand Down Expand Up @@ -133,18 +141,18 @@ func validateArray(log logr.Logger, resourceArray, patternArray []interface{}, o
case map[string]interface{}:
// This is special case, because maps in arrays can have anchors that must be
// processed with the special way affecting the entire array
path, err := validateArrayOfMaps(log, resourceArray, typedPatternElement, originPattern, path, ac)
elemPath, err := validateArrayOfMaps(log, resourceArray, typedPatternElement, originPattern, path, ac)
if err != nil {
return path, err
return elemPath, err
}
default:
// In all other cases - detect type and handle each array element with validateResourceElement
if len(resourceArray) >= len(patternArray) {
for i, patternElement := range patternArray {
currentPath := path + strconv.Itoa(i) + "/"
path, err := validateResourceElement(log, resourceArray[i], patternElement, originPattern, currentPath, ac)
elemPath, err := validateResourceElement(log, resourceArray[i], patternElement, originPattern, currentPath, ac)
if err != nil {
return path, err
return elemPath, err
}
}
} else {
Expand All @@ -167,7 +175,7 @@ func actualizePattern(log logr.Logger, origPattern interface{}, referencePattern
}
// Check for variables
// substitute it from Context
// remove abosolute path
// remove absolute path
// {{ }}
// value :=
actualPath := formAbsolutePath(referencePattern, absolutePath)
Expand Down Expand Up @@ -260,12 +268,12 @@ func getValueFromPattern(log logr.Logger, patternMap map[string]interface{}, key
}
}

path := ""
elemPath := ""

for _, elem := range keys {
path = "/" + elem + path
elemPath = "/" + elem + elemPath
}
return nil, fmt.Errorf("No value found for specified reference: %s", path)
return nil, fmt.Errorf("No value found for specified reference: %s", elemPath)
}

// validateArrayOfMaps gets anchors from pattern array map element, applies anchors logic
Expand Down
54 changes: 54 additions & 0 deletions pkg/engine/validate/validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1356,3 +1356,57 @@ func TestValidateMapElement_OneElementInArrayNotPass(t *testing.T) {
assert.Equal(t, path, "/0/object/0/key2/")
assert.Assert(t, err != nil)
}

func TestValidateMapWildcardKeys(t *testing.T) {
pattern := []byte(`{"metadata" : {"annotations": {"test/*": "value1"}}}`)
resource := []byte(`{"metadata" : {"annotations": {"test/bar": "value1"}}}`)
testValidationPattern(t, pattern, resource, "", true)

pattern = []byte(`{"metadata" : {"annotations": {"test/b??": "v*"}}}`)
resource = []byte(`{"metadata" : {"annotations": {"test/bar": "value1"}}}`)
testValidationPattern(t, pattern, resource, "", true)

pattern = []byte(`{}`)
resource = []byte(`{"metadata" : {"annotations": {"test/bar": "value1"}}}`)
testValidationPattern(t, pattern, resource, "", true)

pattern = []byte(`{"metadata" : {"annotations": {"test/b??": "v*"}}}`)
resource = []byte(`{"metadata" : {"labels": {"test/bar": "value1"}}}`)
testValidationPattern(t, pattern, resource, "/metadata/annotations/", false)

pattern = []byte(`{"metadata" : {"labels": {"*/test": "foo"}}}`)
resource = []byte(`{"metadata" : {"labels": {"foo/test": "foo"}}}`)
testValidationPattern(t, pattern, resource, "", true)

pattern = []byte(`{"metadata" : {"labels": {"foo/123*": "bar"}}}`)
resource = []byte(`{"metadata" : {"labels": {"foo/12?": "bar", "foo/123": "bar"}}}`)
testValidationPattern(t, pattern, resource, "", true)

pattern = []byte(`{"metadata" : {"labels": {"foo/123*": "bar"}}}`)
resource = []byte(`{"metadata" : {"labels": {"foo/12?": "bar", "foo/123": "bar2"}}}`)
testValidationPattern(t, pattern, resource, "/metadata/labels/foo/123*/", false)

pattern = []byte(`{"metadata" : {"labels": {"foo/1*": "bar", "foo/4*": "bar2"}}}`)
resource = []byte(`{"metadata" : {"labels": {"foo/123": "bar", "foo/456": "bar2"}}}`)
testValidationPattern(t, pattern, resource, "", true)

pattern = []byte(`{"metadata" : {"labels": {"foo/1*": "bar", "foo/4*": "bar2"}}}`)
resource = []byte(`{"metadata" : {"labels": {"foo/123": "bar"}}}`)
testValidationPattern(t, pattern, resource, "/metadata/labels/foo/4*/", false)
}

func testValidationPattern(t *testing.T, patternBytes []byte, resourceBytes []byte, path string, nilErr bool) {
var pattern, resource interface{}
err := json.Unmarshal(patternBytes, &pattern)
assert.NilError(t, err)
err = json.Unmarshal(resourceBytes, &resource)
assert.NilError(t, err)

p, err := validateResourceElement(log.Log, resource, pattern, pattern, "/", common.NewAnchorMap())
assert.Equal(t, p, path)
if nilErr {
assert.NilError(t, err)
} else {
assert.Assert(t, err != nil)
}
}
5 changes: 3 additions & 2 deletions pkg/engine/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ func validateResource(log logr.Logger, ctx context.EvalInterface, policy kyverno
log.V(4).Info("resource fails the match description", "reason", err.Error())
continue
}

// add configmap json data to context
if err := AddResourceToContext(log, rule.Context, resCache, jsonContext); err != nil {
log.V(4).Info("cannot add configmaps to context", "reason", err.Error())
Expand Down Expand Up @@ -265,7 +266,7 @@ func validatePatterns(log logr.Logger, ctx context.EvalInterface, resource unstr
pattern := validationRule.Pattern
var err error
if pattern, err = variables.SubstituteVars(logger, ctx, pattern); err != nil {
// variable subsitution failed
// variable substitution failed
resp.Success = false
resp.Message = fmt.Sprintf("Validation error: %s; Validation rule '%s' failed. '%s'",
rule.Validation.Message, rule.Name, err)
Expand All @@ -280,7 +281,7 @@ func validatePatterns(log logr.Logger, ctx context.EvalInterface, resource unstr
rule.Validation.Message, rule.Name, path)
return resp
}
// rule application successful

logger.V(4).Info("successfully processed rule")
resp.Success = true
resp.Message = fmt.Sprintf("Validation rule '%s' succeeded.", rule.Name)
Expand Down
131 changes: 131 additions & 0 deletions pkg/engine/wildcards/wildcards.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package wildcards

import (
commonAnchor "github.com/kyverno/kyverno/pkg/engine/anchor/common"
"github.com/minio/minio/pkg/wildcard"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"strings"
)

// ReplaceInSelector replaces label selector keys and values containing
// wildcard characters with matching keys and values from the resource labels.
func ReplaceInSelector(labelSelector *metav1.LabelSelector, resourceLabels map[string]string) {
result := replaceWildcardsInMap(labelSelector.MatchLabels, resourceLabels)
labelSelector.MatchLabels = result
}

func replaceWildcardsInMap(patternMap map[string]string, resourceMap map[string]string) map[string]string {
result := map[string]string{}
for k, v := range patternMap {
if hasWildcards(k) || hasWildcards(v) {
matchK, matchV := expandWildcards(k, v, resourceMap, true)
result[matchK] = matchV
} else {
result[k] = v
}
}

return result
}

func hasWildcards(s string) bool {
return strings.Contains(s, "*") || strings.Contains(s, "?")
}

func expandWildcards(k, v string, resourceMap map[string]string, replace bool) (key string, val string) {
for k1, v1 := range resourceMap {
if wildcard.Match(k, k1) {
if wildcard.Match(v, v1) {
return k1, v1
}
}
}

if replace {
k = replaceWildCardChars(k)
v = replaceWildCardChars(v)
}

return k, v
}

// replaceWildCardChars will replace '*' and '?' characters which are not
// supported by Kubernetes with a '0'.
func replaceWildCardChars(s string) string {
s = strings.Replace(s, "*", "0", -1)
s = strings.Replace(s, "?", "0", -1)
return s
}

// ExpandInMetadata substitutes wildcard characters in map keys for metadata.labels and
// metadata.annotations that are present in a validation pattern. Values are not substituted
// here, as they are evaluated separately while processing the validation pattern.
func ExpandInMetadata(patternMap, resourceMap map[string]interface{}) map[string]interface{} {

patternMetadata := patternMap["metadata"]
if patternMetadata == nil {
return patternMap
}

resourceMetadata := resourceMap["metadata"]
if resourceMetadata == nil {
return patternMap
}

metadata := patternMetadata.(map[string]interface{})
labels := expandWildcardsInTag("labels", patternMetadata, resourceMetadata)
if labels != nil {
metadata["labels"] = labels
}

annotations := expandWildcardsInTag("annotations", patternMetadata, resourceMetadata)
if annotations != nil {
metadata["annotations"] = annotations
}

return patternMap
}

func expandWildcardsInTag(tag string, patternMetadata, resourceMetadata interface{}) map[string]interface{} {
patternData := getValueAsStringMap(tag, patternMetadata)
if patternData == nil {
return nil
}

resourceData := getValueAsStringMap(tag, resourceMetadata)
if resourceData == nil {
return nil
}

results := map[string]interface{}{}
for k, v := range patternData {
if hasWildcards(k) {
newKey := commonAnchor.RemoveAnchor(k)
matchK, _ := expandWildcards(newKey, v, resourceData, false)
matchK = strings.Replace(k, newKey, matchK, 1)
results[matchK] = v
} else {
results[k] = v
}
}

return results
}

func getValueAsStringMap(key string, dataMap interface{}) map[string]string {
if dataMap == nil {
return nil
}

val := dataMap.(map[string]interface{})[key]
if val == nil {
return nil
}

result := map[string]string{}
for k, v := range val.(map[string]interface{}) {
result[k] = v.(string)
}

return result
}

0 comments on commit 44afdf2

Please sign in to comment.