1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2025-03-22 07:41:10 +00:00

feat(vp): implement gctx in context library (#12055)

* feat(vp): implement gctx in context library

Signed-off-by: Khaled Emara <khaled.emara@nirmata.com>

* test(cel): add chainsaw test for validating policies gctx

Signed-off-by: Khaled Emara <khaled.emara@nirmata.com>

---------

Signed-off-by: Khaled Emara <khaled.emara@nirmata.com>
Signed-off-by: ShutingZhao <shuting@nirmata.com>
Co-authored-by: Vishal Choudhary <vishal.choudhary@nirmata.com>
Co-authored-by: shuting <shuting@nirmata.com>
Co-authored-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>
This commit is contained in:
Khaled Emara 2025-03-06 11:27:03 +02:00 committed by GitHub
parent 637f756994
commit c61d0735e3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 314 additions and 27 deletions

View file

@ -35,6 +35,7 @@ import (
"github.com/kyverno/kyverno/pkg/clients/dclient"
"github.com/kyverno/kyverno/pkg/config"
engineapi "github.com/kyverno/kyverno/pkg/engine/api"
gctxstore "github.com/kyverno/kyverno/pkg/globalcontext/store"
"github.com/kyverno/kyverno/pkg/imageverification/imagedataloader"
gitutils "github.com/kyverno/kyverno/pkg/utils/git"
policyvalidation "github.com/kyverno/kyverno/pkg/validation/policy"
@ -348,11 +349,13 @@ func (c *ApplyCommandConfig) applyValidatingPolicies(
}
eng := engine.NewEngine(provider, namespaceProvider, matching.NewMatcher())
// TODO: mock when no cluster provided
gctxStore := gctxstore.New()
var contextProvider celpolicy.Context
if dclient != nil {
contextProvider, err = celpolicy.NewContextProvider(
dclient,
[]imagedataloader.Option{imagedataloader.WithLocalCredentials(c.RegistryAccess)},
gctxStore,
)
if err != nil {
return nil, err

View file

@ -593,9 +593,11 @@ func main() {
backgroundServiceAccountName,
reportsServiceAccountName,
)
contextProvider, err := celpolicy.NewContextProvider(
setup.KyvernoDynamicClient,
nil,
gcstore,
// []imagedataloader.Option{imagedataloader.WithLocalCredentials(c.RegistryAccess)},
)
if err != nil {

View file

@ -27,13 +27,18 @@ func (c *impl) get_configmap_string_string(args ...ref.Val) ref.Val {
}
}
func (c *impl) get_globalreference_string(ctx ref.Val, name ref.Val) ref.Val {
if self, err := utils.ConvertToNative[Context](ctx); err != nil {
func (c *impl) get_globalreference_string(args ...ref.Val) ref.Val {
if len(args) != 3 {
return types.NewErr("expected 3 arguments, got %d", len(args))
}
if self, err := utils.ConvertToNative[Context](args[0]); err != nil {
return types.WrapErr(err)
} else if name, err := utils.ConvertToNative[string](name); err != nil {
} else if name, err := utils.ConvertToNative[string](args[1]); err != nil {
return types.WrapErr(err)
} else if projection, err := utils.ConvertToNative[string](args[2]); err != nil {
return types.WrapErr(err)
} else {
globalRef, err := self.GetGlobalReference(name)
globalRef, err := self.GetGlobalReference(name, projection)
if err != nil {
// Errors are not expected here since Parse is a more lenient parser than ParseRequestURI.
return types.NewErr("failed to get global reference: %v", err)

View file

@ -2,10 +2,12 @@ package context
import (
"context"
"errors"
"strings"
"testing"
"github.com/google/cel-go/cel"
"github.com/kyverno/kyverno/pkg/globalcontext/store"
"github.com/kyverno/kyverno/pkg/imageverification/imagedataloader"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@ -13,7 +15,7 @@ import (
type ctx struct {
GetConfigMapFunc func(string, string) (unstructured.Unstructured, error)
GetGlobalReferenceFunc func(string) (any, error)
GetGlobalReferenceFunc func(string, string) (any, error)
GetImageDataFunc func(string) (*imagedataloader.ImageData, error)
ListResourcesFunc func(string, string, string) (*unstructured.UnstructuredList, error)
GetResourcesFunc func(string, string, string, string) (*unstructured.Unstructured, error)
@ -23,8 +25,8 @@ func (mock *ctx) GetConfigMap(ns string, n string) (unstructured.Unstructured, e
return mock.GetConfigMapFunc(ns, n)
}
func (mock *ctx) GetGlobalReference(n string) (any, error) {
return mock.GetGlobalReferenceFunc(n)
func (mock *ctx) GetGlobalReference(n, p string) (any, error) {
return mock.GetGlobalReferenceFunc(n, p)
}
func (mock *ctx) GetImageData(n string) (*imagedataloader.ImageData, error) {
@ -71,6 +73,33 @@ func Test_impl_get_configmap_string_string(t *testing.T) {
assert.True(t, called)
}
type mockGctxStore struct {
data map[string]store.Entry
}
func (m *mockGctxStore) Get(name string) (store.Entry, bool) {
entry, ok := m.data[name]
return entry, ok
}
func (m *mockGctxStore) Set(name string, data store.Entry) {
if m.data == nil {
m.data = make(map[string]store.Entry)
}
m.data[name] = data
}
type mockEntry struct {
data any
err error
}
func (m *mockEntry) Get(_ string) (any, error) {
return m.data, m.err
}
func (m *mockEntry) Stop() {}
func Test_impl_get_globalreference_string(t *testing.T) {
opts := Lib()
base, err := cel.NewEnv(opts)
@ -82,28 +111,83 @@ func Test_impl_get_globalreference_string(t *testing.T) {
env, err := base.Extend(options...)
assert.NoError(t, err)
assert.NotNil(t, env)
ast, issues := env.Compile(`context.GetGlobalReference("foo")`)
ast, issues := env.Compile(`context.GetGlobalReference("foo", "bar")`)
assert.Nil(t, issues)
assert.NotNil(t, ast)
prog, err := env.Program(ast)
assert.NoError(t, err)
assert.NotNil(t, prog)
called := false
data := map[string]any{
"context": Context{&ctx{
GetGlobalReferenceFunc: func(string) (any, error) {
type foo struct {
s string
}
called = true
return foo{"bar"}, nil
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 returns error",
gctxStoreData: map[string]store.Entry{
"foo": &mockEntry{err: errors.New("get entry error")},
},
}},
expectedError: "get entry error",
},
{
name: "global context entry returns string",
gctxStoreData: map[string]store.Entry{
"foo": &mockEntry{data: "stringValue"},
},
expectedValue: "stringValue",
},
{
name: "global context entry returns map",
gctxStoreData: map[string]store.Entry{
"foo": &mockEntry{data: map[string]interface{}{"key": "value"}},
},
expectedValue: map[string]interface{}{"key": "value"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockStore := &mockGctxStore{data: tt.gctxStoreData}
data := map[string]any{
"context": Context{&ctx{
GetGlobalReferenceFunc: func(name string, path string) (any, error) {
ent, ok := mockStore.Get(name)
if !ok {
return nil, errors.New("global context entry not found")
}
return ent.Get(path)
},
}},
}
out, _, err := prog.Eval(data)
if tt.expectedError != "" {
assert.Error(t, err)
assert.Contains(t, err.Error(), tt.expectedError)
} else {
assert.NoError(t, err)
if tt.expectedValue == nil {
assert.Nil(t, out.Value())
} else {
assert.NotNil(t, out)
if expectedUnstructured, ok := tt.expectedValue.(unstructured.Unstructured); ok {
actualUnstructured, ok := out.Value().(unstructured.Unstructured)
assert.True(t, ok, "Expected unstructured.Unstructured, got %T", out.Value())
assert.Equal(t, expectedUnstructured, actualUnstructured)
} else {
assert.Equal(t, tt.expectedValue, out.Value())
}
}
}
})
}
out, _, err := prog.Eval(data)
assert.NoError(t, err)
assert.NotNil(t, out)
assert.True(t, called)
}
func Test_impl_get_imagedata_string(t *testing.T) {

View file

@ -52,7 +52,7 @@ func (c *lib) extendEnv(env *cel.Env) (*cel.Env, error) {
},
"GetGlobalReference": {
// TODO: should not use DynType in return
cel.MemberOverload("get_globalreference_string", []*cel.Type{ContextType, types.StringType}, types.DynType, cel.BinaryBinding(impl.get_globalreference_string)),
cel.MemberOverload("get_globalreference_string", []*cel.Type{ContextType, types.StringType, types.StringType}, types.DynType, cel.FunctionBinding(impl.get_globalreference_string)),
},
"GetImageData": {
// TODO: should not use DynType in return

View file

@ -15,7 +15,7 @@ var (
type ContextInterface interface {
GetConfigMap(string, string) (unstructured.Unstructured, error)
GetGlobalReference(string) (any, error)
GetGlobalReference(string, string) (any, error)
GetImageData(string) (*imagedataloader.ImageData, error)
ListResource(apiVersion, resource, namespace string) (*unstructured.UnstructuredList, error)
GetResource(apiVersion, resource, namespace, name string) (*unstructured.Unstructured, error)

View file

@ -2,14 +2,18 @@ package policy
import (
"context"
"encoding/json"
"errors"
contextlib "github.com/kyverno/kyverno/pkg/cel/libs/context"
"github.com/kyverno/kyverno/pkg/clients/dclient"
"github.com/kyverno/kyverno/pkg/config"
gctxstore "github.com/kyverno/kyverno/pkg/globalcontext/store"
"github.com/kyverno/kyverno/pkg/imageverification/imagedataloader"
kubeutils "github.com/kyverno/kyverno/pkg/utils/kube"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
@ -21,9 +25,14 @@ type contextProvider struct {
client kubernetes.Interface
dclient dynamic.Interface
imagedata imagedataloader.Fetcher
gctxStore gctxstore.Store
}
func NewContextProvider(client dclient.Interface, imageOpts []imagedataloader.Option) (Context, error) {
func NewContextProvider(
client dclient.Interface,
imageOpts []imagedataloader.Option,
gctxStore gctxstore.Store,
) (Context, error) {
idl, err := imagedataloader.New(client.GetKubeClient().CoreV1().Secrets(config.KyvernoNamespace()), imageOpts...)
if err != nil {
return nil, err
@ -32,6 +41,7 @@ func NewContextProvider(client dclient.Interface, imageOpts []imagedataloader.Op
client: client.GetKubeClient(),
dclient: client.GetDynamicInterface(),
imagedata: idl,
gctxStore: gctxStore,
}, nil
}
@ -47,8 +57,38 @@ func (cp *contextProvider) GetConfigMap(namespace string, name string) (unstruct
return *out, nil
}
func (cp *contextProvider) GetGlobalReference(string) (any, error) {
return nil, nil
func (cp *contextProvider) GetGlobalReference(name, projection string) (any, error) {
ent, ok := cp.gctxStore.Get(name)
if !ok {
return nil, errors.New("global context entry not found")
}
data, err := ent.Get(projection)
if err != nil {
return nil, err
}
if isLikelyKubernetesObject(data) {
out, err := kubeutils.ObjToUnstructured(data)
if err != nil {
return nil, err
}
if out != nil {
return *out, nil
} else {
return nil, errors.New("failed to convert to Unstructured")
}
} else {
raw, err := json.Marshal(data)
if err != nil {
return nil, err
}
apiData := map[string]interface{}{}
err = json.Unmarshal(raw, &apiData)
if err != nil {
return nil, err
}
return data, nil
}
}
func (cp *contextProvider) GetImageData(image string) (*imagedataloader.ImageData, error) {
@ -56,6 +96,24 @@ func (cp *contextProvider) GetImageData(image string) (*imagedataloader.ImageDat
return cp.imagedata.FetchImageData(context.TODO(), image)
}
func isLikelyKubernetesObject(data any) bool {
if data == nil {
return false
}
if m, ok := data.(map[string]interface{}); ok {
_, hasAPIVersion := m["apiVersion"]
_, hasKind := m["kind"]
return hasAPIVersion && hasKind
}
if _, ok := data.(runtime.Object); ok {
return true
}
return false
}
func (cp *contextProvider) ListResource(apiVersion, resource, namespace string) (*unstructured.UnstructuredList, error) {
groupVersion, err := schema.ParseGroupVersion(apiVersion)
if err != nil {

View file

@ -16,6 +16,7 @@ import (
"github.com/kyverno/kyverno/pkg/engine"
engineapi "github.com/kyverno/kyverno/pkg/engine/api"
"github.com/kyverno/kyverno/pkg/engine/jmespath"
gctxstore "github.com/kyverno/kyverno/pkg/globalcontext/store"
reportutils "github.com/kyverno/kyverno/pkg/utils/report"
"go.uber.org/multierr"
admissionv1 "k8s.io/api/admission/v1"
@ -154,12 +155,14 @@ func (s *scanner) ScanResource(
func(name string) *corev1.Namespace { return ns },
matching.NewMatcher(),
)
gctxStore := gctxstore.New()
// create context provider
context, err := celpolicy.NewContextProvider(
s.client,
nil,
// TODO
// []imagedataloader.Option{imagedataloader.WithLocalCredentials(c.RegistryAccess)},
gctxStore,
)
if err != nil {
logger.Error(err, "failed to create cel context provider")

View file

@ -0,0 +1,11 @@
## Description
This test verifies that Global Context Entries are evaluated correctly.
## Expected Behavior
`new-deployment` should be created.
## Reference Issues

View file

@ -0,0 +1,26 @@
apiVersion: chainsaw.kyverno.io/v1alpha1
kind: Test
metadata:
name: apicall-correct
spec:
steps:
- try:
- apply:
file: namespace.yaml
- apply:
file: main-deployment.yaml
- apply:
file: gctxentry.yaml
- sleep:
duration: 3s
- name: create policy
try:
- create:
file: policy.yaml
- sleep:
duration: 3s
- try:
- apply:
file: new-deployment.yaml
- assert:
file: new-deployment-exists.yaml

View file

@ -0,0 +1,8 @@
apiVersion: kyverno.io/v2alpha1
kind: GlobalContextEntry
metadata:
name: gctxentry-apicall-correct
spec:
apiCall:
urlPath: "/apis/apps/v1/namespaces/test-globalcontext-apicall-correct/deployments"
refreshInterval: 1h

View file

@ -0,0 +1,28 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: main-deployment
namespace: test-globalcontext-apicall-correct
labels:
app: main-deployment
spec:
replicas: 1
selector:
matchLabels:
app: main-deployment
template:
metadata:
labels:
app: main-deployment
spec:
containers:
- name: pause
image: registry.k8s.io/pause:latest
resources:
requests:
cpu: 10m
memory: 10Mi
limits:
cpu: 10m
memory: 10Mi
terminationGracePeriodSeconds: 0

View file

@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: test-globalcontext-apicall-correct

View file

@ -0,0 +1,7 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: new-deployment
namespace: test-globalcontext-apicall-correct
labels:
app: new-deployment

View file

@ -0,0 +1,28 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: new-deployment
namespace: test-globalcontext-apicall-correct
labels:
app: new-deployment
spec:
replicas: 1
selector:
matchLabels:
app: new-deployment
template:
metadata:
labels:
app: new-deployment
spec:
containers:
- name: pause
image: registry.k8s.io/pause:latest
resources:
requests:
cpu: 10m
memory: 10Mi
limits:
cpu: 10m
memory: 10Mi
terminationGracePeriodSeconds: 0

View file

@ -0,0 +1,20 @@
apiVersion: policies.kyverno.io/v1alpha1
kind: ValidatingPolicy
metadata:
name: cpol-apicall-correct
spec:
matchConstraints:
resourceRules:
- apiGroups: []
apiVersions: [v1]
operations: [CREATE, UPDATE]
resources: [pods]
variables:
- name: dcount
expression: >-
context.GetGlobalReference("gctxentry-apicall-correct", "")
validations:
- expression: >-
variables.dcount != 0
message: >-
main-deployment should exist