diff --git a/cmd/cli/kubectl-kyverno/jp/function/function.go b/cmd/cli/kubectl-kyverno/jp/function/function.go index 92b22e9848..da5743cba0 100644 --- a/cmd/cli/kubectl-kyverno/jp/function/function.go +++ b/cmd/cli/kubectl-kyverno/jp/function/function.go @@ -4,6 +4,7 @@ import ( "fmt" "strings" + "github.com/kyverno/kyverno/pkg/config" "github.com/kyverno/kyverno/pkg/engine/jmespath" "github.com/spf13/cobra" "golang.org/x/exp/slices" @@ -34,7 +35,7 @@ func Command() *cobra.Command { } func printFunctions(names ...string) { - functions := jmespath.GetFunctions() + functions := jmespath.GetFunctions(config.NewDefaultConfiguration(false)) slices.SortFunc(functions, func(a, b jmespath.FunctionEntry) bool { return a.String() < b.String() }) diff --git a/pkg/engine/jmespath/arithmetic_test.go b/pkg/engine/jmespath/arithmetic_test.go index 7bdabf9f50..4b4c56fdd5 100644 --- a/pkg/engine/jmespath/arithmetic_test.go +++ b/pkg/engine/jmespath/arithmetic_test.go @@ -82,7 +82,7 @@ func Test_Add(t *testing.T) { } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - jp, err := newJMESPath(tc.test) + jp, err := newJMESPath(cfg, tc.test) assert.NilError(t, err) result, err := jp.Search("") @@ -228,7 +228,7 @@ func Test_Sum(t *testing.T) { } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - jp, err := newJMESPath(tc.test) + jp, err := newJMESPath(cfg, tc.test) assert.NilError(t, err) result, err := jp.Search("") @@ -327,7 +327,7 @@ func Test_Subtract(t *testing.T) { } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - jp, err := newJMESPath(tc.test) + jp, err := newJMESPath(cfg, tc.test) assert.NilError(t, err) result, err := jp.Search("") @@ -426,7 +426,7 @@ func Test_Multiply(t *testing.T) { } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - jp, err := newJMESPath(tc.test) + jp, err := newJMESPath(cfg, tc.test) assert.NilError(t, err) result, err := jp.Search("") @@ -588,7 +588,7 @@ func Test_Divide(t *testing.T) { } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - jp, err := newJMESPath(tc.test) + jp, err := newJMESPath(cfg, tc.test) assert.NilError(t, err) result, err := jp.Search("") @@ -744,7 +744,7 @@ func Test_Modulo(t *testing.T) { } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - jp, err := newJMESPath(tc.test) + jp, err := newJMESPath(cfg, tc.test) assert.NilError(t, err) result, err := jp.Search("") diff --git a/pkg/engine/jmespath/functions.go b/pkg/engine/jmespath/functions.go index 6210e78294..35d1dd3519 100644 --- a/pkg/engine/jmespath/functions.go +++ b/pkg/engine/jmespath/functions.go @@ -20,6 +20,8 @@ import ( trunc "github.com/aquilax/truncate" "github.com/blang/semver/v4" gojmespath "github.com/jmespath/go-jmespath" + "github.com/kyverno/kyverno/pkg/config" + imageutils "github.com/kyverno/kyverno/pkg/utils/image" wildcard "github.com/kyverno/kyverno/pkg/utils/wildcard" regen "github.com/zach-klippenstein/goregen" "golang.org/x/crypto/cryptobyte" @@ -66,9 +68,10 @@ var ( objectFromLists = "object_from_lists" random = "random" x509_decode = "x509_decode" + imageNormalize = "image_normalize" ) -func GetFunctions() []FunctionEntry { +func GetFunctions(configuration config.Configuration) []FunctionEntry { return []FunctionEntry{{ FunctionEntry: gojmespath.FunctionEntry{ Name: compare, @@ -541,6 +544,16 @@ func GetFunctions() []FunctionEntry { }, ReturnType: []jpType{jpString}, Note: "returns the result of rounding time down to a multiple of duration", + }, { + FunctionEntry: gojmespath.FunctionEntry{ + Name: imageNormalize, + Arguments: []argSpec{ + {Types: []jpType{jpString}}, + }, + Handler: jpImageNormalize(configuration), + }, + ReturnType: []jpType{jpString}, + Note: "normalizes an image reference", }} } @@ -1099,3 +1112,15 @@ func jpX509Decode(arguments []interface{}) (interface{}, error) { return res, nil } + +func jpImageNormalize(configuration config.Configuration) gojmespath.JpFunction { + return func(arguments []interface{}) (interface{}, error) { + if image, err := validateArg(imageNormalize, arguments, 0, reflect.String); err != nil { + return nil, err + } else if infos, err := imageutils.GetImageInfo(image.String(), configuration); err != nil { + return nil, formatError(genericError, imageNormalize, err) + } else { + return infos.String(), nil + } + } +} diff --git a/pkg/engine/jmespath/functions_test.go b/pkg/engine/jmespath/functions_test.go index 1ef75f2cd2..bd4facff71 100644 --- a/pkg/engine/jmespath/functions_test.go +++ b/pkg/engine/jmespath/functions_test.go @@ -7,9 +7,12 @@ import ( "runtime" "testing" + "github.com/kyverno/kyverno/pkg/config" "gotest.tools/assert" ) +var cfg = config.NewDefaultConfiguration(false) + func Test_Compare(t *testing.T) { testCases := []struct { jmesPath string @@ -30,7 +33,7 @@ func Test_Compare(t *testing.T) { } for _, tc := range testCases { t.Run(tc.jmesPath, func(t *testing.T) { - jp, err := newJMESPath(tc.jmesPath) + jp, err := newJMESPath(cfg, tc.jmesPath) assert.NilError(t, err) result, err := jp.Search("") @@ -57,7 +60,7 @@ func Test_ParseJsonSerde(t *testing.T) { } for _, tc := range testCases { t.Run(tc, func(t *testing.T) { - jp, err := newJMESPath(fmt.Sprintf(`to_string(parse_json('%s'))`, tc)) + jp, err := newJMESPath(cfg, fmt.Sprintf(`to_string(parse_json('%s'))`, tc)) assert.NilError(t, err) result, err := jp.Search("") @@ -88,7 +91,7 @@ func Test_ParseJsonComplex(t *testing.T) { } for _, tc := range testCases { t.Run(tc.input, func(t *testing.T) { - jp, err := newJMESPath(tc.input) + jp, err := newJMESPath(cfg, tc.input) assert.NilError(t, err) result, err := jp.Search("") @@ -165,7 +168,7 @@ bar: null } for _, tc := range testCases { t.Run(tc.input, func(t *testing.T) { - jp, err := newJMESPath(fmt.Sprintf(`parse_yaml('%s')`, tc.input)) + jp, err := newJMESPath(cfg, fmt.Sprintf(`parse_yaml('%s')`, tc.input)) assert.NilError(t, err) result, err := jp.Search("") assert.NilError(t, err) @@ -194,7 +197,7 @@ func Test_EqualFold(t *testing.T) { } for _, tc := range testCases { t.Run(tc.jmesPath, func(t *testing.T) { - jp, err := newJMESPath(tc.jmesPath) + jp, err := newJMESPath(cfg, tc.jmesPath) assert.NilError(t, err) result, err := jp.Search("") @@ -231,7 +234,7 @@ func Test_Replace(t *testing.T) { } for _, tc := range testCases { t.Run(tc.jmesPath, func(t *testing.T) { - jp, err := newJMESPath(tc.jmesPath) + jp, err := newJMESPath(cfg, tc.jmesPath) assert.NilError(t, err) result, err := jp.Search("") @@ -245,7 +248,7 @@ func Test_Replace(t *testing.T) { } func Test_ReplaceAll(t *testing.T) { - jp, err := newJMESPath("replace_all('Lorem ipsum dolor sit amet', 'ipsum', 'muspi')") + jp, err := newJMESPath(cfg, "replace_all('Lorem ipsum dolor sit amet', 'ipsum', 'muspi')") assert.NilError(t, err) result, err := jp.Search("") @@ -276,7 +279,7 @@ func Test_ToUpper(t *testing.T) { } for _, tc := range testCases { t.Run(tc.jmesPath, func(t *testing.T) { - jp, err := newJMESPath(tc.jmesPath) + jp, err := newJMESPath(cfg, tc.jmesPath) assert.NilError(t, err) result, err := jp.Search("") @@ -309,7 +312,7 @@ func Test_ToLower(t *testing.T) { } for _, tc := range testCases { t.Run(tc.jmesPath, func(t *testing.T) { - jp, err := newJMESPath(tc.jmesPath) + jp, err := newJMESPath(cfg, tc.jmesPath) assert.NilError(t, err) result, err := jp.Search("") @@ -323,7 +326,7 @@ func Test_ToLower(t *testing.T) { } func Test_Trim(t *testing.T) { - jp, err := newJMESPath("trim('¡¡¡Hello, Gophers!!!', '!¡')") + jp, err := newJMESPath(cfg, "trim('¡¡¡Hello, Gophers!!!', '!¡')") assert.NilError(t, err) result, err := jp.Search("") @@ -394,7 +397,7 @@ func Test_TrimPrefix(t *testing.T) { } func Test_Split(t *testing.T) { - jp, err := newJMESPath("split('Hello, Gophers', ', ')") + jp, err := newJMESPath(cfg, "split('Hello, Gophers', ', ')") assert.NilError(t, err) result, err := jp.Search("") @@ -407,7 +410,7 @@ func Test_Split(t *testing.T) { } func Test_HasPrefix(t *testing.T) { - jp, err := newJMESPath("starts_with('Gophers', 'Go')") + jp, err := newJMESPath(cfg, "starts_with('Gophers', 'Go')") assert.NilError(t, err) result, err := jp.Search("") @@ -419,7 +422,7 @@ func Test_HasPrefix(t *testing.T) { } func Test_HasSuffix(t *testing.T) { - jp, err := newJMESPath("ends_with('Amigo', 'go')") + jp, err := newJMESPath(cfg, "ends_with('Amigo', 'go')") assert.NilError(t, err) result, err := jp.Search("") @@ -434,7 +437,7 @@ func Test_RegexMatch(t *testing.T) { data := make(map[string]interface{}) data["foo"] = "hgf'b1a2r'b12g" - query, err := newJMESPath("regex_match('12.*', foo)") + query, err := newJMESPath(cfg, "regex_match('12.*', foo)") assert.NilError(t, err) result, err := query.Search(data) @@ -446,7 +449,7 @@ func Test_RegexMatchWithNumber(t *testing.T) { data := make(map[string]interface{}) data["foo"] = -12.0 - query, err := newJMESPath("regex_match('12.*', abs(foo))") + query, err := newJMESPath(cfg, "regex_match('12.*', abs(foo))") assert.NilError(t, err) result, err := query.Search(data) @@ -458,7 +461,7 @@ func Test_PatternMatch(t *testing.T) { data := make(map[string]interface{}) data["foo"] = "prefix-foo" - query, err := newJMESPath("pattern_match('prefix-*', foo)") + query, err := newJMESPath(cfg, "pattern_match('prefix-*', foo)") assert.NilError(t, err) result, err := query.Search(data) @@ -470,7 +473,7 @@ func Test_PatternMatchWithNumber(t *testing.T) { data := make(map[string]interface{}) data["foo"] = -12.0 - query, err := newJMESPath("pattern_match('12*', abs(foo))") + query, err := newJMESPath(cfg, "pattern_match('12*', abs(foo))") assert.NilError(t, err) result, err := query.Search(data) @@ -497,7 +500,7 @@ func Test_RegexReplaceAll(t *testing.T) { var resource interface{} err := json.Unmarshal(resourceRaw, &resource) assert.NilError(t, err) - query, err := newJMESPath(`regex_replace_all('([Hh]e|G)l', spec.field, '${2}G')`) + query, err := newJMESPath(cfg, `regex_replace_all('([Hh]e|G)l', spec.field, '${2}G')`) assert.NilError(t, err) res, err := query.Search(resource) @@ -528,7 +531,7 @@ func Test_RegexReplaceAllLiteral(t *testing.T) { err := json.Unmarshal(resourceRaw, &resource) assert.NilError(t, err) - query, err := newJMESPath(`regex_replace_all_literal('[Hh]el?', spec.field, 'G')`) + query, err := newJMESPath(cfg, `regex_replace_all_literal('[Hh]el?', spec.field, 'G')`) assert.NilError(t, err) res, err := query.Search(resource) @@ -583,7 +586,7 @@ func Test_LabelMatch(t *testing.T) { err := json.Unmarshal(tc.resource, &resource) assert.NilError(t, err) - query, err := newJMESPath("label_match(`" + tc.test + "`, metadata.labels)") + query, err := newJMESPath(cfg, "label_match(`"+tc.test+"`, metadata.labels)") assert.NilError(t, err) res, err := query.Search(resource) @@ -627,7 +630,7 @@ func Test_JpToBoolean(t *testing.T) { } func Test_Base64Decode(t *testing.T) { - jp, err := newJMESPath("base64_decode('SGVsbG8sIHdvcmxkIQ==')") + jp, err := newJMESPath(cfg, "base64_decode('SGVsbG8sIHdvcmxkIQ==')") assert.NilError(t, err) result, err := jp.Search("") @@ -639,7 +642,7 @@ func Test_Base64Decode(t *testing.T) { } func Test_Base64Encode(t *testing.T) { - jp, err := newJMESPath("base64_encode('Hello, world!')") + jp, err := newJMESPath(cfg, "base64_encode('Hello, world!')") assert.NilError(t, err) result, err := jp.Search("") @@ -669,7 +672,7 @@ func Test_Base64Decode_Secret(t *testing.T) { err := json.Unmarshal(resourceRaw, &resource) assert.NilError(t, err) - query, err := newJMESPath(`base64_decode(data.example1)`) + query, err := newJMESPath(cfg, `base64_decode(data.example1)`) assert.NilError(t, err) res, err := query.Search(resource) @@ -744,7 +747,7 @@ func Test_PathCanonicalize(t *testing.T) { } for _, tc := range testCases { t.Run(tc.jmesPath, func(t *testing.T) { - jp, err := newJMESPath(tc.jmesPath) + jp, err := newJMESPath(cfg, tc.jmesPath) assert.NilError(t, err) result, err := jp.Search("") @@ -793,7 +796,7 @@ func Test_Truncate(t *testing.T) { } for _, tc := range testCases { t.Run(tc.jmesPath, func(t *testing.T) { - jp, err := newJMESPath(tc.jmesPath) + jp, err := newJMESPath(cfg, tc.jmesPath) assert.NilError(t, err) result, err := jp.Search("") @@ -830,7 +833,7 @@ func Test_SemverCompare(t *testing.T) { } for _, tc := range testCases { t.Run(tc.jmesPath, func(t *testing.T) { - jp, err := newJMESPath(tc.jmesPath) + jp, err := newJMESPath(cfg, tc.jmesPath) assert.NilError(t, err) result, err := jp.Search("") @@ -877,7 +880,7 @@ func Test_Items(t *testing.T) { } for i, tc := range testCases { t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) { - query, err := newJMESPath("items(`" + tc.object + "`,`" + tc.keyName + "`,`" + tc.valName + "`)") + query, err := newJMESPath(cfg, "items(`"+tc.object+"`,`"+tc.keyName+"`,`"+tc.valName+"`)") assert.NilError(t, err) res, err := query.Search("") @@ -928,7 +931,7 @@ func Test_ObjectFromLists(t *testing.T) { } for i, tc := range testCases { t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) { - query, err := newJMESPath("object_from_lists(`" + tc.keys + "`,`" + tc.values + "`)") + query, err := newJMESPath(cfg, "object_from_lists(`"+tc.keys+"`,`"+tc.values+"`)") assert.NilError(t, err) res, err := query.Search("") assert.NilError(t, err) @@ -1022,7 +1025,7 @@ UFOZZVoELaasWS559wy8og39Eq21dDMynb8Bndn/ }} for _, tc := range testCases { t.Run(tc.jmesPath, func(t *testing.T) { - jp, err := newJMESPath(tc.jmesPath) + jp, err := newJMESPath(cfg, tc.jmesPath) assert.NilError(t, err) result, err := jp.Search("") @@ -1396,3 +1399,59 @@ func Test_jpfToLower(t *testing.T) { }) } } + +func Test_ImageNormalize(t *testing.T) { + testCases := []struct { + jmesPath string + expectedResult string + wantErr bool + }{ + { + jmesPath: "image_normalize('nginx')", + expectedResult: "docker.io/nginx:latest", + }, + { + jmesPath: "image_normalize('docker.io/nginx')", + expectedResult: "docker.io/nginx:latest", + }, + { + jmesPath: "image_normalize('docker.io/library/nginx')", + expectedResult: "docker.io/library/nginx:latest", + }, + { + jmesPath: "image_normalize('ghcr.io/library/nginx')", + expectedResult: "ghcr.io/library/nginx:latest", + }, + { + jmesPath: "image_normalize('ghcr.io/nginx')", + expectedResult: "ghcr.io/nginx:latest", + }, + { + jmesPath: "image_normalize('ghcr.io/nginx:latest')", + expectedResult: "ghcr.io/nginx:latest", + }, + { + jmesPath: "image_normalize('ghcr.io/nginx:1.2')", + expectedResult: "ghcr.io/nginx:1.2", + }, + { + jmesPath: "image_normalize('')", + wantErr: true, + }, + } + for _, tc := range testCases { + t.Run(tc.jmesPath, func(t *testing.T) { + jp, err := newJMESPath(cfg, tc.jmesPath) + assert.NilError(t, err) + result, err := jp.Search("") + if tc.wantErr { + assert.Error(t, err, "JMESPath function 'image_normalize': bad image: docker.io/: invalid reference format") + } else { + assert.NilError(t, err) + res, ok := result.(string) + assert.Assert(t, ok) + assert.Equal(t, res, tc.expectedResult) + } + }) + } +} diff --git a/pkg/engine/jmespath/interface.go b/pkg/engine/jmespath/interface.go index bcd41f11a9..ef9e9395ca 100644 --- a/pkg/engine/jmespath/interface.go +++ b/pkg/engine/jmespath/interface.go @@ -22,7 +22,7 @@ func New(configuration config.Configuration) Interface { } func (i implementation) Query(query string) (Query, error) { - return newJMESPath(query) + return newJMESPath(i.configuration, query) } func (i implementation) Search(query string, data interface{}) (interface{}, error) { diff --git a/pkg/engine/jmespath/new.go b/pkg/engine/jmespath/new.go index edcc07a900..cefec5896f 100644 --- a/pkg/engine/jmespath/new.go +++ b/pkg/engine/jmespath/new.go @@ -2,14 +2,15 @@ package jmespath import ( gojmespath "github.com/jmespath/go-jmespath" + "github.com/kyverno/kyverno/pkg/config" ) -func newJMESPath(query string) (*gojmespath.JMESPath, error) { +func newJMESPath(configuration config.Configuration, query string) (*gojmespath.JMESPath, error) { jp, err := gojmespath.Compile(query) if err != nil { return nil, err } - for _, function := range GetFunctions() { + for _, function := range GetFunctions(configuration) { jp.Register(function.FunctionEntry) } return jp, nil diff --git a/pkg/engine/jmespath/new_test.go b/pkg/engine/jmespath/new_test.go index 76ba21e606..f6f44708da 100644 --- a/pkg/engine/jmespath/new_test.go +++ b/pkg/engine/jmespath/new_test.go @@ -25,7 +25,7 @@ func TestNew(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - _, err := newJMESPath(tt.args.query) + _, err := newJMESPath(cfg, tt.args.query) if (err != nil) != tt.wantErr { t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/pkg/engine/jmespath/time_test.go b/pkg/engine/jmespath/time_test.go index 7debd0ceb4..13a8908029 100644 --- a/pkg/engine/jmespath/time_test.go +++ b/pkg/engine/jmespath/time_test.go @@ -25,7 +25,7 @@ func Test_TimeSince(t *testing.T) { } for i, tc := range testCases { t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) { - query, err := newJMESPath(tc.test) + query, err := newJMESPath(cfg, tc.test) assert.NilError(t, err) res, err := query.Search("") @@ -55,7 +55,7 @@ func Test_TimeToCron(t *testing.T) { } for i, tc := range testCases { t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) { - query, err := newJMESPath(tc.test) + query, err := newJMESPath(cfg, tc.test) assert.NilError(t, err) res, err := query.Search("") @@ -85,7 +85,7 @@ func Test_TimeAdd(t *testing.T) { } for i, tc := range testCases { t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) { - query, err := newJMESPath(tc.test) + query, err := newJMESPath(cfg, tc.test) assert.NilError(t, err) res, err := query.Search("") @@ -115,7 +115,7 @@ func Test_TimeParse(t *testing.T) { } for i, tc := range testCases { t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) { - query, err := newJMESPath(tc.test) + query, err := newJMESPath(cfg, tc.test) assert.NilError(t, err) res, err := query.Search("") @@ -145,7 +145,7 @@ func Test_TimeUtc(t *testing.T) { } for i, tc := range testCases { t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) { - query, err := newJMESPath(tc.test) + query, err := newJMESPath(cfg, tc.test) assert.NilError(t, err) res, err := query.Search("") @@ -171,7 +171,7 @@ func Test_TimeDiff(t *testing.T) { } for i, tc := range testCases { t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) { - query, err := newJMESPath(tc.test) + query, err := newJMESPath(cfg, tc.test) assert.NilError(t, err) res, err := query.Search("")