From 4c251bcffd65f412ecd23a23d0c9a5088edfa1ad Mon Sep 17 00:00:00 2001 From: Sebastian Widmer <widmer.sebastian@gmail.com> Date: Mon, 29 Nov 2021 17:13:07 +0100 Subject: [PATCH] Add `pattern_match` custom JMESPath function analogous to `regex_match` (#2717) * Add `pattern_match` custom JMESPath function analogous to `regex_match` Signed-off-by: Sebastian Widmer <sebastian.widmer@vshn.net> * Add CLI test for the custom `pattern_match` function Signed-off-by: Sebastian Widmer <sebastian.widmer@vshn.net> --- pkg/engine/jmespath/functions.go | 24 +++++++++++++++++++ pkg/engine/jmespath/functions_test.go | 24 +++++++++++++++++++ test/cli/test/custom-functions/policy.yaml | 22 +++++++++++++++++ test/cli/test/custom-functions/resources.yaml | 18 ++++++++++++-- test/cli/test/custom-functions/test.yaml | 14 +++++++++-- 5 files changed, 98 insertions(+), 4 deletions(-) diff --git a/pkg/engine/jmespath/functions.go b/pkg/engine/jmespath/functions.go index 746792c45b..9fb00d6c8e 100644 --- a/pkg/engine/jmespath/functions.go +++ b/pkg/engine/jmespath/functions.go @@ -11,6 +11,7 @@ import ( "time" gojmespath "github.com/jmespath/go-jmespath" + "github.com/minio/pkg/wildcard" ) var ( @@ -39,6 +40,7 @@ var ( regexReplaceAll = "regex_replace_all" regexReplaceAllLiteral = "regex_replace_all_literal" regexMatch = "regex_match" + patternMatch = "pattern_match" labelMatch = "label_match" add = "add" subtract = "subtract" @@ -149,6 +151,14 @@ func getFunctions() []*gojmespath.FunctionEntry { }, Handler: jpRegexMatch, }, + { + Name: patternMatch, + Arguments: []ArgSpec{ + {Types: []JpType{JpString}}, + {Types: []JpType{JpString, JpNumber}}, + }, + Handler: jpPatternMatch, + }, { // Validates if label (param1) would match pod/host/etc labels (param2) Name: labelMatch, @@ -420,6 +430,20 @@ func jpRegexMatch(arguments []interface{}) (interface{}, error) { return regexp.Match(regex.String(), []byte(src)) } +func jpPatternMatch(arguments []interface{}) (interface{}, error) { + pattern, err := validateArg(regexMatch, arguments, 0, reflect.String) + if err != nil { + return nil, err + } + + src, err := ifaceToString(arguments[1]) + if err != nil { + return nil, fmt.Errorf(invalidArgumentTypeError, regexMatch, 2, "String or Real") + } + + return wildcard.Match(pattern.String(), src), nil +} + func jpLabelMatch(arguments []interface{}) (interface{}, error) { labelMap, ok := arguments[0].(map[string]interface{}) diff --git a/pkg/engine/jmespath/functions_test.go b/pkg/engine/jmespath/functions_test.go index b6a0d784bc..0809f491db 100644 --- a/pkg/engine/jmespath/functions_test.go +++ b/pkg/engine/jmespath/functions_test.go @@ -268,6 +268,30 @@ func Test_RegexMatchWithNumber(t *testing.T) { assert.Equal(t, true, result) } +func Test_PatternMatch(t *testing.T) { + data := make(map[string]interface{}) + data["foo"] = "prefix-foo" + + query, err := New("pattern_match('prefix-*', foo)") + assert.NilError(t, err) + + result, err := query.Search(data) + assert.NilError(t, err) + assert.Equal(t, true, result) +} + +func Test_PatternMatchWithNumber(t *testing.T) { + data := make(map[string]interface{}) + data["foo"] = -12.0 + + query, err := New("pattern_match('12*', abs(foo))") + assert.NilError(t, err) + + result, err := query.Search(data) + assert.NilError(t, err) + assert.Equal(t, true, result) +} + func Test_RegexReplaceAll(t *testing.T) { resourceRaw := []byte(` { diff --git a/test/cli/test/custom-functions/policy.yaml b/test/cli/test/custom-functions/policy.yaml index 15e6b1dafb..be2e77b4f6 100644 --- a/test/cli/test/custom-functions/policy.yaml +++ b/test/cli/test/custom-functions/policy.yaml @@ -17,3 +17,25 @@ spec: - key: "{{base64_decode(request.object.data.value)}}" operator: NotEquals value: "{{request.object.metadata.labels.value}}" +--- +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: pattern-match +spec: + validationFailureAction: enforce + background: false + rules: + - match: + all: + - resources: + kinds: + - Namespace + name: label-must-match-pattern + validate: + deny: + conditions: + all: + - key: "{{pattern_match('prefix-*', request.object.metadata.labels.value)}}" + operator: Equals + value: false diff --git a/test/cli/test/custom-functions/resources.yaml b/test/cli/test/custom-functions/resources.yaml index a050faac52..57f01097e8 100644 --- a/test/cli/test/custom-functions/resources.yaml +++ b/test/cli/test/custom-functions/resources.yaml @@ -1,7 +1,7 @@ apiVersion: v1 kind: Secret metadata: - name: test-match + name: base64-test-match labels: value: hello type: Opaque @@ -11,9 +11,23 @@ data: apiVersion: v1 kind: Secret metadata: - name: test-no-match + name: base64-test-no-match labels: value: hello type: Opaque data: value: Z29vZGJ5ZQ== +--- +apiVersion: v1 +kind: Namespace +metadata: + name: pattern-match-test-match + labels: + value: prefix-test +--- +apiVersion: v1 +kind: Namespace +metadata: + name: pattern-match-test-no-match + labels: + value: test diff --git a/test/cli/test/custom-functions/test.yaml b/test/cli/test/custom-functions/test.yaml index cf804c652a..996f9bfacf 100644 --- a/test/cli/test/custom-functions/test.yaml +++ b/test/cli/test/custom-functions/test.yaml @@ -6,11 +6,21 @@ resources: results: - policy: base64 rule: secret-value-must-match-label - resource: test-match + resource: base64-test-match kind: Secret status: pass - policy: base64 rule: secret-value-must-match-label - resource: test-no-match + resource: base64-test-no-match kind: Secret status: fail + - policy: pattern-match + rule: label-must-match-pattern + resource: pattern-match-test-match + kind: Namespace + status: pass + - policy: pattern-match + rule: label-must-match-pattern + resource: pattern-match-test-no-match + kind: Namespace + status: fail