mirror of
https://github.com/kyverno/kyverno.git
synced 2025-03-20 23:02:36 +00:00
feat: add imagedata cel lib (#12442)
Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>
This commit is contained in:
parent
d3305512d4
commit
2bb687550c
26 changed files with 279 additions and 214 deletions
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -12,8 +12,6 @@ import (
|
|||
|
||||
const libraryName = "kyverno.http"
|
||||
|
||||
var HTTPType = types.DynType
|
||||
|
||||
type lib struct{}
|
||||
|
||||
func Lib() cel.EnvOption {
|
||||
|
|
27
pkg/cel/libs/http/types.go
Normal file
27
pkg/cel/libs/http/types.go
Normal file
|
@ -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,
|
||||
},
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
26
pkg/cel/libs/imagedata/impl.go
Normal file
26
pkg/cel/libs/imagedata/impl.go
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
58
pkg/cel/libs/imagedata/impl_test.go
Normal file
58
pkg/cel/libs/imagedata/impl_test.go
Normal file
|
@ -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:"))
|
||||
}
|
58
pkg/cel/libs/imagedata/lib.go
Normal file
58
pkg/cel/libs/imagedata/lib.go
Normal file
|
@ -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...)
|
||||
}
|
20
pkg/cel/libs/imagedata/lib_test.go
Normal file
20
pkg/cel/libs/imagedata/lib_test.go
Normal file
|
@ -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())
|
||||
}
|
15
pkg/cel/libs/imagedata/types.go
Normal file
15
pkg/cel/libs/imagedata/types.go
Normal file
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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{}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue