From 2bb687550cb4daf625bfdb154790e4d3374472c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Charles-Edouard=20Br=C3=A9t=C3=A9ch=C3=A9?= Date: Wed, 19 Mar 2025 10:03:23 +0100 Subject: [PATCH] feat: add imagedata cel lib (#12442) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Charles-Edouard Brétéché --- pkg/cel/engine/vpolengine.go | 2 +- pkg/cel/libs/globalcontext/impl_test.go | 48 +++++++-------- pkg/cel/libs/http/http.go | 41 +++---------- pkg/cel/libs/http/impl_test.go | 12 ++-- pkg/cel/libs/http/lib.go | 2 - pkg/cel/libs/http/types.go | 27 +++++++++ pkg/cel/libs/image/lib.go | 4 +- pkg/cel/libs/image/lib_test.go | 2 +- pkg/cel/libs/image/types.go | 4 +- pkg/cel/libs/imagedata/impl.go | 26 +++++++++ pkg/cel/libs/imagedata/impl_test.go | 58 +++++++++++++++++++ pkg/cel/libs/imagedata/lib.go | 58 +++++++++++++++++++ pkg/cel/libs/imagedata/lib_test.go | 20 +++++++ pkg/cel/libs/imagedata/types.go | 15 +++++ pkg/cel/libs/resource/impl.go | 15 ----- pkg/cel/libs/resource/impl_test.go | 49 ---------------- pkg/cel/libs/resource/lib.go | 15 ----- pkg/cel/libs/resource/mock.go | 4 +- pkg/cel/libs/resource/types.go | 35 +---------- pkg/cel/policy/compiler.go | 6 +- pkg/cel/policy/context.go | 6 +- pkg/cel/policy/fake_context.go | 2 +- pkg/cel/policy/policy.go | 33 ++++++----- pkg/cel/policy/utils.go | 4 +- pkg/imageverification/evaluator/compiler.go | 3 +- .../context/image-data/policy.yaml | 2 +- 26 files changed, 279 insertions(+), 214 deletions(-) create mode 100644 pkg/cel/libs/http/types.go create mode 100644 pkg/cel/libs/imagedata/impl.go create mode 100644 pkg/cel/libs/imagedata/impl_test.go create mode 100644 pkg/cel/libs/imagedata/lib.go create mode 100644 pkg/cel/libs/imagedata/lib_test.go create mode 100644 pkg/cel/libs/imagedata/types.go diff --git a/pkg/cel/engine/vpolengine.go b/pkg/cel/engine/vpolengine.go index 824cbd4519..49bbb0ec50 100644 --- a/pkg/cel/engine/vpolengine.go +++ b/pkg/cel/engine/vpolengine.go @@ -206,7 +206,7 @@ func (e *engine) matchPolicy(policy CompiledValidatingPolicy, attr admission.Att return false, -1, nil } -func (e *engine) handlePolicy(ctx context.Context, policy CompiledValidatingPolicy, jsonPayload interface{}, attr admission.Attributes, request *admissionv1.AdmissionRequest, namespace runtime.Object, context policy.ContextInterface) ValidatingPolicyResponse { +func (e *engine) handlePolicy(ctx context.Context, policy CompiledValidatingPolicy, jsonPayload any, attr admission.Attributes, request *admissionv1.AdmissionRequest, namespace runtime.Object, context policy.ContextInterface) ValidatingPolicyResponse { response := ValidatingPolicyResponse{ Actions: policy.Actions, Policy: policy.Policy, diff --git a/pkg/cel/libs/globalcontext/impl_test.go b/pkg/cel/libs/globalcontext/impl_test.go index 0575ee3137..2533d74de7 100644 --- a/pkg/cel/libs/globalcontext/impl_test.go +++ b/pkg/cel/libs/globalcontext/impl_test.go @@ -28,41 +28,34 @@ func Test_impl_get_globalreference_string_string(t *testing.T) { prog, err := env.Program(ast) assert.NoError(t, err) assert.NotNil(t, prog) - tests := []struct { name string gctxStoreData map[string]store.Entry expectedValue any expectedError string - }{ - { - name: "global context entry not found", - gctxStoreData: map[string]store.Entry{}, - expectedError: "global context entry not found", + }{{ + name: "global context entry not found", + gctxStoreData: map[string]store.Entry{}, + expectedError: "global context entry not found", + }, { + name: "global context entry returns error", + gctxStoreData: map[string]store.Entry{ + "foo": &resource.MockEntry{Err: errors.New("get entry error")}, }, - { - name: "global context entry returns error", - gctxStoreData: map[string]store.Entry{ - "foo": &resource.MockEntry{Err: errors.New("get entry error")}, - }, - expectedError: "get entry error", + expectedError: "get entry error", + }, { + name: "global context entry returns string", + gctxStoreData: map[string]store.Entry{ + "foo": &resource.MockEntry{Data: "stringValue"}, }, - { - name: "global context entry returns string", - gctxStoreData: map[string]store.Entry{ - "foo": &resource.MockEntry{Data: "stringValue"}, - }, - expectedValue: "stringValue", + expectedValue: "stringValue", + }, { + name: "global context entry returns map", + gctxStoreData: map[string]store.Entry{ + "foo": &resource.MockEntry{Data: map[string]any{"key": "value"}}, }, - { - name: "global context entry returns map", - gctxStoreData: map[string]store.Entry{ - "foo": &resource.MockEntry{Data: map[string]interface{}{"key": "value"}}, - }, - expectedValue: map[string]interface{}{"key": "value"}, - }, - } - + expectedValue: map[string]any{"key": "value"}, + }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockStore := &resource.MockGctxStore{Data: tt.gctxStoreData} @@ -78,7 +71,6 @@ func Test_impl_get_globalreference_string_string(t *testing.T) { }}, } out, _, err := prog.Eval(data) - if tt.expectedError != "" { assert.Error(t, err) assert.Contains(t, err.Error(), tt.expectedError) diff --git a/pkg/cel/libs/http/http.go b/pkg/cel/libs/http/http.go index 14033cc6e8..31a6c278be 100644 --- a/pkg/cel/libs/http/http.go +++ b/pkg/cel/libs/http/http.go @@ -14,25 +14,15 @@ import ( "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" ) -type HttpInterface interface { - Get(url string, headers map[string]string) (map[string]any, error) - Post(url string, data map[string]any, headers map[string]string) (map[string]any, error) - Client(caBundle string) (HttpInterface, error) -} - -type ClientInterface interface { +type clientInterface interface { Do(*http.Request) (*http.Response, error) } -type HTTP struct { - HttpInterface +type httpProvider struct { + client clientInterface } -type HttpProvider struct { - client ClientInterface -} - -func (r *HttpProvider) Get(url string, headers map[string]string) (map[string]any, error) { +func (r *httpProvider) Get(url string, headers map[string]string) (map[string]any, error) { req, err := http.NewRequestWithContext(context.TODO(), "GET", url, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %v", err) @@ -43,12 +33,11 @@ func (r *HttpProvider) Get(url string, headers map[string]string) (map[string]an return r.executeRequest(r.client, req) } -func (r *HttpProvider) Post(url string, data map[string]any, headers map[string]string) (map[string]any, error) { +func (r *httpProvider) Post(url string, data map[string]any, headers map[string]string) (map[string]any, error) { body, err := buildRequestData(data) if err != nil { return nil, fmt.Errorf("failed to encode request data: %v", err) } - req, err := http.NewRequestWithContext(context.TODO(), "POST", url, body) if err != nil { return nil, fmt.Errorf("failed to create request: %v", err) @@ -59,31 +48,26 @@ func (r *HttpProvider) Post(url string, data map[string]any, headers map[string] return r.executeRequest(r.client, req) } -func (r *HttpProvider) executeRequest(client ClientInterface, req *http.Request) (map[string]any, error) { +func (r *httpProvider) executeRequest(client clientInterface, req *http.Request) (map[string]any, error) { resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("request failed: %v", err) } defer resp.Body.Close() - if resp.StatusCode < 200 || resp.StatusCode >= 300 { return nil, fmt.Errorf("HTTP %s", resp.Status) } - body := make(map[string]any) - if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { return nil, fmt.Errorf("Unable to decode JSON body %v", err) } - return body, nil } -func (r *HttpProvider) Client(caBundle string) (HttpInterface, error) { +func (r *httpProvider) Client(caBundle string) (HttpInterface, error) { if caBundle == "" { return r, nil } - caCertPool := x509.NewCertPool() if ok := caCertPool.AppendCertsFromPEM([]byte(caBundle)); !ok { return nil, fmt.Errorf("failed to parse PEM CA bundle for APICall") @@ -94,7 +78,7 @@ func (r *HttpProvider) Client(caBundle string) (HttpInterface, error) { MinVersion: tls.VersionTLS12, }, } - return &HttpProvider{ + return &httpProvider{ client: &http.Client{ Transport: tracing.Transport(transport, otelhttp.WithFilter(tracing.RequestFilterIsInSpan)), }, @@ -106,14 +90,5 @@ func buildRequestData(data map[string]any) (io.Reader, error) { if err := json.NewEncoder(buffer).Encode(data); err != nil { return nil, fmt.Errorf("failed to encode HTTP POST data %v: %w", data, err) } - return buffer, nil } - -func NewHTTP() HTTP { - return HTTP{ - HttpInterface: &HttpProvider{ - client: http.DefaultClient, - }, - } -} diff --git a/pkg/cel/libs/http/impl_test.go b/pkg/cel/libs/http/impl_test.go index cf48037f70..e1628bb941 100644 --- a/pkg/cel/libs/http/impl_test.go +++ b/pkg/cel/libs/http/impl_test.go @@ -55,7 +55,7 @@ func Test_impl_get_request(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, prog) out, _, err := prog.Eval(map[string]any{ - "http": HTTP{&HttpProvider{ + "http": HTTP{&httpProvider{ client: testClient{ doFunc: func(req *http.Request) (*http.Response, error) { assert.Equal(t, req.URL.String(), "http://localhost:8080") @@ -90,7 +90,7 @@ func Test_impl_get_request_with_headers(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, prog) out, _, err := prog.Eval(map[string]any{ - "http": HTTP{&HttpProvider{ + "http": HTTP{&httpProvider{ client: testClient{ doFunc: func(req *http.Request) (*http.Response, error) { assert.Equal(t, req.URL.String(), "http://localhost:8080") @@ -126,7 +126,7 @@ func Test_impl_post_request(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, prog) out, _, err := prog.Eval(map[string]any{ - "http": HTTP{&HttpProvider{ + "http": HTTP{&httpProvider{ client: testClient{ doFunc: func(req *http.Request) (*http.Response, error) { assert.Equal(t, req.URL.String(), "http://localhost:8080") @@ -165,7 +165,7 @@ func Test_impl_post_request_with_headers(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, prog) out, _, err := prog.Eval(map[string]any{ - "http": HTTP{&HttpProvider{ + "http": HTTP{&httpProvider{ client: testClient{ doFunc: func(req *http.Request) (*http.Response, error) { assert.Equal(t, req.URL.String(), "http://localhost:8080") @@ -207,9 +207,9 @@ func Test_impl_http_client_string(t *testing.T) { assert.NotNil(t, prog) out, _, err := prog.Eval(map[string]any{ "pem": pemExample, - "http": HTTP{&HttpProvider{}}, + "http": HTTP{&httpProvider{}}, }) assert.NoError(t, err) - reqProvider := out.Value().(*HttpProvider) + reqProvider := out.Value().(*httpProvider) assert.NotNil(t, reqProvider) } diff --git a/pkg/cel/libs/http/lib.go b/pkg/cel/libs/http/lib.go index 29f6fe646b..7c34033b8b 100644 --- a/pkg/cel/libs/http/lib.go +++ b/pkg/cel/libs/http/lib.go @@ -12,8 +12,6 @@ import ( const libraryName = "kyverno.http" -var HTTPType = types.DynType - type lib struct{} func Lib() cel.EnvOption { diff --git a/pkg/cel/libs/http/types.go b/pkg/cel/libs/http/types.go new file mode 100644 index 0000000000..e7993f1b36 --- /dev/null +++ b/pkg/cel/libs/http/types.go @@ -0,0 +1,27 @@ +package http + +import ( + "net/http" + + "github.com/google/cel-go/common/types" +) + +var HTTPType = types.NewOpaqueType("http.HTTP") + +type HttpInterface interface { + Get(url string, headers map[string]string) (map[string]any, error) + Post(url string, data map[string]any, headers map[string]string) (map[string]any, error) + Client(caBundle string) (HttpInterface, error) +} + +type HTTP struct { + HttpInterface +} + +func NewHTTP() HTTP { + return HTTP{ + HttpInterface: &httpProvider{ + client: http.DefaultClient, + }, + } +} diff --git a/pkg/cel/libs/image/lib.go b/pkg/cel/libs/image/lib.go index 4d3521d7c8..4310e69b11 100644 --- a/pkg/cel/libs/image/lib.go +++ b/pkg/cel/libs/image/lib.go @@ -7,6 +7,8 @@ import ( "github.com/google/go-containerregistry/pkg/name" ) +const libraryName = "kyverno.image" + func ImageLib() cel.EnvOption { return cel.Lib(imageLib) } @@ -16,7 +18,7 @@ var imageLib = &imageLibType{} type imageLibType struct{} func (*imageLibType) LibraryName() string { - return "kyverno.Image" + return libraryName } func (*imageLibType) Types() []*cel.Type { diff --git a/pkg/cel/libs/image/lib_test.go b/pkg/cel/libs/image/lib_test.go index 24068c26fd..261589f900 100644 --- a/pkg/cel/libs/image/lib_test.go +++ b/pkg/cel/libs/image/lib_test.go @@ -66,7 +66,7 @@ func testImageLib(t *testing.T, expr string, expectResult ref.Val, expectRuntime if err != nil { t.Fatalf("%v", err) } - res, _, err := prog.Eval(map[string]interface{}{}) + res, _, err := prog.Eval(map[string]any{}) if len(expectRuntimeErrPattern) > 0 { if err == nil { t.Fatalf("no runtime error thrown. Expected: %v", expectRuntimeErrPattern) diff --git a/pkg/cel/libs/image/types.go b/pkg/cel/libs/image/types.go index 0853256dd5..89e2a25083 100644 --- a/pkg/cel/libs/image/types.go +++ b/pkg/cel/libs/image/types.go @@ -24,7 +24,7 @@ type Image struct { ImageReference } -func (v Image) ConvertToNative(typeDesc reflect.Type) (interface{}, error) { +func (v Image) ConvertToNative(typeDesc reflect.Type) (any, error) { if reflect.TypeOf(v.ImageReference).AssignableTo(typeDesc) { return v.ImageReference, nil } @@ -55,6 +55,6 @@ func (v Image) Type() ref.Type { return ImageType } -func (v Image) Value() interface{} { +func (v Image) Value() any { return v.ImageReference } diff --git a/pkg/cel/libs/imagedata/impl.go b/pkg/cel/libs/imagedata/impl.go new file mode 100644 index 0000000000..04293f81c5 --- /dev/null +++ b/pkg/cel/libs/imagedata/impl.go @@ -0,0 +1,26 @@ +package imagedata + +import ( + "github.com/google/cel-go/common/types" + "github.com/google/cel-go/common/types/ref" + "github.com/kyverno/kyverno/pkg/cel/utils" +) + +type impl struct { + types.Adapter +} + +func (c *impl) get_imagedata_string(ctx ref.Val, image ref.Val) ref.Val { + if self, err := utils.ConvertToNative[Context](ctx); err != nil { + return types.WrapErr(err) + } else if image, err := utils.ConvertToNative[string](image); err != nil { + return types.WrapErr(err) + } else { + globalRef, err := self.GetImageData(image) + if err != nil { + // Errors are not expected here since Parse is a more lenient parser than ParseRequestURI. + return types.NewErr("failed to get image data: %v", err) + } + return c.NativeToValue(globalRef) + } +} diff --git a/pkg/cel/libs/imagedata/impl_test.go b/pkg/cel/libs/imagedata/impl_test.go new file mode 100644 index 0000000000..02aeadcc49 --- /dev/null +++ b/pkg/cel/libs/imagedata/impl_test.go @@ -0,0 +1,58 @@ +package imagedata + +import ( + "context" + "encoding/json" + "strings" + "testing" + + "github.com/google/cel-go/cel" + "github.com/kyverno/kyverno/pkg/cel/libs/resource" + "github.com/kyverno/kyverno/pkg/imageverification/imagedataloader" + "github.com/stretchr/testify/assert" +) + +func Test_impl_get_imagedata_string(t *testing.T) { + opts := Lib() + base, err := cel.NewEnv(opts) + assert.NoError(t, err) + assert.NotNil(t, base) + options := []cel.EnvOption{ + cel.Variable("imagedata", ContextType), + } + env, err := base.Extend(options...) + assert.NoError(t, err) + assert.NotNil(t, env) + ast, issues := env.Compile(`imagedata.Get("ghcr.io/kyverno/kyverno:latest").resolvedImage`) + assert.Nil(t, issues) + assert.NotNil(t, ast) + prog, err := env.Program(ast) + assert.NoError(t, err) + assert.NotNil(t, prog) + data := map[string]any{ + "imagedata": Context{&resource.MockCtx{ + GetImageDataFunc: func(image string) (map[string]any, error) { + idl, err := imagedataloader.New(nil) + assert.NoError(t, err) + data, err := idl.FetchImageData(context.TODO(), image) + if err != nil { + return nil, err + } + raw, err := json.Marshal(data.Data()) + if err != nil { + return nil, err + } + var apiData map[string]any + err = json.Unmarshal(raw, &apiData) + if err != nil { + return nil, err + } + return apiData, nil + }, + }, + }} + out, _, err := prog.Eval(data) + assert.NoError(t, err) + resolvedImg := out.Value().(string) + assert.True(t, strings.HasPrefix(resolvedImg, "ghcr.io/kyverno/kyverno:latest@sha256:")) +} diff --git a/pkg/cel/libs/imagedata/lib.go b/pkg/cel/libs/imagedata/lib.go new file mode 100644 index 0000000000..bd77489bc1 --- /dev/null +++ b/pkg/cel/libs/imagedata/lib.go @@ -0,0 +1,58 @@ +package imagedata + +import ( + "reflect" + + "github.com/google/cel-go/cel" + "github.com/google/cel-go/common/types" + "github.com/google/cel-go/ext" +) + +const libraryName = "kyverno.imagedata" + +type lib struct{} + +func Lib() cel.EnvOption { + // create the cel lib env option + return cel.Lib(&lib{}) +} + +func (*lib) LibraryName() string { + return libraryName +} + +func (c *lib) CompileOptions() []cel.EnvOption { + return []cel.EnvOption{ + ext.NativeTypes(reflect.TypeFor[Context]()), + c.extendEnv, + } +} + +func (*lib) ProgramOptions() []cel.ProgramOption { + return []cel.ProgramOption{} +} + +func (c *lib) extendEnv(env *cel.Env) (*cel.Env, error) { + // create implementation, recording the envoy types aware adapter + impl := impl{ + Adapter: env.CELTypeAdapter(), + } + // build our function overloads + libraryDecls := map[string][]cel.FunctionOpt{ + "Get": { + cel.MemberOverload( + "imagedata_get_string", + []*cel.Type{ContextType, types.StringType}, + types.DynType, + cel.BinaryBinding(impl.get_imagedata_string), + ), + }, + } + // create env options corresponding to our function overloads + options := []cel.EnvOption{} + for name, overloads := range libraryDecls { + options = append(options, cel.Function(name, overloads...)) + } + // extend environment with our function overloads + return env.Extend(options...) +} diff --git a/pkg/cel/libs/imagedata/lib_test.go b/pkg/cel/libs/imagedata/lib_test.go new file mode 100644 index 0000000000..6e976aaf4a --- /dev/null +++ b/pkg/cel/libs/imagedata/lib_test.go @@ -0,0 +1,20 @@ +package imagedata + +import ( + "testing" + + "github.com/google/cel-go/cel" + "github.com/stretchr/testify/assert" +) + +func TestLib(t *testing.T) { + opts := Lib() + env, err := cel.NewEnv(opts) + assert.NoError(t, err) + assert.NotNil(t, env) +} + +func Test_lib_LibraryName(t *testing.T) { + var l lib + assert.Equal(t, libraryName, l.LibraryName()) +} diff --git a/pkg/cel/libs/imagedata/types.go b/pkg/cel/libs/imagedata/types.go new file mode 100644 index 0000000000..90aa85ca7b --- /dev/null +++ b/pkg/cel/libs/imagedata/types.go @@ -0,0 +1,15 @@ +package imagedata + +import ( + "github.com/google/cel-go/common/types" +) + +var ContextType = types.NewOpaqueType("imagedata.Context") + +type ContextInterface interface { + GetImageData(string) (map[string]any, error) +} + +type Context struct { + ContextInterface +} diff --git a/pkg/cel/libs/resource/impl.go b/pkg/cel/libs/resource/impl.go index bacec9e896..ad7958c429 100644 --- a/pkg/cel/libs/resource/impl.go +++ b/pkg/cel/libs/resource/impl.go @@ -10,21 +10,6 @@ type impl struct { types.Adapter } -func (c *impl) get_imagedata_string(ctx ref.Val, image ref.Val) ref.Val { - if self, err := utils.ConvertToNative[Context](ctx); err != nil { - return types.WrapErr(err) - } else if image, err := utils.ConvertToNative[string](image); err != nil { - return types.WrapErr(err) - } else { - globalRef, err := self.GetImageData(image) - if err != nil { - // Errors are not expected here since Parse is a more lenient parser than ParseRequestURI. - return types.NewErr("failed to get image data: %v", err) - } - return c.NativeToValue(globalRef) - } -} - func (c *impl) list_resources_string_string_string(args ...ref.Val) ref.Val { if self, err := utils.ConvertToNative[Context](args[0]); err != nil { return types.WrapErr(err) diff --git a/pkg/cel/libs/resource/impl_test.go b/pkg/cel/libs/resource/impl_test.go index 5996eefce7..ce00862c2c 100644 --- a/pkg/cel/libs/resource/impl_test.go +++ b/pkg/cel/libs/resource/impl_test.go @@ -1,62 +1,13 @@ package resource import ( - "context" - "encoding/json" - "strings" "testing" "github.com/google/cel-go/cel" - "github.com/kyverno/kyverno/pkg/imageverification/imagedataloader" "github.com/stretchr/testify/assert" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) -func Test_impl_get_imagedata_string(t *testing.T) { - opts := Lib() - base, err := cel.NewEnv(opts) - assert.NoError(t, err) - assert.NotNil(t, base) - options := []cel.EnvOption{ - cel.Variable("resource", ContextType), - } - env, err := base.Extend(options...) - assert.NoError(t, err) - assert.NotNil(t, env) - ast, issues := env.Compile(`resource.GetImageData("ghcr.io/kyverno/kyverno:latest").resolvedImage`) - assert.Nil(t, issues) - assert.NotNil(t, ast) - prog, err := env.Program(ast) - assert.NoError(t, err) - assert.NotNil(t, prog) - data := map[string]any{ - "resource": Context{&MockCtx{ - GetImageDataFunc: func(image string) (map[string]interface{}, error) { - idl, err := imagedataloader.New(nil) - assert.NoError(t, err) - data, err := idl.FetchImageData(context.TODO(), image) - if err != nil { - return nil, err - } - raw, err := json.Marshal(data.Data()) - if err != nil { - return nil, err - } - apiData := map[string]interface{}{} - err = json.Unmarshal(raw, &apiData) - if err != nil { - return nil, err - } - return apiData, nil - }, - }, - }} - out, _, err := prog.Eval(data) - assert.NoError(t, err) - resolvedImg := out.Value().(string) - assert.True(t, strings.HasPrefix(resolvedImg, "ghcr.io/kyverno/kyverno:latest@sha256:")) -} - func Test_impl_get_resource_string_string_string_string(t *testing.T) { opts := Lib() base, err := cel.NewEnv(opts) diff --git a/pkg/cel/libs/resource/lib.go b/pkg/cel/libs/resource/lib.go index 7593b36a77..cefd11ffc8 100644 --- a/pkg/cel/libs/resource/lib.go +++ b/pkg/cel/libs/resource/lib.go @@ -6,7 +6,6 @@ import ( "github.com/google/cel-go/cel" "github.com/google/cel-go/common/types" "github.com/google/cel-go/ext" - apiservercel "k8s.io/apiserver/pkg/cel" ) const libraryName = "kyverno.resource" @@ -18,12 +17,6 @@ func Lib() cel.EnvOption { return cel.Lib(&lib{}) } -func Types() []*apiservercel.DeclType { - return []*apiservercel.DeclType{ - imageDataType, - } -} - func (*lib) LibraryName() string { return libraryName } @@ -64,14 +57,6 @@ func (c *lib) extendEnv(env *cel.Env) (*cel.Env, error) { cel.FunctionBinding(impl.get_resource_string_string_string_string), ), }, - "GetImageData": { - cel.MemberOverload( - "resource_getimagedata_string", - []*cel.Type{ContextType, types.StringType}, - types.DynType, - cel.BinaryBinding(impl.get_imagedata_string), - ), - }, } // create env options corresponding to our function overloads options := []cel.EnvOption{} diff --git a/pkg/cel/libs/resource/mock.go b/pkg/cel/libs/resource/mock.go index a85f4495de..09cfbd5f3b 100644 --- a/pkg/cel/libs/resource/mock.go +++ b/pkg/cel/libs/resource/mock.go @@ -8,7 +8,7 @@ import ( // MOCK FOR TESTING type MockCtx struct { GetGlobalReferenceFunc func(string, string) (any, error) - GetImageDataFunc func(string) (map[string]interface{}, error) + GetImageDataFunc func(string) (map[string]any, error) ListResourcesFunc func(string, string, string) (*unstructured.UnstructuredList, error) GetResourceFunc func(string, string, string, string) (*unstructured.Unstructured, error) } @@ -17,7 +17,7 @@ func (mock *MockCtx) GetGlobalReference(n, p string) (any, error) { return mock.GetGlobalReferenceFunc(n, p) } -func (mock *MockCtx) GetImageData(n string) (map[string]interface{}, error) { +func (mock *MockCtx) GetImageData(n string) (map[string]any, error) { return mock.GetImageDataFunc(n) } diff --git a/pkg/cel/libs/resource/types.go b/pkg/cel/libs/resource/types.go index 9726da557f..8763dd7f73 100644 --- a/pkg/cel/libs/resource/types.go +++ b/pkg/cel/libs/resource/types.go @@ -3,16 +3,11 @@ package resource import ( "github.com/google/cel-go/common/types" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - apiservercel "k8s.io/apiserver/pkg/cel" ) -var ( - ContextType = types.NewOpaqueType("resource.Context") - imageDataType = BuildImageDataType() -) +var ContextType = types.NewOpaqueType("resource.Context") type ContextInterface interface { - GetImageData(string) (map[string]interface{}, error) ListResources(apiVersion, resource, namespace string) (*unstructured.UnstructuredList, error) GetResource(apiVersion, resource, namespace, name string) (*unstructured.Unstructured, error) } @@ -20,31 +15,3 @@ type ContextInterface interface { type Context struct { ContextInterface } - -func BuildImageDataType() *apiservercel.DeclType { - f := make([]*apiservercel.DeclField, 0) - f = append(f, - field("image", apiservercel.StringType, true), - field("resolvedImage", apiservercel.StringType, true), - field("registry", apiservercel.StringType, true), - field("repository", apiservercel.StringType, true), - field("tag", apiservercel.StringType, false), - field("digest", apiservercel.StringType, false), - field("imageIndex", apiservercel.DynType, false), - field("manifest", apiservercel.DynType, true), - field("config", apiservercel.DynType, true), - ) - return apiservercel.NewObjectType("imageData", fields(f...)) -} - -func field(name string, declType *apiservercel.DeclType, required bool) *apiservercel.DeclField { - return apiservercel.NewDeclField(name, declType, required, nil, nil) -} - -func fields(fields ...*apiservercel.DeclField) map[string]*apiservercel.DeclField { - result := make(map[string]*apiservercel.DeclField, len(fields)) - for _, f := range fields { - result[f.Name] = f - } - return result -} diff --git a/pkg/cel/policy/compiler.go b/pkg/cel/policy/compiler.go index 88f9ee54d0..1c3de6605f 100644 --- a/pkg/cel/policy/compiler.go +++ b/pkg/cel/policy/compiler.go @@ -10,6 +10,7 @@ import ( vpolautogen "github.com/kyverno/kyverno/pkg/cel/autogen" "github.com/kyverno/kyverno/pkg/cel/libs/globalcontext" "github.com/kyverno/kyverno/pkg/cel/libs/http" + "github.com/kyverno/kyverno/pkg/cel/libs/imagedata" "github.com/kyverno/kyverno/pkg/cel/libs/resource" "github.com/kyverno/kyverno/pkg/cel/libs/user" admissionregistrationv1 "k8s.io/api/admissionregistration/v1" @@ -20,6 +21,7 @@ import ( const ( GlobalContextKey = "globalcontext" HttpKey = "http" + ImageDataKey = "imagedata" NamespaceObjectKey = "namespaceObject" ObjectKey = "object" OldObjectKey = "oldObject" @@ -110,10 +112,10 @@ func (c *compiler) compileForKubernetes(policy *policiesv1alpha1.ValidatingPolic } var declTypes []*apiservercel.DeclType declTypes = append(declTypes, NamespaceType, RequestType) - declTypes = append(declTypes, resource.Types()...) options := []cel.EnvOption{ cel.Variable(GlobalContextKey, globalcontext.ContextType), cel.Variable(HttpKey, http.HTTPType), + cel.Variable(ImageDataKey, imagedata.ContextType), cel.Variable(NamespaceObjectKey, NamespaceType.CelType()), cel.Variable(ObjectKey, cel.DynType), cel.Variable(OldObjectKey, cel.DynType), @@ -132,7 +134,7 @@ func (c *compiler) compileForKubernetes(policy *policiesv1alpha1.ValidatingPolic panic(err) } options = append(options, declOptions...) - options = append(options, resource.Lib(), http.Lib(), user.Lib()) + options = append(options, globalcontext.Lib(), http.Lib(), imagedata.Lib(), resource.Lib(), user.Lib()) // TODO: params, authorizer, authorizer.requestResource ? env, err := base.Extend(options...) if err != nil { diff --git a/pkg/cel/policy/context.go b/pkg/cel/policy/context.go index 495a28e1be..ac4616ad32 100644 --- a/pkg/cel/policy/context.go +++ b/pkg/cel/policy/context.go @@ -66,7 +66,7 @@ func (cp *contextProvider) GetGlobalReference(name, projection string) (any, err if err != nil { return nil, err } - apiData := map[string]interface{}{} + apiData := map[string]any{} err = json.Unmarshal(raw, &apiData) if err != nil { return nil, err @@ -75,7 +75,7 @@ func (cp *contextProvider) GetGlobalReference(name, projection string) (any, err } } -func (cp *contextProvider) GetImageData(image string) (map[string]interface{}, error) { +func (cp *contextProvider) GetImageData(image string) (map[string]any, error) { // TODO: get image credentials from image verification policies? data, err := cp.imagedata.FetchImageData(context.TODO(), image) if err != nil { @@ -89,7 +89,7 @@ func isLikelyKubernetesObject(data any) bool { return false } - if m, ok := data.(map[string]interface{}); ok { + if m, ok := data.(map[string]any); ok { _, hasAPIVersion := m["apiVersion"] _, hasKind := m["kind"] return hasAPIVersion && hasKind diff --git a/pkg/cel/policy/fake_context.go b/pkg/cel/policy/fake_context.go index ac4a928631..3d80e35717 100644 --- a/pkg/cel/policy/fake_context.go +++ b/pkg/cel/policy/fake_context.go @@ -43,7 +43,7 @@ func (cp *FakeContextProvider) GetGlobalReference(string, string) (any, error) { panic("not implemented") } -func (cp *FakeContextProvider) GetImageData(string) (map[string]interface{}, error) { +func (cp *FakeContextProvider) GetImageData(string) (map[string]any, error) { panic("not implemented") } diff --git a/pkg/cel/policy/policy.go b/pkg/cel/policy/policy.go index b6907454db..9d50f1c1df 100644 --- a/pkg/cel/policy/policy.go +++ b/pkg/cel/policy/policy.go @@ -9,9 +9,10 @@ import ( "github.com/google/cel-go/common/types" "github.com/google/cel-go/common/types/ref" policiesv1alpha1 "github.com/kyverno/kyverno/api/policies.kyverno.io/v1alpha1" - globalcontextlib "github.com/kyverno/kyverno/pkg/cel/libs/globalcontext" + "github.com/kyverno/kyverno/pkg/cel/libs/globalcontext" "github.com/kyverno/kyverno/pkg/cel/libs/http" - resourcelib "github.com/kyverno/kyverno/pkg/cel/libs/resource" + "github.com/kyverno/kyverno/pkg/cel/libs/imagedata" + "github.com/kyverno/kyverno/pkg/cel/libs/resource" "github.com/kyverno/kyverno/pkg/cel/utils" "go.uber.org/multierr" admissionv1 "k8s.io/api/admission/v1" @@ -33,12 +34,13 @@ type EvaluationResult struct { } type ContextInterface interface { - resourcelib.ContextInterface - globalcontextlib.ContextInterface + globalcontext.ContextInterface + imagedata.ContextInterface + resource.ContextInterface } type CompiledPolicy interface { - Evaluate(context.Context, interface{}, admission.Attributes, *admissionv1.AdmissionRequest, runtime.Object, ContextInterface, int) (*EvaluationResult, error) + Evaluate(context.Context, any, admission.Attributes, *admissionv1.AdmissionRequest, runtime.Object, ContextInterface, int) (*EvaluationResult, error) } type CompiledValidation struct { @@ -71,17 +73,17 @@ type compiledPolicy struct { } type evaluationData struct { - Namespace interface{} - Object interface{} - OldObject interface{} - Request interface{} + Namespace any + Object any + OldObject any + Request any Context ContextInterface Variables *lazy.MapValue } func (p *compiledPolicy) Evaluate( ctx context.Context, - json interface{}, + json any, attr admission.Attributes, request *admissionv1.AdmissionRequest, namespace runtime.Object, @@ -98,7 +100,7 @@ func (p *compiledPolicy) Evaluate( func (p *compiledPolicy) evaluateJson( ctx context.Context, - json interface{}, + json any, ) (*EvaluationResult, error) { data := evaluationData{ Object: json, @@ -168,13 +170,14 @@ func (p *compiledPolicy) evaluateWithData( vars := lazy.NewMapValue(VariablesType) dataNew := map[string]any{ - ResourceKey: resourcelib.Context{ContextInterface: data.Context}, - GlobalContextKey: globalcontextlib.Context{ContextInterface: data.Context}, + GlobalContextKey: globalcontext.Context{ContextInterface: data.Context}, HttpKey: http.NewHTTP(), + ImageDataKey: imagedata.Context{ContextInterface: data.Context}, NamespaceObjectKey: data.Namespace, ObjectKey: data.Object, OldObjectKey: data.OldObject, RequestKey: data.Request, + ResourceKey: resource.Context{ContextInterface: data.Context}, VariablesKey: vars, } for name, variable := range variables { @@ -313,7 +316,7 @@ func (p *compiledPolicy) match( } } -func convertObjectToUnstructured(obj interface{}) (*unstructured.Unstructured, error) { +func convertObjectToUnstructured(obj any) (*unstructured.Unstructured, error) { if obj == nil || reflect.ValueOf(obj).IsNil() { return &unstructured.Unstructured{Object: nil}, nil } @@ -324,7 +327,7 @@ func convertObjectToUnstructured(obj interface{}) (*unstructured.Unstructured, e return &unstructured.Unstructured{Object: ret}, nil } -func objectToResolveVal(r runtime.Object) (interface{}, error) { +func objectToResolveVal(r runtime.Object) (any, error) { if r == nil || reflect.ValueOf(r).IsNil() { return nil, nil } diff --git a/pkg/cel/policy/utils.go b/pkg/cel/policy/utils.go index ea8243cc6e..5c21045030 100644 --- a/pkg/cel/policy/utils.go +++ b/pkg/cel/policy/utils.go @@ -2,12 +2,12 @@ package policy import "encoding/json" -func getValue(data any) (map[string]interface{}, error) { +func getValue(data any) (map[string]any, error) { raw, err := json.Marshal(data) if err != nil { return nil, err } - apiData := map[string]interface{}{} + apiData := map[string]any{} err = json.Unmarshal(raw, &apiData) if err != nil { return nil, err diff --git a/pkg/imageverification/evaluator/compiler.go b/pkg/imageverification/evaluator/compiler.go index 33cdd0470f..ee4687e4a5 100644 --- a/pkg/imageverification/evaluator/compiler.go +++ b/pkg/imageverification/evaluator/compiler.go @@ -6,6 +6,7 @@ import ( engine "github.com/kyverno/kyverno/pkg/cel" "github.com/kyverno/kyverno/pkg/cel/libs/globalcontext" "github.com/kyverno/kyverno/pkg/cel/libs/http" + "github.com/kyverno/kyverno/pkg/cel/libs/imagedata" "github.com/kyverno/kyverno/pkg/cel/libs/imageverify" "github.com/kyverno/kyverno/pkg/cel/libs/resource" "github.com/kyverno/kyverno/pkg/cel/libs/user" @@ -79,7 +80,7 @@ func (c *compiler) Compile(ivpolicy *policiesv1alpha1.ImageVerificationPolicy) ( for _, declType := range declTypes { options = append(options, cel.Types(declType.CelType())) } - options = append(options, imageverify.Lib(c.ictx, ivpolicy, c.lister), resource.Lib(), http.Lib(), user.Lib()) + options = append(options, globalcontext.Lib(), http.Lib(), imagedata.Lib(), imageverify.Lib(c.ictx, ivpolicy, c.lister), resource.Lib(), user.Lib()) env, err := base.Extend(options...) if err != nil { return nil, append(allErrs, field.InternalError(nil, err)) diff --git a/test/conformance/chainsaw/validating-policies/context/image-data/policy.yaml b/test/conformance/chainsaw/validating-policies/context/image-data/policy.yaml index 2fc812b57c..0fb6716efb 100644 --- a/test/conformance/chainsaw/validating-policies/context/image-data/policy.yaml +++ b/test/conformance/chainsaw/validating-policies/context/image-data/policy.yaml @@ -12,7 +12,7 @@ spec: variables: - name: image expression: >- - resource.GetImageData("ghcr.io/kyverno/kyverno:latest") + imagedata.Get("ghcr.io/kyverno/kyverno:latest") - name: accept expression: >- variables.image != null