diff --git a/pkg/engine/anchor/anchor.go b/pkg/engine/anchor/anchor.go index 95d1f9d15a..5dc175e47e 100644 --- a/pkg/engine/anchor/anchor.go +++ b/pkg/engine/anchor/anchor.go @@ -1,277 +1,123 @@ package anchor import ( - "fmt" - "strconv" - - "github.com/go-logr/logr" - "github.com/kyverno/kyverno/pkg/logging" + "regexp" + "strings" ) -// ValidationHandler for element processes -type ValidationHandler interface { - Handle(handler resourceElementHandler, resourceMap map[string]interface{}, originPattern interface{}, ac *AnchorKey) (string, error) +type AnchorType string + +const ( + Condition AnchorType = "" + Global AnchorType = "<" + Negation AnchorType = "X" + AddIfNotPresent AnchorType = "+" + Equality AnchorType = "=" + Existence AnchorType = "^" +) + +var regex = regexp.MustCompile(`^(?P<modifier>[+<=X^])?\((?P<key>.+)\)$`) + +// Anchor interface +type Anchor interface { + // Type returns the anchor type + Type() AnchorType + // Key returns the anchor key + Key() string + // String returns the anchor string + String() string } -type resourceElementHandler = func(log logr.Logger, resourceElement, patternElement, originPattern interface{}, path string, ac *AnchorKey) (string, error) +type anchor struct { + modifier AnchorType + key string +} -// CreateElementHandler factory to process elements -func CreateElementHandler(element string, pattern interface{}, path string) ValidationHandler { - switch { - case IsConditionAnchor(element): - return NewConditionAnchorHandler(element, pattern, path) - case IsGlobalAnchor(element): - return NewGlobalAnchorHandler(element, pattern, path) - case IsExistenceAnchor(element): - return NewExistenceHandler(element, pattern, path) - case IsEqualityAnchor(element): - return NewEqualityHandler(element, pattern, path) - case IsNegationAnchor(element): - return NewNegationHandler(element, pattern, path) - default: - return NewDefaultHandler(element, pattern, path) +// Parse parses a string, returns nil if not an anchor +func Parse(str string) Anchor { + str = strings.TrimSpace(str) + values := regex.FindStringSubmatch(str) + if len(values) == 0 { + return nil + } + return New(AnchorType(values[1]), values[2]) +} + +// New creates an anchor +func New(modifier AnchorType, key string) Anchor { + if key == "" { + return nil + } + return anchor{ + modifier: modifier, + key: key, } } -// NewNegationHandler returns instance of negation handler -func NewNegationHandler(anchor string, pattern interface{}, path string) ValidationHandler { - return NegationHandler{ - anchor: anchor, - pattern: pattern, - path: path, +// String returns the anchor string. +// Will return an empty string if key is empty. +func String(modifier AnchorType, key string) string { + if key == "" { + return "" } + return string(modifier) + "(" + key + ")" } -// NegationHandler provides handler for check if the tag in anchor is not defined -type NegationHandler struct { - anchor string - pattern interface{} - path string +func (a anchor) Type() AnchorType { + return a.modifier } -// Handle process negation handler -func (nh NegationHandler) Handle(handler resourceElementHandler, resourceMap map[string]interface{}, originPattern interface{}, ac *AnchorKey) (string, error) { - anchorKey, _ := RemoveAnchor(nh.anchor) - currentPath := nh.path + anchorKey + "/" - // if anchor is present in the resource then fail - if _, ok := resourceMap[anchorKey]; ok { - // no need to process elements in value as key cannot be present in resource - ac.AnchorError = NewNegationAnchorError(fmt.Sprintf("%s is not allowed", currentPath)) - return currentPath, ac.AnchorError.Error() - } - // key is not defined in the resource - return "", nil +func (a anchor) Key() string { + return a.key } -// NewEqualityHandler returens instance of equality handler -func NewEqualityHandler(anchor string, pattern interface{}, path string) ValidationHandler { - return EqualityHandler{ - anchor: anchor, - pattern: pattern, - path: path, - } +func (a anchor) String() string { + return String(a.modifier, a.key) } -// EqualityHandler provides handler for non anchor element -type EqualityHandler struct { - anchor string - pattern interface{} - path string -} - -// Handle processed condition anchor -func (eh EqualityHandler) Handle(handler resourceElementHandler, resourceMap map[string]interface{}, originPattern interface{}, ac *AnchorKey) (string, error) { - anchorKey, _ := RemoveAnchor(eh.anchor) - currentPath := eh.path + anchorKey + "/" - // check if anchor is present in resource - if value, ok := resourceMap[anchorKey]; ok { - // validate the values of the pattern - returnPath, err := handler(logging.GlobalLogger(), value, eh.pattern, originPattern, currentPath, ac) - if err != nil { - return returnPath, err - } - return "", nil - } - return "", nil -} - -// NewDefaultHandler returns handler for non anchor elements -func NewDefaultHandler(element string, pattern interface{}, path string) ValidationHandler { - return DefaultHandler{ - element: element, - pattern: pattern, - path: path, - } -} - -// DefaultHandler provides handler for non anchor element -type DefaultHandler struct { - element string - pattern interface{} - path string -} - -// Handle process non anchor element -func (dh DefaultHandler) Handle(handler resourceElementHandler, resourceMap map[string]interface{}, originPattern interface{}, ac *AnchorKey) (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("%s/%s not found", dh.path, dh.element) - } else { - path, err := handler(logging.GlobalLogger(), resourceMap[dh.element], dh.pattern, originPattern, currentPath, ac) - if err != nil { - return path, err - } - } - return "", nil -} - -// NewConditionAnchorHandler returns an instance of condition acnhor handler -func NewConditionAnchorHandler(anchor string, pattern interface{}, path string) ValidationHandler { - return ConditionAnchorHandler{ - anchor: anchor, - pattern: pattern, - path: path, - } -} - -// ConditionAnchorHandler provides handler for condition anchor -type ConditionAnchorHandler struct { - anchor string - pattern interface{} - path string -} - -// Handle processed condition anchor -func (ch ConditionAnchorHandler) Handle(handler resourceElementHandler, resourceMap map[string]interface{}, originPattern interface{}, ac *AnchorKey) (string, error) { - anchorKey, _ := RemoveAnchor(ch.anchor) - currentPath := ch.path + anchorKey + "/" - // check if anchor is present in resource - if value, ok := resourceMap[anchorKey]; ok { - // validate the values of the pattern - returnPath, err := handler(logging.GlobalLogger(), value, ch.pattern, originPattern, currentPath, ac) - if err != nil { - ac.AnchorError = NewConditionalAnchorError(err.Error()) - return returnPath, ac.AnchorError.Error() - } - return "", nil - } else { - msg := "conditional anchor key doesn't exist in the resource" - return currentPath, NewConditionalAnchorError(msg).Error() - } -} - -// NewGlobalAnchorHandler returns an instance of condition acnhor handler -func NewGlobalAnchorHandler(anchor string, pattern interface{}, path string) ValidationHandler { - return GlobalAnchorHandler{ - anchor: anchor, - pattern: pattern, - path: path, - } -} - -// GlobalAnchorHandler provides handler for global condition anchor -type GlobalAnchorHandler struct { - anchor string - pattern interface{} - path string -} - -// Handle processed global condition anchor -func (gh GlobalAnchorHandler) Handle(handler resourceElementHandler, resourceMap map[string]interface{}, originPattern interface{}, ac *AnchorKey) (string, error) { - anchorKey, _ := RemoveAnchor(gh.anchor) - currentPath := gh.path + anchorKey + "/" - // check if anchor is present in resource - if value, ok := resourceMap[anchorKey]; ok { - // validate the values of the pattern - returnPath, err := handler(logging.GlobalLogger(), value, gh.pattern, originPattern, currentPath, ac) - if err != nil { - ac.AnchorError = NewGlobalAnchorError(err.Error()) - return returnPath, ac.AnchorError.Error() - } - return "", nil - } - return "", nil -} - -// NewExistenceHandler returns existence handler -func NewExistenceHandler(anchor string, pattern interface{}, path string) ValidationHandler { - return ExistenceHandler{ - anchor: anchor, - pattern: pattern, - path: path, - } -} - -// ExistenceHandler provides handlers to process exitence anchor handler -type ExistenceHandler struct { - anchor string - pattern interface{} - path string -} - -// Handle processes the existence anchor handler -func (eh ExistenceHandler) Handle(handler resourceElementHandler, resourceMap map[string]interface{}, originPattern interface{}, ac *AnchorKey) (string, error) { - // skip is used by existence anchor to not process further if condition is not satisfied - anchorKey, _ := RemoveAnchor(eh.anchor) - currentPath := eh.path + anchorKey + "/" - // check if anchor is present in resource - if value, ok := resourceMap[anchorKey]; ok { - // Existence anchor can only exist on resource value type of list - switch typedResource := value.(type) { - case []interface{}: - typedPattern, ok := eh.pattern.([]interface{}) - if !ok { - return currentPath, fmt.Errorf("invalid pattern type %T: Pattern has to be of list to compare against resource", eh.pattern) +// IsOneOf returns checks if anchor is one of the given types +func IsOneOf(a Anchor, types ...AnchorType) bool { + if a != nil { + for _, t := range types { + if t == a.Type() { + return true } - // loop all item in the pattern array - errorPath := "" - var err error - for _, patternMap := range typedPattern { - typedPatternMap, ok := patternMap.(map[string]interface{}) - if !ok { - return currentPath, fmt.Errorf("invalid pattern type %T: Pattern has to be of type map to compare against items in resource", eh.pattern) - } - errorPath, err = validateExistenceListResource(handler, typedResource, typedPatternMap, originPattern, currentPath, ac) - if err != nil { - return errorPath, err - } - } - return errorPath, err - default: - return currentPath, fmt.Errorf("invalid resource type %T: Existence ^ () anchor can be used only on list/array type resource", value) } } - return "", nil + return false } -func validateExistenceListResource(handler resourceElementHandler, resourceList []interface{}, patternMap map[string]interface{}, originPattern interface{}, path string, ac *AnchorKey) (string, error) { - // the idea is all the element in the pattern array should be present atleast once in the resource list - // if non satisfy then throw an error - for i, resourceElement := range resourceList { - currentPath := path + strconv.Itoa(i) + "/" - _, err := handler(logging.GlobalLogger(), resourceElement, patternMap, originPattern, currentPath, ac) - if err == nil { - // condition is satisfied, dont check further - return "", nil - } - } - // none of the existence checks worked, so thats a failure sceanario - return path, fmt.Errorf("existence anchor validation failed at path %s", path) +// ContainsCondition returns true if anchor is either condition anchor or global condition anchor +func ContainsCondition(a Anchor) bool { + return IsOneOf(a, Condition, Global) } -// GetAnchorsResourcesFromMap returns map of anchors -func GetAnchorsResourcesFromMap(patternMap map[string]interface{}) (map[string]interface{}, map[string]interface{}) { - anchors := map[string]interface{}{} - resources := map[string]interface{}{} - for key, value := range patternMap { - if IsConditionAnchor(key) || IsExistenceAnchor(key) || IsEqualityAnchor(key) || IsNegationAnchor(key) { - anchors[key] = value - continue - } - resources[key] = value - } - - return anchors, resources +// IsCondition checks for condition anchor +func IsCondition(a Anchor) bool { + return IsOneOf(a, Condition) +} + +// IsGlobal checks for global condition anchor +func IsGlobal(a Anchor) bool { + return IsOneOf(a, Global) +} + +// IsNegation checks for negation anchor +func IsNegation(a Anchor) bool { + return IsOneOf(a, Negation) +} + +// IsAddIfNotPresent checks for addition anchor +func IsAddIfNotPresent(a Anchor) bool { + return IsOneOf(a, AddIfNotPresent) +} + +// IsEquality checks for equality anchor +func IsEquality(a Anchor) bool { + return IsOneOf(a, Equality) +} + +// IsExistence checks for existence anchor +func IsExistence(a Anchor) bool { + return IsOneOf(a, Existence) } diff --git a/pkg/engine/anchor/anchorKey.go b/pkg/engine/anchor/anchorKey.go deleted file mode 100644 index 44c50c4ea1..0000000000 --- a/pkg/engine/anchor/anchorKey.go +++ /dev/null @@ -1,167 +0,0 @@ -package anchor - -import ( - "errors" - "fmt" - "strings" -) - -// IsNegationAnchorError checks if error message has negation anchor error string -func IsNegationAnchorError(msg string) bool { - return strings.Contains(msg, NegationAnchorErrMsg) -} - -// IsConditionalAnchorError checks if error message has conditional anchor error string -func IsConditionalAnchorError(msg string) bool { - return strings.Contains(msg, ConditionalAnchorErrMsg) -} - -// IsGlobalAnchorError checks if error message has global anchor error string -func IsGlobalAnchorError(msg string) bool { - return strings.Contains(msg, GlobalAnchorErrMsg) -} - -// NewNegationAnchorError returns a new instance of NegationAnchorError -func NewNegationAnchorError(msg string) ValidateAnchorError { - return ValidateAnchorError{ - Err: NegationAnchorErr, - Message: fmt.Sprintf("%s: %s", NegationAnchorErrMsg, msg), - } -} - -// IsNegationAnchorError checks if the error is a negation anchor error -func (e ValidateAnchorError) IsNegationAnchorError() bool { - return e.Err == NegationAnchorErr -} - -// NewConditionalAnchorError returns a new instance of ConditionalAnchorError -func NewConditionalAnchorError(msg string) ValidateAnchorError { - return ValidateAnchorError{ - Err: ConditionalAnchorErr, - Message: fmt.Sprintf("%s: %s", ConditionalAnchorErrMsg, msg), - } -} - -// IsConditionAnchorError checks if the error is a conditional anchor error -func (e ValidateAnchorError) IsConditionAnchorError() bool { - return e.Err == ConditionalAnchorErr -} - -// NewGlobalAnchorError returns a new instance of GlobalAnchorError -func NewGlobalAnchorError(msg string) ValidateAnchorError { - return ValidateAnchorError{ - Err: GlobalAnchorErr, - Message: fmt.Sprintf("%s: %s", GlobalAnchorErrMsg, msg), - } -} - -// IsGlobalAnchorError checks if the error is a global anchor error -func (e ValidateAnchorError) IsGlobalAnchorError() bool { - return e.Err == GlobalAnchorErr -} - -// IsNil checks if the error isn't populated -func (e ValidateAnchorError) IsNil() bool { - return e == ValidateAnchorError{} -} - -// Error returns an error instance of the anchor error -func (e ValidateAnchorError) Error() error { - return errors.New(e.Message) -} - -// AnchorError is the const specification of anchor errors -type AnchorError int - -const ( - // ConditionalAnchorErr refers to condition violation - ConditionalAnchorErr AnchorError = iota - - // GlobalAnchorErr refers to global condition violation - GlobalAnchorErr - - // NegationAnchorErr refers to negation violation - NegationAnchorErr -) - -// ValidateAnchorError represents the error type of validation anchors -type ValidateAnchorError struct { - Err AnchorError - Message string -} - -// NegationAnchorErrMsg - the error message for negation anchor error -var NegationAnchorErrMsg = "negation anchor matched in resource" - -// ConditionalAnchorErrMsg - the error message for conditional anchor error -var ConditionalAnchorErrMsg = "conditional anchor mismatch" - -// GlobalAnchorErrMsg - the error message for global anchor error -var GlobalAnchorErrMsg = "global anchor mismatch" - -// AnchorKey - contains map of anchors -type AnchorKey struct { - // anchorMap - for each anchor key in the patterns it will maintain information if the key exists in the resource - // if anchor key of the pattern exists in the resource then (key)=true else (key)=false - anchorMap map[string]bool - // AnchorError - used in validate to break execution of the recursion when if condition fails - AnchorError ValidateAnchorError -} - -// NewAnchorMap -initialize anchorMap -func NewAnchorMap() *AnchorKey { - return &AnchorKey{anchorMap: make(map[string]bool)} -} - -// IsAnchorError - if any of the anchor key doesn't exists in the resource then it will return true -// if any of (key)=false then return IsAnchorError() as true -// if all the keys exists in the pattern exists in resource then return IsAnchorError() as false -func (ac *AnchorKey) IsAnchorError() bool { - for _, v := range ac.anchorMap { - if !v { - return true - } - } - return false -} - -// CheckAnchorInResource checks if condition anchor key has values -func (ac *AnchorKey) CheckAnchorInResource(pattern interface{}, resource interface{}) { - switch typed := pattern.(type) { - case map[string]interface{}: - for key := range typed { - if IsConditionAnchor(key) || IsExistenceAnchor(key) || IsNegationAnchor(key) { - val, ok := ac.anchorMap[key] - if !ok { - ac.anchorMap[key] = false - } else if ok && val { - continue - } - if doesAnchorsKeyHasValue(key, resource) { - ac.anchorMap[key] = true - } - } - } - } -} - -// Checks if anchor key has value in resource -func doesAnchorsKeyHasValue(key string, resource interface{}) bool { - akey, _ := RemoveAnchor(key) - switch typed := resource.(type) { - case map[string]interface{}: - if _, ok := typed[akey]; ok { - return true - } - return false - case []interface{}: - for _, value := range typed { - if doesAnchorsKeyHasValue(key, value) { - return true - } - } - return false - default: - return false - } -} diff --git a/pkg/engine/anchor/anchor_test.go b/pkg/engine/anchor/anchor_test.go new file mode 100644 index 0000000000..4f540e512c --- /dev/null +++ b/pkg/engine/anchor/anchor_test.go @@ -0,0 +1,653 @@ +package anchor + +import ( + "reflect" + "testing" +) + +func TestNew(t *testing.T) { + type args struct { + modifier AnchorType + key string + } + tests := []struct { + name string + args args + want Anchor + }{{ + args: args{Condition, ""}, + want: nil, + }, { + args: args{Global, ""}, + want: nil, + }, { + args: args{Negation, ""}, + want: nil, + }, { + args: args{AddIfNotPresent, ""}, + want: nil, + }, { + args: args{Equality, ""}, + want: nil, + }, { + args: args{Existence, ""}, + want: nil, + }, { + args: args{Condition, "test"}, + want: anchor{Condition, "test"}, + }, { + args: args{Global, "test"}, + want: anchor{Global, "test"}, + }, { + args: args{Negation, "test"}, + want: anchor{Negation, "test"}, + }, { + args: args{AddIfNotPresent, "test"}, + want: anchor{AddIfNotPresent, "test"}, + }, { + args: args{Equality, "test"}, + want: anchor{Equality, "test"}, + }, { + args: args{Existence, "test"}, + want: anchor{Existence, "test"}, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := New(tt.args.modifier, tt.args.key); !reflect.DeepEqual(got, tt.want) { + t.Errorf("New() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestString(t *testing.T) { + type args struct { + modifier AnchorType + key string + } + tests := []struct { + name string + args args + want string + }{{ + args: args{Condition, ""}, + want: "", + }, { + args: args{Global, ""}, + want: "", + }, { + args: args{Negation, ""}, + want: "", + }, { + args: args{AddIfNotPresent, ""}, + want: "", + }, { + args: args{Equality, ""}, + want: "", + }, { + args: args{Existence, ""}, + want: "", + }, { + args: args{Condition, "test"}, + want: "(test)", + }, { + args: args{Global, "test"}, + want: "<(test)", + }, { + args: args{Negation, "test"}, + want: "X(test)", + }, { + args: args{AddIfNotPresent, "test"}, + want: "+(test)", + }, { + args: args{Equality, "test"}, + want: "=(test)", + }, { + args: args{Existence, "test"}, + want: "^(test)", + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := String(tt.args.modifier, tt.args.key); got != tt.want { + t.Errorf("String() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestIsOneOf(t *testing.T) { + type args struct { + a Anchor + types []AnchorType + } + tests := []struct { + name string + args args + want bool + }{{ + args: args{}, + want: false, + }, { + args: args{nil, []AnchorType{Condition, Negation}}, + want: false, + }, { + args: args{New(Condition, "test"), nil}, + want: false, + }, { + args: args{New(Condition, "test"), []AnchorType{}}, + want: false, + }, { + args: args{New(Condition, "test"), []AnchorType{Condition}}, + want: true, + }, { + args: args{New(Condition, "test"), []AnchorType{Condition, Negation}}, + want: true, + }, { + args: args{New(Condition, "test"), []AnchorType{Negation, Global}}, + want: false, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := IsOneOf(tt.args.a, tt.args.types...); got != tt.want { + t.Errorf("IsOneOf() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestParse(t *testing.T) { + type args struct { + str string + } + tests := []struct { + name string + args args + want Anchor + }{ + { + args: args{"(something)"}, + want: anchor{Condition, "something"}, + }, { + args: args{"()"}, + want: nil, + }, { + args: args{"something"}, + want: nil, + }, { + args: args{"(something"}, + want: nil, + }, { + args: args{"something)"}, + want: nil, + }, { + args: args{"so)m(et(hin)g"}, + want: nil, + }, { + args: args{""}, + want: nil, + }, { + args: args{"^(abc)"}, + want: anchor{Existence, "abc"}, + }, { + args: args{"^(abc"}, + want: nil, + }, { + args: args{"^abc"}, + want: nil, + }, { + args: args{"^()"}, + want: nil, + }, { + args: args{"(abc)"}, + want: anchor{Condition, "abc"}, + }, { + args: args{"=(abc)"}, + want: anchor{Equality, "abc"}, + }, { + args: args{"=(abc"}, + want: nil, + }, { + args: args{"=abc"}, + want: nil, + }, { + args: args{"+(abc)"}, + want: anchor{AddIfNotPresent, "abc"}, + }, { + args: args{"+(abc"}, + want: nil, + }, { + args: args{"+abc"}, + want: nil, + }, { + args: args{"X(abc)"}, + want: anchor{Negation, "abc"}, + }, { + args: args{"X(abc"}, + want: nil, + }, { + args: args{"Xabc"}, + want: nil, + }, { + args: args{"<(abc)"}, + want: anchor{Global, "abc"}, + }, { + args: args{"<(abc"}, + want: nil, + }, { + args: args{"<abc"}, + want: nil, + }, { + args: args{"(abc)"}, + want: anchor{Condition, "abc"}, + }, { + args: args{"(abc"}, + want: nil, + }, { + args: args{"abc"}, + want: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := Parse(tt.args.str); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Parse() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_anchor_Type(t *testing.T) { + type fields struct { + modifier AnchorType + key string + } + tests := []struct { + name string + fields fields + want AnchorType + }{{ + fields: fields{Condition, "abc"}, + want: Condition, + }, { + fields: fields{Global, "abc"}, + want: Global, + }, { + fields: fields{Negation, "abc"}, + want: Negation, + }, { + fields: fields{AddIfNotPresent, "abc"}, + want: AddIfNotPresent, + }, { + fields: fields{Equality, "abc"}, + want: Equality, + }, { + fields: fields{Existence, "abc"}, + want: Existence, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := anchor{ + modifier: tt.fields.modifier, + key: tt.fields.key, + } + if got := a.Type(); got != tt.want { + t.Errorf("anchor.Type() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_anchor_Key(t *testing.T) { + type fields struct { + modifier AnchorType + key string + } + tests := []struct { + name string + fields fields + want string + }{{ + fields: fields{Condition, "abc"}, + want: "abc", + }, { + fields: fields{Global, "abc"}, + want: "abc", + }, { + fields: fields{Negation, "abc"}, + want: "abc", + }, { + fields: fields{AddIfNotPresent, "abc"}, + want: "abc", + }, { + fields: fields{Equality, "abc"}, + want: "abc", + }, { + fields: fields{Existence, "abc"}, + want: "abc", + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := anchor{ + modifier: tt.fields.modifier, + key: tt.fields.key, + } + if got := a.Key(); got != tt.want { + t.Errorf("anchor.Key() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_anchor_String(t *testing.T) { + type fields struct { + modifier AnchorType + key string + } + tests := []struct { + name string + fields fields + want string + }{{ + fields: fields{Condition, "abc"}, + want: "(abc)", + }, { + fields: fields{Global, "abc"}, + want: "<(abc)", + }, { + fields: fields{Negation, "abc"}, + want: "X(abc)", + }, { + fields: fields{AddIfNotPresent, "abc"}, + want: "+(abc)", + }, { + fields: fields{Equality, "abc"}, + want: "=(abc)", + }, { + fields: fields{Existence, "abc"}, + want: "^(abc)", + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := anchor{ + modifier: tt.fields.modifier, + key: tt.fields.key, + } + if got := a.String(); got != tt.want { + t.Errorf("anchor.String() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestIsCondition(t *testing.T) { + type args struct { + a Anchor + } + tests := []struct { + name string + args args + want bool + }{{ + args: args{nil}, + want: false, + }, { + args: args{New(Condition, "abc")}, + want: true, + }, { + args: args{New(Global, "abc")}, + want: false, + }, { + args: args{New(Negation, "abc")}, + want: false, + }, { + args: args{New(AddIfNotPresent, "abc")}, + want: false, + }, { + args: args{New(Equality, "abc")}, + want: false, + }, { + args: args{New(Existence, "abc")}, + want: false, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := IsCondition(tt.args.a); got != tt.want { + t.Errorf("IsCondition() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestIsGlobal(t *testing.T) { + type args struct { + a Anchor + } + tests := []struct { + name string + args args + want bool + }{{ + args: args{nil}, + want: false, + }, { + args: args{New(Condition, "abc")}, + want: false, + }, { + args: args{New(Global, "abc")}, + want: true, + }, { + args: args{New(Negation, "abc")}, + want: false, + }, { + args: args{New(AddIfNotPresent, "abc")}, + want: false, + }, { + args: args{New(Equality, "abc")}, + want: false, + }, { + args: args{New(Existence, "abc")}, + want: false, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := IsGlobal(tt.args.a); got != tt.want { + t.Errorf("IsGlobal() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestIsNegation(t *testing.T) { + type args struct { + a Anchor + } + tests := []struct { + name string + args args + want bool + }{{ + args: args{nil}, + want: false, + }, { + args: args{New(Condition, "abc")}, + want: false, + }, { + args: args{New(Global, "abc")}, + want: false, + }, { + args: args{New(Negation, "abc")}, + want: true, + }, { + args: args{New(AddIfNotPresent, "abc")}, + want: false, + }, { + args: args{New(Equality, "abc")}, + want: false, + }, { + args: args{New(Existence, "abc")}, + want: false, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := IsNegation(tt.args.a); got != tt.want { + t.Errorf("IsNegation() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestIsAddIfNotPresent(t *testing.T) { + type args struct { + a Anchor + } + tests := []struct { + name string + args args + want bool + }{{ + args: args{nil}, + want: false, + }, { + args: args{New(Condition, "abc")}, + want: false, + }, { + args: args{New(Global, "abc")}, + want: false, + }, { + args: args{New(Negation, "abc")}, + want: false, + }, { + args: args{New(AddIfNotPresent, "abc")}, + want: true, + }, { + args: args{New(Equality, "abc")}, + want: false, + }, { + args: args{New(Existence, "abc")}, + want: false, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := IsAddIfNotPresent(tt.args.a); got != tt.want { + t.Errorf("IsAddIfNotPresent() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestIsEquality(t *testing.T) { + type args struct { + a Anchor + } + tests := []struct { + name string + args args + want bool + }{{ + args: args{nil}, + want: false, + }, { + args: args{New(Condition, "abc")}, + want: false, + }, { + args: args{New(Global, "abc")}, + want: false, + }, { + args: args{New(Negation, "abc")}, + want: false, + }, { + args: args{New(AddIfNotPresent, "abc")}, + want: false, + }, { + args: args{New(Equality, "abc")}, + want: true, + }, { + args: args{New(Existence, "abc")}, + want: false, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := IsEquality(tt.args.a); got != tt.want { + t.Errorf("IsEquality() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestIsExistence(t *testing.T) { + type args struct { + a Anchor + } + tests := []struct { + name string + args args + want bool + }{{ + args: args{nil}, + want: false, + }, { + args: args{New(Condition, "abc")}, + want: false, + }, { + args: args{New(Global, "abc")}, + want: false, + }, { + args: args{New(Negation, "abc")}, + want: false, + }, { + args: args{New(AddIfNotPresent, "abc")}, + want: false, + }, { + args: args{New(Equality, "abc")}, + want: false, + }, { + args: args{New(Existence, "abc")}, + want: true, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := IsExistence(tt.args.a); got != tt.want { + t.Errorf("IsExistence() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestContainsCondition(t *testing.T) { + type args struct { + a Anchor + } + tests := []struct { + name string + args args + want bool + }{{ + args: args{nil}, + want: false, + }, { + args: args{New(Condition, "abc")}, + want: true, + }, { + args: args{New(Global, "abc")}, + want: true, + }, { + args: args{New(Negation, "abc")}, + want: false, + }, { + args: args{New(AddIfNotPresent, "abc")}, + want: false, + }, { + args: args{New(Equality, "abc")}, + want: false, + }, { + args: args{New(Existence, "abc")}, + want: false, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := ContainsCondition(tt.args.a); got != tt.want { + t.Errorf("ContainsCondition() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/engine/anchor/anchormap.go b/pkg/engine/anchor/anchormap.go new file mode 100644 index 0000000000..9e45f2d45b --- /dev/null +++ b/pkg/engine/anchor/anchormap.go @@ -0,0 +1,44 @@ +package anchor + +// AnchorMap - contains map of anchors +type AnchorMap struct { + // anchorMap - for each anchor key in the patterns it will maintain information if the key exists in the resource + // if anchor key of the pattern exists in the resource then (key)=true else (key)=false + anchorMap map[string]bool + // AnchorError - used in validate to break execution of the recursion when if condition fails + AnchorError validateAnchorError +} + +// NewAnchorMap -initialize anchorMap +func NewAnchorMap() *AnchorMap { + return &AnchorMap{anchorMap: map[string]bool{}} +} + +// KeysAreMissing - if any of the anchor key doesn't exists in the resource then it will return true +// if any of (key)=false then return KeysAreMissing() as true +// if all the keys exists in the pattern exists in resource then return KeysAreMissing() as false +func (ac *AnchorMap) KeysAreMissing() bool { + for _, v := range ac.anchorMap { + if !v { + return true + } + } + return false +} + +// CheckAnchorInResource checks if condition anchor key has values +func (ac *AnchorMap) CheckAnchorInResource(pattern map[string]interface{}, resource interface{}) { + for key := range pattern { + if a := Parse(key); IsCondition(a) || IsExistence(a) || IsNegation(a) { + val, ok := ac.anchorMap[key] + if !ok { + ac.anchorMap[key] = false + } else if ok && val { + continue + } + if resourceHasValueForKey(resource, a.Key()) { + ac.anchorMap[key] = true + } + } + } +} diff --git a/pkg/engine/anchor/anchormap_test.go b/pkg/engine/anchor/anchormap_test.go new file mode 100644 index 0000000000..7c4e86c0b8 --- /dev/null +++ b/pkg/engine/anchor/anchormap_test.go @@ -0,0 +1,66 @@ +package anchor + +import ( + "reflect" + "testing" +) + +func TestNewAnchorMap(t *testing.T) { + tests := []struct { + name string + want *AnchorMap + }{{ + want: &AnchorMap{anchorMap: map[string]bool{}}, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := NewAnchorMap(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewAnchorMap() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestAnchorMap_KeysAreMissing(t *testing.T) { + type fields struct { + anchorMap map[string]bool + AnchorError validateAnchorError + } + tests := []struct { + name string + fields fields + want bool + }{{ + fields: fields{ + anchorMap: map[string]bool{}, + }, + want: false, + }, { + fields: fields{ + anchorMap: map[string]bool{ + "a": true, + "b": false, + }, + }, + want: true, + }, { + fields: fields{ + anchorMap: map[string]bool{ + "a": true, + "b": true, + }, + }, + want: false, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ac := &AnchorMap{ + anchorMap: tt.fields.anchorMap, + AnchorError: tt.fields.AnchorError, + } + if got := ac.KeysAreMissing(); got != tt.want { + t.Errorf("AnchorMap.KeysAreMissing() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/engine/anchor/common.go b/pkg/engine/anchor/common.go deleted file mode 100644 index 5500a26d55..0000000000 --- a/pkg/engine/anchor/common.go +++ /dev/null @@ -1,127 +0,0 @@ -package anchor - -import ( - "path" - "strings" -) - -// IsAnchor is a function handler -type IsAnchor func(str string) bool - -// IsConditionAnchor checks for condition anchor -func IsConditionAnchor(str string) bool { - if len(str) < 2 { - return false - } - - return (str[0] == '(' && str[len(str)-1] == ')') -} - -// IsGlobalAnchor checks for global condition anchor -func IsGlobalAnchor(str string) bool { - left := "<(" - right := ")" - if len(str) < len(left)+len(right) { - return false - } - - leftMatch := strings.TrimSpace(str[:len(left)]) == left - rightMatch := strings.TrimSpace(str[len(str)-len(right):]) == right - return leftMatch && rightMatch -} - -// ContainsCondition returns true, if str is either condition anchor or -// global condition anchor -func ContainsCondition(str string) bool { - return IsConditionAnchor(str) || IsGlobalAnchor(str) -} - -// IsNegationAnchor checks for negation anchor -func IsNegationAnchor(str string) bool { - left := "X(" - right := ")" - if len(str) < len(left)+len(right) { - return false - } - // TODO: trim spaces ? - return (str[:len(left)] == left && str[len(str)-len(right):] == right) -} - -// IsAddIfNotPresentAnchor checks for addition anchor -func IsAddIfNotPresentAnchor(key string) bool { - const left = "+(" - const right = ")" - - if len(key) < len(left)+len(right) { - return false - } - - return left == key[:len(left)] && right == key[len(key)-len(right):] -} - -// IsEqualityAnchor checks for equality anchor -func IsEqualityAnchor(str string) bool { - left := "=(" - right := ")" - if len(str) < len(left)+len(right) { - return false - } - // TODO: trim spaces ? - return (str[:len(left)] == left && str[len(str)-len(right):] == right) -} - -// IsExistenceAnchor checks for existence anchor -func IsExistenceAnchor(str string) bool { - left := "^(" - right := ")" - - if len(str) < len(left)+len(right) { - return false - } - - return (str[:len(left)] == left && str[len(str)-len(right):] == right) -} - -// IsNonAnchor checks that key does not have any anchor -func IsNonAnchor(str string) bool { - key, _ := RemoveAnchor(str) - return str == key -} - -// RemoveAnchor remove anchor from the given key. It returns -// the anchor-free tag value and the prefix of the anchor. -func RemoveAnchor(key string) (string, string) { - if IsConditionAnchor(key) { - return key[1 : len(key)-1], key[0:1] - } - - if IsExistenceAnchor(key) || IsAddIfNotPresentAnchor(key) || IsEqualityAnchor(key) || IsNegationAnchor(key) || IsGlobalAnchor(key) { - return key[2 : len(key)-1], key[0:2] - } - - return key, "" -} - -// RemoveAnchorsFromPath removes all anchor from path string -func RemoveAnchorsFromPath(str string) string { - components := strings.Split(str, "/") - if components[0] == "" { - components = components[1:] - } - - for i, component := range components { - components[i], _ = RemoveAnchor(component) - } - - newPath := path.Join(components...) - if path.IsAbs(str) { - newPath = "/" + newPath - } - return newPath -} - -// AddAnchor adds an anchor with the supplied prefix. -// The suffix is assumed to be ")". -func AddAnchor(key, anchorPrefix string) string { - return anchorPrefix + key + ")" -} diff --git a/pkg/engine/anchor/common_test.go b/pkg/engine/anchor/common_test.go deleted file mode 100644 index 66ce5869b7..0000000000 --- a/pkg/engine/anchor/common_test.go +++ /dev/null @@ -1,68 +0,0 @@ -package anchor - -import ( - "testing" - - "gotest.tools/assert" -) - -func TestWrappedWithParentheses_StringIsWrappedWithParentheses(t *testing.T) { - str := "(something)" - assert.Assert(t, IsConditionAnchor(str)) -} - -func TestWrappedWithParentheses_StringHasOnlyParentheses(t *testing.T) { - str := "()" - assert.Assert(t, IsConditionAnchor(str)) -} - -func TestWrappedWithParentheses_StringHasNoParentheses(t *testing.T) { - str := "something" - assert.Assert(t, !IsConditionAnchor(str)) -} - -func TestWrappedWithParentheses_StringHasLeftParentheses(t *testing.T) { - str := "(something" - assert.Assert(t, !IsConditionAnchor(str)) -} - -func TestWrappedWithParentheses_StringHasRightParentheses(t *testing.T) { - str := "something)" - assert.Assert(t, !IsConditionAnchor(str)) -} - -func TestWrappedWithParentheses_StringParenthesesInside(t *testing.T) { - str := "so)m(et(hin)g" - assert.Assert(t, !IsConditionAnchor(str)) -} - -func TestWrappedWithParentheses_Empty(t *testing.T) { - str := "" - assert.Assert(t, !IsConditionAnchor(str)) -} - -func TestIsExistenceAnchor_Yes(t *testing.T) { - assert.Assert(t, IsExistenceAnchor("^(abc)")) -} - -func TestIsExistenceAnchor_NoRightBracket(t *testing.T) { - assert.Assert(t, !IsExistenceAnchor("^(abc")) -} - -func TestIsExistenceAnchor_OnlyHat(t *testing.T) { - assert.Assert(t, !IsExistenceAnchor("^abc")) -} - -func TestIsExistenceAnchor_ConditionAnchor(t *testing.T) { - assert.Assert(t, !IsExistenceAnchor("(abc)")) -} - -func TestRemoveAnchorsFromPath_WorksWithAbsolutePath(t *testing.T) { - newPath := RemoveAnchorsFromPath("/path/(to)/X(anchors)") - assert.Equal(t, newPath, "/path/to/anchors") -} - -func TestRemoveAnchorsFromPath_WorksWithRelativePath(t *testing.T) { - newPath := RemoveAnchorsFromPath("path/(to)/X(anchors)") - assert.Equal(t, newPath, "path/to/anchors") -} diff --git a/pkg/engine/anchor/error.go b/pkg/engine/anchor/error.go new file mode 100644 index 0000000000..732a2d1400 --- /dev/null +++ b/pkg/engine/anchor/error.go @@ -0,0 +1,89 @@ +package anchor + +import ( + "fmt" + "strings" +) + +// anchorError is the const specification of anchor errors +type anchorError int + +const ( + // conditionalAnchorErr refers to condition violation + conditionalAnchorErr anchorError = iota + // globalAnchorErr refers to global condition violation + globalAnchorErr + // negationAnchorErr refers to negation violation + negationAnchorErr +) + +const ( + // negationAnchorErrMsg - the error message for negation anchor error + negationAnchorErrMsg = "negation anchor matched in resource" + // conditionalAnchorErrMsg - the error message for conditional anchor error + conditionalAnchorErrMsg = "conditional anchor mismatch" + // globalAnchorErrMsg - the error message for global anchor error + globalAnchorErrMsg = "global anchor mismatch" +) + +// validateAnchorError represents the error type of validation anchors +type validateAnchorError struct { + err anchorError + message string +} + +// Error implements error interface +func (e validateAnchorError) Error() string { + return e.message +} + +// newNegationAnchorError returns a new instance of validateAnchorError +func newValidateAnchorError(err anchorError, prefix, msg string) validateAnchorError { + return validateAnchorError{ + err: err, + message: fmt.Sprintf("%s: %s", prefix, msg), + } +} + +// newNegationAnchorError returns a new instance of NegationAnchorError +func newNegationAnchorError(msg string) validateAnchorError { + return newValidateAnchorError(negationAnchorErr, negationAnchorErrMsg, msg) +} + +// newConditionalAnchorError returns a new instance of ConditionalAnchorError +func newConditionalAnchorError(msg string) validateAnchorError { + return newValidateAnchorError(conditionalAnchorErr, conditionalAnchorErrMsg, msg) +} + +// newGlobalAnchorError returns a new instance of GlobalAnchorError +func newGlobalAnchorError(msg string) validateAnchorError { + return newValidateAnchorError(globalAnchorErr, globalAnchorErrMsg, msg) +} + +// isError checks if error matches the given error type +func isError(err error, code anchorError, msg string) bool { + if err != nil { + if t, ok := err.(validateAnchorError); ok { + return t.err == code + } else { + // TODO: we shouldn't need this, error is not properly propagated + return strings.Contains(err.Error(), msg) + } + } + return false +} + +// IsNegationAnchorError checks if error is a negation anchor error +func IsNegationAnchorError(err error) bool { + return isError(err, negationAnchorErr, negationAnchorErrMsg) +} + +// IsConditionalAnchorError checks if error is a conditional anchor error +func IsConditionalAnchorError(err error) bool { + return isError(err, conditionalAnchorErr, conditionalAnchorErrMsg) +} + +// IsGlobalAnchorError checks if error is a global global anchor error +func IsGlobalAnchorError(err error) bool { + return isError(err, globalAnchorErr, globalAnchorErrMsg) +} diff --git a/pkg/engine/anchor/error_test.go b/pkg/engine/anchor/error_test.go new file mode 100644 index 0000000000..be1c9e175e --- /dev/null +++ b/pkg/engine/anchor/error_test.go @@ -0,0 +1,276 @@ +package anchor + +import ( + "errors" + "reflect" + "testing" +) + +func Test_validateAnchorError_Error(t *testing.T) { + type fields struct { + err anchorError + message string + } + tests := []struct { + name string + fields fields + want string + }{{ + fields: fields{ + err: negationAnchorErr, + message: "test", + }, + want: "test", + }, { + fields: fields{ + err: conditionalAnchorErr, + message: "test", + }, + want: "test", + }, { + fields: fields{ + err: globalAnchorErr, + message: "test", + }, + want: "test", + }, { + fields: fields{ + err: globalAnchorErr, + message: "", + }, + want: "", + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := validateAnchorError{ + err: tt.fields.err, + message: tt.fields.message, + } + if got := e.Error(); got != tt.want { + t.Errorf("validateAnchorError.Error() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_newNegationAnchorError(t *testing.T) { + type args struct { + msg string + } + tests := []struct { + name string + args args + want validateAnchorError + }{{ + args: args{ + msg: "test", + }, + want: validateAnchorError{ + err: negationAnchorErr, + message: "negation anchor matched in resource: test", + }, + }, { + want: validateAnchorError{ + err: negationAnchorErr, + message: "negation anchor matched in resource: ", + }, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := newNegationAnchorError(tt.args.msg); !reflect.DeepEqual(got, tt.want) { + t.Errorf("newNegationAnchorError() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_newConditionalAnchorError(t *testing.T) { + type args struct { + msg string + } + tests := []struct { + name string + args args + want validateAnchorError + }{{ + args: args{ + msg: "test", + }, + want: validateAnchorError{ + err: conditionalAnchorErr, + message: "conditional anchor mismatch: test", + }, + }, { + want: validateAnchorError{ + err: conditionalAnchorErr, + message: "conditional anchor mismatch: ", + }, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := newConditionalAnchorError(tt.args.msg); !reflect.DeepEqual(got, tt.want) { + t.Errorf("newConditionalAnchorError() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_newGlobalAnchorError(t *testing.T) { + type args struct { + msg string + } + tests := []struct { + name string + args args + want validateAnchorError + }{{ + args: args{ + msg: "test", + }, + want: validateAnchorError{ + err: globalAnchorErr, + message: "global anchor mismatch: test", + }, + }, { + want: validateAnchorError{ + err: globalAnchorErr, + message: "global anchor mismatch: ", + }, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := newGlobalAnchorError(tt.args.msg); !reflect.DeepEqual(got, tt.want) { + t.Errorf("newGlobalAnchorError() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestIsNegationAnchorError(t *testing.T) { + type args struct { + err error + } + tests := []struct { + name string + args args + want bool + }{{ + args: args{ + err: nil, + }, + want: false, + }, { + args: args{ + err: errors.New("negation anchor matched in resource: test"), + }, + want: true, + }, { + args: args{ + err: newConditionalAnchorError("test"), + }, + want: false, + }, { + args: args{ + err: newGlobalAnchorError("test"), + }, + want: false, + }, { + args: args{ + err: newNegationAnchorError("test"), + }, + want: true, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := IsNegationAnchorError(tt.args.err); got != tt.want { + t.Errorf("IsNegationAnchorError() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestIsConditionalAnchorError(t *testing.T) { + type args struct { + err error + } + tests := []struct { + name string + args args + want bool + }{{ + args: args{ + err: nil, + }, + want: false, + }, { + args: args{ + err: errors.New("conditional anchor mismatch: test"), + }, + want: true, + }, { + args: args{ + err: newConditionalAnchorError("test"), + }, + want: true, + }, { + args: args{ + err: newGlobalAnchorError("test"), + }, + want: false, + }, { + args: args{ + err: newNegationAnchorError("test"), + }, + want: false, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := IsConditionalAnchorError(tt.args.err); got != tt.want { + t.Errorf("IsConditionalAnchorError() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestIsGlobalAnchorError(t *testing.T) { + type args struct { + err error + } + tests := []struct { + name string + args args + want bool + }{{ + args: args{ + err: nil, + }, + want: false, + }, { + args: args{ + err: errors.New("global anchor mismatch: test"), + }, + want: true, + }, { + args: args{ + err: newConditionalAnchorError("test"), + }, + want: false, + }, { + args: args{ + err: newGlobalAnchorError("test"), + }, + want: true, + }, { + args: args{ + err: newNegationAnchorError("test"), + }, + want: false, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := IsGlobalAnchorError(tt.args.err); got != tt.want { + t.Errorf("IsGlobalAnchorError() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/engine/anchor/handlers.go b/pkg/engine/anchor/handlers.go new file mode 100644 index 0000000000..fa9cfe4a56 --- /dev/null +++ b/pkg/engine/anchor/handlers.go @@ -0,0 +1,275 @@ +package anchor + +import ( + "fmt" + "strconv" + + "github.com/go-logr/logr" + "github.com/kyverno/kyverno/pkg/logging" +) + +type resourceElementHandler = func( + log logr.Logger, + resourceElement interface{}, + patternElement interface{}, + originPattern interface{}, + path string, + ac *AnchorMap, +) (string, error) + +// ValidationHandler for element processes +type ValidationHandler interface { + Handle( + handler resourceElementHandler, + resourceMap map[string]interface{}, + originPattern interface{}, + ac *AnchorMap, + ) (string, error) +} + +// CreateElementHandler factory to process elements +func CreateElementHandler(element string, pattern interface{}, path string) ValidationHandler { + if anchor := Parse(element); anchor != nil { + switch { + case IsCondition(anchor): + return newConditionAnchorHandler(anchor, pattern, path) + case IsGlobal(anchor): + return newGlobalAnchorHandler(anchor, pattern, path) + case IsExistence(anchor): + return newExistenceHandler(anchor, pattern, path) + case IsEquality(anchor): + return newEqualityHandler(anchor, pattern, path) + case IsNegation(anchor): + return newNegationHandler(anchor, pattern, path) + } + } + return newDefaultHandler(element, pattern, path) +} + +// negationHandler provides handler for check if the tag in anchor is not defined +type negationHandler struct { + anchor Anchor + pattern interface{} + path string +} + +// newNegationHandler returns instance of negation handler +func newNegationHandler(anchor Anchor, pattern interface{}, path string) ValidationHandler { + return negationHandler{ + anchor: anchor, + pattern: pattern, + path: path, + } +} + +// Handle process negation handler +func (nh negationHandler) Handle(handler resourceElementHandler, resourceMap map[string]interface{}, originPattern interface{}, ac *AnchorMap) (string, error) { + anchorKey := nh.anchor.Key() + currentPath := nh.path + anchorKey + "/" + // if anchor is present in the resource then fail + if _, ok := resourceMap[anchorKey]; ok { + // no need to process elements in value as key cannot be present in resource + ac.AnchorError = newNegationAnchorError(fmt.Sprintf("%s is not allowed", currentPath)) + return currentPath, ac.AnchorError + } + // key is not defined in the resource + return "", nil +} + +// equalityHandler provides handler for non anchor element +type equalityHandler struct { + anchor Anchor + pattern interface{} + path string +} + +// newEqualityHandler returens instance of equality handler +func newEqualityHandler(anchor Anchor, pattern interface{}, path string) ValidationHandler { + return equalityHandler{ + anchor: anchor, + pattern: pattern, + path: path, + } +} + +// Handle processed condition anchor +func (eh equalityHandler) Handle(handler resourceElementHandler, resourceMap map[string]interface{}, originPattern interface{}, ac *AnchorMap) (string, error) { + anchorKey := eh.anchor.Key() + currentPath := eh.path + anchorKey + "/" + // check if anchor is present in resource + if value, ok := resourceMap[anchorKey]; ok { + // validate the values of the pattern + returnPath, err := handler(logging.GlobalLogger(), value, eh.pattern, originPattern, currentPath, ac) + if err != nil { + return returnPath, err + } + return "", nil + } + return "", nil +} + +// defaultHandler provides handler for non anchor element +type defaultHandler struct { + element string + pattern interface{} + path string +} + +// newDefaultHandler returns handler for non anchor elements +func newDefaultHandler(element string, pattern interface{}, path string) ValidationHandler { + return defaultHandler{ + element: element, + pattern: pattern, + path: path, + } +} + +// Handle process non anchor element +func (dh defaultHandler) Handle(handler resourceElementHandler, resourceMap map[string]interface{}, originPattern interface{}, ac *AnchorMap) (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("%s/%s not found", dh.path, dh.element) + } else { + path, err := handler(logging.GlobalLogger(), resourceMap[dh.element], dh.pattern, originPattern, currentPath, ac) + if err != nil { + return path, err + } + } + return "", nil +} + +// conditionAnchorHandler provides handler for condition anchor +type conditionAnchorHandler struct { + anchor Anchor + pattern interface{} + path string +} + +// newConditionAnchorHandler returns an instance of condition acnhor handler +func newConditionAnchorHandler(anchor Anchor, pattern interface{}, path string) ValidationHandler { + return conditionAnchorHandler{ + anchor: anchor, + pattern: pattern, + path: path, + } +} + +// Handle processed condition anchor +func (ch conditionAnchorHandler) Handle(handler resourceElementHandler, resourceMap map[string]interface{}, originPattern interface{}, ac *AnchorMap) (string, error) { + anchorKey := ch.anchor.Key() + currentPath := ch.path + anchorKey + "/" + // check if anchor is present in resource + if value, ok := resourceMap[anchorKey]; ok { + // validate the values of the pattern + returnPath, err := handler(logging.GlobalLogger(), value, ch.pattern, originPattern, currentPath, ac) + if err != nil { + ac.AnchorError = newConditionalAnchorError(err.Error()) + return returnPath, ac.AnchorError + } + return "", nil + } else { + msg := "conditional anchor key doesn't exist in the resource" + return currentPath, newConditionalAnchorError(msg) + } +} + +// globalAnchorHandler provides handler for global condition anchor +type globalAnchorHandler struct { + anchor Anchor + pattern interface{} + path string +} + +// newGlobalAnchorHandler returns an instance of condition acnhor handler +func newGlobalAnchorHandler(anchor Anchor, pattern interface{}, path string) ValidationHandler { + return globalAnchorHandler{ + anchor: anchor, + pattern: pattern, + path: path, + } +} + +// Handle processed global condition anchor +func (gh globalAnchorHandler) Handle(handler resourceElementHandler, resourceMap map[string]interface{}, originPattern interface{}, ac *AnchorMap) (string, error) { + anchorKey := gh.anchor.Key() + currentPath := gh.path + anchorKey + "/" + // check if anchor is present in resource + if value, ok := resourceMap[anchorKey]; ok { + // validate the values of the pattern + returnPath, err := handler(logging.GlobalLogger(), value, gh.pattern, originPattern, currentPath, ac) + if err != nil { + ac.AnchorError = newGlobalAnchorError(err.Error()) + return returnPath, ac.AnchorError + } + return "", nil + } + return "", nil +} + +// existenceHandler provides handlers to process exitence anchor handler +type existenceHandler struct { + anchor Anchor + pattern interface{} + path string +} + +// newExistenceHandler returns existence handler +func newExistenceHandler(anchor Anchor, pattern interface{}, path string) ValidationHandler { + return existenceHandler{ + anchor: anchor, + pattern: pattern, + path: path, + } +} + +// Handle processes the existence anchor handler +func (eh existenceHandler) Handle(handler resourceElementHandler, resourceMap map[string]interface{}, originPattern interface{}, ac *AnchorMap) (string, error) { + // skip is used by existence anchor to not process further if condition is not satisfied + anchorKey := eh.anchor.Key() + currentPath := eh.path + anchorKey + "/" + // check if anchor is present in resource + if value, ok := resourceMap[anchorKey]; ok { + // Existence anchor can only exist on resource value type of list + switch typedResource := value.(type) { + case []interface{}: + typedPattern, ok := eh.pattern.([]interface{}) + if !ok { + return currentPath, fmt.Errorf("invalid pattern type %T: Pattern has to be of list to compare against resource", eh.pattern) + } + // loop all item in the pattern array + errorPath := "" + var err error + for _, patternMap := range typedPattern { + typedPatternMap, ok := patternMap.(map[string]interface{}) + if !ok { + return currentPath, fmt.Errorf("invalid pattern type %T: Pattern has to be of type map to compare against items in resource", eh.pattern) + } + errorPath, err = validateExistenceListResource(handler, typedResource, typedPatternMap, originPattern, currentPath, ac) + if err != nil { + return errorPath, err + } + } + return errorPath, err + default: + return currentPath, fmt.Errorf("invalid resource type %T: Existence ^ () anchor can be used only on list/array type resource", value) + } + } + return "", nil +} + +func validateExistenceListResource(handler resourceElementHandler, resourceList []interface{}, patternMap map[string]interface{}, originPattern interface{}, path string, ac *AnchorMap) (string, error) { + // the idea is all the element in the pattern array should be present atleast once in the resource list + // if non satisfy then throw an error + for i, resourceElement := range resourceList { + currentPath := path + strconv.Itoa(i) + "/" + _, err := handler(logging.GlobalLogger(), resourceElement, patternMap, originPattern, currentPath, ac) + if err == nil { + // condition is satisfied, dont check further + return "", nil + } + } + // none of the existence checks worked, so thats a failure sceanario + return path, fmt.Errorf("existence anchor validation failed at path %s", path) +} diff --git a/pkg/engine/anchor/utils.go b/pkg/engine/anchor/utils.go new file mode 100644 index 0000000000..79063bc35f --- /dev/null +++ b/pkg/engine/anchor/utils.go @@ -0,0 +1,60 @@ +package anchor + +import ( + "path" + "strings" +) + +// GetAnchorsResourcesFromMap returns maps of anchors and resources +func GetAnchorsResourcesFromMap(patternMap map[string]interface{}) (map[string]interface{}, map[string]interface{}) { + anchors := map[string]interface{}{} + resources := map[string]interface{}{} + for key, value := range patternMap { + if a := Parse(key); IsCondition(a) || IsExistence(a) || IsEquality(a) || IsNegation(a) { + anchors[key] = value + } else { + resources[key] = value + } + } + return anchors, resources +} + +// RemoveAnchorsFromPath removes all anchor from path string +func RemoveAnchorsFromPath(str string) string { + parts := strings.Split(str, "/") + if parts[0] == "" { + parts = parts[1:] + } + for i, part := range parts { + if a := Parse(part); a != nil { + parts[i] = a.Key() + } else { + parts[i] = part + } + } + newPath := path.Join(parts...) + if path.IsAbs(str) { + newPath = "/" + newPath + } + return newPath +} + +// resourceHasValueForKey checks if a resource has value for a given key +func resourceHasValueForKey(resource interface{}, key string) bool { + switch typed := resource.(type) { + case map[string]interface{}: + if _, ok := typed[key]; ok { + return true + } + return false + case []interface{}: + for _, value := range typed { + if resourceHasValueForKey(value, key) { + return true + } + } + return false + default: + return false + } +} diff --git a/pkg/engine/anchor/utils_test.go b/pkg/engine/anchor/utils_test.go new file mode 100644 index 0000000000..d47c143780 --- /dev/null +++ b/pkg/engine/anchor/utils_test.go @@ -0,0 +1,126 @@ +package anchor + +import ( + "reflect" + "testing" +) + +func TestRemoveAnchorsFromPath(t *testing.T) { + tests := []struct { + name string + str string + want string + }{{ + str: "/path/(to)/X(anchors)", + want: "/path/to/anchors", + }, { + str: "path/(to)/X(anchors)", + want: "path/to/anchors", + }, { + str: "../(to)/X(anchors)", + want: "../to/anchors", + }, { + str: "/path/(to)/X(anchors)", + want: "/path/to/anchors", + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := RemoveAnchorsFromPath(tt.str); got != tt.want { + t.Errorf("RemoveAnchorsFromPath() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestGetAnchorsResourcesFromMap(t *testing.T) { + tests := []struct { + name string + patternMap map[string]interface{} + wantAnchors map[string]interface{} + wantResources map[string]interface{} + }{{ + patternMap: map[string]interface{}{ + "spec": "test", + }, + wantAnchors: map[string]interface{}{}, + wantResources: map[string]interface{}{ + "spec": "test", + }, + }, { + patternMap: map[string]interface{}{ + "(spec)": "test", + }, + wantAnchors: map[string]interface{}{ + "(spec)": "test", + }, + wantResources: map[string]interface{}{}, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + anchors, resources := GetAnchorsResourcesFromMap(tt.patternMap) + if !reflect.DeepEqual(anchors, tt.wantAnchors) { + t.Errorf("GetAnchorsResourcesFromMap() anchors = %v, want %v", anchors, tt.wantAnchors) + } + if !reflect.DeepEqual(resources, tt.wantResources) { + t.Errorf("GetAnchorsResourcesFromMap() resources = %v, want %v", resources, tt.wantResources) + } + }) + } +} + +func Test_resourceHasValueForKey(t *testing.T) { + type args struct { + resource interface{} + key string + } + tests := []struct { + name string + args args + want bool + }{{ + args: args{ + resource: map[string]interface{}{ + "spec": 123, + }, + key: "spec", + }, + want: true, + }, { + args: args{ + resource: map[string]interface{}{ + "spec": 123, + }, + key: "metadata", + }, + want: false, + }, { + args: args{ + resource: []interface{}{1, 2, 3}, + key: "spec", + }, + want: false, + }, { + args: args{ + resource: []interface{}{ + map[string]interface{}{ + "spec": 123, + }, + }, + key: "spec", + }, + want: true, + }, { + args: args{ + resource: 123, + key: "spec", + }, + want: false, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := resourceHasValueForKey(tt.args.resource, tt.args.key); got != tt.want { + t.Errorf("resourceHasValueForKey() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/engine/mutate/patch/strategicPreprocessing.go b/pkg/engine/mutate/patch/strategicPreprocessing.go index cdf473ce58..3d0d07cb5f 100644 --- a/pkg/engine/mutate/patch/strategicPreprocessing.go +++ b/pkg/engine/mutate/patch/strategicPreprocessing.go @@ -73,12 +73,10 @@ func walkMap(logger logr.Logger, pattern, resource *yaml.RNode) error { if err := validateConditions(logger, pattern, resource); err != nil { return err // do not wrap condition errors } - - isNotAnchor := func(key string) bool { - return !hasAnchor(key) + isNotAnchor := func(a anchor.Anchor) bool { + return !hasAnchor(a) } - - nonAnchors, err := filterKeys(pattern, isNotAnchor) + nonAnchors, err := nonAnchorKeys(pattern, isNotAnchor) if err != nil { return err } @@ -133,7 +131,7 @@ func processListOfMaps(logger logr.Logger, pattern, resource *yaml.RNode) error for _, patternElement := range patternElements { // If pattern has conditions, look for matching elements and process them hasAnyAnchor := hasAnchors(patternElement, hasAnchor) - hasGlobalConditions := hasAnchors(patternElement, anchor.IsGlobalAnchor) + hasGlobalConditions := hasAnchors(patternElement, anchor.IsGlobal) if hasAnyAnchor { anyGlobalConditionPassed := false var lastGlobalAnchorError error = nil @@ -238,12 +236,12 @@ func isGlobalConditionError(err error) bool { // If caller handles map, it must stop processing and skip entire rule. func validateConditions(logger logr.Logger, pattern, resource *yaml.RNode) error { var err error - err = validateConditionsInternal(logger, pattern, resource, anchor.IsGlobalAnchor) + err = validateConditionsInternal(logger, pattern, resource, anchor.IsGlobal) if err != nil { return NewGlobalConditionError(err) } - err = validateConditionsInternal(logger, pattern, resource, anchor.IsConditionAnchor) + err = validateConditionsInternal(logger, pattern, resource, anchor.IsCondition) if err != nil { return NewConditionError(err) } @@ -255,62 +253,72 @@ func validateConditions(logger logr.Logger, pattern, resource *yaml.RNode) error // Remove anchor from pattern, if field already exists. // Remove anchor wrapping from key, if field does not exist in the resource. func handleAddIfNotPresentAnchor(pattern, resource *yaml.RNode) (int, error) { - anchors, err := filterKeys(pattern, anchor.IsAddIfNotPresentAnchor) + anchors, err := filterKeys(pattern, anchor.IsAddIfNotPresent) if err != nil { return 0, err } for _, a := range anchors { - key, _ := anchor.RemoveAnchor(a) + key := a.Key() if resource != nil && resource.Field(key) != nil { // Resource already has this field. // Delete the field with addIfNotPresent anchor from patch. - err = pattern.PipeE(yaml.Clear(a)) + err = pattern.PipeE(yaml.Clear(a.String())) if err != nil { return 0, err } } else { // Remove anchor tags from patch field key. - renameField(a, key, pattern) + renameField(a.String(), key, pattern) } } return len(anchors), nil } -func filterKeys(pattern *yaml.RNode, condition func(string) bool) ([]string, error) { +func filterKeys(pattern *yaml.RNode, condition func(anchor.Anchor) bool) ([]anchor.Anchor, error) { if !isMappingNode(pattern) { return nil, nil } - - keys := make([]string, 0) fields, err := pattern.Fields() if err != nil { - return keys, err + return nil, err } + var anchors []anchor.Anchor + for _, field := range fields { + if a := anchor.Parse(field); a != nil && condition(a) { + anchors = append(anchors, a) + } + } + return anchors, nil +} - for _, key := range fields { - if condition(key) { - keys = append(keys, key) - continue +func nonAnchorKeys(pattern *yaml.RNode, condition func(anchor.Anchor) bool) ([]string, error) { + if !isMappingNode(pattern) { + return nil, nil + } + fields, err := pattern.Fields() + if err != nil { + return nil, err + } + var keys []string + for _, field := range fields { + if a := anchor.Parse(field); a == nil || condition(a) { + keys = append(keys, field) } } return keys, nil } func isMappingNode(node *yaml.RNode) bool { - if err := yaml.ErrorIfInvalid(node, yaml.MappingNode); err != nil { - return false - } - - return true + return yaml.ErrorIfInvalid(node, yaml.MappingNode) == nil } -func hasAnchor(key string) bool { - return anchor.ContainsCondition(key) || anchor.IsAddIfNotPresentAnchor(key) +func hasAnchor(a anchor.Anchor) bool { + return anchor.ContainsCondition(a) || anchor.IsAddIfNotPresent(a) } -func hasAnchors(pattern *yaml.RNode, isAnchor func(key string) bool) bool { +func hasAnchors(pattern *yaml.RNode, isAnchor func(anchor.Anchor) bool) bool { ynode := pattern.YNode() //nolint:ifshort if ynode.Kind == yaml.MappingNode { fields, err := pattern.Fields() @@ -319,10 +327,9 @@ func hasAnchors(pattern *yaml.RNode, isAnchor func(key string) bool) bool { } for _, key := range fields { - if isAnchor(key) { + if a := anchor.Parse(key); a != nil && isAnchor(a) { return true } - patternNode := pattern.Field(key) if !patternNode.IsNilOrEmpty() { if hasAnchors(patternNode.Value, isAnchor) { @@ -331,8 +338,7 @@ func hasAnchors(pattern *yaml.RNode, isAnchor func(key string) bool) bool { } } } else if ynode.Kind == yaml.ScalarNode { - v := ynode.Value - return anchor.ContainsCondition(v) + return anchor.ContainsCondition(anchor.Parse(ynode.Value)) } else if ynode.Kind == yaml.SequenceNode { elements, _ := pattern.Elements() for _, e := range elements { @@ -352,7 +358,6 @@ func renameField(name, newName string, pattern *yaml.RNode) { if field == nil { return } - field.Key.YNode().Value = newName } @@ -402,7 +407,7 @@ func deleteConditionElements(pattern *yaml.RNode) error { } for _, field := range fields { - deleteScalar := anchor.ContainsCondition(field) + deleteScalar := anchor.ContainsCondition(anchor.Parse(field)) canDelete, err := deleteAnchors(pattern.Field(field).Value, deleteScalar, false) if err != nil { return err @@ -438,22 +443,20 @@ func deleteAnchors(node *yaml.RNode, deleteScalar, traverseMappingNodes bool) (b } func deleteAnchorsInMap(node *yaml.RNode, traverseMappingNodes bool) (bool, error) { - conditions, err := filterKeys(node, anchor.ContainsCondition) + anchors, err := filterKeys(node, anchor.ContainsCondition) if err != nil { return false, err } - // remove all conditional anchors with no child nodes first anchorsExist := false - for _, condition := range conditions { - field := node.Field(condition) + for _, a := range anchors { + field := node.Field(a.String()) shouldDelete, err := deleteAnchors(field.Value, true, traverseMappingNodes) if err != nil { return false, err } - if shouldDelete { - if err := node.PipeE(yaml.Clear(condition)); err != nil { + if err := node.PipeE(yaml.Clear(a.String())); err != nil { return false, err } } else { @@ -502,14 +505,12 @@ func stripAnchorsFromNode(node *yaml.RNode, key string) error { if err != nil { return err } - for _, a := range anchors { - k, _ := anchor.RemoveAnchor(a) + k := a.Key() if key == "" || k == key { - renameField(a, k, node) + renameField(a.String(), k, node) } } - return nil } @@ -562,19 +563,17 @@ func deleteListElement(list *yaml.RNode, i int) { list.YNode().Content = append(content[:i], content[i+1:]...) } -func validateConditionsInternal(logger logr.Logger, pattern, resource *yaml.RNode, filter func(string) bool) error { - conditions, err := filterKeys(pattern, filter) +func validateConditionsInternal(logger logr.Logger, pattern, resource *yaml.RNode, filter func(anchor.Anchor) bool) error { + anchors, err := filterKeys(pattern, filter) if err != nil { return err } - - for _, condition := range conditions { - conditionKey, _ := anchor.RemoveAnchor(condition) + for _, a := range anchors { + conditionKey := a.Key() if resource == nil || resource.Field(conditionKey) == nil { return fmt.Errorf("could not found \"%s\" key in the resource", conditionKey) } - - patternValue := pattern.Field(condition).Value + patternValue := pattern.Field(a.String()).Value resourceValue := resource.Field(conditionKey).Value if count, err := handleAddIfNotPresentAnchor(patternValue, resourceValue); err != nil { return err diff --git a/pkg/engine/mutate/patch/strategicPreprocessing_test.go b/pkg/engine/mutate/patch/strategicPreprocessing_test.go index 628bbea94e..41e638a4fe 100644 --- a/pkg/engine/mutate/patch/strategicPreprocessing_test.go +++ b/pkg/engine/mutate/patch/strategicPreprocessing_test.go @@ -986,7 +986,7 @@ func Test_FilterKeys_NoConditions(t *testing.T) { }`) pattern := yaml.MustParse(string(patternRaw)) - conditions, err := filterKeys(pattern, anchor.IsConditionAnchor) + conditions, err := filterKeys(pattern, anchor.IsCondition) assert.NilError(t, err) assert.Equal(t, len(conditions), 0) @@ -1000,18 +1000,18 @@ func Test_FilterKeys_ConditionsArePresent(t *testing.T) { }`) pattern := yaml.MustParse(string(patternRaw)) - conditions, err := filterKeys(pattern, anchor.IsConditionAnchor) + conditions, err := filterKeys(pattern, anchor.IsCondition) assert.NilError(t, err) assert.Equal(t, len(conditions), 2) - assert.Equal(t, conditions[0], "(key2)") - assert.Equal(t, conditions[1], "(key3)") + assert.Equal(t, conditions[0].String(), "(key2)") + assert.Equal(t, conditions[1].String(), "(key3)") } func Test_FilterKeys_EmptyList(t *testing.T) { patternRaw := []byte(`{}`) pattern := yaml.MustParse(string(patternRaw)) - conditions, err := filterKeys(pattern, anchor.IsConditionAnchor) + conditions, err := filterKeys(pattern, anchor.IsCondition) assert.NilError(t, err) assert.Equal(t, len(conditions), 0) diff --git a/pkg/engine/utils/utils.go b/pkg/engine/utils/utils.go index e3ea8afb74..e8f40a66ac 100644 --- a/pkg/engine/utils/utils.go +++ b/pkg/engine/utils/utils.go @@ -2,7 +2,7 @@ package utils import ( jsonpatch "github.com/evanphx/json-patch/v5" - commonAnchor "github.com/kyverno/kyverno/pkg/engine/anchor" + "github.com/kyverno/kyverno/pkg/engine/anchor" "github.com/kyverno/kyverno/pkg/logging" jsonutils "github.com/kyverno/kyverno/pkg/utils/json" ) @@ -48,12 +48,10 @@ func ApplyPatchNew(resource, patch []byte) ([]byte, error) { // GetAnchorsFromMap gets the conditional anchor map func GetAnchorsFromMap(anchorsMap map[string]interface{}) map[string]interface{} { result := make(map[string]interface{}) - for key, value := range anchorsMap { - if commonAnchor.IsConditionAnchor(key) { + if anchor.IsCondition(anchor.Parse(key)) { result[key] = value } } - return result } diff --git a/pkg/engine/validate/utils.go b/pkg/engine/validate/utils.go index ee369bf1b6..4cbc5fce9f 100644 --- a/pkg/engine/validate/utils.go +++ b/pkg/engine/validate/utils.go @@ -4,7 +4,7 @@ import ( "container/list" "sort" - commonAnchors "github.com/kyverno/kyverno/pkg/engine/anchor" + "github.com/kyverno/kyverno/pkg/engine/anchor" ) // Checks if pattern has anchors @@ -44,7 +44,7 @@ func getSortedNestedAnchorResource(resources map[string]interface{}) *list.List for _, k := range keys { v := resources[k] - if commonAnchors.IsGlobalAnchor(k) { + if anchor.IsGlobal(anchor.Parse(k)) { sortedResourceKeys.PushFront(k) continue } @@ -61,7 +61,7 @@ func getSortedNestedAnchorResource(resources map[string]interface{}) *list.List func getAnchorsFromMap(anchorsMap map[string]interface{}) map[string]interface{} { result := make(map[string]interface{}) for key, value := range anchorsMap { - if commonAnchors.IsConditionAnchor(key) || commonAnchors.IsExistenceAnchor(key) || commonAnchors.IsEqualityAnchor(key) || commonAnchors.IsNegationAnchor(key) || commonAnchors.IsGlobalAnchor(key) { + if a := anchor.Parse(key); anchor.IsCondition(a) || anchor.IsExistence(a) || anchor.IsEquality(a) || anchor.IsNegation(a) || anchor.IsGlobal(a) { result[key] = value } } diff --git a/pkg/engine/validate/validate.go b/pkg/engine/validate/validate.go index ba78926445..58cb965f2d 100644 --- a/pkg/engine/validate/validate.go +++ b/pkg/engine/validate/validate.go @@ -44,7 +44,7 @@ func MatchPattern(logger logr.Logger, resource, pattern interface{}) error { } // check if an anchor defined in the policy rule is missing in the resource - if ac.IsAnchorError() { + if ac.KeysAreMissing() { logger.V(3).Info("missing anchor in resource") return &PatternError{err, "", false} } @@ -57,18 +57,18 @@ func MatchPattern(logger logr.Logger, resource, pattern interface{}) error { func skip(err error) bool { // if conditional or global anchors report errors, the rule does not apply to the resource - return anchor.IsConditionalAnchorError(err.Error()) || anchor.IsGlobalAnchorError(err.Error()) + return anchor.IsConditionalAnchorError(err) || anchor.IsGlobalAnchorError(err) } func fail(err error) bool { // if negation anchors report errors, the rule will fail - return anchor.IsNegationAnchorError(err.Error()) + return anchor.IsNegationAnchorError(err) } // 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, ac *anchor.AnchorKey) (string, error) { +func validateResourceElement(log logr.Logger, resourceElement, patternElement, originPattern interface{}, path string, ac *anchor.AnchorMap) (string, error) { switch typedPatternElement := patternElement.(type) { // map case map[string]interface{}: @@ -115,7 +115,7 @@ 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 *anchor.AnchorKey) (string, error) { +func validateMap(log logr.Logger, resourceMap, patternMap map[string]interface{}, origPattern interface{}, path string, ac *anchor.AnchorMap) (string, error) { patternMap = wildcards.ExpandInMetadata(patternMap, resourceMap) // check if there is anchor in pattern // Phase 1 : Evaluate all the anchors @@ -160,7 +160,7 @@ func validateMap(log logr.Logger, resourceMap, patternMap map[string]interface{} return "", nil } -func validateArray(log logr.Logger, resourceArray, patternArray []interface{}, originPattern interface{}, path string, ac *anchor.AnchorKey) (string, error) { +func validateArray(log logr.Logger, resourceArray, patternArray []interface{}, originPattern interface{}, path string, ac *anchor.AnchorMap) (string, error) { if len(patternArray) == 0 { return path, fmt.Errorf("pattern Array empty") } @@ -215,7 +215,7 @@ func validateArray(log logr.Logger, resourceArray, patternArray []interface{}, o // validateArrayOfMaps gets anchors from pattern array map element, applies anchors logic // and then validates each map due to the pattern -func validateArrayOfMaps(log logr.Logger, resourceMapArray []interface{}, patternMap map[string]interface{}, originPattern interface{}, path string, ac *anchor.AnchorKey) (string, error) { +func validateArrayOfMaps(log logr.Logger, resourceMapArray []interface{}, patternMap map[string]interface{}, originPattern interface{}, path string, ac *anchor.AnchorMap) (string, error) { applyCount := 0 skipErrors := make([]error, 0) for i, resourceElement := range resourceMapArray { diff --git a/pkg/engine/wildcards/wildcards.go b/pkg/engine/wildcards/wildcards.go index 1919a01c1e..45706799c4 100644 --- a/pkg/engine/wildcards/wildcards.go +++ b/pkg/engine/wildcards/wildcards.go @@ -3,7 +3,7 @@ package wildcards import ( "strings" - commonAnchor "github.com/kyverno/kyverno/pkg/engine/anchor" + "github.com/kyverno/kyverno/pkg/engine/anchor" wildcard "github.com/kyverno/kyverno/pkg/utils/wildcard" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -27,7 +27,6 @@ func replaceWildcardsInMapKeyValues(patternMap map[string]string, resourceMap ma result[k] = v } } - return result } @@ -41,12 +40,10 @@ func expandWildcards(k, v string, resourceMap map[string]string, matchValue, rep } } } - if replace { k = replaceWildCardChars(k) v = replaceWildCardChars(v) } - return k, v } @@ -78,23 +75,22 @@ func ExpandInMetadata(patternMap, resourceMap map[string]interface{}) map[string if labels != nil { metadata[labelsKey] = labels } - annotationsKey, annotations := expandWildcardsInTag("annotations", patternMetadata, resourceMetadata) if annotations != nil { metadata[annotationsKey] = annotations } - return patternMap } func getPatternValue(tag string, pattern map[string]interface{}) (string, interface{}) { for k, v := range pattern { - k2, _ := commonAnchor.RemoveAnchor(k) - if k2 == tag { + if k == tag { + return k, v + } + if a := anchor.Parse(k); a != nil && a.Key() == tag { return k, v } } - return "", nil } @@ -140,17 +136,16 @@ func replaceWildcardsInMapKeys(patternData, resourceData map[string]string) map[ results := map[string]interface{}{} for k, v := range patternData { if wildcard.ContainsWildcard(k) { - anchorFreeKey, anchorPrefix := commonAnchor.RemoveAnchor(k) - matchK, _ := expandWildcards(anchorFreeKey, v, resourceData, false, false) - if anchorPrefix != "" { - matchK = commonAnchor.AddAnchor(matchK, anchorPrefix) + if a := anchor.Parse(k); a != nil { + matchK, _ := expandWildcards(a.Key(), v, resourceData, false, false) + results[anchor.String(a.Type(), matchK)] = v + } else { + matchK, _ := expandWildcards(k, v, resourceData, false, false) + results[matchK] = v } - - results[matchK] = v } else { results[k] = v } } - return results } diff --git a/pkg/policy/common/validate_pattern.go b/pkg/policy/common/validate_pattern.go index de34838830..cd4c008582 100644 --- a/pkg/policy/common/validate_pattern.go +++ b/pkg/policy/common/validate_pattern.go @@ -2,19 +2,18 @@ package common import ( "fmt" - "regexp" "strconv" - commonAnchors "github.com/kyverno/kyverno/pkg/engine/anchor" + "github.com/kyverno/kyverno/pkg/engine/anchor" ) // ValidatePattern validates the pattern -func ValidatePattern(patternElement interface{}, path string, supportedAnchors []commonAnchors.IsAnchor) (string, error) { +func ValidatePattern(patternElement interface{}, path string, isSupported func(anchor.Anchor) bool) (string, error) { switch typedPatternElement := patternElement.(type) { case map[string]interface{}: - return validateMap(typedPatternElement, path, supportedAnchors) + return validateMap(typedPatternElement, path, isSupported) case []interface{}: - return validateArray(typedPatternElement, path, supportedAnchors) + return validateArray(typedPatternElement, path, isSupported) case string, float64, int, int64, bool, nil: // TODO: check operator return "", nil @@ -23,30 +22,21 @@ func ValidatePattern(patternElement interface{}, path string, supportedAnchors [ } } -func validateMap(patternMap map[string]interface{}, path string, supportedAnchors []commonAnchors.IsAnchor) (string, error) { +func validateMap(patternMap map[string]interface{}, path string, isSupported func(anchor.Anchor) bool) (string, error) { // check if anchors are defined for key, value := range patternMap { // if key is anchor - // check regex () -> this is anchor - // () - // single char () - re, err := regexp.Compile(`^.?\(.+\)$`) - if err != nil { - return path + "/" + key, fmt.Errorf("unable to parse the field %s: %v", key, err) - } - - matched := re.MatchString(key) + a := anchor.Parse(key) // check the type of anchor - if matched { + if a != nil { // some type of anchor // check if valid anchor - if !checkAnchors(key, supportedAnchors) { + if !checkAnchors(a, isSupported) { return path + "/" + key, fmt.Errorf("unsupported anchor %s", key) } - // addition check for existence anchor // value must be of type list - if commonAnchors.IsExistenceAnchor(key) { + if anchor.IsExistence(a) { typedValue, ok := value.([]interface{}) if !ok { return path + "/" + key, fmt.Errorf("existence anchor should have value of type list") @@ -58,29 +48,27 @@ func validateMap(patternMap map[string]interface{}, path string, supportedAnchor } } // lets validate the values now :) - if errPath, err := ValidatePattern(value, path+"/"+key, supportedAnchors); err != nil { + if errPath, err := ValidatePattern(value, path+"/"+key, isSupported); err != nil { return errPath, err } } return "", nil } -func validateArray(patternArray []interface{}, path string, supportedAnchors []commonAnchors.IsAnchor) (string, error) { +func validateArray(patternArray []interface{}, path string, isSupported func(anchor.Anchor) bool) (string, error) { for i, patternElement := range patternArray { currentPath := path + strconv.Itoa(i) + "/" // lets validate the values now :) - if errPath, err := ValidatePattern(patternElement, currentPath, supportedAnchors); err != nil { + if errPath, err := ValidatePattern(patternElement, currentPath, isSupported); err != nil { return errPath, err } } return "", nil } -func checkAnchors(key string, supportedAnchors []commonAnchors.IsAnchor) bool { - for _, f := range supportedAnchors { - if f(key) { - return true - } +func checkAnchors(a anchor.Anchor, isSupported func(anchor.Anchor) bool) bool { + if isSupported == nil { + return false } - return false + return isSupported(a) } diff --git a/pkg/policy/generate/validate.go b/pkg/policy/generate/validate.go index 0d1e2875c5..2f4a29a94d 100644 --- a/pkg/policy/generate/validate.go +++ b/pkg/policy/generate/validate.go @@ -8,7 +8,6 @@ import ( "github.com/go-logr/logr" kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1" "github.com/kyverno/kyverno/pkg/clients/dclient" - commonAnchors "github.com/kyverno/kyverno/pkg/engine/anchor" "github.com/kyverno/kyverno/pkg/engine/variables" "github.com/kyverno/kyverno/pkg/policy/common" kubeutils "github.com/kyverno/kyverno/pkg/utils/kube" @@ -79,7 +78,7 @@ func (g *Generate) Validate() (string, error) { if target := rule.GetData(); target != nil { // TODO: is this required ?? as anchors can only be on pattern and not resource // we can add this check by not sure if its needed here - if path, err := common.ValidatePattern(target, "/", []commonAnchors.IsAnchor{}); err != nil { + if path, err := common.ValidatePattern(target, "/", nil); err != nil { return fmt.Sprintf("data.%s", path), fmt.Errorf("anchors not supported on generate resources: %v", err) } } diff --git a/pkg/policy/validate/validate.go b/pkg/policy/validate/validate.go index 5d0d88e6a2..7c9d792a03 100644 --- a/pkg/policy/validate/validate.go +++ b/pkg/policy/validate/validate.go @@ -4,7 +4,7 @@ import ( "fmt" kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1" - commonAnchors "github.com/kyverno/kyverno/pkg/engine/anchor" + "github.com/kyverno/kyverno/pkg/engine/anchor" "github.com/kyverno/kyverno/pkg/policy/common" ) @@ -30,7 +30,13 @@ func (v *Validate) Validate() (string, error) { } if target := v.rule.GetPattern(); target != nil { - if path, err := common.ValidatePattern(target, "/", []commonAnchors.IsAnchor{commonAnchors.IsConditionAnchor, commonAnchors.IsExistenceAnchor, commonAnchors.IsEqualityAnchor, commonAnchors.IsNegationAnchor, commonAnchors.IsGlobalAnchor}); err != nil { + if path, err := common.ValidatePattern(target, "/", func(a anchor.Anchor) bool { + return anchor.IsCondition(a) || + anchor.IsExistence(a) || + anchor.IsEquality(a) || + anchor.IsNegation(a) || + anchor.IsGlobal(a) + }); err != nil { return fmt.Sprintf("pattern.%s", path), err } } @@ -41,7 +47,13 @@ func (v *Validate) Validate() (string, error) { return "anyPattern", fmt.Errorf("failed to deserialize anyPattern, expect array: %v", err) } for i, pattern := range anyPattern { - if path, err := common.ValidatePattern(pattern, "/", []commonAnchors.IsAnchor{commonAnchors.IsConditionAnchor, commonAnchors.IsExistenceAnchor, commonAnchors.IsEqualityAnchor, commonAnchors.IsNegationAnchor, commonAnchors.IsGlobalAnchor}); err != nil { + if path, err := common.ValidatePattern(pattern, "/", func(a anchor.Anchor) bool { + return anchor.IsCondition(a) || + anchor.IsExistence(a) || + anchor.IsEquality(a) || + anchor.IsNegation(a) || + anchor.IsGlobal(a) + }); err != nil { return fmt.Sprintf("anyPattern[%d].%s", i, path), err } }