diff --git a/go.mod b/go.mod index eea78cccecb1..d6407e9332b7 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,6 @@ require ( github.com/onsi/ginkgo v1.14.1 github.com/onsi/gomega v1.10.2 github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.0.1 // indirect github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/pkg/errors v0.9.1 @@ -51,6 +50,7 @@ require ( // Added for go1.13 migration https://github.com/golang/go/issues/32805 replace ( github.com/gorilla/rpc v1.2.0+incompatible => github.com/gorilla/rpc v1.2.0 + github.com/jmespath/go-jmespath => github.com/kyverno/go-jmespath v0.4.1-0.20210302163943-f30eab0a3ed6 k8s.io/code-generator => k8s.io/code-generator v0.0.0-20200306081859-6a048a382944 k8s.io/component-base => k8s.io/component-base v0.0.0-20190612130303-4062e14deebe ) diff --git a/go.sum b/go.sum index faf5fd3ab428..4b40ff2fc7c6 100644 --- a/go.sum +++ b/go.sum @@ -476,8 +476,8 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOl github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jcmturner/gofork v0.0.0-20190328161633-dc7c13fece03/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= -github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v0.0.0-20180701071628-ab8a2e0c74be/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= @@ -523,6 +523,8 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kurin/blazer v0.5.4-0.20190613185654-cf2f27cc0be3/go.mod h1:4FCXMUWo9DllR2Do4TtBd377ezyAJ51vB5uTBjt0pGU= +github.com/kyverno/go-jmespath v0.4.1-0.20210302163943-f30eab0a3ed6 h1:9rJUAc/XxL6wV4i+3X56wcRM8tDhoo0l7DIieuXPGfk= +github.com/kyverno/go-jmespath v0.4.1-0.20210302163943-f30eab0a3ed6/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/lensesio/tableprinter v0.0.0-20201125135848-89e81fc956e7 h1:k/1ku0yehLCPqERCHkIHMDqDg1R02AcCScRuHbamU3s= github.com/lensesio/tableprinter v0.0.0-20201125135848-89e81fc956e7/go.mod h1:YR/zYthNdWfO8+0IOyHDcIDBBBS2JMnYUIwSsnwmRqU= github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= @@ -652,8 +654,6 @@ github.com/onsi/gomega v1.10.2 h1:aY/nuoWlKJud2J6U0E3NWsjlg+0GtwXxgEqthRdzlcs= github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI= -github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6 h1:lNCW6THrCKBiJBpz8kbVGjC7MgdCGKwuvBgc7LoD6sw= github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= diff --git a/pkg/engine/context/evaluate.go b/pkg/engine/context/evaluate.go index 05bfec7551b3..fb818197411c 100644 --- a/pkg/engine/context/evaluate.go +++ b/pkg/engine/context/evaluate.go @@ -5,7 +5,7 @@ import ( "fmt" "strings" - jmespath "github.com/jmespath/go-jmespath" + jmespath "github.com/kyverno/kyverno/pkg/engine/jmespath" ) //Query the JSON context with JMESPATH search path @@ -25,7 +25,7 @@ func (ctx *Context) Query(query string) (interface{}, error) { } // compile the query - queryPath, err := jmespath.Compile(query) + queryPath, err := jmespath.New(query) if err != nil { ctx.log.Error(err, "incorrect query", "query", query) return emptyResult, fmt.Errorf("incorrect query %s: %v", query, err) diff --git a/pkg/engine/jmespath/functions.go b/pkg/engine/jmespath/functions.go new file mode 100644 index 000000000000..4bfad9f5f454 --- /dev/null +++ b/pkg/engine/jmespath/functions.go @@ -0,0 +1,381 @@ +package jmespath + +import ( + "errors" + "fmt" + "reflect" + "regexp" + "strconv" + "strings" + + gojmespath "github.com/jmespath/go-jmespath" +) + +var ( + JpObject = gojmespath.JpObject + JpString = gojmespath.JpString + JpNumber = gojmespath.JpNumber + JpArray = gojmespath.JpArray + JpArrayString = gojmespath.JpArrayString +) + +type ( + JpType = gojmespath.JpType + ArgSpec = gojmespath.ArgSpec +) + +// function names +var ( + compare = "compare" + contains = "contains" + equalFold = "equal_fold" + replace = "replace" + replaceAll = "replace_all" + toUpper = "to_upper" + toLower = "to_lower" + trim = "trim" + split = "split" + equals = "equals" + regexReplaceAll = "regex_replace_all" + regexReplaceAllLiteral = "regex_replace_all_literal" + regexMatch = "regex_match" +) + +const errorPrefix = "JMESPath function '%s': " +const invalidArgumentTypeError = errorPrefix + "%d argument is expected of %s type" +const genericError = errorPrefix + "%s" + +func getFunctions() []*gojmespath.FunctionEntry { + return []*gojmespath.FunctionEntry{ + { + Name: compare, + Arguments: []ArgSpec{ + {Types: []JpType{JpString}}, + {Types: []JpType{JpString}}, + }, + Handler: jpfCompare, + }, + { + Name: contains, + Arguments: []ArgSpec{ + {Types: []JpType{JpString}}, + {Types: []JpType{JpString}}, + }, + Handler: jpfContains, + }, + { + Name: equalFold, + Arguments: []ArgSpec{ + {Types: []JpType{JpString}}, + {Types: []JpType{JpString}}, + }, + Handler: jpfEqualFold, + }, + { + Name: replace, + Arguments: []ArgSpec{ + {Types: []JpType{JpString}}, + {Types: []JpType{JpString}}, + {Types: []JpType{JpString}}, + {Types: []JpType{JpNumber}}, + }, + Handler: jpfReplace, + }, + { + Name: replaceAll, + Arguments: []ArgSpec{ + {Types: []JpType{JpString}}, + {Types: []JpType{JpString}}, + {Types: []JpType{JpString}}, + }, + Handler: jpfReplaceAll, + }, + { + Name: toUpper, + Arguments: []ArgSpec{ + {Types: []JpType{JpString}}, + }, + Handler: jpfToUpper, + }, + { + Name: toLower, + Arguments: []ArgSpec{ + {Types: []JpType{JpString}}, + }, + Handler: jpfToLower, + }, + { + Name: trim, + Arguments: []ArgSpec{ + {Types: []JpType{JpString}}, + {Types: []JpType{JpString}}, + }, + Handler: jpfTrim, + }, + { + Name: split, + Arguments: []ArgSpec{ + {Types: []JpType{JpString}}, + {Types: []JpType{JpString}}, + }, + Handler: jpfSplit, + }, + { + Name: regexReplaceAll, + Arguments: []ArgSpec{ + {Types: []JpType{JpString}}, + {Types: []JpType{JpString, JpNumber}}, + {Types: []JpType{JpString, JpNumber}}, + }, + Handler: jpRegexReplaceAll, + }, + { + Name: regexReplaceAllLiteral, + Arguments: []ArgSpec{ + {Types: []JpType{JpString}}, + {Types: []JpType{JpString, JpNumber}}, + {Types: []JpType{JpString, JpNumber}}, + }, + Handler: jpRegexReplaceAllLiteral, + }, + { + Name: regexMatch, + Arguments: []ArgSpec{ + {Types: []JpType{JpString}}, + {Types: []JpType{JpString, JpNumber}}, + }, + Handler: jpRegexMatch, + }, + } + +} + +func jpfCompare(arguments []interface{}) (interface{}, error) { + var err error + a, err := validateArg(compare, arguments, 0, reflect.String) + if err != nil { + return nil, err + } + + b, err := validateArg(compare, arguments, 1, reflect.String) + if err != nil { + return nil, err + } + + return strings.Compare(a.String(), b.String()), nil +} + +func jpfContains(arguments []interface{}) (interface{}, error) { + var err error + str, err := validateArg(contains, arguments, 0, reflect.String) + if err != nil { + return nil, err + } + + substr, err := validateArg(contains, arguments, 1, reflect.String) + if err != nil { + return nil, err + } + + return strings.Contains(str.String(), substr.String()), nil +} + +func jpfEqualFold(arguments []interface{}) (interface{}, error) { + var err error + a, err := validateArg(equalFold, arguments, 0, reflect.String) + if err != nil { + return nil, err + } + + b, err := validateArg(equalFold, arguments, 1, reflect.String) + if err != nil { + return nil, err + } + + return strings.EqualFold(a.String(), b.String()), nil +} + +func jpfReplace(arguments []interface{}) (interface{}, error) { + var err error + str, err := validateArg(replace, arguments, 0, reflect.String) + if err != nil { + return nil, err + } + + old, err := validateArg(replace, arguments, 1, reflect.String) + if err != nil { + return nil, err + } + + new, err := validateArg(replace, arguments, 2, reflect.String) + if err != nil { + return nil, err + } + + n, err := validateArg(replace, arguments, 3, reflect.Float64) + if err != nil { + return nil, err + } + + return strings.Replace(str.String(), old.String(), new.String(), int(n.Float())), nil +} + +func jpfReplaceAll(arguments []interface{}) (interface{}, error) { + var err error + str, err := validateArg(replaceAll, arguments, 0, reflect.String) + if err != nil { + return nil, err + } + + old, err := validateArg(replaceAll, arguments, 1, reflect.String) + if err != nil { + return nil, err + } + + new, err := validateArg(replaceAll, arguments, 2, reflect.String) + if err != nil { + return nil, err + } + + return strings.ReplaceAll(str.String(), old.String(), new.String()), nil +} + +func jpfToUpper(arguments []interface{}) (interface{}, error) { + var err error + str, err := validateArg(toUpper, arguments, 0, reflect.String) + if err != nil { + return nil, err + } + + return strings.ToUpper(str.String()), nil +} + +func jpfToLower(arguments []interface{}) (interface{}, error) { + var err error + str, err := validateArg(toLower, arguments, 0, reflect.String) + if err != nil { + return nil, err + } + + return strings.ToLower(str.String()), nil +} + +func jpfTrim(arguments []interface{}) (interface{}, error) { + var err error + str, err := validateArg(trim, arguments, 0, reflect.String) + if err != nil { + return nil, err + } + + cutset, err := validateArg(trim, arguments, 1, reflect.String) + if err != nil { + return nil, err + } + + return strings.Trim(str.String(), cutset.String()), nil +} + +func jpfSplit(arguments []interface{}) (interface{}, error) { + var err error + str, err := validateArg(split, arguments, 0, reflect.String) + if err != nil { + return nil, err + } + + sep, err := validateArg(split, arguments, 1, reflect.String) + if err != nil { + return nil, err + } + + return strings.Split(str.String(), sep.String()), nil +} + +func jpRegexReplaceAll(arguments []interface{}) (interface{}, error) { + var err error + regex, err := validateArg(regexReplaceAll, arguments, 0, reflect.String) + if err != nil { + return nil, err + } + + src, err := ifaceToString(arguments[1]) + if err != nil { + return nil, fmt.Errorf(invalidArgumentTypeError, regexReplaceAll, 2, "String or Real") + } + + repl, err := ifaceToString(arguments[2]) + if err != nil { + return nil, fmt.Errorf(invalidArgumentTypeError, regexReplaceAll, 3, "String or Real") + } + + reg, err := regexp.Compile(regex.String()) + if err != nil { + return nil, fmt.Errorf(genericError, regexReplaceAll, err.Error()) + } + return string(reg.ReplaceAll([]byte(src), []byte(repl))), nil +} + +func jpRegexReplaceAllLiteral(arguments []interface{}) (interface{}, error) { + var err error + regex, err := validateArg(regexReplaceAllLiteral, arguments, 0, reflect.String) + if err != nil { + return nil, err + } + + src, err := ifaceToString(arguments[1]) + if err != nil { + return nil, fmt.Errorf(invalidArgumentTypeError, regexReplaceAllLiteral, 2, "String or Real") + } + + repl, err := ifaceToString(arguments[2]) + if err != nil { + return nil, fmt.Errorf(invalidArgumentTypeError, regexReplaceAllLiteral, 3, "String or Real") + } + + reg, err := regexp.Compile(regex.String()) + if err != nil { + return nil, fmt.Errorf(genericError, regexReplaceAllLiteral, err.Error()) + } + return string(reg.ReplaceAllLiteral([]byte(src), []byte(repl))), nil +} + +func jpRegexMatch(arguments []interface{}) (interface{}, error) { + var err error + regex, 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 regexp.Match(regex.String(), []byte(src)) +} + +// InterfaceToString casts an interface to a string type +func ifaceToString(iface interface{}) (string, error) { + switch iface.(type) { + case int: + return strconv.Itoa(iface.(int)), nil + case float64: + return strconv.FormatFloat(iface.(float64), 'f', -1, 32), nil + case float32: + return strconv.FormatFloat(iface.(float64), 'f', -1, 32), nil + case string: + return iface.(string), nil + case bool: + return strconv.FormatBool(iface.(bool)), nil + default: + return "", errors.New("error, undefined type cast") + } +} + +func validateArg(f string, arguments []interface{}, index int, expectedType reflect.Kind) (reflect.Value, error) { + arg := reflect.ValueOf(arguments[index]) + if arg.Type().Kind() != expectedType { + return reflect.Value{}, fmt.Errorf(invalidArgumentTypeError, equalFold, index+1, expectedType.String()) + } + + return arg, nil +} diff --git a/pkg/engine/jmespath/functions_test.go b/pkg/engine/jmespath/functions_test.go new file mode 100644 index 000000000000..b282c317138b --- /dev/null +++ b/pkg/engine/jmespath/functions_test.go @@ -0,0 +1,245 @@ +package jmespath + +import ( + "encoding/json" + "testing" + + "gotest.tools/assert" +) + +func TestJMESPathFunctions_CompareEqualStrings(t *testing.T) { + jp, err := New("compare('a', 'a')") + assert.NilError(t, err) + + result, err := jp.Search("") + assert.NilError(t, err) + + equal, ok := result.(int) + assert.Assert(t, ok) + assert.Equal(t, equal, 0) +} + +func TestJMESPathFunctions_CompareDifferentStrings(t *testing.T) { + jp, err := New("compare('a', 'b')") + assert.NilError(t, err) + + result, err := jp.Search("") + assert.NilError(t, err) + + equal, ok := result.(int) + assert.Assert(t, ok) + assert.Equal(t, equal, -1) +} + +func TestJMESPathFunctions_Contains(t *testing.T) { + jp, err := New("contains('string', 'str')") + assert.NilError(t, err) + + result, err := jp.Search("") + assert.NilError(t, err) + + contains, ok := result.(bool) + assert.Assert(t, ok) + assert.Assert(t, contains) +} + +func TestJMESPathFunctions_EqualFold(t *testing.T) { + jp, err := New("equal_fold('Go', 'go')") + assert.NilError(t, err) + + result, err := jp.Search("") + assert.NilError(t, err) + + equal, ok := result.(bool) + assert.Assert(t, ok) + assert.Assert(t, equal) +} + +func TestJMESPathFunctions_Replace(t *testing.T) { + // Can't use integer literals due to + // https://github.com/jmespath/go-jmespath/issues/27 + // + // TODO: fix this in https://github.com/kyverno/go-jmespath + // + + jp, err := New("replace('Lorem ipsum dolor sit amet', 'ipsum', 'muspi', `-1`)") + assert.NilError(t, err) + + result, err := jp.Search("") + assert.NilError(t, err) + + replaced, ok := result.(string) + assert.Assert(t, ok) + assert.Equal(t, replaced, "Lorem muspi dolor sit amet") +} + +func TestJMESPathFunctions_ReplaceAll(t *testing.T) { + jp, err := New("replace_all('Lorem ipsum dolor sit amet', 'ipsum', 'muspi')") + assert.NilError(t, err) + + result, err := jp.Search("") + assert.NilError(t, err) + + replaced, ok := result.(string) + assert.Assert(t, ok) + assert.Equal(t, replaced, "Lorem muspi dolor sit amet") +} + +func TestJMESPathFunctions_ToUpper(t *testing.T) { + jp, err := New("to_upper('abc')") + assert.NilError(t, err) + + result, err := jp.Search("") + assert.NilError(t, err) + + upper, ok := result.(string) + assert.Assert(t, ok) + assert.Equal(t, upper, "ABC") +} + +func TestJMESPathFunctions_ToLower(t *testing.T) { + jp, err := New("to_lower('AbC')") + assert.NilError(t, err) + + result, err := jp.Search("") + assert.NilError(t, err) + + lower, ok := result.(string) + assert.Assert(t, ok) + assert.Equal(t, lower, "abc") +} + +func TestJMESPathFunctions_Trim(t *testing.T) { + jp, err := New("trim('¡¡¡Hello, Gophers!!!', '!¡')") + assert.NilError(t, err) + + result, err := jp.Search("") + assert.NilError(t, err) + + trim, ok := result.(string) + assert.Assert(t, ok) + assert.Equal(t, trim, "Hello, Gophers") +} + +func TestJMESPathFunctions_Split(t *testing.T) { + jp, err := New("split('Hello, Gophers', ', ')") + assert.NilError(t, err) + + result, err := jp.Search("") + assert.NilError(t, err) + + split, ok := result.([]string) + assert.Assert(t, ok) + assert.Equal(t, split[0], "Hello") + assert.Equal(t, split[1], "Gophers") +} + +func TestJMESPathFunctions_HasPrefix(t *testing.T) { + jp, err := New("starts_with('Gophers', 'Go')") + assert.NilError(t, err) + + result, err := jp.Search("") + assert.NilError(t, err) + + split, ok := result.(bool) + assert.Assert(t, ok) + assert.Equal(t, split, true) +} + +func TestJMESPathFunctions_HasSuffix(t *testing.T) { + jp, err := New("ends_with('Amigo', 'go')") + assert.NilError(t, err) + + result, err := jp.Search("") + assert.NilError(t, err) + + split, ok := result.(bool) + assert.Assert(t, ok) + assert.Equal(t, split, true) +} + +func Test_regexMatch(t *testing.T) { + data := make(map[string]interface{}) + data["foo"] = "hgf'b1a2r'b12g" + + query, err := New("regex_match('12.*', foo)") + assert.NilError(t, err) + + result, err := query.Search(data) + assert.NilError(t, err) + assert.Equal(t, true, result) +} + +func Test_regexMatchWithNumber(t *testing.T) { + data := make(map[string]interface{}) + data["foo"] = -12.0 + + query, err := New("regex_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(` + { + "metadata": { + "name": "temp", + "namespace": "ns_first" + }, + "spec": { + "namespace": "ns_first", + "name": "temp_other", + "field" : "Hello world, helworldlo" + } + } + `) + expected := "Glo world, Gworldlo" + + var resource interface{} + err := json.Unmarshal(resourceRaw, &resource) + assert.NilError(t, err) + query, err := New(`regex_replace_all('([Hh]e|G)l', spec.field, '${2}G')`) + assert.NilError(t, err) + + res, err := query.Search(resource) + assert.NilError(t, err) + + result, ok := res.(string) + assert.Assert(t, ok) + assert.Equal(t, string(result), expected) +} + +func Test_regexReplaceAllLiteral(t *testing.T) { + resourceRaw := []byte(` + { + "metadata": { + "name": "temp", + "namespace": "ns_first" + }, + "spec": { + "namespace": "ns_first", + "name": "temp_other", + "field" : "Hello world, helworldlo" + } + } + `) + expected := "Glo world, Gworldlo" + + var resource interface{} + err := json.Unmarshal(resourceRaw, &resource) + assert.NilError(t, err) + + query, err := New(`regex_replace_all_literal('[Hh]el?', spec.field, 'G')`) + assert.NilError(t, err) + + res, err := query.Search(resource) + assert.NilError(t, err) + + result, ok := res.(string) + assert.Assert(t, ok) + + assert.Equal(t, string(result), expected) +} diff --git a/pkg/engine/jmespath/new.go b/pkg/engine/jmespath/new.go new file mode 100644 index 000000000000..5714de69db52 --- /dev/null +++ b/pkg/engine/jmespath/new.go @@ -0,0 +1,18 @@ +package jmespath + +import ( + gojmespath "github.com/jmespath/go-jmespath" +) + +func New(query string) (*gojmespath.JMESPath, error) { + jp, err := gojmespath.Compile(query) + if err != nil { + return nil, err + } + + for _, function := range getFunctions() { + jp.Register(function) + } + + return jp, nil +} diff --git a/pkg/engine/jsonContext.go b/pkg/engine/jsonContext.go index 207086f14aee..7c5c8b08a155 100644 --- a/pkg/engine/jsonContext.go +++ b/pkg/engine/jsonContext.go @@ -8,9 +8,9 @@ import ( "strings" "github.com/go-logr/logr" - "github.com/jmespath/go-jmespath" kyverno "github.com/kyverno/kyverno/pkg/api/kyverno/v1" "github.com/kyverno/kyverno/pkg/engine/context" + jmespath "github.com/kyverno/kyverno/pkg/engine/jmespath" "github.com/kyverno/kyverno/pkg/engine/variables" "github.com/kyverno/kyverno/pkg/resourcecache" "k8s.io/client-go/dynamic/dynamiclister" @@ -88,7 +88,7 @@ func loadAPIData(logger logr.Logger, entry kyverno.ContextEntry, ctx *PolicyCont } func applyJMESPath(jmesPath string, jsonData []byte) (interface{}, error) { - jp, err := jmespath.Compile(jmesPath) + jp, err := jmespath.New(jmesPath) if err != nil { return nil, fmt.Errorf("failed to compile JMESPath: %s, error: %v", jmesPath, err) } diff --git a/pkg/engine/variables/vars.go b/pkg/engine/variables/vars.go index 11943e740452..2711ce9849a2 100644 --- a/pkg/engine/variables/vars.go +++ b/pkg/engine/variables/vars.go @@ -185,6 +185,11 @@ func substituteVariablesIfAny(log logr.Logger, ctx context.EvalInterface) jsonUt for _, v := range vars { variable := replaceBracesAndTrimSpaces(v) + if variable == "@" { + currentPath := getJMESPath(data.Path) + variable = strings.Replace(variable, "@", fmt.Sprintf("request.object%s", currentPath), -1) + } + operation, err := ctx.Query("request.operation") if err == nil && operation == "DELETE" { variable = strings.ReplaceAll(variable, "request.object", "request.oldObject") @@ -228,6 +233,13 @@ func substituteVariablesIfAny(log logr.Logger, ctx context.EvalInterface) jsonUt }) } +// getJMESPath converts path to JMES format +func getJMESPath(rawPath string) string { + path := strings.ReplaceAll(rawPath, "/", ".") + regex := regexp.MustCompile(`\.([\d])\.`) + return string(regex.ReplaceAll([]byte(path), []byte("[$1]."))) +} + func substituteVarInPattern(pattern, variable string, value interface{}) (string, error) { var stringToSubstitute string diff --git a/pkg/engine/variables/vars_test.go b/pkg/engine/variables/vars_test.go index 6b17f98bef80..759cdb788798 100644 --- a/pkg/engine/variables/vars_test.go +++ b/pkg/engine/variables/vars_test.go @@ -135,6 +135,149 @@ func Test_subVars_failed(t *testing.T) { } } +func Test_subVars_with_JMESPath_At(t *testing.T) { + patternMap := []byte(` + { + "kind": "{{@}}", + "data": { + "rules": [ + { + "apiGroups": [ + "{{request.object.metadata.name}}" + ], + "resources": [ + "namespaces" + ], + "verbs": [ + "*" + ], + "resourceNames": [ + "{{request.object.metadata.name}}" + ] + } + ] + } + } + `) + + resourceRaw := []byte(` + { + "kind": "foo", + "name": "bar", + "metadata": { + "name": "temp", + "namespace": "n1" + }, + "spec": { + "namespace": "n1", + "name": "temp1" + } + } + `) + + expected := []byte(`{"data":{"rules":[{"apiGroups":["temp"],"resourceNames":["temp"],"resources":["namespaces"],"verbs":["*"]}]},"kind":"foo"}`) + + var pattern, resource interface{} + var err error + err = json.Unmarshal(patternMap, &pattern) + assert.NilError(t, err) + err = json.Unmarshal(resourceRaw, &resource) + assert.NilError(t, err) + // context + ctx := context.NewContext() + err = ctx.AddResource(resourceRaw) + assert.NilError(t, err) + + output, err := SubstituteAll(log.Log, ctx, pattern) + assert.NilError(t, err) + out, err := json.Marshal(output) + assert.NilError(t, err) + assert.Equal(t, string(out), string(expected)) +} + +func Test_subVars_withRegexMatch(t *testing.T) { + patternMap := []byte(` + { + "port": "{{ regex_match('(443)', '{{@}}') }}", + "name": "ns-owner-{{request.object.metadata.name}}" + } + `) + + resourceRaw := []byte(` + { + "port": "443", + "metadata": { + "name": "temp", + "namespace": "n1" + }, + "spec": { + "namespace": "n1", + "name": "temp1" + } + } + `) + expected := []byte(`{"name":"ns-owner-temp","port":true}`) + + var pattern, resource interface{} + var err error + err = json.Unmarshal(patternMap, &pattern) + assert.NilError(t, err) + err = json.Unmarshal(resourceRaw, &resource) + assert.NilError(t, err) + // context + ctx := context.NewContext() + err = ctx.AddResource(resourceRaw) + assert.NilError(t, err) + + output, err := SubstituteAll(log.Log, ctx, pattern) + assert.NilError(t, err) + out, err := json.Marshal(output) + assert.NilError(t, err) + fmt.Print(string(out)) + assert.Equal(t, string(out), string(expected)) +} + +func Test_subVars_withRegexReplaceAll(t *testing.T) { + patternMap := []byte(` + { + "port": "{{ regex_replace_all_literal('.*', '{{@}}', '1313') }}", + "name": "ns-owner-{{request.object.metadata.name}}" + } + `) + + resourceRaw := []byte(` + { + "port": "43123", + "metadata": { + "name": "temp", + "namespace": "n1" + }, + "spec": { + "namespace": "n1", + "name": "temp1" + } + } + `) + expected := []byte(`{"name":"ns-owner-temp","port":"1313"}`) + + var pattern, resource interface{} + var err error + err = json.Unmarshal(patternMap, &pattern) + assert.NilError(t, err) + err = json.Unmarshal(resourceRaw, &resource) + assert.NilError(t, err) + // context + ctx := context.NewContext() + err = ctx.AddResource(resourceRaw) + assert.NilError(t, err) + + output, err := SubstituteAll(log.Log, ctx, pattern) + assert.NilError(t, err) + out, err := json.Marshal(output) + assert.NilError(t, err) + assert.Equal(t, string(out), string(expected)) +} + func Test_ReplacingPathWhenDeleting(t *testing.T) { patternRaw := []byte(`"{{request.object.metadata.annotations.target}}"`)