diff --git a/go.mod b/go.mod index 1c2f2d3d8430..d78e242734c7 100644 --- a/go.mod +++ b/go.mod @@ -28,7 +28,7 @@ require ( github.com/jmoiron/jsonq v0.0.0-20150511023944-e874b168d07e github.com/julienschmidt/httprouter v1.3.0 github.com/kataras/tablewriter v0.0.0-20180708051242-e063d29b7c23 - github.com/kyverno/go-jmespath v0.4.1-0.20230705123211-d067dc3d6613 + github.com/kyverno/go-jmespath v0.4.1-0.20230906134905-62fa64b71f91 github.com/lensesio/tableprinter v0.0.0-20201125135848-89e81fc956e7 github.com/notaryproject/notation-core-go v1.0.0 github.com/notaryproject/notation-go v1.0.0 diff --git a/go.sum b/go.sum index c5f1fde5d4a4..76e3ccf50923 100644 --- a/go.sum +++ b/go.sum @@ -1022,8 +1022,8 @@ github.com/kunwardeep/paralleltest v1.0.2/go.mod h1:ZPqNm1fVHPllh5LPVujzbVz1JN2G github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/kyoh86/exportloopref v0.1.8/go.mod h1:1tUcJeiioIs7VWe5gcOObrux3lb66+sBqGZrRkMwPgg= -github.com/kyverno/go-jmespath v0.4.1-0.20230705123211-d067dc3d6613 h1:M0uOLuCAZydi/vZy7uvNhwaIge0HFMdfqQYOKw7kgnQ= -github.com/kyverno/go-jmespath v0.4.1-0.20230705123211-d067dc3d6613/go.mod h1:yzDHaKovQy16rjN4kFnjF+IdNoN4p1ndw+va6+B8zUU= +github.com/kyverno/go-jmespath v0.4.1-0.20230906134905-62fa64b71f91 h1:n63aowZk61f65e6OJxuql8BS/hCv8LZxz/eCO4+2NfM= +github.com/kyverno/go-jmespath v0.4.1-0.20230906134905-62fa64b71f91/go.mod h1:yzDHaKovQy16rjN4kFnjF+IdNoN4p1ndw+va6+B8zUU= github.com/kyverno/go-jmespath/internal/testify v1.5.2-0.20230630133209-945021c749d9 h1:lL311dF3a2aeNibJj8v+uhFU3XkvRHZmCtAdSPOrQYY= github.com/kyverno/go-jmespath/internal/testify v1.5.2-0.20230630133209-945021c749d9/go.mod h1:XRxUGHIiCy1WYma1CdfdO1WOhIe8dLPTENaZr5D1ex4= github.com/ldez/gomoddirectives v0.2.1/go.mod h1:sGicqkRgBOg//JfpXwkB9Hj0X5RyJ7mlACM5B9f6Me4= diff --git a/pkg/engine/context/context.go b/pkg/engine/context/context.go index 53230059a7f2..6a81e5354c0f 100644 --- a/pkg/engine/context/context.go +++ b/pkg/engine/context/context.go @@ -2,23 +2,28 @@ package context import ( "encoding/csv" - "encoding/json" "fmt" + "regexp" "strings" - "sync" - jsonpatch "github.com/evanphx/json-patch/v5" + jsoniter "github.com/json-iterator/go" kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1" kyvernov1beta1 "github.com/kyverno/kyverno/api/kyverno/v1beta1" "github.com/kyverno/kyverno/pkg/config" "github.com/kyverno/kyverno/pkg/engine/jmespath" + "github.com/kyverno/kyverno/pkg/engine/jsonutils" "github.com/kyverno/kyverno/pkg/logging" apiutils "github.com/kyverno/kyverno/pkg/utils/api" admissionv1 "k8s.io/api/admission/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" ) -var logger = logging.WithName("context") +var ( + logger = logging.WithName("context") + json = jsoniter.ConfigCompatibleWithStandardLibrary + ReservedKeys = regexp.MustCompile(`request|serviceAccountName|serviceAccountNamespace|element|elementIndex|@|images|image|([a-z_0-9]+\()[^{}]`) +) // EvalInterface is used to query and inspect context data // TODO: move to contextapi to prevent circular dependencies @@ -26,6 +31,9 @@ type EvalInterface interface { // Query accepts a JMESPath expression and returns matching data Query(query string) (interface{}, error) + // Operation returns the admission operation i.e. "request.operation" + QueryOperation() string + // HasChanged accepts a JMESPath expression and compares matching data in the // request.object and request.oldObject context fields. If the data has changed // it return `true`. If the data has not changed it returns false. If either @@ -100,50 +108,69 @@ type Interface interface { EvalInterface - // AddJSON merges the json with context - addJSON(dataRaw []byte) error + // AddJSON merges the json map with context + addJSON(dataMap map[string]interface{}) error } // Context stores the data resources as JSON type context struct { jp jmespath.Interface - mutex sync.RWMutex - jsonRaw []byte - jsonRawCheckpoints [][]byte + jsonRaw map[string]interface{} + jsonRawCheckpoints []map[string]interface{} images map[string]map[string]apiutils.ImageInfo + operation kyvernov1.AdmissionOperation deferred DeferredLoaders } // NewContext returns a new context func NewContext(jp jmespath.Interface) Interface { - return NewContextFromRaw(jp, []byte(`{}`)) + return NewContextFromRaw(jp, map[string]interface{}{}) } // NewContextFromRaw returns a new context initialized with raw data -func NewContextFromRaw(jp jmespath.Interface, raw []byte) Interface { +func NewContextFromRaw(jp jmespath.Interface, raw map[string]interface{}) Interface { return &context{ jp: jp, jsonRaw: raw, - jsonRawCheckpoints: make([][]byte, 0), + jsonRawCheckpoints: make([]map[string]interface{}, 0), deferred: NewDeferredLoaders(), } } // addJSON merges json data -func (ctx *context) addJSON(dataRaw []byte) error { - ctx.mutex.Lock() - defer ctx.mutex.Unlock() - json, err := jsonpatch.MergeMergePatches(ctx.jsonRaw, dataRaw) - if err != nil { - return fmt.Errorf("failed to merge JSON data: %w", err) - } - ctx.jsonRaw = json +func (ctx *context) addJSON(dataMap map[string]interface{}) error { + mergeMaps(dataMap, ctx.jsonRaw) return nil } +func (ctx *context) QueryOperation() string { + if ctx.operation != "" { + return string(ctx.operation) + } + + if requestMap, val := ctx.jsonRaw["request"].(map[string]interface{}); val { + if op, val := requestMap["operation"].(string); val { + return op + } + } + + return "" +} + // AddRequest adds an admission request to context func (ctx *context) AddRequest(request admissionv1.AdmissionRequest) error { - return addToContext(ctx, request, "request") + // an AdmissionRequest needs to be marshaled / unmarshaled as + // JSON to properly convert types of runtime.RawExtension + mapObj, err := jsonutils.DocumentToUntyped(request) + if err != nil { + return err + } + if err := addToContext(ctx, mapObj, "request"); err != nil { + return err + } + + ctx.operation = kyvernov1.AdmissionOperation(request.Operation) + return nil } func (ctx *context) AddVariable(key string, value interface{}) error { @@ -200,12 +227,21 @@ func (ctx *context) SetTargetResource(data map[string]interface{}) error { // AddOperation data at path: request.operation func (ctx *context) AddOperation(data string) error { - return addToContext(ctx, data, "request", "operation") + if err := addToContext(ctx, data, "request", "operation"); err != nil { + return err + } + + ctx.operation = kyvernov1.AdmissionOperation(data) + return nil } // AddUserInfo adds userInfo at path request.userInfo func (ctx *context) AddUserInfo(userRequestInfo kyvernov1beta1.RequestInfo) error { - return addToContext(ctx, userRequestInfo, "request") + if data, err := toUnstructured(&userRequestInfo); err == nil { + return addToContext(ctx, data, "request") + } else { + return err + } } // AddServiceAccount removes prefix 'system:serviceaccount:' and namespace, then loads only SA name and SA namespace @@ -225,33 +261,14 @@ func (ctx *context) AddServiceAccount(userName string) error { saName = groups[1] saNamespace = groups[0] } - saNameObj := struct { - SA string `json:"serviceAccountName"` - }{ - SA: saName, - } - saNameRaw, err := json.Marshal(saNameObj) - if err != nil { - logger.Error(err, "failed to marshal the SA") - return err + data := map[string]interface{}{ + "serviceAccountName": saName, + "serviceAccountNamespace": saNamespace, } - if err := ctx.addJSON(saNameRaw); err != nil { + if err := ctx.addJSON(data); err != nil { return err } - saNsObj := struct { - SA string `json:"serviceAccountNamespace"` - }{ - SA: saNamespace, - } - saNsRaw, err := json.Marshal(saNsObj) - if err != nil { - logger.Error(err, "failed to marshal the SA namespace") - return err - } - if err := ctx.addJSON(saNsRaw); err != nil { - return err - } logger.V(4).Info("Adding service account", "service account name", saName, "service account namespace", saNamespace) return nil } @@ -267,8 +284,8 @@ func (ctx *context) AddElement(data interface{}, index, nesting int) error { data = map[string]interface{}{ "element": data, nestedElement: data, - "elementIndex": index, - nestedElementIndex: index, + "elementIndex": int64(index), + nestedElementIndex: int64(index), } return addToContext(ctx, data) } @@ -295,9 +312,33 @@ func (ctx *context) AddImageInfos(resource *unstructured.Unstructured, cfg confi return nil } ctx.images = images + utm, err := convertImagesToUntyped(images) + if err != nil { + return err + } + + logging.V(4).Info("updated image info", "images", utm) + return addToContext(ctx, utm, "images") +} - logging.V(4).Info("updated image info", "images", images) - return addToContext(ctx, images, "images") +func convertImagesToUntyped(images map[string]map[string]apiutils.ImageInfo) (map[string]interface{}, error) { + results := map[string]interface{}{} + for containerType, v := range images { + imgMap := map[string]interface{}{} + for containerName, imageInfo := range v { + img, err := toUnstructured(&imageInfo.ImageInfo) + if err != nil { + return nil, err + } + + img["jsonPointer"] = imageInfo.Pointer + imgMap[containerName] = img + } + + results[containerType] = imgMap + } + + return results, nil } func (ctx *context) GenerateCustomImageInfo(resource *unstructured.Unstructured, imageExtractorConfigs kyvernov1.ImageExtractorConfigs, cfg config.Configuration) (map[string]map[string]apiutils.ImageInfo, error) { @@ -321,13 +362,23 @@ func (ctx *context) ImageInfo() map[string]map[string]apiutils.ImageInfo { // Checkpoint creates a copy of the current internal state and // pushes it into a stack of stored states. func (ctx *context) Checkpoint() { - ctx.mutex.Lock() - defer ctx.mutex.Unlock() - jsonRawCheckpoint := make([]byte, len(ctx.jsonRaw)) - copy(jsonRawCheckpoint, ctx.jsonRaw) + jsonRawCheckpoint := ctx.copyContext(ctx.jsonRaw) ctx.jsonRawCheckpoints = append(ctx.jsonRawCheckpoints, jsonRawCheckpoint) } +func (ctx *context) copyContext(in map[string]interface{}) map[string]interface{} { + out := make(map[string]interface{}, len(in)) + for k, v := range in { + if ReservedKeys.MatchString(k) { + out[k] = v + } else { + out[k] = runtime.DeepCopyJSONValue(v) + } + } + + return out +} + // Restore sets the internal state to the last checkpoint, and removes the checkpoint. func (ctx *context) Restore() { ctx.reset(true) @@ -344,20 +395,19 @@ func (ctx *context) reset(restore bool) { } } -func (ctx *context) resetCheckpoint(removeCheckpoint bool) bool { - ctx.mutex.Lock() - defer ctx.mutex.Unlock() - +func (ctx *context) resetCheckpoint(restore bool) bool { if len(ctx.jsonRawCheckpoints) == 0 { return false } n := len(ctx.jsonRawCheckpoints) - 1 jsonRawCheckpoint := ctx.jsonRawCheckpoints[n] - ctx.jsonRaw = make([]byte, len(jsonRawCheckpoint)) - copy(ctx.jsonRaw, jsonRawCheckpoint) - if removeCheckpoint { + + if restore { ctx.jsonRawCheckpoints = ctx.jsonRawCheckpoints[:n] + ctx.jsonRaw = jsonRawCheckpoint + } else { + ctx.jsonRaw = ctx.copyContext(jsonRawCheckpoint) } return true diff --git a/pkg/engine/context/deferred_test.go b/pkg/engine/context/deferred_test.go index e6fa3c818fd1..67e8f234d743 100644 --- a/pkg/engine/context/deferred_test.go +++ b/pkg/engine/context/deferred_test.go @@ -95,8 +95,8 @@ func TestDeferredLoaderMismatch(t *testing.T) { func newContext() *context { return &context{ jp: jp, - jsonRaw: []byte(`{}`), - jsonRawCheckpoints: make([][]byte, 0), + jsonRaw: make(map[string]interface{}), + jsonRawCheckpoints: make([]map[string]interface{}, 0), deferred: NewDeferredLoaders(), } } @@ -290,7 +290,7 @@ func TestDeferredCheckpointRestore(t *testing.T) { func TestDeferredForloop(t *testing.T) { ctx := newContext() - addDeferred(ctx, "value", -1) + addDeferred(ctx, "value", float64(-1)) ctx.Checkpoint() for i := 0; i < 5; i++ { @@ -299,7 +299,7 @@ func TestDeferredForloop(t *testing.T) { assert.Equal(t, float64(i-1), val) ctx.Reset() - mock, _ := addDeferred(ctx, "value", i) + mock, _ := addDeferred(ctx, "value", float64(i)) val, err = ctx.Query("value") assert.NilError(t, err) assert.Equal(t, float64(i), val) diff --git a/pkg/engine/context/evaluate.go b/pkg/engine/context/evaluate.go index 1624ea1b5915..a8a70096781b 100644 --- a/pkg/engine/context/evaluate.go +++ b/pkg/engine/context/evaluate.go @@ -1,7 +1,6 @@ package context import ( - "encoding/json" "fmt" "strings" @@ -25,13 +24,7 @@ func (ctx *context) Query(query string) (interface{}, error) { return nil, fmt.Errorf("incorrect query %s: %v", query, err) } // search - ctx.mutex.RLock() - defer ctx.mutex.RUnlock() - var data interface{} - if err := json.Unmarshal(ctx.jsonRaw, &data); err != nil { - return nil, fmt.Errorf("failed to unmarshal context: %w", err) - } - result, err := queryPath.Search(data) + result, err := queryPath.Search(ctx.jsonRaw) if err != nil { return nil, fmt.Errorf("JMESPath query failed: %w", err) } diff --git a/pkg/engine/context/evaluate_test.go b/pkg/engine/context/evaluate_test.go index c41c57e90dfd..6612d5e6bbe6 100644 --- a/pkg/engine/context/evaluate_test.go +++ b/pkg/engine/context/evaluate_test.go @@ -3,6 +3,7 @@ package context import ( "testing" + kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1" "github.com/stretchr/testify/assert" admissionv1 "k8s.io/api/admission/v1" ) @@ -65,3 +66,23 @@ func createTestContext(obj, oldObj string) Interface { ctx.AddRequest(request) return ctx } + +func TestQueryOperation(t *testing.T) { + ctx := createTestContext(`{"a": {"b": 1, "c": 2}, "d": 3}`, `{"a": {"b": 2, "c": 2}, "d": 4}`) + assert.Equal(t, ctx.QueryOperation(), "UPDATE") + request := admissionv1.AdmissionRequest{ + Operation: admissionv1.Delete, + } + + err := ctx.AddRequest(request) + assert.Nil(t, err) + assert.Equal(t, ctx.QueryOperation(), "DELETE") + + err = ctx.AddOperation(string(kyvernov1.Connect)) + assert.Nil(t, err) + assert.Equal(t, ctx.QueryOperation(), "CONNECT") + + err = ctx.AddRequest(admissionv1.AdmissionRequest{}) + assert.Nil(t, err) + assert.Equal(t, ctx.QueryOperation(), "") +} diff --git a/pkg/engine/context/loaders/variable.go b/pkg/engine/context/loaders/variable.go index 355fdaa689b3..c4cc69c678e6 100644 --- a/pkg/engine/context/loaders/variable.go +++ b/pkg/engine/context/loaders/variable.go @@ -8,6 +8,7 @@ import ( kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1" enginecontext "github.com/kyverno/kyverno/pkg/engine/context" "github.com/kyverno/kyverno/pkg/engine/jmespath" + "github.com/kyverno/kyverno/pkg/engine/jsonutils" "github.com/kyverno/kyverno/pkg/engine/variables" ) @@ -58,7 +59,7 @@ func (vl *variableLoader) loadVariable() (err error) { var defaultValue interface{} = nil if entry.Variable.Default != nil { - value, err := variables.DocumentToUntyped(entry.Variable.Default) + value, err := jsonutils.DocumentToUntyped(entry.Variable.Default) if err != nil { return fmt.Errorf("invalid default for variable %s", entry.Name) } @@ -71,7 +72,7 @@ func (vl *variableLoader) loadVariable() (err error) { var output interface{} = defaultValue if entry.Variable.Value != nil { - value, _ := variables.DocumentToUntyped(entry.Variable.Value) + value, _ := jsonutils.DocumentToUntyped(entry.Variable.Value) variable, err := variables.SubstituteAll(logger, ctx, value) if err != nil { return fmt.Errorf("failed to substitute variables in context entry %s %s: %v", entry.Name, entry.Variable.Value, err) diff --git a/pkg/engine/context/mock_context.go b/pkg/engine/context/mock_context.go index a3f66505fba8..6fdff695b93e 100644 --- a/pkg/engine/context/mock_context.go +++ b/pkg/engine/context/mock_context.go @@ -64,6 +64,16 @@ func (ctx *MockContext) Query(query string) (interface{}, error) { } } +func (ctx *MockContext) QueryOperation() string { + if op, err := ctx.Query("request.operation"); err != nil { + if op != nil { + return op.(string) + } + } + + return "" +} + func (ctx *MockContext) isVariableDefined(variable string) bool { for _, pattern := range ctx.getVariables() { if wildcard.Match(pattern, variable) { diff --git a/pkg/engine/context/utils.go b/pkg/engine/context/utils.go index a07d9c07f0ff..6724c69a22f9 100644 --- a/pkg/engine/context/utils.go +++ b/pkg/engine/context/utils.go @@ -1,16 +1,14 @@ package context import ( - "encoding/json" + "reflect" + + "k8s.io/apimachinery/pkg/runtime" ) // AddJSONObject merges json data -func AddJSONObject(ctx Interface, data interface{}) error { - jsonBytes, err := json.Marshal(data) - if err != nil { - return err - } - return ctx.addJSON(jsonBytes) +func AddJSONObject(ctx Interface, data map[string]interface{}) error { + return ctx.addJSON(data) } func AddResource(ctx Interface, dataRaw []byte) error { @@ -32,19 +30,61 @@ func AddOldResource(ctx Interface, dataRaw []byte) error { } func addToContext(ctx *context, data interface{}, tags ...string) error { - dataRaw, err := json.Marshal(push(data, tags...)) - if err != nil { - logger.Error(err, "failed to marshal the resource") + if v, err := convertStructs(data); err != nil { return err + } else { + dataRaw := push(v, tags...) + return ctx.addJSON(dataRaw) } - return ctx.addJSON(dataRaw) } -func push(data interface{}, tags ...string) interface{} { +// convertStructs converts structs, and pointers-to-structs, to map[string]interface{} +func convertStructs(value interface{}) (interface{}, error) { + if value != nil { + v := reflect.ValueOf(value) + if v.Kind() == reflect.Struct { + return toUnstructured(value) + } + + if v.Kind() == reflect.Ptr { + ptrVal := v.Elem() + if ptrVal.Kind() == reflect.Struct { + return toUnstructured(value) + } + } + } + + return value, nil +} + +func push(data interface{}, tags ...string) map[string]interface{} { for i := len(tags) - 1; i >= 0; i-- { data = map[string]interface{}{ tags[i]: data, } } - return data + + return data.(map[string]interface{}) +} + +// mergeMaps merges srcMap entries into destMap +func mergeMaps(srcMap, destMap map[string]interface{}) { + for k, v := range srcMap { + if nextSrcMap, ok := v.(map[string]interface{}); ok { + if nextDestMap, ok := destMap[k].(map[string]interface{}); ok { + mergeMaps(nextSrcMap, nextDestMap) + } else { + destMap[k] = nextSrcMap + } + } else { + destMap[k] = v + } + } +} + +// toUnstructured converts a struct with JSON tags to a map[string]interface{} +func toUnstructured(typedStruct interface{}) (map[string]interface{}, error) { + converter := runtime.DefaultUnstructuredConverter + u, err := converter.ToUnstructured(typedStruct) + return u, err } diff --git a/pkg/engine/context/utils_test.go b/pkg/engine/context/utils_test.go new file mode 100644 index 000000000000..f404bc7a95f3 --- /dev/null +++ b/pkg/engine/context/utils_test.go @@ -0,0 +1,107 @@ +package context + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMergeMaps(t *testing.T) { + map1 := map[string]interface{}{ + "strVal": "bar1", + "strVal2": "bar2", + "intVal": 2, + "arrayVal": []string{"1", "2", "3"}, + "mapVal": map[string]interface{}{ + "foo": "bar", + }, + "mapVal2": map[string]interface{}{ + "foo2": map[string]interface{}{ + "foo3": 3, + }, + }, + } + + map2 := map[string]interface{}{ + "strVal": "bar2", + "intVal": 3, + "intVal2": 3, + "arrayVal": []string{"1", "2", "3", "4"}, + "mapVal": map[string]interface{}{ + "foo1": "bar1", + "foo2": "bar2", + }, + } + + mergeMaps(map1, map2) + + assert.Equal(t, "bar1", map2["strVal"]) + assert.Equal(t, "bar2", map2["strVal2"]) + assert.Equal(t, 2, map2["intVal"]) + assert.Equal(t, 3, map2["intVal2"]) + assert.Equal(t, []string{"1", "2", "3"}, map2["arrayVal"]) + assert.Equal(t, map[string]interface{}{"foo": "bar", "foo1": "bar1", "foo2": "bar2"}, map2["mapVal"]) + assert.Equal(t, map1["mapVal2"], map2["mapVal2"]) + + requestObj := map[string]interface{}{ + "request": map[string]interface{}{ + "object": map[string]interface{}{ + "foo": "bar", + }, + }, + } + + ctxMap := map[string]interface{}{} + mergeMaps(requestObj, ctxMap) + + r := ctxMap["request"].(map[string]interface{}) + o := r["object"].(map[string]interface{}) + assert.Equal(t, o["foo"], "bar") + + requestObj2 := map[string]interface{}{ + "request": map[string]interface{}{ + "object": map[string]interface{}{ + "foo": "bar2", + "foo2": "bar2", + }, + }, + } + + mergeMaps(requestObj2, ctxMap) + r2 := ctxMap["request"].(map[string]interface{}) + o2 := r2["object"].(map[string]interface{}) + assert.Equal(t, "bar2", o2["foo"]) + assert.Equal(t, "bar2", o2["foo"]) + + request3 := map[string]interface{}{ + "request": map[string]interface{}{ + "userInfo": "user1", + }, + } + + mergeMaps(request3, ctxMap) + r3 := ctxMap["request"].(map[string]interface{}) + o3 := r3["object"].(map[string]interface{}) + assert.NotNil(t, o3) + assert.Equal(t, "bar2", o2["foo"]) + assert.Equal(t, "bar2", o2["foo"]) + assert.Equal(t, "user1", r3["userInfo"]) +} + +func TestStructToUntypedMap(t *testing.T) { + type SampleStuct struct { + Name string `json:"name"` + ID int32 `json:"identifier"` + } + + sample := &SampleStuct{ + Name: "user1", + ID: 12345, + } + + result, err := toUnstructured(sample) + assert.Nil(t, err) + + assert.Equal(t, "user1", result["name"]) + assert.Equal(t, int64(12345), result["identifier"]) +} diff --git a/pkg/engine/jmespath/arithmetic_test.go b/pkg/engine/jmespath/arithmetic_test.go index 4da8c7ec6e67..35e62679b017 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(cfg, tc.test) + jp, err := jmespathInterface.Query(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(cfg, tc.test) + jp, err := jmespathInterface.Query(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(cfg, tc.test) + jp, err := jmespathInterface.Query(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(cfg, tc.test) + jp, err := jmespathInterface.Query(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(cfg, tc.test) + jp, err := jmespathInterface.Query(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(cfg, tc.test) + jp, err := jmespathInterface.Query(tc.test) assert.NilError(t, err) result, err := jp.Search("") @@ -813,7 +813,7 @@ func Test_Round(t *testing.T) { } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - jp, err := newJMESPath(cfg, tc.test) + jp, err := jmespathInterface.Query(tc.test) assert.NilError(t, err) result, err := jp.Search("") diff --git a/pkg/engine/jmespath/functions_test.go b/pkg/engine/jmespath/functions_test.go index 22408814f89f..ff0b5909a93e 100644 --- a/pkg/engine/jmespath/functions_test.go +++ b/pkg/engine/jmespath/functions_test.go @@ -11,7 +11,7 @@ import ( "gotest.tools/assert" ) -var cfg = config.NewDefaultConfiguration(false) +var jmespathInterface = newImplementation(config.NewDefaultConfiguration(false)) func Test_Compare(t *testing.T) { testCases := []struct { @@ -33,7 +33,7 @@ func Test_Compare(t *testing.T) { } for _, tc := range testCases { t.Run(tc.jmesPath, func(t *testing.T) { - jp, err := newJMESPath(cfg, tc.jmesPath) + jp, err := jmespathInterface.Query(tc.jmesPath) assert.NilError(t, err) result, err := jp.Search("") @@ -60,7 +60,7 @@ func Test_ParseJsonSerde(t *testing.T) { } for _, tc := range testCases { t.Run(tc, func(t *testing.T) { - jp, err := newJMESPath(cfg, fmt.Sprintf(`to_string(parse_json('%s'))`, tc)) + jp, err := jmespathInterface.Query(fmt.Sprintf(`to_string(parse_json('%s'))`, tc)) assert.NilError(t, err) result, err := jp.Search("") @@ -91,7 +91,7 @@ func Test_ParseJsonComplex(t *testing.T) { } for _, tc := range testCases { t.Run(tc.input, func(t *testing.T) { - jp, err := newJMESPath(cfg, tc.input) + jp, err := jmespathInterface.Query(tc.input) assert.NilError(t, err) result, err := jp.Search("") @@ -168,7 +168,7 @@ bar: null } for _, tc := range testCases { t.Run(tc.input, func(t *testing.T) { - jp, err := newJMESPath(cfg, fmt.Sprintf(`parse_yaml('%s')`, tc.input)) + jp, err := jmespathInterface.Query(fmt.Sprintf(`parse_yaml('%s')`, tc.input)) assert.NilError(t, err) result, err := jp.Search("") assert.NilError(t, err) @@ -197,7 +197,7 @@ func Test_EqualFold(t *testing.T) { } for _, tc := range testCases { t.Run(tc.jmesPath, func(t *testing.T) { - jp, err := newJMESPath(cfg, tc.jmesPath) + jp, err := jmespathInterface.Query(tc.jmesPath) assert.NilError(t, err) result, err := jp.Search("") @@ -234,7 +234,7 @@ func Test_Replace(t *testing.T) { } for _, tc := range testCases { t.Run(tc.jmesPath, func(t *testing.T) { - jp, err := newJMESPath(cfg, tc.jmesPath) + jp, err := jmespathInterface.Query(tc.jmesPath) assert.NilError(t, err) result, err := jp.Search("") @@ -248,7 +248,7 @@ func Test_Replace(t *testing.T) { } func Test_ReplaceAll(t *testing.T) { - jp, err := newJMESPath(cfg, "replace_all('Lorem ipsum dolor sit amet', 'ipsum', 'muspi')") + jp, err := jmespathInterface.Query("replace_all('Lorem ipsum dolor sit amet', 'ipsum', 'muspi')") assert.NilError(t, err) result, err := jp.Search("") @@ -279,7 +279,7 @@ func Test_ToUpper(t *testing.T) { } for _, tc := range testCases { t.Run(tc.jmesPath, func(t *testing.T) { - jp, err := newJMESPath(cfg, tc.jmesPath) + jp, err := jmespathInterface.Query(tc.jmesPath) assert.NilError(t, err) result, err := jp.Search("") @@ -312,7 +312,7 @@ func Test_ToLower(t *testing.T) { } for _, tc := range testCases { t.Run(tc.jmesPath, func(t *testing.T) { - jp, err := newJMESPath(cfg, tc.jmesPath) + jp, err := jmespathInterface.Query(tc.jmesPath) assert.NilError(t, err) result, err := jp.Search("") @@ -326,7 +326,7 @@ func Test_ToLower(t *testing.T) { } func Test_Trim(t *testing.T) { - jp, err := newJMESPath(cfg, "trim('¡¡¡Hello, Gophers!!!', '!¡')") + jp, err := jmespathInterface.Query("trim('¡¡¡Hello, Gophers!!!', '!¡')") assert.NilError(t, err) result, err := jp.Search("") @@ -397,7 +397,7 @@ func Test_TrimPrefix(t *testing.T) { } func Test_Split(t *testing.T) { - jp, err := newJMESPath(cfg, "split('Hello, Gophers', ', ')") + jp, err := jmespathInterface.Query("split('Hello, Gophers', ', ')") assert.NilError(t, err) result, err := jp.Search("") @@ -410,7 +410,7 @@ func Test_Split(t *testing.T) { } func Test_HasPrefix(t *testing.T) { - jp, err := newJMESPath(cfg, "starts_with('Gophers', 'Go')") + jp, err := jmespathInterface.Query("starts_with('Gophers', 'Go')") assert.NilError(t, err) result, err := jp.Search("") @@ -422,7 +422,7 @@ func Test_HasPrefix(t *testing.T) { } func Test_HasSuffix(t *testing.T) { - jp, err := newJMESPath(cfg, "ends_with('Amigo', 'go')") + jp, err := jmespathInterface.Query("ends_with('Amigo', 'go')") assert.NilError(t, err) result, err := jp.Search("") @@ -437,7 +437,7 @@ func Test_RegexMatch(t *testing.T) { data := make(map[string]interface{}) data["foo"] = "hgf'b1a2r'b12g" - query, err := newJMESPath(cfg, "regex_match('12.*', foo)") + query, err := jmespathInterface.Query("regex_match('12.*', foo)") assert.NilError(t, err) result, err := query.Search(data) @@ -449,7 +449,7 @@ func Test_RegexMatchWithNumber(t *testing.T) { data := make(map[string]interface{}) data["foo"] = -12.0 - query, err := newJMESPath(cfg, "regex_match('12.*', abs(foo))") + query, err := jmespathInterface.Query("regex_match('12.*', abs(foo))") assert.NilError(t, err) result, err := query.Search(data) @@ -461,7 +461,7 @@ func Test_PatternMatch(t *testing.T) { data := make(map[string]interface{}) data["foo"] = "prefix-foo" - query, err := newJMESPath(cfg, "pattern_match('prefix-*', foo)") + query, err := jmespathInterface.Query("pattern_match('prefix-*', foo)") assert.NilError(t, err) result, err := query.Search(data) @@ -473,7 +473,7 @@ func Test_PatternMatchWithNumber(t *testing.T) { data := make(map[string]interface{}) data["foo"] = -12.0 - query, err := newJMESPath(cfg, "pattern_match('12*', abs(foo))") + query, err := jmespathInterface.Query("pattern_match('12*', abs(foo))") assert.NilError(t, err) result, err := query.Search(data) @@ -500,7 +500,7 @@ func Test_RegexReplaceAll(t *testing.T) { var resource interface{} err := json.Unmarshal(resourceRaw, &resource) assert.NilError(t, err) - query, err := newJMESPath(cfg, `regex_replace_all('([Hh]e|G)l', spec.field, '${2}G')`) + query, err := jmespathInterface.Query(`regex_replace_all('([Hh]e|G)l', spec.field, '${2}G')`) assert.NilError(t, err) res, err := query.Search(resource) @@ -531,7 +531,7 @@ func Test_RegexReplaceAllLiteral(t *testing.T) { err := json.Unmarshal(resourceRaw, &resource) assert.NilError(t, err) - query, err := newJMESPath(cfg, `regex_replace_all_literal('[Hh]el?', spec.field, 'G')`) + query, err := jmespathInterface.Query(`regex_replace_all_literal('[Hh]el?', spec.field, 'G')`) assert.NilError(t, err) res, err := query.Search(resource) @@ -586,7 +586,7 @@ func Test_LabelMatch(t *testing.T) { err := json.Unmarshal(tc.resource, &resource) assert.NilError(t, err) - query, err := newJMESPath(cfg, "label_match(`"+tc.test+"`, metadata.labels)") + query, err := jmespathInterface.Query("label_match(`" + tc.test + "`, metadata.labels)") assert.NilError(t, err) res, err := query.Search(resource) @@ -630,7 +630,7 @@ func Test_JpToBoolean(t *testing.T) { } func Test_Base64Decode(t *testing.T) { - jp, err := newJMESPath(cfg, "base64_decode('SGVsbG8sIHdvcmxkIQ==')") + jp, err := jmespathInterface.Query("base64_decode('SGVsbG8sIHdvcmxkIQ==')") assert.NilError(t, err) result, err := jp.Search("") @@ -642,7 +642,7 @@ func Test_Base64Decode(t *testing.T) { } func Test_Base64Encode(t *testing.T) { - jp, err := newJMESPath(cfg, "base64_encode('Hello, world!')") + jp, err := jmespathInterface.Query("base64_encode('Hello, world!')") assert.NilError(t, err) result, err := jp.Search("") @@ -672,7 +672,7 @@ func Test_Base64Decode_Secret(t *testing.T) { err := json.Unmarshal(resourceRaw, &resource) assert.NilError(t, err) - query, err := newJMESPath(cfg, `base64_decode(data.example1)`) + query, err := jmespathInterface.Query(`base64_decode(data.example1)`) assert.NilError(t, err) res, err := query.Search(resource) @@ -747,7 +747,7 @@ func Test_PathCanonicalize(t *testing.T) { } for _, tc := range testCases { t.Run(tc.jmesPath, func(t *testing.T) { - jp, err := newJMESPath(cfg, tc.jmesPath) + jp, err := jmespathInterface.Query(tc.jmesPath) assert.NilError(t, err) result, err := jp.Search("") @@ -796,7 +796,7 @@ func Test_Truncate(t *testing.T) { } for _, tc := range testCases { t.Run(tc.jmesPath, func(t *testing.T) { - jp, err := newJMESPath(cfg, tc.jmesPath) + jp, err := jmespathInterface.Query(tc.jmesPath) assert.NilError(t, err) result, err := jp.Search("") @@ -833,7 +833,7 @@ func Test_SemverCompare(t *testing.T) { } for _, tc := range testCases { t.Run(tc.jmesPath, func(t *testing.T) { - jp, err := newJMESPath(cfg, tc.jmesPath) + jp, err := jmespathInterface.Query(tc.jmesPath) assert.NilError(t, err) result, err := jp.Search("") @@ -992,7 +992,7 @@ func Test_Lookup(t *testing.T) { } for i, tc := range testCases { t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) { - query, err := newJMESPath(cfg, "lookup(`"+tc.collection+"`,`"+tc.key+"`)") + query, err := jmespathInterface.Query("lookup(`" + tc.collection + "`,`" + tc.key + "`)") assert.NilError(t, err) result, err := query.Search("") @@ -1034,7 +1034,7 @@ func Test_Lookup_InvalidArgs(t *testing.T) { } for i, tc := range testCases { t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) { - query, err := newJMESPath(cfg, "lookup(`"+tc.collection+"`,`"+tc.key+"`)") + query, err := jmespathInterface.Query("lookup(`" + tc.collection + "`,`" + tc.key + "`)") assert.NilError(t, err) _, err = query.Search("") @@ -1077,7 +1077,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(cfg, "items(`"+tc.object+"`,`"+tc.keyName+"`,`"+tc.valName+"`)") + query, err := jmespathInterface.Query("items(`" + tc.object + "`,`" + tc.keyName + "`,`" + tc.valName + "`)") assert.NilError(t, err) res, err := query.Search("") @@ -1128,7 +1128,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(cfg, "object_from_lists(`"+tc.keys+"`,`"+tc.values+"`)") + query, err := jmespathInterface.Query("object_from_lists(`" + tc.keys + "`,`" + tc.values + "`)") assert.NilError(t, err) res, err := query.Search("") assert.NilError(t, err) @@ -1245,7 +1245,7 @@ ZDGRs55xuoeLDJ/ZRFf9bI+IaCUd1YrfYcHIl3G87Av+r49YVwqRDT0VDV7uLgqn } for _, tc := range testCases { t.Run(tc.jmesPath, func(t *testing.T) { - jp, err := newJMESPath(cfg, tc.jmesPath) + jp, err := jmespathInterface.Query(tc.jmesPath) assert.NilError(t, err) result, err := jp.Search("") @@ -1661,7 +1661,7 @@ func Test_ImageNormalize(t *testing.T) { } for _, tc := range testCases { t.Run(tc.jmesPath, func(t *testing.T) { - jp, err := newJMESPath(cfg, tc.jmesPath) + jp, err := jmespathInterface.Query(tc.jmesPath) assert.NilError(t, err) result, err := jp.Search("") if tc.wantErr { diff --git a/pkg/engine/jmespath/interface.go b/pkg/engine/jmespath/interface.go index ef9e9395ca54..0d0e3d51613a 100644 --- a/pkg/engine/jmespath/interface.go +++ b/pkg/engine/jmespath/interface.go @@ -1,6 +1,9 @@ package jmespath -import "github.com/kyverno/kyverno/pkg/config" +import ( + gojmespath "github.com/kyverno/go-jmespath" + "github.com/kyverno/kyverno/pkg/config" +) type Query interface { Search(interface{}) (interface{}, error) @@ -12,23 +15,17 @@ type Interface interface { } type implementation struct { - configuration config.Configuration + interpreter gojmespath.Interpreter } func New(configuration config.Configuration) Interface { - return implementation{ - configuration: configuration, - } + return newImplementation(configuration) } func (i implementation) Query(query string) (Query, error) { - return newJMESPath(i.configuration, query) + return newJMESPath(i.interpreter, query) } func (i implementation) Search(query string, data interface{}) (interface{}, error) { - if query, err := i.Query(query); err != nil { - return nil, err - } else { - return query.Search(data) - } + return newExecution(i.interpreter, query, data) } diff --git a/pkg/engine/jmespath/new.go b/pkg/engine/jmespath/new.go index a954b539f39f..4cb8a86801be 100644 --- a/pkg/engine/jmespath/new.go +++ b/pkg/engine/jmespath/new.go @@ -5,13 +5,34 @@ import ( "github.com/kyverno/kyverno/pkg/config" ) -func newJMESPath(configuration config.Configuration, query string) (*gojmespath.JMESPath, error) { - jp, err := gojmespath.Compile(query) +func newJMESPath(intr gojmespath.Interpreter, query string) (*gojmespath.JMESPath, error) { + parser := gojmespath.NewParser() + ast, err := parser.Parse(query) if err != nil { return nil, err } - for _, function := range GetFunctions(configuration) { - jp.Register(function.FunctionEntry) + + return gojmespath.NewJMESPath(ast, intr), nil +} + +func newImplementation(configuration config.Configuration) Interface { + i := gojmespath.NewInterpreter() + functions := GetFunctions(configuration) + for _, f := range functions { + i.Register(f.FunctionEntry) } - return jp, nil + + return implementation{ + interpreter: i, + } +} + +func newExecution(intr gojmespath.Interpreter, query string, data interface{}) (interface{}, error) { + parser := gojmespath.NewParser() + ast, err := parser.Parse(query) + if err != nil { + return nil, err + } + + return intr.Execute(ast, data) } diff --git a/pkg/engine/jmespath/new_test.go b/pkg/engine/jmespath/new_test.go index f6f44708dadc..9b4044e32b46 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(cfg, tt.args.query) + _, err := jmespathInterface.Query(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 13a89080292a..c0935567527a 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(cfg, tc.test) + query, err := jmespathInterface.Query(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(cfg, tc.test) + query, err := jmespathInterface.Query(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(cfg, tc.test) + query, err := jmespathInterface.Query(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(cfg, tc.test) + query, err := jmespathInterface.Query(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(cfg, tc.test) + query, err := jmespathInterface.Query(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(cfg, tc.test) + query, err := jmespathInterface.Query(tc.test) assert.NilError(t, err) res, err := query.Search("") diff --git a/pkg/engine/jsonutils/convert.go b/pkg/engine/jsonutils/convert.go new file mode 100644 index 000000000000..6b38dfc77679 --- /dev/null +++ b/pkg/engine/jsonutils/convert.go @@ -0,0 +1,22 @@ +package jsonutils + +import jsoniter "github.com/json-iterator/go" + +var json = jsoniter.ConfigCompatibleWithStandardLibrary + +// DocumentToUntyped converts a typed object to JSON data +// i.e. string, []interface{}, map[string]interface{} +func DocumentToUntyped(doc interface{}) (interface{}, error) { + jsonDoc, err := json.Marshal(doc) + if err != nil { + return nil, err + } + + var untyped interface{} + err = json.Unmarshal(jsonDoc, &untyped) + if err != nil { + return nil, err + } + + return untyped, nil +} diff --git a/pkg/engine/jsonutils/traverse_test.go b/pkg/engine/jsonutils/traverse_test.go index e32c13a1698c..b01defe54a9d 100644 --- a/pkg/engine/jsonutils/traverse_test.go +++ b/pkg/engine/jsonutils/traverse_test.go @@ -1,7 +1,6 @@ package jsonutils import ( - "encoding/json" "testing" "gotest.tools/assert" diff --git a/pkg/engine/policycontext/policy_context.go b/pkg/engine/policycontext/policy_context.go index 7986294cfb48..b7a26cbbbbaa 100644 --- a/pkg/engine/policycontext/policy_context.go +++ b/pkg/engine/policycontext/policy_context.go @@ -193,7 +193,8 @@ func NewPolicyContext( configuration config.Configuration, ) (*PolicyContext, error) { enginectx := enginectx.NewContext(jp) - if err := enginectx.AddResource(resource.Object); err != nil { + // add resource clone as it may be modified by mutate rules + if err := enginectx.AddResource(resource.DeepCopy().Object); err != nil { return nil, err } if err := enginectx.AddNamespace(resource.GetNamespace()); err != nil { diff --git a/pkg/engine/utils/foreach.go b/pkg/engine/utils/foreach.go index 1c88048f6a1a..c0e49607809c 100644 --- a/pkg/engine/utils/foreach.go +++ b/pkg/engine/utils/foreach.go @@ -5,7 +5,7 @@ import ( engineapi "github.com/kyverno/kyverno/pkg/engine/api" enginecontext "github.com/kyverno/kyverno/pkg/engine/context" - "github.com/kyverno/kyverno/pkg/engine/variables" + "github.com/kyverno/kyverno/pkg/engine/jsonutils" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) @@ -31,7 +31,7 @@ func InvertedElement(elements []interface{}) { } func AddElementToContext(ctx engineapi.PolicyContext, element interface{}, index, nesting int, elementScope *bool) error { - data, err := variables.DocumentToUntyped(element) + data, err := jsonutils.DocumentToUntyped(element) if err != nil { return err } diff --git a/pkg/engine/utils/utils.go b/pkg/engine/utils/utils.go index 1bab7ee793eb..3e0e1e03fe8e 100644 --- a/pkg/engine/utils/utils.go +++ b/pkg/engine/utils/utils.go @@ -14,8 +14,16 @@ import ( ) func IsDeleteRequest(ctx engineapi.PolicyContext) bool { + if ctx == nil { + return false + } + + if op := ctx.Operation(); string(op) != "" { + return op == kyvernov1.Delete + } + + // if the NewResource is empty, the request is a DELETE newResource := ctx.NewResource() - // if the OldResource is not empty, and the NewResource is empty, the request is a DELETE return IsEmptyUnstructured(&newResource) } diff --git a/pkg/engine/variables/variables_test.go b/pkg/engine/variables/variables_test.go index a59295c3d063..80bcfa78ade1 100644 --- a/pkg/engine/variables/variables_test.go +++ b/pkg/engine/variables/variables_test.go @@ -1,7 +1,6 @@ package variables import ( - "encoding/json" "reflect" "testing" diff --git a/pkg/engine/variables/vars.go b/pkg/engine/variables/vars.go index bfe33c24f085..977317c4cf9b 100644 --- a/pkg/engine/variables/vars.go +++ b/pkg/engine/variables/vars.go @@ -1,13 +1,13 @@ package variables import ( - "encoding/json" "errors" "fmt" "path" "strings" "github.com/go-logr/logr" + jsoniter "github.com/json-iterator/go" gojmespath "github.com/kyverno/go-jmespath" kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1" "github.com/kyverno/kyverno/pkg/engine/anchor" @@ -19,6 +19,8 @@ import ( "github.com/kyverno/kyverno/pkg/utils/jsonpointer" ) +var json = jsoniter.ConfigCompatibleWithStandardLibrary + // ReplaceAllVars replaces all variables with the value defined in the replacement function // This is used to avoid validation errors func ReplaceAllVars(src string, repl func(string) string) string { @@ -58,7 +60,7 @@ func SubstituteAll(log logr.Logger, ctx context.EvalInterface, document interfac } func SubstituteAllInPreconditions(log logr.Logger, ctx context.EvalInterface, document interface{}) (interface{}, error) { - untypedDoc, err := DocumentToUntyped(document) + untypedDoc, err := jsonUtils.DocumentToUntyped(document) if err != nil { return nil, err } @@ -66,7 +68,7 @@ func SubstituteAllInPreconditions(log logr.Logger, ctx context.EvalInterface, do } func SubstituteAllInType[T any](log logr.Logger, ctx context.EvalInterface, t *T) (*T, error) { - untyped, err := DocumentToUntyped(t) + untyped, err := jsonUtils.DocumentToUntyped(t) if err != nil { return nil, err } @@ -99,23 +101,6 @@ func SubstituteAllInRule(log logr.Logger, ctx context.EvalInterface, rule kyvern return *result, nil } -// DocumentToUntyped converts a typed object to JSON data i.e. -// string, []interface{}, map[string]interface{} -func DocumentToUntyped(doc interface{}) (interface{}, error) { - jsonDoc, err := json.Marshal(doc) - if err != nil { - return nil, err - } - - var untyped interface{} - err = json.Unmarshal(jsonDoc, &untyped) - if err != nil { - return nil, err - } - - return untyped, nil -} - func untypedToTyped[T any](untyped interface{}) (*T, error) { jsonRule, err := json.Marshal(untyped) if err != nil { @@ -184,7 +169,7 @@ func substituteAll(log logr.Logger, ctx context.EvalInterface, document interfac func SubstituteAllForceMutate(log logr.Logger, ctx context.Interface, typedRule kyvernov1.Rule) (_ kyvernov1.Rule, err error) { var rule interface{} - rule, err = DocumentToUntyped(typedRule) + rule, err = jsonUtils.DocumentToUntyped(typedRule) if err != nil { return kyvernov1.Rule{}, err } @@ -407,11 +392,13 @@ func isDeleteRequest(ctx context.EvalInterface) bool { if ctx == nil { return false } - operation, err := ctx.Query("request.operation") - if err == nil && operation == "DELETE" { - return true + + if op := ctx.QueryOperation(); op != "" { + return ctx.QueryOperation() == "DELETE" } - return false + + operation, err := ctx.Query("request.operation") + return err == nil && operation == "DELETE" } func substituteVarInPattern(prefix, pattern, variable string, value interface{}) (string, error) { diff --git a/pkg/engine/variables/vars_test.go b/pkg/engine/variables/vars_test.go index efea0182cef8..9da6959f6cbe 100644 --- a/pkg/engine/variables/vars_test.go +++ b/pkg/engine/variables/vars_test.go @@ -1,8 +1,6 @@ package variables import ( - "bytes" - "encoding/json" "fmt" "strings" "testing" @@ -208,11 +206,6 @@ func Test_subVars_with_JMESPath_At(t *testing.T) { }`) var err error - - expected := new(bytes.Buffer) - err = json.Compact(expected, expectedRaw) - assert.NilError(t, err) - var pattern, resource interface{} err = json.Unmarshal(patternMap, &pattern) assert.NilError(t, err) @@ -227,7 +220,7 @@ func Test_subVars_with_JMESPath_At(t *testing.T) { assert.NilError(t, err) out, err := json.Marshal(output) assert.NilError(t, err) - assert.Equal(t, string(out), expected.String()) + assert.Equal(t, string(out), compact(t, expectedRaw)) } func Test_subVars_withRegexMatch(t *testing.T) { @@ -268,10 +261,6 @@ func Test_subVars_withRegexMatch(t *testing.T) { var err error - expected := new(bytes.Buffer) - err = json.Compact(expected, expectedRaw) - assert.NilError(t, err) - var pattern, resource interface{} err = json.Unmarshal(patternMap, &pattern) assert.NilError(t, err) @@ -286,7 +275,7 @@ func Test_subVars_withRegexMatch(t *testing.T) { assert.NilError(t, err) out, err := json.Marshal(output) assert.NilError(t, err) - assert.Equal(t, string(out), expected.String()) + assert.Equal(t, string(out), compact(t, expectedRaw)) } func Test_subVars_withMerge(t *testing.T) { @@ -298,10 +287,6 @@ func Test_subVars_withMerge(t *testing.T) { var err error - expected := new(bytes.Buffer) - err = json.Compact(expected, expectedRaw) - assert.NilError(t, err) - var pattern, resource interface{} err = json.Unmarshal(patternMap, &pattern) assert.NilError(t, err) @@ -316,7 +301,17 @@ func Test_subVars_withMerge(t *testing.T) { assert.NilError(t, err) out, err := json.Marshal(output) assert.NilError(t, err) - assert.Equal(t, string(out), expected.String()) + assert.Equal(t, string(out), compact(t, expectedRaw)) +} + +func compact(t *testing.T, in []byte) string { + var tmp map[string]interface{} + err := json.Unmarshal(in, &tmp) + assert.NilError(t, err) + + out, err := json.Marshal(tmp) + assert.NilError(t, err) + return string(out) } func Test_subVars_withRegexReplaceAll(t *testing.T) { @@ -393,10 +388,12 @@ func Test_ReplacingPathWhenDeleting(t *testing.T) { var pattern interface{} var err error err = json.Unmarshal(patternRaw, &pattern) - if err != nil { - t.Error(err) - } - ctx := context.NewContextFromRaw(jp, resourceRaw) + assert.NilError(t, err) + + ctxMap, err := unmarshalToMap(resourceRaw) + assert.NilError(t, err) + + ctx := context.NewContextFromRaw(jp, ctxMap) assert.NilError(t, err) pattern, err = SubstituteAll(logr.Discard(), ctx, pattern) @@ -405,6 +402,15 @@ func Test_ReplacingPathWhenDeleting(t *testing.T) { assert.Equal(t, fmt.Sprintf("%v", pattern), "bar") } +func unmarshalToMap(jsonBytes []byte) (map[string]interface{}, error) { + var data map[string]interface{} + if err := json.Unmarshal(jsonBytes, &data); err != nil { + return nil, err + } + + return data, nil +} + func Test_ReplacingNestedVariableWhenDeleting(t *testing.T) { patternRaw := []byte(`"{{request.object.metadata.annotations.{{request.object.metadata.annotations.targetnew}}}}"`) @@ -428,12 +434,12 @@ func Test_ReplacingNestedVariableWhenDeleting(t *testing.T) { var pattern interface{} var err error err = json.Unmarshal(patternRaw, &pattern) - if err != nil { - t.Error(err) - } - ctx := context.NewContextFromRaw(jp, resourceRaw) assert.NilError(t, err) + ctxMap, err := unmarshalToMap(resourceRaw) + assert.NilError(t, err) + + ctx := context.NewContextFromRaw(jp, ctxMap) pattern, err = SubstituteAll(logr.Discard(), ctx, pattern) assert.NilError(t, err) @@ -633,7 +639,10 @@ func Test_variableSubstitution_array(t *testing.T) { err := json.Unmarshal(ruleRaw, &rule) assert.NilError(t, err) - ctx := context.NewContextFromRaw(jp, configmapRaw) + ctxMap, err := unmarshalToMap(configmapRaw) + assert.NilError(t, err) + + ctx := context.NewContextFromRaw(jp, ctxMap) context.AddResource(ctx, resourceRaw) vars, err := SubstituteAllInRule(logr.Discard(), ctx, rule) @@ -1171,7 +1180,11 @@ func Test_ReplacingEscpNestedVariableWhenDeleting(t *testing.T) { if err != nil { t.Error(err) } - ctx := context.NewContextFromRaw(jp, resourceRaw) + + ctxMap, err := unmarshalToMap(resourceRaw) + assert.NilError(t, err) + + ctx := context.NewContextFromRaw(jp, ctxMap) assert.NilError(t, err) pattern, err = SubstituteAll(logr.Discard(), ctx, pattern) diff --git a/pkg/validation/policy/validate.go b/pkg/validation/policy/validate.go index b6c5f1483d29..ece70d89c512 100644 --- a/pkg/validation/policy/validate.go +++ b/pkg/validation/policy/validate.go @@ -38,7 +38,7 @@ import ( ) var ( - allowedVariables = regexp.MustCompile(`request\.|serviceAccountName|serviceAccountNamespace|element|elementIndex|@|images|images\.|image\.|([a-z_0-9]+\()[^{}]`) + allowedVariables = enginecontext.ReservedKeys allowedVariablesBackground = regexp.MustCompile(`request\.|element|elementIndex|@|images|images\.|image\.|([a-z_0-9]+\()[^{}]`) allowedVariablesInTarget = regexp.MustCompile(`request\.|serviceAccountName|serviceAccountNamespace|element|elementIndex|@|images|images\.|image\.|target\.|([a-z_0-9]+\()[^{}]`) allowedVariablesBackgroundInTarget = regexp.MustCompile(`request\.|element|elementIndex|@|images|images\.|image\.|target\.|([a-z_0-9]+\()[^{}]`)