mirror of
https://github.com/kyverno/kyverno.git
synced 2025-03-21 23:32:27 +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:
parent
637f756994
commit
c61d0735e3
16 changed files with 314 additions and 27 deletions
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
## Description
|
||||
|
||||
This test verifies that Global Context Entries are evaluated correctly.
|
||||
|
||||
## Expected Behavior
|
||||
|
||||
`new-deployment` should be created.
|
||||
|
||||
## Reference Issues
|
||||
|
||||
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,4 @@
|
|||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: test-globalcontext-apicall-correct
|
|
@ -0,0 +1,7 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: new-deployment
|
||||
namespace: test-globalcontext-apicall-correct
|
||||
labels:
|
||||
app: new-deployment
|
|
@ -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
|
|
@ -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
|
Loading…
Add table
Reference in a new issue