mirror of
https://github.com/kyverno/kyverno.git
synced 2025-03-05 07:26:55 +00:00
JMESPath custom functions (#1772)
* JMESPath: Support regex expressions Signed-off-by: Max Goncharenko <kacejot@fex.net> * JMESPath: Add string functions Signed-off-by: Max Goncharenko <kacejot@fex.net> * Removed {{$}} variable handling logic Signed-off-by: Max Goncharenko <kacejot@fex.net> * Name all functions in snake case; Update error message; Fix {{@}} behavior Signed-off-by: Max Goncharenko <kacejot@fex.net>
This commit is contained in:
parent
4a4fdc54ee
commit
6a0305674a
9 changed files with 808 additions and 9 deletions
2
go.mod
2
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
|
||||
)
|
||||
|
|
8
go.sum
8
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=
|
||||
|
|
|
@ -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)
|
||||
|
|
381
pkg/engine/jmespath/functions.go
Normal file
381
pkg/engine/jmespath/functions.go
Normal file
|
@ -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
|
||||
}
|
245
pkg/engine/jmespath/functions_test.go
Normal file
245
pkg/engine/jmespath/functions_test.go
Normal file
|
@ -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)
|
||||
}
|
18
pkg/engine/jmespath/new.go
Normal file
18
pkg/engine/jmespath/new.go
Normal file
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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}}"`)
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue