From 18fbc48b0155485cd78ec4d0e6050ccbb7d8e058 Mon Sep 17 00:00:00 2001 From: Alexander Zielenski Date: Mon, 22 Jan 2024 17:31:52 -0800 Subject: [PATCH] refactor: implement VAP off of policy plugin fw --- .../plugin/policy/validating/accessor.go | 95 ++ .../plugin/policy/validating/admission.go | 197 --- .../policy/validating/admission_test.go | 1362 ++++------------- .../policy/validating/controller_reconcile.go | 551 ------- .../{controller.go => dispatcher.go} | 235 +-- .../plugin/policy/validating/plugin.go | 197 +++ .../policy/validating/typechecking_test.go | 24 + 7 files changed, 702 insertions(+), 1959 deletions(-) create mode 100644 staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/accessor.go delete mode 100644 staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/admission.go delete mode 100644 staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/controller_reconcile.go rename staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/{controller.go => dispatcher.go} (71%) create mode 100644 staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/plugin.go diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/accessor.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/accessor.go new file mode 100644 index 00000000000..22bd6150ad6 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/accessor.go @@ -0,0 +1,95 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package validating + +import ( + "k8s.io/api/admissionregistration/v1beta1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apiserver/pkg/admission/plugin/policy/generic" +) + +func NewValidatingAdmissionPolicyAccessor(obj *v1beta1.ValidatingAdmissionPolicy) generic.PolicyAccessor { + return &validatingAdmissionPolicyAccessor{ + ValidatingAdmissionPolicy: obj, + } +} + +func NewValidatingAdmissionPolicyBindingAccessor(obj *v1beta1.ValidatingAdmissionPolicyBinding) generic.BindingAccessor { + return &validatingAdmissionPolicyBindingAccessor{ + ValidatingAdmissionPolicyBinding: obj, + } +} + +type validatingAdmissionPolicyAccessor struct { + *v1beta1.ValidatingAdmissionPolicy +} + +func (v *validatingAdmissionPolicyAccessor) GetNamespace() string { + return v.Namespace +} + +func (v *validatingAdmissionPolicyAccessor) GetName() string { + return v.Name +} + +func (v *validatingAdmissionPolicyAccessor) GetParamKind() *schema.GroupVersionKind { + paramKind := v.Spec.ParamKind + if paramKind == nil { + return nil + } + + groupVersion, err := schema.ParseGroupVersion(paramKind.APIVersion) + if err != nil { + // A validatingadmissionpolicy which passes validation should have + // a parseable APIVersion for its ParamKind, so this should never happen + // if the policy is valid. + // + // Return a bogus but non-nil GVK that will throw an error about the + // invalid APIVersion when the param is looked up. + return &schema.GroupVersionKind{ + Group: paramKind.APIVersion, + Version: "", + Kind: paramKind.Kind, + } + } + + return &schema.GroupVersionKind{ + Group: groupVersion.Group, + Version: groupVersion.Version, + Kind: paramKind.Kind, + } +} + +type validatingAdmissionPolicyBindingAccessor struct { + *v1beta1.ValidatingAdmissionPolicyBinding +} + +func (v *validatingAdmissionPolicyBindingAccessor) GetNamespace() string { + return v.Namespace +} + +func (v *validatingAdmissionPolicyBindingAccessor) GetName() string { + return v.Name +} + +func (v *validatingAdmissionPolicyBindingAccessor) GetPolicyName() types.NamespacedName { + return types.NamespacedName{ + Namespace: "", + Name: v.Spec.PolicyName, + } +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/admission.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/admission.go deleted file mode 100644 index 6f9290bbe1d..00000000000 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/admission.go +++ /dev/null @@ -1,197 +0,0 @@ -/* -Copyright 2022 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package validating - -import ( - "context" - "errors" - "fmt" - "io" - - "k8s.io/apimachinery/pkg/api/meta" - "k8s.io/apiserver/pkg/authorization/authorizer" - "k8s.io/apiserver/pkg/features" - "k8s.io/client-go/dynamic" - "k8s.io/component-base/featuregate" - - "k8s.io/apiserver/pkg/admission" - "k8s.io/apiserver/pkg/admission/initializer" - "k8s.io/client-go/informers" - "k8s.io/client-go/kubernetes" -) - -//////////////////////////////////////////////////////////////////////////////// -// Plugin Definition -//////////////////////////////////////////////////////////////////////////////// - -// Definition for CEL admission plugin. This is the entry point into the -// CEL admission control system. -// -// Each plugin is asked to validate every object update. - -const ( - // PluginName indicates the name of admission plug-in - PluginName = "ValidatingAdmissionPolicy" -) - -// Register registers a plugin -func Register(plugins *admission.Plugins) { - plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) { - return NewPlugin() - }) -} - -//////////////////////////////////////////////////////////////////////////////// -// Plugin Initialization & Dependency Injection -//////////////////////////////////////////////////////////////////////////////// - -type celAdmissionPlugin struct { - *admission.Handler - evaluator CELPolicyEvaluator - - inspectedFeatureGates bool - enabled bool - - // Injected Dependencies - informerFactory informers.SharedInformerFactory - client kubernetes.Interface - restMapper meta.RESTMapper - dynamicClient dynamic.Interface - stopCh <-chan struct{} - authorizer authorizer.Authorizer -} - -var _ initializer.WantsExternalKubeInformerFactory = &celAdmissionPlugin{} -var _ initializer.WantsExternalKubeClientSet = &celAdmissionPlugin{} -var _ initializer.WantsRESTMapper = &celAdmissionPlugin{} -var _ initializer.WantsDynamicClient = &celAdmissionPlugin{} -var _ initializer.WantsDrainedNotification = &celAdmissionPlugin{} -var _ initializer.WantsAuthorizer = &celAdmissionPlugin{} -var _ admission.InitializationValidator = &celAdmissionPlugin{} -var _ admission.ValidationInterface = &celAdmissionPlugin{} - -func NewPlugin() (admission.Interface, error) { - return &celAdmissionPlugin{ - Handler: admission.NewHandler(admission.Connect, admission.Create, admission.Delete, admission.Update), - }, nil -} - -func (c *celAdmissionPlugin) SetExternalKubeInformerFactory(f informers.SharedInformerFactory) { - c.informerFactory = f -} - -func (c *celAdmissionPlugin) SetExternalKubeClientSet(client kubernetes.Interface) { - c.client = client -} - -func (c *celAdmissionPlugin) SetRESTMapper(mapper meta.RESTMapper) { - c.restMapper = mapper -} - -func (c *celAdmissionPlugin) SetDynamicClient(client dynamic.Interface) { - c.dynamicClient = client -} - -func (c *celAdmissionPlugin) SetDrainedNotification(stopCh <-chan struct{}) { - c.stopCh = stopCh -} - -func (c *celAdmissionPlugin) SetAuthorizer(authorizer authorizer.Authorizer) { - c.authorizer = authorizer -} -func (c *celAdmissionPlugin) InspectFeatureGates(featureGates featuregate.FeatureGate) { - if featureGates.Enabled(features.ValidatingAdmissionPolicy) { - c.enabled = true - } - c.inspectedFeatureGates = true -} - -// ValidateInitialization - once clientset and informer factory are provided, creates and starts the admission controller -func (c *celAdmissionPlugin) ValidateInitialization() error { - if !c.inspectedFeatureGates { - return fmt.Errorf("%s did not see feature gates", PluginName) - } - if !c.enabled { - return nil - } - if c.informerFactory == nil { - return errors.New("missing informer factory") - } - if c.client == nil { - return errors.New("missing kubernetes client") - } - if c.restMapper == nil { - return errors.New("missing rest mapper") - } - if c.dynamicClient == nil { - return errors.New("missing dynamic client") - } - if c.stopCh == nil { - return errors.New("missing stop channel") - } - if c.authorizer == nil { - return errors.New("missing authorizer") - } - c.evaluator = NewAdmissionController(c.informerFactory, c.client, c.restMapper, c.dynamicClient, c.authorizer) - if err := c.evaluator.ValidateInitialization(); err != nil { - return err - } - - c.SetReadyFunc(c.evaluator.HasSynced) - go c.evaluator.Run(c.stopCh) - return nil -} - -//////////////////////////////////////////////////////////////////////////////// -// admission.ValidationInterface -//////////////////////////////////////////////////////////////////////////////// - -func (c *celAdmissionPlugin) Handles(operation admission.Operation) bool { - return true -} - -func (c *celAdmissionPlugin) Validate( - ctx context.Context, - a admission.Attributes, - o admission.ObjectInterfaces, -) (err error) { - if !c.enabled { - return nil - } - - // isPolicyResource determines if an admission.Attributes object is describing - // the admission of a ValidatingAdmissionPolicy or a ValidatingAdmissionPolicyBinding - if isPolicyResource(a) { - return - } - - if !c.WaitForReady() { - return admission.NewForbidden(a, fmt.Errorf("not yet ready to handle request")) - } - - return c.evaluator.Validate(ctx, a, o) -} - -func isPolicyResource(attr admission.Attributes) bool { - gvk := attr.GetResource() - if gvk.Group == "admissionregistration.k8s.io" { - if gvk.Resource == "validatingadmissionpolicies" || gvk.Resource == "validatingadmissionpolicybindings" { - return true - } - } - return false -} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/admission_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/admission_test.go index 8e26fa44974..cc8ee5ccbe4 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/admission_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/admission_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package validating +package validating_test import ( "context" @@ -25,70 +25,29 @@ import ( "testing" "time" - celgo "github.com/google/cel-go/cel" "github.com/stretchr/testify/require" - admissionv1 "k8s.io/api/admission/v1" - admissionRegistrationv1 "k8s.io/api/admissionregistration/v1" "k8s.io/api/admissionregistration/v1beta1" v1 "k8s.io/api/core/v1" - k8serrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" utiljson "k8s.io/apimachinery/pkg/util/json" "k8s.io/apimachinery/pkg/util/sets" - "k8s.io/apimachinery/pkg/util/wait" "k8s.io/apiserver/pkg/admission" - "k8s.io/apiserver/pkg/admission/initializer" - "k8s.io/apiserver/pkg/admission/plugin/cel" - "k8s.io/apiserver/pkg/admission/plugin/policy/internal/generic" - "k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions" + "k8s.io/apiserver/pkg/admission/plugin/policy/generic" + "k8s.io/apiserver/pkg/admission/plugin/policy/matching" + "k8s.io/apiserver/pkg/admission/plugin/policy/validating" auditinternal "k8s.io/apiserver/pkg/apis/audit" "k8s.io/apiserver/pkg/authorization/authorizer" - "k8s.io/apiserver/pkg/cel/environment" - "k8s.io/apiserver/pkg/features" "k8s.io/apiserver/pkg/warning" - dynamicfake "k8s.io/client-go/dynamic/fake" - "k8s.io/client-go/informers" - "k8s.io/client-go/kubernetes/fake" - clienttesting "k8s.io/client-go/testing" - "k8s.io/client-go/tools/cache" - "k8s.io/component-base/featuregate" - "k8s.io/klog/v2" ) var ( - scheme *runtime.Scheme = func() *runtime.Scheme { - res := runtime.NewScheme() - res.AddKnownTypeWithName(paramsGVK, &unstructured.Unstructured{}) - res.AddKnownTypeWithName(schema.GroupVersionKind{ - Group: paramsGVK.Group, - Version: paramsGVK.Version, - Kind: paramsGVK.Kind + "List", - }, &unstructured.UnstructuredList{}) - - res.AddKnownTypeWithName(clusterScopedParamsGVK, &unstructured.Unstructured{}) - res.AddKnownTypeWithName(schema.GroupVersionKind{ - Group: clusterScopedParamsGVK.Group, - Version: clusterScopedParamsGVK.Version, - Kind: clusterScopedParamsGVK.Kind + "List", - }, &unstructured.UnstructuredList{}) - - if err := v1beta1.AddToScheme(res); err != nil { - panic(err) - } - - if err := fake.AddToScheme(res); err != nil { - panic(err) - } - - return res - }() - clusterScopedParamsGVK schema.GroupVersionKind = schema.GroupVersionKind{ Group: "example.com", Version: "v1", @@ -101,28 +60,6 @@ var ( Kind: "ParamsConfig", } - fakeRestMapper *meta.DefaultRESTMapper = func() *meta.DefaultRESTMapper { - res := meta.NewDefaultRESTMapper([]schema.GroupVersion{ - { - Group: "", - Version: "v1", - }, - }) - - res.Add(paramsGVK, meta.RESTScopeNamespace) - res.Add(clusterScopedParamsGVK, meta.RESTScopeRoot) - res.Add(definitionGVK, meta.RESTScopeRoot) - res.Add(bindingGVK, meta.RESTScopeRoot) - res.Add(v1.SchemeGroupVersion.WithKind("ConfigMap"), meta.RESTScopeNamespace) - return res - }() - - definitionGVK schema.GroupVersionKind = must3(scheme.ObjectKinds(&v1beta1.ValidatingAdmissionPolicy{}))[0] - bindingGVK schema.GroupVersionKind = must3(scheme.ObjectKinds(&v1beta1.ValidatingAdmissionPolicyBinding{}))[0] - - definitionsGVR schema.GroupVersionResource = must(fakeRestMapper.RESTMapping(definitionGVK.GroupKind(), definitionGVK.Version)).Resource - bindingsGVR schema.GroupVersionResource = must(fakeRestMapper.RESTMapping(bindingGVK.GroupKind(), bindingGVK.Version)).Resource - // Common objects denyPolicy *v1beta1.ValidatingAdmissionPolicy = &v1beta1.ValidatingAdmissionPolicy{ ObjectMeta: metav1.ObjectMeta{ @@ -249,100 +186,82 @@ func newClusterScopedParam(name string, labels map[string]string) *unstructured. return res } -// Interface which has fake compile functionality for use in tests -// So that we can test the controller without pulling in any CEL functionality +var _ validating.Validator = validateFunc(nil) + +type validateFunc func( + ctx context.Context, + matchResource schema.GroupVersionResource, + versionedAttr *admission.VersionedAttributes, + versionedParams runtime.Object, + namespace *v1.Namespace, + runtimeCELCostBudget int64, + authz authorizer.Authorizer) validating.ValidateResult + type fakeCompiler struct { - CompileFuncs map[string]func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter + ValidateFuncs map[types.NamespacedName]validating.Validator + + lock sync.Mutex + NumCompiles map[types.NamespacedName]int } -var _ cel.FilterCompiler = &fakeCompiler{} - -func (f *fakeCompiler) HasSynced() bool { - return true +func (f *fakeCompiler) getNumCompiles(p *validating.Policy) int { + f.lock.Lock() + defer f.lock.Unlock() + return f.NumCompiles[types.NamespacedName{ + Name: p.Name, + Namespace: p.Namespace, + }] } -func (f *fakeCompiler) Compile( - expressions []cel.ExpressionAccessor, - options cel.OptionalVariableDeclarations, - envType environment.Type, -) cel.Filter { - if len(expressions) > 0 && expressions[0] != nil { - key := expressions[0].GetExpression() - if fun, ok := f.CompileFuncs[key]; ok { - return fun(expressions, options) +func (f *fakeCompiler) RegisterDefinition(definition *validating.Policy, vf validateFunc) { + if f.ValidateFuncs == nil { + f.ValidateFuncs = make(map[types.NamespacedName]validating.Validator) + } + + f.ValidateFuncs[types.NamespacedName{ + Name: definition.Name, + Namespace: definition.Namespace, + }] = vf +} + +func (f *fakeCompiler) CompilePolicy(policy *validating.Policy) validating.Validator { + nn := types.NamespacedName{ + Name: policy.Name, + Namespace: policy.Namespace, + } + + defer func() { + f.lock.Lock() + defer f.lock.Unlock() + if f.NumCompiles == nil { + f.NumCompiles = make(map[types.NamespacedName]int) } - } - return &fakeFilter{} + f.NumCompiles[nn]++ + }() + return f.ValidateFuncs[nn] } -func (f *fakeCompiler) RegisterDefinition(definition *v1beta1.ValidatingAdmissionPolicy, compileFunc func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter) { - //Key must be something that we can decipher from the inputs to Validate so using expression which will be passed to validate on the filter - key := definition.Spec.Validations[0].Expression - if compileFunc != nil { - if f.CompileFuncs == nil { - f.CompileFuncs = make(map[string]func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter) - } - f.CompileFuncs[key] = compileFunc - } +func (f validateFunc) Validate( + ctx context.Context, + matchResource schema.GroupVersionResource, + versionedAttr *admission.VersionedAttributes, + versionedParams runtime.Object, + namespace *v1.Namespace, + runtimeCELCostBudget int64, + authz authorizer.Authorizer, +) validating.ValidateResult { + return f( + ctx, + matchResource, + versionedAttr, + versionedParams, + namespace, + runtimeCELCostBudget, + authz, + ) } -var _ cel.ExpressionAccessor = &fakeEvalRequest{} - -type fakeEvalRequest struct { - Key string -} - -func (f *fakeEvalRequest) GetExpression() string { - return "" -} - -func (f *fakeEvalRequest) ReturnTypes() []*celgo.Type { - return []*celgo.Type{celgo.BoolType} -} - -var _ cel.Filter = &fakeFilter{} - -type fakeFilter struct { - keyId string -} - -func (f *fakeFilter) ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *admissionv1.AdmissionRequest, inputs cel.OptionalVariableBindings, namespace *v1.Namespace, runtimeCELCostBudget int64) ([]cel.EvaluationResult, int64, error) { - return []cel.EvaluationResult{}, 0, nil -} - -func (f *fakeFilter) CompilationErrors() []error { - return []error{} -} - -var _ Validator = &fakeValidator{} - -type fakeValidator struct { - validationFilter, auditAnnotationFilter, messageFilter *fakeFilter - ValidateFunc func(ctx context.Context, matchResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult -} - -func (f *fakeValidator) RegisterDefinition(definition *v1beta1.ValidatingAdmissionPolicy, validateFunc func(ctx context.Context, matchResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult) { - //Key must be something that we can decipher from the inputs to Validate so using message which will be on the validationCondition object of evalResult - var key string - if len(definition.Spec.Validations) > 0 { - key = definition.Spec.Validations[0].Expression - } else { - key = definition.Spec.AuditAnnotations[0].Key - } - - if validatorMap == nil { - validatorMap = make(map[string]*fakeValidator) - } - - f.ValidateFunc = validateFunc - validatorMap[key] = f -} - -func (f *fakeValidator) Validate(ctx context.Context, matchResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { - return f.ValidateFunc(ctx, matchResource, versionedAttr, versionedParams, namespace, runtimeCELCostBudget, authz) -} - -var _ Matcher = &fakeMatcher{} +var _ validating.Matcher = &fakeMatcher{} func (f *fakeMatcher) ValidateInitialization() error { return nil @@ -354,20 +273,20 @@ func (f *fakeMatcher) GetNamespace(name string) (*v1.Namespace, error) { type fakeMatcher struct { DefaultMatch bool - DefinitionMatchFuncs map[namespacedName]func(*v1beta1.ValidatingAdmissionPolicy, admission.Attributes) bool - BindingMatchFuncs map[namespacedName]func(*v1beta1.ValidatingAdmissionPolicyBinding, admission.Attributes) bool + DefinitionMatchFuncs map[types.NamespacedName]func(*v1beta1.ValidatingAdmissionPolicy, admission.Attributes) bool + BindingMatchFuncs map[types.NamespacedName]func(*v1beta1.ValidatingAdmissionPolicyBinding, admission.Attributes) bool } func (f *fakeMatcher) RegisterDefinition(definition *v1beta1.ValidatingAdmissionPolicy, matchFunc func(*v1beta1.ValidatingAdmissionPolicy, admission.Attributes) bool) { namespace, name := definition.Namespace, definition.Name - key := namespacedName{ - name: name, - namespace: namespace, + key := types.NamespacedName{ + Name: name, + Namespace: namespace, } if matchFunc != nil { if f.DefinitionMatchFuncs == nil { - f.DefinitionMatchFuncs = make(map[namespacedName]func(*v1beta1.ValidatingAdmissionPolicy, admission.Attributes) bool) + f.DefinitionMatchFuncs = make(map[types.NamespacedName]func(*v1beta1.ValidatingAdmissionPolicy, admission.Attributes) bool) } f.DefinitionMatchFuncs[key] = matchFunc } @@ -375,14 +294,14 @@ func (f *fakeMatcher) RegisterDefinition(definition *v1beta1.ValidatingAdmission func (f *fakeMatcher) RegisterBinding(binding *v1beta1.ValidatingAdmissionPolicyBinding, matchFunc func(*v1beta1.ValidatingAdmissionPolicyBinding, admission.Attributes) bool) { namespace, name := binding.Namespace, binding.Name - key := namespacedName{ - name: name, - namespace: namespace, + key := types.NamespacedName{ + Name: name, + Namespace: namespace, } if matchFunc != nil { if f.BindingMatchFuncs == nil { - f.BindingMatchFuncs = make(map[namespacedName]func(*v1beta1.ValidatingAdmissionPolicyBinding, admission.Attributes) bool) + f.BindingMatchFuncs = make(map[types.NamespacedName]func(*v1beta1.ValidatingAdmissionPolicyBinding, admission.Attributes) bool) } f.BindingMatchFuncs[key] = matchFunc } @@ -392,9 +311,9 @@ func (f *fakeMatcher) RegisterBinding(binding *v1beta1.ValidatingAdmissionPolicy // resource request func (f *fakeMatcher) DefinitionMatches(a admission.Attributes, o admission.ObjectInterfaces, definition *v1beta1.ValidatingAdmissionPolicy) (bool, schema.GroupVersionResource, schema.GroupVersionKind, error) { namespace, name := definition.Namespace, definition.Name - key := namespacedName{ - name: name, - namespace: namespace, + key := types.NamespacedName{ + Name: name, + Namespace: namespace, } if fun, ok := f.DefinitionMatchFuncs[key]; ok { return fun(definition, a), a.GetResource(), a.GetKind(), nil @@ -408,9 +327,9 @@ func (f *fakeMatcher) DefinitionMatches(a admission.Attributes, o admission.Obje // resource request func (f *fakeMatcher) BindingMatches(a admission.Attributes, o admission.ObjectInterfaces, binding *v1beta1.ValidatingAdmissionPolicyBinding) (bool, error) { namespace, name := binding.Namespace, binding.Name - key := namespacedName{ - name: name, - namespace: namespace, + key := types.NamespacedName{ + Name: name, + Namespace: namespace, } if fun, ok := f.BindingMatchFuncs[key]; ok { return fun(binding, a), nil @@ -420,13 +339,7 @@ func (f *fakeMatcher) BindingMatches(a admission.Attributes, o admission.ObjectI return f.DefaultMatch, nil } -var validatorMap map[string]*fakeValidator - -func reset() { - validatorMap = make(map[string]*fakeValidator) -} - -func setupFakeTest(t *testing.T, comp *fakeCompiler, match *fakeMatcher) (plugin admission.ValidationInterface, paramTracker, policyTracker clienttesting.ObjectTracker, controller *celAdmissionController) { +func setupFakeTest(t *testing.T, comp *fakeCompiler, match *fakeMatcher) *generic.PolicyTestContext[*validating.Policy, *validating.PolicyBinding, validating.Validator] { return setupTestCommon(t, comp, match, true) } @@ -439,274 +352,52 @@ func setupFakeTest(t *testing.T, comp *fakeCompiler, match *fakeMatcher) (plugin // PolicyTracker expects FakePolicyDefinition and FakePolicyBinding types // !TODO: refactor this test/framework to remove startInformers argument and // clean up the return args, and in general make it more accessible. -func setupTestCommon(t *testing.T, compiler cel.FilterCompiler, matcher Matcher, shouldStartInformers bool) (plugin admission.ValidationInterface, paramTracker, policyTracker clienttesting.ObjectTracker, controller *celAdmissionController) { - testContext, testContextCancel := context.WithCancel(context.Background()) +func setupTestCommon( + t *testing.T, + compiler *fakeCompiler, + matcher validating.Matcher, + shouldStartInformers bool, +) *generic.PolicyTestContext[*validating.Policy, *validating.PolicyBinding, validating.Validator] { + testContext, testContextCancel, err := generic.NewPolicyTestContext( + validating.NewValidatingAdmissionPolicyAccessor, + validating.NewValidatingAdmissionPolicyBindingAccessor, + func(p *validating.Policy) validating.Validator { + return compiler.CompilePolicy(p) + }, + func(a authorizer.Authorizer, m *matching.Matcher) generic.Dispatcher[validating.PolicyHook] { + coolMatcher := matcher + if coolMatcher == nil { + coolMatcher = validating.NewMatcher(m) + } + return validating.NewDispatcher(a, coolMatcher) + }, + nil, + []meta.RESTMapping{ + { + Resource: paramsGVK.GroupVersion().WithResource("paramsconfigs"), + GroupVersionKind: paramsGVK, + Scope: meta.RESTScopeNamespace, + }, + { + Resource: clusterScopedParamsGVK.GroupVersion().WithResource("clusterscopedparamsconfigs"), + GroupVersionKind: clusterScopedParamsGVK, + Scope: meta.RESTScopeRoot, + }, + { + Resource: schema.GroupVersionResource{Group: "admissionregistration.k8s.io", Version: "v1beta1", Resource: "validatingadmissionpolicies"}, + GroupVersionKind: schema.GroupVersionKind{Group: "admissionregistration.k8s.io", Version: "v1beta1", Kind: "ValidatingAdmissionPolicy"}, + Scope: meta.RESTScopeRoot, + }, + }, + ) + require.NoError(t, err) t.Cleanup(testContextCancel) - fakeAuthorizer := fakeAuthorizer{} - dynamicClient := dynamicfake.NewSimpleDynamicClient(scheme) - - fakeClient := fake.NewSimpleClientset() - fakeInformerFactory := informers.NewSharedInformerFactory(fakeClient, time.Second) - featureGate := featuregate.NewFeatureGate() - err := featureGate.Add(map[featuregate.Feature]featuregate.FeatureSpec{ - features.ValidatingAdmissionPolicy: { - Default: true, PreRelease: featuregate.Alpha}}) - if err != nil { - t.Fatalf("Unable to add feature gate: %v", err) - } - err = featureGate.SetFromMap(map[string]bool{string(features.ValidatingAdmissionPolicy): true}) - if err != nil { - t.Fatalf("Unable to store flag gate: %v", err) + if shouldStartInformers { + require.NoError(t, testContext.Start()) } - plug, err := NewPlugin() - require.NoError(t, err) - - handler := plug.(*celAdmissionPlugin) - handler.enabled = true - - genericInitializer := initializer.New(fakeClient, dynamicClient, fakeInformerFactory, fakeAuthorizer, featureGate, testContext.Done()) - genericInitializer.Initialize(handler) - handler.SetRESTMapper(fakeRestMapper) - err = admission.ValidateInitialization(handler) - require.NoError(t, err) - require.True(t, handler.enabled) - - // Override compiler used by controller for tests - controller = handler.evaluator.(*celAdmissionController) - controller.policyController.filterCompiler = compiler - controller.policyController.newValidator = func(validationFilter cel.Filter, celMatcher matchconditions.Matcher, auditAnnotationFilter, messageFilter cel.Filter, fail *admissionRegistrationv1.FailurePolicyType) Validator { - f := validationFilter.(*fakeFilter) - v := validatorMap[f.keyId] - v.validationFilter = f - v.messageFilter = f - v.auditAnnotationFilter = auditAnnotationFilter.(*fakeFilter) - return v - } - controller.policyController.matcher = matcher - - t.Cleanup(func() { - testContextCancel() - // wait for informer factory to shutdown - fakeInformerFactory.Shutdown() - }) - - if !shouldStartInformers { - return handler, dynamicClient.Tracker(), fakeClient.Tracker(), controller - } - - // Make sure to start the fake informers - fakeInformerFactory.Start(testContext.Done()) - - // Wait for admission controller to begin its object watches - // This is because there is a very rare (0.05% on my machine) race doing the - // initial List+Watch if an object is added after the list, but before the - // watch it could be missed. - // - // This is only due to the fact that NewSimpleClientset above ignores - // LastSyncResourceVersion on watch calls, so do it does not provide "catch up" - // which may have been added since the call to list. - if !cache.WaitForNamedCacheSync("initial sync", testContext.Done(), handler.evaluator.HasSynced) { - t.Fatal("failed to do perform initial cache sync") - } - - // WaitForCacheSync only tells us the list was performed. - // Keep changing an object until it is observable, then remove it - - i := 0 - - dummyPolicy := &v1beta1.ValidatingAdmissionPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: "dummypolicy.example.com", - Annotations: map[string]string{ - "myValue": fmt.Sprint(i), - }, - }, - } - - dummyBinding := &v1beta1.ValidatingAdmissionPolicyBinding{ - ObjectMeta: metav1.ObjectMeta{ - Name: "dummybinding.example.com", - Annotations: map[string]string{ - "myValue": fmt.Sprint(i), - }, - }, - } - - require.NoError(t, fakeClient.Tracker().Create(definitionsGVR, dummyPolicy, dummyPolicy.Namespace)) - require.NoError(t, fakeClient.Tracker().Create(bindingsGVR, dummyBinding, dummyBinding.Namespace)) - - wait.PollWithContext(testContext, 100*time.Millisecond, 300*time.Millisecond, func(ctx context.Context) (done bool, err error) { - defer func() { - i += 1 - }() - - dummyPolicy.Annotations = map[string]string{ - "myValue": fmt.Sprint(i), - } - dummyBinding.Annotations = dummyPolicy.Annotations - - require.NoError(t, fakeClient.Tracker().Update(definitionsGVR, dummyPolicy, dummyPolicy.Namespace)) - require.NoError(t, fakeClient.Tracker().Update(bindingsGVR, dummyBinding, dummyBinding.Namespace)) - - if obj, err := controller.getCurrentObject(dummyPolicy); obj == nil || err != nil { - return false, nil - } - - if obj, err := controller.getCurrentObject(dummyBinding); obj == nil || err != nil { - return false, nil - } - - return true, nil - }) - - require.NoError(t, fakeClient.Tracker().Delete(definitionsGVR, dummyPolicy.Namespace, dummyPolicy.Name)) - require.NoError(t, fakeClient.Tracker().Delete(bindingsGVR, dummyBinding.Namespace, dummyBinding.Name)) - - return handler, dynamicClient.Tracker(), fakeClient.Tracker(), controller -} - -// Gets the last reconciled value in the controller of an object with the same -// gvk and name as the given object -// -// If the object is not found both the error and object will be nil. -func (c *celAdmissionController) getCurrentObject(obj runtime.Object) (runtime.Object, error) { - accessor, err := meta.Accessor(obj) - if err != nil { - return nil, err - } - - c.policyController.mutex.RLock() - defer c.policyController.mutex.RUnlock() - - switch obj.(type) { - case *v1beta1.ValidatingAdmissionPolicyBinding: - nn := getNamespaceName(accessor.GetNamespace(), accessor.GetName()) - info, ok := c.policyController.bindingInfos[nn] - if !ok { - return nil, nil - } - - return info.lastReconciledValue, nil - case *v1beta1.ValidatingAdmissionPolicy: - nn := getNamespaceName(accessor.GetNamespace(), accessor.GetName()) - info, ok := c.policyController.definitionInfo[nn] - if !ok { - return nil, nil - } - - return info.lastReconciledValue, nil - default: - // If test isn't trying to fetch a policy or binding, assume it is - // fetching a param - paramSourceGVK := obj.GetObjectKind().GroupVersionKind() - paramKind := v1beta1.ParamKind{ - APIVersion: paramSourceGVK.GroupVersion().String(), - Kind: paramSourceGVK.Kind, - } - - var paramInformer generic.Informer[runtime.Object] - if paramInfo, ok := c.policyController.paramsCRDControllers[paramKind]; ok { - paramInformer = paramInfo.controller.Informer() - } else { - // Treat unknown CRD the same as not found - return nil, nil - } - - // Param type. Just check informer for its GVK - var item runtime.Object - var err error - if namespace := accessor.GetNamespace(); len(namespace) > 0 { - item, err = paramInformer.Namespaced(namespace).Get(accessor.GetName()) - } else { - item, err = paramInformer.Get(accessor.GetName()) - } - - if err != nil { - if k8serrors.IsNotFound(err) { - return nil, nil - } - return nil, err - } - - return item, nil - } -} - -// Waits for the given objects to have been the latest reconciled values of -// their gvk/name in the controller -func waitForReconcile(ctx context.Context, controller *celAdmissionController, objects ...runtime.Object) error { - return wait.PollWithContext(ctx, 100*time.Millisecond, 1*time.Second, func(ctx context.Context) (done bool, err error) { - defer func() { - if done { - // force admission controller to refresh the information it - // uses for validation now that it is done in the background - controller.refreshPolicies() - } - }() - for _, obj := range objects { - - objMeta, err := meta.Accessor(obj) - if err != nil { - return false, fmt.Errorf("error getting meta accessor for original %T object (%v): %w", obj, obj, err) - } - - currentValue, err := controller.getCurrentObject(obj) - if err != nil { - return false, fmt.Errorf("error getting current object: %w", err) - } else if currentValue == nil { - // Object not found, but not an error. Keep waiting. - klog.Infof("%v not found. keep waiting", objMeta.GetName()) - return false, nil - } - - valueMeta, err := meta.Accessor(currentValue) - if err != nil { - return false, fmt.Errorf("error getting meta accessor for current %T object (%v): %w", currentValue, currentValue, err) - } - - if len(objMeta.GetResourceVersion()) == 0 { - return false, fmt.Errorf("%s named %s has no resource version. please ensure your test objects have an RV", - obj.GetObjectKind().GroupVersionKind().String(), objMeta.GetName()) - } else if len(valueMeta.GetResourceVersion()) == 0 { - return false, fmt.Errorf("%s named %s has no resource version. please ensure your test objects have an RV", - currentValue.GetObjectKind().GroupVersionKind().String(), valueMeta.GetName()) - } else if objMeta.GetResourceVersion() != valueMeta.GetResourceVersion() { - klog.Infof("%v has RV %v. want RV %v", objMeta.GetName(), objMeta.GetResourceVersion(), objMeta.GetResourceVersion()) - return false, nil - } - } - - return true, nil - }) -} - -// Waits for the admissoin controller to have no knowledge of the objects -// with the given GVKs and namespace/names -func waitForReconcileDeletion(ctx context.Context, controller *celAdmissionController, objects ...runtime.Object) error { - return wait.PollWithContext(ctx, 200*time.Millisecond, 3*time.Hour, func(ctx context.Context) (done bool, err error) { - defer func() { - if done { - // force admission controller to refresh the information it - // uses for validation now that it is done in the background - controller.refreshPolicies() - } - }() - - for _, obj := range objects { - currentValue, err := controller.getCurrentObject(obj) - if err != nil { - return false, err - } - - if currentValue != nil { - return false, nil - } - } - - return true, nil - }) + return testContext } func attributeRecord( @@ -717,23 +408,13 @@ func attributeRecord( panic("both `old` and `new` may not be nil") } - accessor, err := meta.Accessor(new) - if err != nil { - panic(err) - } - // one of old/new may be nil, but not both example := new if example == nil { example = old } - gvk := example.GetObjectKind().GroupVersionKind() - if gvk.Empty() { - // If gvk is not populated, try to fetch it from the scheme - gvk = must3(scheme.ObjectKinds(example))[0] - } - mapping, err := fakeRestMapper.RESTMapping(gvk.GroupKind(), gvk.Version) + accessor, err := meta.Accessor(example) if err != nil { panic(err) } @@ -742,10 +423,10 @@ func attributeRecord( Attributes: admission.NewAttributesRecord( new, old, - gvk, + example.GetObjectKind().GroupVersionKind(), accessor.GetNamespace(), accessor.GetName(), - mapping.Resource, + schema.GroupVersionResource{}, "", operation, nil, @@ -759,26 +440,11 @@ func ptrTo[T any](obj T) *T { return &obj } -func must[T any](val T, err error) T { - if err != nil { - panic(err) - } - return val -} - -func must3[T any, I any](val T, _ I, err error) T { - if err != nil { - panic(err) - } - return val -} - // ////////////////////////////////////////////////////////////////////////////// // Functionality Tests // ////////////////////////////////////////////////////////////////////////////// func TestPluginNotReady(t *testing.T) { - reset() compiler := &fakeCompiler{} matcher := &fakeMatcher{ DefaultMatch: true, @@ -786,8 +452,8 @@ func TestPluginNotReady(t *testing.T) { // Show that an unstarted informer (or one that has failed its listwatch) // will show proper error from plugin - handler, _, _, _ := setupTestCommon(t, compiler, matcher, false) - err := handler.Validate( + ctx := setupTestCommon(t, compiler, matcher, false) + err := ctx.Plugin.Dispatch( context.Background(), // Object is irrelevant/unchecked for this test. Just test that // the evaluator is executed, and returns a denial @@ -798,8 +464,8 @@ func TestPluginNotReady(t *testing.T) { require.ErrorContains(t, err, "not yet ready to handle request") // Show that by now starting the informer, the error is dissipated - handler, _, _, _ = setupTestCommon(t, compiler, matcher, true) - err = handler.Validate( + ctx = setupTestCommon(t, compiler, matcher, true) + err = ctx.Plugin.Dispatch( context.Background(), // Object is irrelevant/unchecked for this test. Just test that // the evaluator is executed, and returns a denial @@ -811,56 +477,35 @@ func TestPluginNotReady(t *testing.T) { } func TestBasicPolicyDefinitionFailure(t *testing.T) { - reset() - testContext, testContextCancel := context.WithCancel(context.Background()) - defer testContextCancel() - datalock := sync.Mutex{} numCompiles := 0 compiler := &fakeCompiler{} - validator := &fakeValidator{} matcher := &fakeMatcher{ DefaultMatch: true, } - compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter { + compiler.RegisterDefinition(denyPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) validating.ValidateResult { datalock.Lock() numCompiles += 1 datalock.Unlock() - - return &fakeFilter{ - keyId: denyPolicy.Spec.Validations[0].Expression, - } - }) - - validator.RegisterDefinition(denyPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { - return ValidateResult{ - Decisions: []PolicyDecision{ + return validating.ValidateResult{ + Decisions: []validating.PolicyDecision{ { - Action: ActionDeny, + Action: validating.ActionDeny, Message: "Denied", }, }, } }) - handler, paramTracker, tracker, controller := setupFakeTest(t, compiler, matcher) - - require.NoError(t, paramTracker.Add(fakeParams)) - require.NoError(t, tracker.Create(definitionsGVR, denyPolicy, denyPolicy.Namespace)) - require.NoError(t, tracker.Create(bindingsGVR, denyBinding, denyBinding.Namespace)) - - // Wait for controller to reconcile given objects - require.NoError(t, - waitForReconcile( - testContext, controller, - fakeParams, denyBinding, denyPolicy)) + testContext := setupFakeTest(t, compiler, matcher) + require.NoError(t, testContext.UpdateAndWait(fakeParams, denyPolicy, denyBinding)) warningRecorder := newWarningRecorder() warnCtx := warning.WithWarningRecorder(testContext, warningRecorder) attr := attributeRecord(nil, fakeParams, admission.Create) - err := handler.Validate( + err := testContext.Plugin.Dispatch( warnCtx, // Object is irrelevant/unchecked for this test. Just test that // the evaluator is executed, and returns a denial @@ -879,36 +524,25 @@ func TestBasicPolicyDefinitionFailure(t *testing.T) { // Shows that if a definition does not match the input, it will not be used. // But with a different input it will be used. func TestDefinitionDoesntMatch(t *testing.T) { - reset() compiler := &fakeCompiler{} - validator := &fakeValidator{} matcher := &fakeMatcher{ DefaultMatch: true, } - handler, paramTracker, tracker, controller := setupFakeTest(t, compiler, matcher) - testContext, testContextCancel := context.WithCancel(context.Background()) - defer testContextCancel() + testContext := setupFakeTest(t, compiler, matcher) datalock := sync.Mutex{} passedParams := []*unstructured.Unstructured{} numCompiles := 0 - compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter { + compiler.RegisterDefinition(denyPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) validating.ValidateResult { datalock.Lock() numCompiles += 1 datalock.Unlock() - - return &fakeFilter{ - keyId: denyPolicy.Spec.Validations[0].Expression, - } - }) - - validator.RegisterDefinition(denyPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { - return ValidateResult{ - Decisions: []PolicyDecision{ + return validating.ValidateResult{ + Decisions: []validating.PolicyDecision{ { - Action: ActionDeny, + Action: validating.ActionDeny, Message: "Denied", }, }, @@ -928,15 +562,7 @@ func TestDefinitionDoesntMatch(t *testing.T) { return len(accessor.GetName())%2 == 0 }) - require.NoError(t, paramTracker.Add(fakeParams)) - require.NoError(t, tracker.Create(definitionsGVR, denyPolicy, denyPolicy.Namespace)) - require.NoError(t, tracker.Create(bindingsGVR, denyBinding, denyBinding.Namespace)) - - // Wait for controller to reconcile given objects - require.NoError(t, - waitForReconcile( - testContext, controller, - fakeParams, denyBinding, denyPolicy)) + require.NoError(t, testContext.UpdateAndWait(fakeParams, denyPolicy, denyBinding)) // Validate a non-matching input. // Should pass validation with no error. @@ -952,7 +578,7 @@ func TestDefinitionDoesntMatch(t *testing.T) { }, } require.NoError(t, - handler.Validate(testContext, + testContext.Plugin.Dispatch(testContext, attributeRecord( nil, nonMatchingParams, admission.Create), &admission.RuntimeObjectInterfaces{})) @@ -971,7 +597,7 @@ func TestDefinitionDoesntMatch(t *testing.T) { }, } require.ErrorContains(t, - handler.Validate(testContext, + testContext.Plugin.Dispatch(testContext, attributeRecord( nil, matchingParams, admission.Create), &admission.RuntimeObjectInterfaces{}), @@ -980,17 +606,12 @@ func TestDefinitionDoesntMatch(t *testing.T) { } func TestReconfigureBinding(t *testing.T) { - reset() - testContext, testContextCancel := context.WithCancel(context.Background()) - defer testContextCancel() - compiler := &fakeCompiler{} - validator := &fakeValidator{} matcher := &fakeMatcher{ DefaultMatch: true, } - handler, paramTracker, tracker, controller := setupFakeTest(t, compiler, matcher) + testContext := setupFakeTest(t, compiler, matcher) datalock := sync.Mutex{} numCompiles := 0 @@ -1009,21 +630,14 @@ func TestReconfigureBinding(t *testing.T) { }, } - compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter { + compiler.RegisterDefinition(denyPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) validating.ValidateResult { datalock.Lock() numCompiles += 1 datalock.Unlock() - - return &fakeFilter{ - keyId: denyPolicy.Spec.Validations[0].Expression, - } - }) - - validator.RegisterDefinition(denyPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { - return ValidateResult{ - Decisions: []PolicyDecision{ + return validating.ValidateResult{ + Decisions: []validating.PolicyDecision{ { - Action: ActionDeny, + Action: validating.ActionDeny, Message: "Denied", }, }, @@ -1046,17 +660,9 @@ func TestReconfigureBinding(t *testing.T) { }, } - require.NoError(t, paramTracker.Add(fakeParams)) - require.NoError(t, tracker.Create(definitionsGVR, denyPolicy, denyPolicy.Namespace)) - require.NoError(t, tracker.Create(bindingsGVR, denyBinding, denyBinding.Namespace)) + require.NoError(t, testContext.UpdateAndWait(fakeParams, denyPolicy, denyBinding)) - // Wait for controller to reconcile given objects - require.NoError(t, - waitForReconcile( - testContext, controller, - fakeParams, denyBinding, denyPolicy)) - - err := handler.Validate( + err := testContext.Plugin.Dispatch( testContext, attributeRecord(nil, fakeParams, admission.Create), &admission.RuntimeObjectInterfaces{}, @@ -1070,13 +676,9 @@ func TestReconfigureBinding(t *testing.T) { require.Equal(t, 1, numCompiles, "expect `Compile` to be called only once") // Update the tracker to point at different params - require.NoError(t, tracker.Update(bindingsGVR, denyBinding2, "")) + require.NoError(t, testContext.UpdateAndWait(denyBinding2)) - // Wait for update to propagate - // Wait for controller to reconcile given objects - require.NoError(t, waitForReconcile(testContext, controller, denyBinding2)) - - err = handler.Validate( + err = testContext.Plugin.Dispatch( testContext, attributeRecord(nil, fakeParams, admission.Create), &admission.RuntimeObjectInterfaces{}, @@ -1085,13 +687,10 @@ func TestReconfigureBinding(t *testing.T) { require.ErrorContains(t, err, "no params found for policy binding with `Deny` parameterNotFoundAction") // Add the missing params - require.NoError(t, paramTracker.Add(fakeParams2)) - - // Wait for update to propagate - require.NoError(t, waitForReconcile(testContext, controller, fakeParams2)) + require.NoError(t, testContext.UpdateAndWait(fakeParams2)) // Expect validation to now fail again. - err = handler.Validate( + err = testContext.Plugin.Dispatch( testContext, attributeRecord(nil, fakeParams, admission.Create), &admission.RuntimeObjectInterfaces{}, @@ -1105,65 +704,45 @@ func TestReconfigureBinding(t *testing.T) { // Shows that a policy which is in effect will stop being in effect when removed func TestRemoveDefinition(t *testing.T) { - reset() - testContext, testContextCancel := context.WithCancel(context.Background()) - defer testContextCancel() - compiler := &fakeCompiler{} - validator := &fakeValidator{} matcher := &fakeMatcher{ DefaultMatch: true, } - handler, paramTracker, tracker, controller := setupFakeTest(t, compiler, matcher) + testContext := setupFakeTest(t, compiler, matcher) datalock := sync.Mutex{} numCompiles := 0 - compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter { + compiler.RegisterDefinition(denyPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) validating.ValidateResult { datalock.Lock() numCompiles += 1 datalock.Unlock() - return &fakeFilter{ - keyId: denyPolicy.Spec.Validations[0].Expression, - } - }) - - validator.RegisterDefinition(denyPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { - return ValidateResult{ - Decisions: []PolicyDecision{ + return validating.ValidateResult{ + Decisions: []validating.PolicyDecision{ { - Action: ActionDeny, + Action: validating.ActionDeny, Message: "Denied", }, }, } }) - require.NoError(t, paramTracker.Add(fakeParams)) - require.NoError(t, tracker.Create(definitionsGVR, denyPolicy, denyPolicy.Namespace)) - require.NoError(t, tracker.Create(bindingsGVR, denyBinding, denyBinding.Namespace)) - - // Wait for controller to reconcile given objects - require.NoError(t, - waitForReconcile( - testContext, controller, - fakeParams, denyBinding, denyPolicy)) + require.NoError(t, testContext.UpdateAndWait(fakeParams, denyPolicy, denyBinding)) record := attributeRecord(nil, fakeParams, admission.Create) require.ErrorContains(t, - handler.Validate( + testContext.Plugin.Dispatch( testContext, record, &admission.RuntimeObjectInterfaces{}, ), `Denied`) - require.NoError(t, tracker.Delete(definitionsGVR, denyPolicy.Namespace, denyPolicy.Name)) - require.NoError(t, waitForReconcileDeletion(testContext, controller, denyPolicy)) + require.NoError(t, testContext.DeleteAndWait(denyPolicy)) - require.NoError(t, handler.Validate( + require.NoError(t, testContext.Plugin.Dispatch( testContext, // Object is irrelevant/unchecked for this test. Just test that // the evaluator is executed, and returns a denial @@ -1174,80 +753,55 @@ func TestRemoveDefinition(t *testing.T) { // Shows that a binding which is in effect will stop being in effect when removed func TestRemoveBinding(t *testing.T) { - reset() - testContext, testContextCancel := context.WithCancel(context.Background()) - defer testContextCancel() - compiler := &fakeCompiler{} - validator := &fakeValidator{} matcher := &fakeMatcher{ DefaultMatch: true, } - handler, paramTracker, tracker, controller := setupFakeTest(t, compiler, matcher) + testContext := setupFakeTest(t, compiler, matcher) datalock := sync.Mutex{} numCompiles := 0 - compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter { + compiler.RegisterDefinition(denyPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) validating.ValidateResult { datalock.Lock() numCompiles += 1 datalock.Unlock() - return &fakeFilter{ - keyId: denyPolicy.Spec.Validations[0].Expression, - } - }) - - validator.RegisterDefinition(denyPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { - return ValidateResult{ - Decisions: []PolicyDecision{ + return validating.ValidateResult{ + Decisions: []validating.PolicyDecision{ { - Action: ActionDeny, + Action: validating.ActionDeny, Message: "Denied", }, }, } }) - require.NoError(t, paramTracker.Add(fakeParams)) - require.NoError(t, tracker.Create(definitionsGVR, denyPolicy, denyPolicy.Namespace)) - require.NoError(t, tracker.Create(bindingsGVR, denyBinding, denyBinding.Namespace)) - - // Wait for controller to reconcile given objects - require.NoError(t, - waitForReconcile( - testContext, controller, - fakeParams, denyBinding, denyPolicy)) + require.NoError(t, testContext.UpdateAndWait(fakeParams, denyPolicy, denyBinding)) record := attributeRecord(nil, fakeParams, admission.Create) require.ErrorContains(t, - handler.Validate( + testContext.Plugin.Dispatch( testContext, record, &admission.RuntimeObjectInterfaces{}, ), `Denied`) - // require.Equal(t, []*unstructured.Unstructured{fakeParams}, passedParams) - require.NoError(t, tracker.Delete(bindingsGVR, denyBinding.Namespace, denyBinding.Name)) - require.NoError(t, waitForReconcileDeletion(testContext, controller, denyBinding)) + require.NoError(t, testContext.DeleteAndWait(denyBinding)) } // Shows that an error is surfaced if a paramSource specified in a binding does // not actually exist func TestInvalidParamSourceGVK(t *testing.T) { - reset() - testContext, testContextCancel := context.WithCancel(context.Background()) - defer testContextCancel() - compiler := &fakeCompiler{} matcher := &fakeMatcher{ DefaultMatch: true, } - handler, _, tracker, controller := setupFakeTest(t, compiler, matcher) + testContext := setupFakeTest(t, compiler, matcher) passedParams := make(chan *unstructured.Unstructured) badPolicy := *denyPolicy @@ -1256,16 +810,9 @@ func TestInvalidParamSourceGVK(t *testing.T) { Kind: "BadParamKind", } - require.NoError(t, tracker.Create(definitionsGVR, &badPolicy, badPolicy.Namespace)) - require.NoError(t, tracker.Create(bindingsGVR, denyBinding, denyBinding.Namespace)) + require.NoError(t, testContext.UpdateAndWait(&badPolicy, denyBinding)) - // Wait for controller to reconcile given objects - require.NoError(t, - waitForReconcile( - testContext, controller, - denyBinding, &badPolicy)) - - err := handler.Validate( + err := testContext.Plugin.Dispatch( testContext, attributeRecord(nil, fakeParams, admission.Create), &admission.RuntimeObjectInterfaces{}, @@ -1283,53 +830,35 @@ func TestInvalidParamSourceGVK(t *testing.T) { // Shows that an error is surfaced if a param specified in a binding does not // actually exist func TestInvalidParamSourceInstanceName(t *testing.T) { - reset() - testContext, testContextCancel := context.WithCancel(context.Background()) - defer testContextCancel() - compiler := &fakeCompiler{} - validator := &fakeValidator{} matcher := &fakeMatcher{ DefaultMatch: true, } - handler, _, tracker, controller := setupFakeTest(t, compiler, matcher) + testContext := setupFakeTest(t, compiler, matcher) datalock := sync.Mutex{} passedParams := []*unstructured.Unstructured{} numCompiles := 0 - compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter { + compiler.RegisterDefinition(denyPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) validating.ValidateResult { datalock.Lock() numCompiles += 1 datalock.Unlock() - return &fakeFilter{ - keyId: denyPolicy.Spec.Validations[0].Expression, - } - }) - - validator.RegisterDefinition(denyPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { - return ValidateResult{ - Decisions: []PolicyDecision{ + return validating.ValidateResult{ + Decisions: []validating.PolicyDecision{ { - Action: ActionDeny, + Action: validating.ActionDeny, Message: "Denied", }, }, } }) - require.NoError(t, tracker.Create(definitionsGVR, denyPolicy, denyPolicy.Namespace)) - require.NoError(t, tracker.Create(bindingsGVR, denyBinding, denyBinding.Namespace)) + require.NoError(t, testContext.UpdateAndWait(denyPolicy, denyBinding)) - // Wait for controller to reconcile given objects - require.NoError(t, - waitForReconcile( - testContext, controller, - denyBinding, denyPolicy)) - - err := handler.Validate( + err := testContext.Plugin.Dispatch( testContext, attributeRecord(nil, fakeParams, admission.Create), &admission.RuntimeObjectInterfaces{}, @@ -1345,62 +874,44 @@ func TestInvalidParamSourceInstanceName(t *testing.T) { // Show that policy still gets evaluated with `nil` param if paramRef & namespaceParamRef // are both unset func TestEmptyParamRef(t *testing.T) { - reset() - testContext, testContextCancel := context.WithCancel(context.Background()) - defer testContextCancel() - compiler := &fakeCompiler{} - validator := &fakeValidator{} matcher := &fakeMatcher{ DefaultMatch: true, } - handler, _, tracker, controller := setupFakeTest(t, compiler, matcher) + testContext := setupFakeTest(t, compiler, matcher) datalock := sync.Mutex{} numCompiles := 0 - compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter { + compiler.RegisterDefinition(denyPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) validating.ValidateResult { datalock.Lock() numCompiles += 1 datalock.Unlock() - return &fakeFilter{ - keyId: denyPolicy.Spec.Validations[0].Expression, - } - }) - - validator.RegisterDefinition(denyPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { // Versioned params must be nil to pass the test if versionedParams != nil { - return ValidateResult{ - Decisions: []PolicyDecision{ + return validating.ValidateResult{ + Decisions: []validating.PolicyDecision{ { - Action: ActionAdmit, + Action: validating.ActionAdmit, }, }, } } - return ValidateResult{ - Decisions: []PolicyDecision{ + return validating.ValidateResult{ + Decisions: []validating.PolicyDecision{ { - Action: ActionDeny, + Action: validating.ActionDeny, Message: "Denied", }, }, } }) - require.NoError(t, tracker.Create(definitionsGVR, denyPolicy, denyPolicy.Namespace)) - require.NoError(t, tracker.Create(bindingsGVR, denyBindingWithNoParamRef, denyBindingWithNoParamRef.Namespace)) + require.NoError(t, testContext.UpdateAndWait(denyPolicy, denyBindingWithNoParamRef)) - // Wait for controller to reconcile given objects - require.NoError(t, - waitForReconcile( - testContext, controller, - denyBindingWithNoParamRef, denyPolicy)) - - err := handler.Validate( + err := testContext.Plugin.Dispatch( testContext, // Object is irrelevant/unchecked for this test. Just test that // the evaluator is executed, and returns a denial @@ -1418,17 +929,12 @@ func TestEmptyParamRef(t *testing.T) { // Also shows that if binding has specified params in this instance then they // are silently ignored. func TestEmptyParamSource(t *testing.T) { - reset() - testContext, testContextCancel := context.WithCancel(context.Background()) - defer testContextCancel() - compiler := &fakeCompiler{} - validator := &fakeValidator{} matcher := &fakeMatcher{ DefaultMatch: true, } - handler, _, tracker, controller := setupFakeTest(t, compiler, matcher) + testContext := setupFakeTest(t, compiler, matcher) datalock := sync.Mutex{} numCompiles := 0 @@ -1437,37 +943,23 @@ func TestEmptyParamSource(t *testing.T) { noParamSourcePolicy := *denyPolicy noParamSourcePolicy.Spec.ParamKind = nil - compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter { + compiler.RegisterDefinition(denyPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) validating.ValidateResult { datalock.Lock() numCompiles += 1 datalock.Unlock() - - return &fakeFilter{ - keyId: denyPolicy.Spec.Validations[0].Expression, - } - }) - - validator.RegisterDefinition(denyPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { - return ValidateResult{ - Decisions: []PolicyDecision{ + return validating.ValidateResult{ + Decisions: []validating.PolicyDecision{ { - Action: ActionDeny, + Action: validating.ActionDeny, Message: "Denied", }, }, } }) - require.NoError(t, tracker.Create(definitionsGVR, &noParamSourcePolicy, noParamSourcePolicy.Namespace)) - require.NoError(t, tracker.Create(bindingsGVR, denyBindingWithNoParamRef, denyBindingWithNoParamRef.Namespace)) + require.NoError(t, testContext.UpdateAndWait(&noParamSourcePolicy, denyBindingWithNoParamRef)) - // Wait for controller to reconcile given objects - require.NoError(t, - waitForReconcile( - testContext, controller, - denyBinding, denyPolicy)) - - err := handler.Validate( + err := testContext.Plugin.Dispatch( testContext, // Object is irrelevant/unchecked for this test. Just test that // the evaluator is executed, and returns a denial @@ -1483,18 +975,12 @@ func TestEmptyParamSource(t *testing.T) { // one policy stops using the param. The expectation is the second policy // keeps behaving normally func TestMultiplePoliciesSharedParamType(t *testing.T) { - reset() - testContext, testContextCancel := context.WithCancel(context.Background()) - defer testContextCancel() - compiler := &fakeCompiler{} - validator1 := &fakeValidator{} - validator2 := &fakeValidator{} matcher := &fakeMatcher{ DefaultMatch: true, } - handler, paramTracker, tracker, controller := setupFakeTest(t, compiler, matcher) + testContext := setupFakeTest(t, compiler, matcher) // Use ConfigMap native-typed param policy1 := *denyPolicy @@ -1535,72 +1021,39 @@ func TestMultiplePoliciesSharedParamType(t *testing.T) { binding2.Name = "denybinding2.example.com" binding2.Spec.PolicyName = policy2.Name - compiles1 := atomic.Int64{} evaluations1 := atomic.Int64{} - - compiles2 := atomic.Int64{} evaluations2 := atomic.Int64{} - compiler.RegisterDefinition(&policy1, func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter { - compiles1.Add(1) - - return &fakeFilter{ - keyId: policy1.Spec.Validations[0].Expression, - } - }) - - validator1.RegisterDefinition(&policy1, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { + compiler.RegisterDefinition(&policy1, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) validating.ValidateResult { evaluations1.Add(1) - return ValidateResult{ - Decisions: []PolicyDecision{ + + return validating.ValidateResult{ + Decisions: []validating.PolicyDecision{ { - Action: ActionAdmit, + Action: validating.ActionAdmit, }, }, } }) - compiler.RegisterDefinition(&policy2, func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter { - compiles2.Add(1) - - return &fakeFilter{ - keyId: policy2.Spec.Validations[0].Expression, - } - }) - - validator2.RegisterDefinition(&policy2, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { + compiler.RegisterDefinition(&policy2, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) validating.ValidateResult { evaluations2.Add(1) - return ValidateResult{ - Decisions: []PolicyDecision{ + return validating.ValidateResult{ + Decisions: []validating.PolicyDecision{ { - Action: ActionDeny, + Action: validating.ActionDeny, Message: "Policy2Denied", }, }, } }) - require.NoError(t, tracker.Create(definitionsGVR, &policy1, policy1.Namespace)) - require.NoError(t, tracker.Create(bindingsGVR, &binding1, binding1.Namespace)) - require.NoError(t, paramTracker.Add(fakeParams)) - - // Wait for controller to reconcile given objects - require.NoError(t, - waitForReconcile( - testContext, controller, - &binding1, &policy1, fakeParams)) + require.NoError(t, testContext.UpdateAndWait(fakeParams, &policy1, &binding1)) // Make sure policy 1 is created and bound to the params type first - require.NoError(t, tracker.Create(definitionsGVR, &policy2, policy2.Namespace)) - require.NoError(t, tracker.Create(bindingsGVR, &binding2, binding2.Namespace)) + require.NoError(t, testContext.UpdateAndWait(&policy2, &binding2)) - // Wait for controller to reconcile given objects - require.NoError(t, - waitForReconcile( - testContext, controller, - &binding1, &binding2, &policy1, &policy2, fakeParams)) - - err := handler.Validate( + err := testContext.Plugin.Dispatch( testContext, // Object is irrelevant/unchecked for this test. Just test that // the evaluator is executed, and returns admit meaning the params @@ -1610,9 +1063,9 @@ func TestMultiplePoliciesSharedParamType(t *testing.T) { ) require.ErrorContains(t, err, `Denied`) - require.EqualValues(t, 1, compiles1.Load()) + require.EqualValues(t, 1, compiler.getNumCompiles(&policy1)) require.EqualValues(t, 1, evaluations1.Load()) - require.EqualValues(t, 1, compiles2.Load()) + require.EqualValues(t, 1, compiler.getNumCompiles(&policy2)) require.EqualValues(t, 1, evaluations2.Load()) // Remove param type from policy1 @@ -1623,14 +1076,9 @@ func TestMultiplePoliciesSharedParamType(t *testing.T) { binding1.Spec.ParamRef = nil binding1.ResourceVersion = "2" - require.NoError(t, tracker.Update(definitionsGVR, &policy1, policy1.Namespace)) - require.NoError(t, tracker.Update(bindingsGVR, &binding1, binding1.Namespace)) - require.NoError(t, - waitForReconcile( - testContext, controller, - &binding1, &policy1)) + require.NoError(t, testContext.UpdateAndWait(&policy1, &binding1)) - err = handler.Validate( + err = testContext.Plugin.Dispatch( testContext, // Object is irrelevant/unchecked for this test. Just test that // the evaluator is executed, and returns admit meaning the params @@ -1640,27 +1088,20 @@ func TestMultiplePoliciesSharedParamType(t *testing.T) { ) require.ErrorContains(t, err, `Policy2Denied`) - require.EqualValues(t, 2, compiles1.Load()) + require.EqualValues(t, 2, compiler.getNumCompiles(&policy1)) require.EqualValues(t, 2, evaluations1.Load()) - require.EqualValues(t, 1, compiles2.Load()) + require.EqualValues(t, 1, compiler.getNumCompiles(&policy2)) require.EqualValues(t, 2, evaluations2.Load()) } // Shows that we can refer to native-typed params just fine // (as opposed to CRD params) func TestNativeTypeParam(t *testing.T) { - reset() - testContext, testContextCancel := context.WithCancel(context.Background()) - defer testContextCancel() - compiler := &fakeCompiler{} - validator := &fakeValidator{} matcher := &fakeMatcher{ DefaultMatch: true, } - handler, _, tracker, controller := setupFakeTest(t, compiler, matcher) - - compiles := atomic.Int64{} + testContext := setupFakeTest(t, compiler, matcher) evaluations := atomic.Int64{} // Use ConfigMap native-typed param @@ -1670,30 +1111,22 @@ func TestNativeTypeParam(t *testing.T) { Kind: "ConfigMap", } - compiler.RegisterDefinition(&nativeTypeParamPolicy, func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter { - compiles.Add(1) - - return &fakeFilter{ - keyId: nativeTypeParamPolicy.Spec.Validations[0].Expression, - } - }) - - validator.RegisterDefinition(&nativeTypeParamPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { + compiler.RegisterDefinition(&nativeTypeParamPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) validating.ValidateResult { evaluations.Add(1) if _, ok := versionedParams.(*v1.ConfigMap); ok { - return ValidateResult{ - Decisions: []PolicyDecision{ + return validating.ValidateResult{ + Decisions: []validating.PolicyDecision{ { - Action: ActionDeny, + Action: validating.ActionDeny, Message: "correct type", }, }, } } - return ValidateResult{ - Decisions: []PolicyDecision{ + return validating.ValidateResult{ + Decisions: []validating.PolicyDecision{ { - Action: ActionDeny, + Action: validating.ActionDeny, Message: "Incorrect param type", }, }, @@ -1714,17 +1147,9 @@ func TestNativeTypeParam(t *testing.T) { "coolkey": "coolvalue", }, } - require.NoError(t, tracker.Create(definitionsGVR, &nativeTypeParamPolicy, nativeTypeParamPolicy.Namespace)) - require.NoError(t, tracker.Create(bindingsGVR, denyBinding, denyBinding.Namespace)) - require.NoError(t, tracker.Add(configMapParam)) + require.NoError(t, testContext.UpdateAndWait(&nativeTypeParamPolicy, denyBinding, configMapParam)) - // Wait for controller to reconcile given objects - require.NoError(t, - waitForReconcile( - testContext, controller, - denyBinding, denyPolicy, configMapParam)) - - err := handler.Validate( + err := testContext.Plugin.Dispatch( testContext, // Object is irrelevant/unchecked for this test. Just test that // the evaluator is executed, and returns admit meaning the params @@ -1734,55 +1159,38 @@ func TestNativeTypeParam(t *testing.T) { ) require.ErrorContains(t, err, "correct type") - require.EqualValues(t, 1, compiles.Load()) + require.EqualValues(t, 1, compiler.getNumCompiles(&nativeTypeParamPolicy)) require.EqualValues(t, 1, evaluations.Load()) } func TestAuditValidationAction(t *testing.T) { - testContext, testContextCancel := context.WithCancel(context.Background()) - defer testContextCancel() - compiler := &fakeCompiler{} - validator := &fakeValidator{} matcher := &fakeMatcher{ DefaultMatch: true, } - handler, _, tracker, controller := setupFakeTest(t, compiler, matcher) + testContext := setupFakeTest(t, compiler, matcher) // Push some fake noParamSourcePolicy := *denyPolicy noParamSourcePolicy.Spec.ParamKind = nil - compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter { - - return &fakeFilter{ - keyId: denyPolicy.Spec.Validations[0].Expression, - } - }) - - validator.RegisterDefinition(denyPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { - return ValidateResult{ - Decisions: []PolicyDecision{ + compiler.RegisterDefinition(denyPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) validating.ValidateResult { + return validating.ValidateResult{ + Decisions: []validating.PolicyDecision{ { - Action: ActionDeny, + Action: validating.ActionDeny, Message: "I'm sorry Dave", }, }, } }) - require.NoError(t, tracker.Create(definitionsGVR, &noParamSourcePolicy, noParamSourcePolicy.Namespace)) - require.NoError(t, tracker.Create(bindingsGVR, denyBindingWithAudit, denyBindingWithAudit.Namespace)) + require.NoError(t, testContext.UpdateAndWait(&noParamSourcePolicy, denyBindingWithAudit)) - // Wait for controller to reconcile given objects - require.NoError(t, - waitForReconcile( - testContext, controller, - denyBindingWithAudit, &noParamSourcePolicy)) attr := attributeRecord(nil, fakeParams, admission.Create) warningRecorder := newWarningRecorder() warnCtx := warning.WithWarningRecorder(testContext, warningRecorder) - err := handler.Validate( + err := testContext.Plugin.Dispatch( warnCtx, attr, &admission.RuntimeObjectInterfaces{}, @@ -1794,10 +1202,10 @@ func TestAuditValidationAction(t *testing.T) { require.Equal(t, 1, len(annotations)) valueJson, ok := annotations["validation.policy.admission.k8s.io/validation_failure"] require.True(t, ok) - var value []validationFailureValue + var value []validating.ValidationFailureValue jsonErr := utiljson.Unmarshal([]byte(valueJson), &value) require.NoError(t, jsonErr) - expected := []validationFailureValue{{ + expected := []validating.ValidationFailureValue{{ ExpressionIndex: 0, Message: "I'm sorry Dave", ValidationActions: []v1beta1.ValidationAction{v1beta1.Audit}, @@ -1810,50 +1218,33 @@ func TestAuditValidationAction(t *testing.T) { } func TestWarnValidationAction(t *testing.T) { - testContext, testContextCancel := context.WithCancel(context.Background()) - defer testContextCancel() - compiler := &fakeCompiler{} - validator := &fakeValidator{} matcher := &fakeMatcher{ DefaultMatch: true, } - handler, _, tracker, controller := setupFakeTest(t, compiler, matcher) + testContext := setupFakeTest(t, compiler, matcher) // Push some fake noParamSourcePolicy := *denyPolicy noParamSourcePolicy.Spec.ParamKind = nil - compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter { - - return &fakeFilter{ - keyId: denyPolicy.Spec.Validations[0].Expression, - } - }) - - validator.RegisterDefinition(denyPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { - return ValidateResult{ - Decisions: []PolicyDecision{ + compiler.RegisterDefinition(denyPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) validating.ValidateResult { + return validating.ValidateResult{ + Decisions: []validating.PolicyDecision{ { - Action: ActionDeny, + Action: validating.ActionDeny, Message: "I'm sorry Dave", }, }, } }) - require.NoError(t, tracker.Create(definitionsGVR, &noParamSourcePolicy, noParamSourcePolicy.Namespace)) - require.NoError(t, tracker.Create(bindingsGVR, denyBindingWithWarn, denyBindingWithWarn.Namespace)) + require.NoError(t, testContext.UpdateAndWait(&noParamSourcePolicy, denyBindingWithWarn)) - // Wait for controller to reconcile given objects - require.NoError(t, - waitForReconcile( - testContext, controller, - denyBindingWithWarn, &noParamSourcePolicy)) attr := attributeRecord(nil, fakeParams, admission.Create) warningRecorder := newWarningRecorder() warnCtx := warning.WithWarningRecorder(testContext, warningRecorder) - err := handler.Validate( + err := testContext.Plugin.Dispatch( warnCtx, attr, &admission.RuntimeObjectInterfaces{}, @@ -1869,50 +1260,33 @@ func TestWarnValidationAction(t *testing.T) { } func TestAllValidationActions(t *testing.T) { - testContext, testContextCancel := context.WithCancel(context.Background()) - defer testContextCancel() - compiler := &fakeCompiler{} - validator := &fakeValidator{} matcher := &fakeMatcher{ DefaultMatch: true, } - handler, _, tracker, controller := setupFakeTest(t, compiler, matcher) + testContext := setupFakeTest(t, compiler, matcher) // Push some fake noParamSourcePolicy := *denyPolicy noParamSourcePolicy.Spec.ParamKind = nil - compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter { - - return &fakeFilter{ - keyId: denyPolicy.Spec.Validations[0].Expression, - } - }) - - validator.RegisterDefinition(denyPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { - return ValidateResult{ - Decisions: []PolicyDecision{ + compiler.RegisterDefinition(denyPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) validating.ValidateResult { + return validating.ValidateResult{ + Decisions: []validating.PolicyDecision{ { - Action: ActionDeny, + Action: validating.ActionDeny, Message: "I'm sorry Dave", }, }, } }) - require.NoError(t, tracker.Create(definitionsGVR, &noParamSourcePolicy, noParamSourcePolicy.Namespace)) - require.NoError(t, tracker.Create(bindingsGVR, denyBindingWithAll, denyBindingWithAll.Namespace)) + require.NoError(t, testContext.UpdateAndWait(&noParamSourcePolicy, denyBindingWithAll)) - // Wait for controller to reconcile given objects - require.NoError(t, - waitForReconcile( - testContext, controller, - denyBindingWithAll, &noParamSourcePolicy)) attr := attributeRecord(nil, fakeParams, admission.Create) warningRecorder := newWarningRecorder() warnCtx := warning.WithWarningRecorder(testContext, warningRecorder) - err := handler.Validate( + err := testContext.Plugin.Dispatch( warnCtx, attr, &admission.RuntimeObjectInterfaces{}, @@ -1925,10 +1299,10 @@ func TestAllValidationActions(t *testing.T) { require.Equal(t, 1, len(annotations)) valueJson, ok := annotations["validation.policy.admission.k8s.io/validation_failure"] require.True(t, ok) - var value []validationFailureValue + var value []validating.ValidationFailureValue jsonErr := utiljson.Unmarshal([]byte(valueJson), &value) require.NoError(t, jsonErr) - expected := []validationFailureValue{{ + expected := []validating.ValidationFailureValue{{ ExpressionIndex: 0, Message: "I'm sorry Dave", ValidationActions: []v1beta1.ValidationAction{v1beta1.Deny, v1beta1.Warn, v1beta1.Audit}, @@ -1941,18 +1315,12 @@ func TestAllValidationActions(t *testing.T) { } func TestNamespaceParamRefName(t *testing.T) { - reset() - testContext, testContextCancel := context.WithCancel(context.Background()) - defer testContextCancel() - compiler := &fakeCompiler{} - validator := &fakeValidator{} matcher := &fakeMatcher{ DefaultMatch: true, } - handler, _, tracker, controller := setupFakeTest(t, compiler, matcher) + testContext := setupFakeTest(t, compiler, matcher) - compiles := atomic.Int64{} evaluations := atomic.Int64{} // Use ConfigMap native-typed param @@ -1966,37 +1334,28 @@ func TestNamespaceParamRefName(t *testing.T) { namespaceParamBinding.Spec.ParamRef = &v1beta1.ParamRef{ Name: "replicas-test.example.com", } - - compiler.RegisterDefinition(&nativeTypeParamPolicy, func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter { - compiles.Add(1) - - return &fakeFilter{ - keyId: nativeTypeParamPolicy.Spec.Validations[0].Expression, - } - }) - lock := sync.Mutex{} observedParamNamespaces := []string{} - validator.RegisterDefinition(&nativeTypeParamPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { + compiler.RegisterDefinition(&nativeTypeParamPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) validating.ValidateResult { lock.Lock() defer lock.Unlock() evaluations.Add(1) if p, ok := versionedParams.(*v1.ConfigMap); ok { observedParamNamespaces = append(observedParamNamespaces, p.Namespace) - return ValidateResult{ - Decisions: []PolicyDecision{ + return validating.ValidateResult{ + Decisions: []validating.PolicyDecision{ { - Action: ActionDeny, + Action: validating.ActionDeny, Message: "correct type", }, }, } } - return ValidateResult{ - Decisions: []PolicyDecision{ + return validating.ValidateResult{ + Decisions: []validating.PolicyDecision{ { - Action: ActionDeny, + Action: validating.ActionDeny, Message: "Incorrect param type", }, }, @@ -2045,22 +1404,12 @@ func TestNamespaceParamRefName(t *testing.T) { "coolkey": "othernamespace", }, } - require.NoError(t, tracker.Create(definitionsGVR, &nativeTypeParamPolicy, nativeTypeParamPolicy.Namespace)) - require.NoError(t, tracker.Create(bindingsGVR, &namespaceParamBinding, namespaceParamBinding.Namespace)) - require.NoError(t, tracker.Add(configMapParam)) - require.NoError(t, tracker.Add(configMapParam2)) - require.NoError(t, tracker.Add(configMapParam3)) - - // Wait for controller to reconcile given objects - require.NoError(t, - waitForReconcile( - testContext, controller, - &namespaceParamBinding, &nativeTypeParamPolicy, configMapParam, configMapParam2, configMapParam3)) + require.NoError(t, testContext.UpdateAndWait(&nativeTypeParamPolicy, &namespaceParamBinding, configMapParam, configMapParam2, configMapParam3)) // Object is irrelevant/unchecked for this test. Just test that // the evaluator is executed with correct namespace, and returns admit // meaning the params passed was a configmap - err := handler.Validate( + err := testContext.Plugin.Dispatch( testContext, attributeRecord(nil, configMapParam, admission.Create), &admission.RuntimeObjectInterfaces{}, @@ -2070,11 +1419,11 @@ func TestNamespaceParamRefName(t *testing.T) { lock.Lock() defer lock.Unlock() require.ErrorContains(t, err, "correct type") - require.EqualValues(t, 1, compiles.Load()) + require.EqualValues(t, 1, compiler.getNumCompiles(&nativeTypeParamPolicy)) require.EqualValues(t, 1, evaluations.Load()) }() - err = handler.Validate( + err = testContext.Plugin.Dispatch( testContext, attributeRecord(nil, configMapParam2, admission.Create), &admission.RuntimeObjectInterfaces{}, @@ -2084,11 +1433,11 @@ func TestNamespaceParamRefName(t *testing.T) { lock.Lock() defer lock.Unlock() require.ErrorContains(t, err, "correct type") - require.EqualValues(t, 1, compiles.Load()) + require.EqualValues(t, 1, compiler.getNumCompiles(&nativeTypeParamPolicy)) require.EqualValues(t, 2, evaluations.Load()) }() - err = handler.Validate( + err = testContext.Plugin.Dispatch( testContext, attributeRecord(nil, configMapParam3, admission.Create), &admission.RuntimeObjectInterfaces{}, @@ -2098,11 +1447,11 @@ func TestNamespaceParamRefName(t *testing.T) { lock.Lock() defer lock.Unlock() require.ErrorContains(t, err, "correct type") - require.EqualValues(t, 1, compiles.Load()) + require.EqualValues(t, 1, compiler.getNumCompiles(&nativeTypeParamPolicy)) require.EqualValues(t, 3, evaluations.Load()) }() - err = handler.Validate( + err = testContext.Plugin.Dispatch( testContext, attributeRecord(nil, configMapParam, admission.Create), &admission.RuntimeObjectInterfaces{}, @@ -2113,7 +1462,7 @@ func TestNamespaceParamRefName(t *testing.T) { defer lock.Unlock() require.ErrorContains(t, err, "correct type") require.EqualValues(t, []string{"default", "mynamespace", "othernamespace", "default"}, observedParamNamespaces) - require.EqualValues(t, 1, compiles.Load()) + require.EqualValues(t, 1, compiler.getNumCompiles(&nativeTypeParamPolicy)) require.EqualValues(t, 4, evaluations.Load()) }() } @@ -2238,7 +1587,6 @@ func testParamRefCase(t *testing.T, paramIsClusterScoped, nameIsSet, namespaceIs } compiler := &fakeCompiler{} - validator := &fakeValidator{} matcher := &fakeMatcher{ DefaultMatch: true, } @@ -2258,25 +1606,19 @@ func testParamRefCase(t *testing.T, paramIsClusterScoped, nameIsSet, namespaceIs return oldParams } - compiler.RegisterDefinition(&policy, func(ea []cel.ExpressionAccessor, ovd cel.OptionalVariableDeclarations) cel.Filter { - return &fakeFilter{ - keyId: policy.Spec.Validations[0].Expression, - } - }) - - validator.RegisterDefinition(&policy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { + compiler.RegisterDefinition(&policy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) validating.ValidateResult { observeParam(versionedParams) - return ValidateResult{ - Decisions: []PolicyDecision{ + return validating.ValidateResult{ + Decisions: []validating.PolicyDecision{ { - Action: ActionDeny, + Action: validating.ActionDeny, Message: "Denied by policy", }, }, } }) - handler, paramTracker, tracker, controller := setupFakeTest(t, compiler, matcher) + testContext := setupFakeTest(t, compiler, matcher) // Create library of params to try to fool the controller params := []*unstructured.Unstructured{ @@ -2304,12 +1646,12 @@ func testParamRefCase(t *testing.T, paramIsClusterScoped, nameIsSet, namespaceIs newClusterScopedParam(matchingParamName+"5", otherNonmatchingLabels), } - require.NoError(t, tracker.Create(definitionsGVR, &policy, policy.Namespace)) - require.NoError(t, tracker.Create(bindingsGVR, &binding, binding.Namespace)) - require.NoError(t, waitForReconcile(context.TODO(), controller, &policy, &binding)) + require.NoError(t, testContext.UpdateAndWait(&policy, &binding)) for _, p := range params { - paramTracker.Add(p) + // Don't wait for these sync the informers would not have been + // created unless bound to a policy + require.NoError(t, testContext.Update(p)) } namespacedRequestObject := newParam("some param", nonMatchingNamespace, nil) @@ -2317,6 +1659,8 @@ func testParamRefCase(t *testing.T, paramIsClusterScoped, nameIsSet, namespaceIs // Validate a namespaced object, and verify that the params being validated // are the ones we would expect + timeoutCtx, timeoutCancel := context.WithTimeout(testContext, 5*time.Second) + defer timeoutCancel() var expectedParamsForNamespacedRequest []*unstructured.Unstructured for _, p := range params { if p.GetAPIVersion() != policy.Spec.ParamKind.APIVersion || p.GetKind() != policy.Spec.ParamKind.Kind { @@ -2328,6 +1672,9 @@ func testParamRefCase(t *testing.T, paramIsClusterScoped, nameIsSet, namespaceIs } if !paramIsClusterScoped { + // If the paramRef has empty namespace and the kind is + // namespaced-scoped, then it only matches params of the same + // namespace if len(paramRef.Namespace) == 0 && p.GetNamespace() != namespacedRequestObject.GetNamespace() { continue } @@ -2358,15 +1705,17 @@ func testParamRefCase(t *testing.T, paramIsClusterScoped, nameIsSet, namespaceIs } expectedParamsForNamespacedRequest = append(expectedParamsForNamespacedRequest, p) - require.NoError(t, waitForReconcile(context.TODO(), controller, p)) + require.NoError(t, testContext.WaitForReconcile(timeoutCtx, p)) } require.NotEmpty(t, expectedParamsForNamespacedRequest, "all test cases should match at least one param") - require.ErrorContains(t, handler.Validate(context.TODO(), attributeRecord(nil, namespacedRequestObject, admission.Create), &admission.RuntimeObjectInterfaces{}), "Denied by policy") + require.ErrorContains(t, testContext.Plugin.Dispatch(context.TODO(), attributeRecord(nil, namespacedRequestObject, admission.Create), &admission.RuntimeObjectInterfaces{}), "Denied by policy") require.ElementsMatch(t, expectedParamsForNamespacedRequest, getAndResetObservedParams(), "should exactly match expected params") // Validate a cluster-scoped object, and verify that the params being validated // are the ones we would expect var expectedParamsForClusterScopedRequest []*unstructured.Unstructured + timeoutCtx, timeoutCancel = context.WithTimeout(testContext, 5*time.Second) + defer timeoutCancel() for _, p := range params { if shouldErrorOnClusterScopedRequests { continue @@ -2404,10 +1753,11 @@ func testParamRefCase(t *testing.T, paramIsClusterScoped, nameIsSet, namespaceIs } expectedParamsForClusterScopedRequest = append(expectedParamsForClusterScopedRequest, p) - require.NoError(t, waitForReconcile(context.TODO(), controller, p)) + require.NoError(t, testContext.WaitForReconcile(timeoutCtx, p)) + } - err := handler.Validate(context.TODO(), attributeRecord(nil, clusterScopedRequestObject, admission.Create), &admission.RuntimeObjectInterfaces{}) + err := testContext.Plugin.Dispatch(context.TODO(), attributeRecord(nil, clusterScopedRequestObject, admission.Create), &admission.RuntimeObjectInterfaces{}) if shouldErrorOnClusterScopedRequests { // Cannot validate cliuster-scoped resources against a paramRef that sets namespace require.ErrorContains(t, err, "failed to configure binding: cannot use namespaced paramRef in policy binding that matches cluster-scoped resources") @@ -2421,33 +1771,18 @@ func testParamRefCase(t *testing.T, paramIsClusterScoped, nameIsSet, namespaceIs // Validate again to make sure NotFoundAction is respected var deleted []runtime.Object for _, p := range expectedParamsForNamespacedRequest { - if paramIsClusterScoped { - require.NoError(t, paramTracker.Delete(paramsGVK.GroupVersion().WithResource("clusterscopedparamsconfigs"), p.GetNamespace(), p.GetName())) - } else { - require.NoError(t, paramTracker.Delete(paramsGVK.GroupVersion().WithResource("paramsconfigs"), p.GetNamespace(), p.GetName())) - } deleted = append(deleted, p) } for _, p := range expectedParamsForClusterScopedRequest { - // Tracker.Delete docs says it wont raise error for not found, but its implmenetation - // pretty plainly does... - rsrsc := "paramsconfigs" - if paramIsClusterScoped { - rsrsc = "clusterscopedparamsconfigs" - } - if err := paramTracker.Delete(paramsGVK.GroupVersion().WithResource(rsrsc), p.GetNamespace(), p.GetName()); err != nil && !k8serrors.IsNotFound(err) { - require.NoError(t, err) - deleted = append(deleted, p) - } + deleted = append(deleted, p) } - require.NoError(t, waitForReconcileDeletion(context.TODO(), controller, deleted...)) - controller.refreshPolicies() + require.NoError(t, testContext.DeleteAndWait(deleted...)) // Check that NotFound is working correctly for both namespaeed & non-namespaced // request object - err = handler.Validate(context.TODO(), attributeRecord(nil, namespacedRequestObject, admission.Create), &admission.RuntimeObjectInterfaces{}) + err = testContext.Plugin.Dispatch(context.TODO(), attributeRecord(nil, namespacedRequestObject, admission.Create), &admission.RuntimeObjectInterfaces{}) if denyNotFound { require.ErrorContains(t, err, "no params found for policy binding with `Deny` parameterNotFoundAction") } else { @@ -2455,7 +1790,7 @@ func testParamRefCase(t *testing.T, paramIsClusterScoped, nameIsSet, namespaceIs } require.Empty(t, getAndResetObservedParams(), "policy should not have been evaluated") - err = handler.Validate(context.TODO(), attributeRecord(nil, clusterScopedRequestObject, admission.Create), &admission.RuntimeObjectInterfaces{}) + err = testContext.Plugin.Dispatch(context.TODO(), attributeRecord(nil, clusterScopedRequestObject, admission.Create), &admission.RuntimeObjectInterfaces{}) if shouldErrorOnClusterScopedRequests { require.ErrorContains(t, err, "failed to configure binding: cannot use namespaced paramRef in policy binding that matches cluster-scoped resources") @@ -2470,18 +1805,12 @@ func testParamRefCase(t *testing.T, paramIsClusterScoped, nameIsSet, namespaceIs // If the ParamKind is ClusterScoped, and namespace param is used. // This is a Configuration Error of the policy func TestNamespaceParamRefClusterScopedParamError(t *testing.T) { - reset() - testContext, testContextCancel := context.WithCancel(context.Background()) - defer testContextCancel() - compiler := &fakeCompiler{} - validator := &fakeValidator{} matcher := &fakeMatcher{ DefaultMatch: true, } - handler, _, tracker, controller := setupFakeTest(t, compiler, matcher) + testContext := setupFakeTest(t, compiler, matcher) - compiles := atomic.Int64{} evaluations := atomic.Int64{} // Use ValidatingAdmissionPolicy for param type since it is cluster-scoped @@ -2497,80 +1826,54 @@ func TestNamespaceParamRefClusterScopedParamError(t *testing.T) { Namespace: "mynamespace", } - compiler.RegisterDefinition(&nativeTypeParamPolicy, func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter { - compiles.Add(1) - - return &fakeFilter{ - keyId: nativeTypeParamPolicy.Spec.Validations[0].Expression, - } - }) - - validator.RegisterDefinition(&nativeTypeParamPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { + compiler.RegisterDefinition(&nativeTypeParamPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) validating.ValidateResult { evaluations.Add(1) if _, ok := versionedParams.(*v1beta1.ValidatingAdmissionPolicy); ok { - return ValidateResult{ - Decisions: []PolicyDecision{ + return validating.ValidateResult{ + Decisions: []validating.PolicyDecision{ { - Action: ActionAdmit, + Action: validating.ActionAdmit, Message: "correct type", }, }, } } - return ValidateResult{ - Decisions: []PolicyDecision{ + return validating.ValidateResult{ + Decisions: []validating.PolicyDecision{ { - Action: ActionDeny, + Action: validating.ActionDeny, Message: fmt.Sprintf("Incorrect param type %T", versionedParams), }, }, } }) - require.NoError(t, tracker.Create(definitionsGVR, &nativeTypeParamPolicy, nativeTypeParamPolicy.Namespace)) - require.NoError(t, tracker.Create(bindingsGVR, &namespaceParamBinding, namespaceParamBinding.Namespace)) - // Wait for controller to reconcile given objects - require.NoError(t, - waitForReconcile( - testContext, controller, - &namespaceParamBinding, &nativeTypeParamPolicy)) + require.NoError(t, testContext.UpdateAndWait(&nativeTypeParamPolicy, &namespaceParamBinding)) // Object is irrelevant/unchecked for this test. Just test that // the evaluator is executed with correct namespace, and returns admit // meaning the params passed was a configmap - err := handler.Validate( + err := testContext.Plugin.Dispatch( testContext, attributeRecord(nil, fakeParams, admission.Create), &admission.RuntimeObjectInterfaces{}, ) require.ErrorContains(t, err, "paramRef.namespace must not be provided for a cluster-scoped `paramKind`") - require.EqualValues(t, 1, compiles.Load()) + require.EqualValues(t, 1, compiler.getNumCompiles(&nativeTypeParamPolicy)) require.EqualValues(t, 0, evaluations.Load()) } func TestAuditAnnotations(t *testing.T) { - testContext, testContextCancel := context.WithCancel(context.Background()) - defer testContextCancel() - compiler := &fakeCompiler{} - validator := &fakeValidator{} matcher := &fakeMatcher{ DefaultMatch: true, } - handler, paramsTracker, tracker, controller := setupFakeTest(t, compiler, matcher) + testContext := setupFakeTest(t, compiler, matcher) // Push some fake policy := *denyPolicy - - compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter { - - return &fakeFilter{ - keyId: denyPolicy.Spec.Validations[0].Expression, - } - }) - - validator.RegisterDefinition(denyPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { + compiler.RegisterDefinition(denyPolicy, func(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) validating.ValidateResult { o, err := meta.Accessor(versionedParams) if err != nil { t.Fatal(err) @@ -2579,21 +1882,21 @@ func TestAuditAnnotations(t *testing.T) { if o.GetName() == "replicas-test2.example.com" { exampleValue = "special-value" } - return ValidateResult{ - AuditAnnotations: []PolicyAuditAnnotation{ + return validating.ValidateResult{ + AuditAnnotations: []validating.PolicyAuditAnnotation{ { Key: "example-key", Value: exampleValue, - Action: AuditAnnotationActionPublish, + Action: validating.AuditAnnotationActionPublish, }, { Key: "excluded-key", Value: "excluded-value", - Action: AuditAnnotationActionExclude, + Action: validating.AuditAnnotationActionExclude, }, { Key: "error-key", - Action: AuditAnnotationActionError, + Action: validating.AuditAnnotationActionError, Error: "example error", }, }, @@ -2612,21 +1915,10 @@ func TestAuditAnnotations(t *testing.T) { denyBinding3.SetName("denybinding3.example.com") denyBinding3.Spec.ParamRef.Name = fakeParams3.GetName() - require.NoError(t, paramsTracker.Add(fakeParams)) - require.NoError(t, paramsTracker.Add(fakeParams2)) - require.NoError(t, paramsTracker.Add(fakeParams3)) - require.NoError(t, tracker.Create(definitionsGVR, &policy, policy.Namespace)) - require.NoError(t, tracker.Create(bindingsGVR, denyBinding, denyBinding.Namespace)) - require.NoError(t, tracker.Create(bindingsGVR, denyBinding2, denyBinding2.Namespace)) - require.NoError(t, tracker.Create(bindingsGVR, denyBinding3, denyBinding3.Namespace)) + require.NoError(t, testContext.UpdateAndWait(fakeParams, fakeParams2, fakeParams3, &policy, denyBinding, denyBinding2, denyBinding3)) - // Wait for controller to reconcile given objects - require.NoError(t, - waitForReconcile( - testContext, controller, - denyBinding, denyBinding2, denyBinding3, denyPolicy, fakeParams, fakeParams2, fakeParams3)) attr := attributeRecord(nil, fakeParams, admission.Create) - err := handler.Validate( + err := testContext.Plugin.Dispatch( testContext, attr, &admission.RuntimeObjectInterfaces{}, @@ -2706,9 +1998,3 @@ func (r *warningRecorder) len() int { defer r.Unlock() return len(r.warnings) } - -type fakeAuthorizer struct{} - -func (f fakeAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) { - return authorizer.DecisionAllow, "", nil -} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/controller_reconcile.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/controller_reconcile.go deleted file mode 100644 index cc2af50b59d..00000000000 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/controller_reconcile.go +++ /dev/null @@ -1,551 +0,0 @@ -/* -Copyright 2022 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package validating - -import ( - "context" - "fmt" - "sync" - "time" - - v1 "k8s.io/api/admissionregistration/v1" - "k8s.io/api/admissionregistration/v1beta1" - corev1 "k8s.io/api/core/v1" - apiequality "k8s.io/apimachinery/pkg/api/equality" - "k8s.io/apimachinery/pkg/api/meta" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - utilruntime "k8s.io/apimachinery/pkg/util/runtime" - "k8s.io/apimachinery/pkg/util/sets" - "k8s.io/apiserver/pkg/admission/plugin/cel" - "k8s.io/apiserver/pkg/admission/plugin/policy/internal/generic" - celmetrics "k8s.io/apiserver/pkg/admission/plugin/policy/validating/metrics" - "k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions" - "k8s.io/apiserver/pkg/cel/environment" - "k8s.io/client-go/dynamic" - "k8s.io/client-go/dynamic/dynamicinformer" - "k8s.io/client-go/informers" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/tools/cache" -) - -type policyController struct { - once sync.Once - context context.Context - dynamicClient dynamic.Interface - informerFactory informers.SharedInformerFactory - restMapper meta.RESTMapper - policyDefinitionsController generic.Controller[*v1beta1.ValidatingAdmissionPolicy] - policyBindingController generic.Controller[*v1beta1.ValidatingAdmissionPolicyBinding] - - // Provided to the policy's Compile function as an injected dependency to - // assist with compiling its expressions to CEL - // pass nil to create filter compiler in demand - filterCompiler cel.FilterCompiler - - matcher Matcher - - newValidator - - client kubernetes.Interface - // Lock which protects - // All Below fields - // All above fields should be assumed constant - mutex sync.RWMutex - - cachedPolicies []policyData - - // controller and metadata - paramsCRDControllers map[v1beta1.ParamKind]*paramInfo - - // Index for each definition namespace/name, contains all binding - // namespace/names known to exist for that definition - definitionInfo map[namespacedName]*definitionInfo - - // Index for each bindings namespace/name. Contains compiled templates - // for the binding depending on the policy/param combination. - bindingInfos map[namespacedName]*bindingInfo - - // Map from namespace/name of a definition to a set of namespace/name - // of bindings which depend on it. - // All keys must have at least one dependent binding - // All binding names MUST exist as a key bindingInfos - definitionsToBindings map[namespacedName]sets.Set[namespacedName] -} - -type newValidator func(validationFilter cel.Filter, celMatcher matchconditions.Matcher, auditAnnotationFilter, messageFilter cel.Filter, failurePolicy *v1.FailurePolicyType) Validator - -func newPolicyController( - restMapper meta.RESTMapper, - client kubernetes.Interface, - dynamicClient dynamic.Interface, - informerFactory informers.SharedInformerFactory, - filterCompiler cel.FilterCompiler, - matcher Matcher, - policiesInformer generic.Informer[*v1beta1.ValidatingAdmissionPolicy], - bindingsInformer generic.Informer[*v1beta1.ValidatingAdmissionPolicyBinding], -) *policyController { - res := &policyController{} - *res = policyController{ - filterCompiler: filterCompiler, - definitionInfo: make(map[namespacedName]*definitionInfo), - bindingInfos: make(map[namespacedName]*bindingInfo), - paramsCRDControllers: make(map[v1beta1.ParamKind]*paramInfo), - definitionsToBindings: make(map[namespacedName]sets.Set[namespacedName]), - matcher: matcher, - newValidator: NewValidator, - policyDefinitionsController: generic.NewController( - policiesInformer, - res.reconcilePolicyDefinition, - generic.ControllerOptions{ - Workers: 1, - Name: "cel-policy-definitions", - }, - ), - policyBindingController: generic.NewController( - bindingsInformer, - res.reconcilePolicyBinding, - generic.ControllerOptions{ - Workers: 1, - Name: "cel-policy-bindings", - }, - ), - restMapper: restMapper, - dynamicClient: dynamicClient, - informerFactory: informerFactory, - client: client, - } - return res -} - -func (c *policyController) Run(ctx context.Context) { - // Only support being run once - c.once.Do(func() { - c.context = ctx - - wg := sync.WaitGroup{} - - wg.Add(1) - go func() { - defer wg.Done() - c.policyDefinitionsController.Run(ctx) - }() - - wg.Add(1) - go func() { - defer wg.Done() - c.policyBindingController.Run(ctx) - }() - - <-ctx.Done() - wg.Wait() - }) -} - -func (c *policyController) HasSynced() bool { - return c.policyDefinitionsController.HasSynced() && c.policyBindingController.HasSynced() -} - -func (c *policyController) reconcilePolicyDefinition(namespace, name string, definition *v1beta1.ValidatingAdmissionPolicy) error { - c.mutex.Lock() - defer c.mutex.Unlock() - err := c.reconcilePolicyDefinitionSpec(namespace, name, definition) - return err -} - -func (c *policyController) reconcilePolicyDefinitionSpec(namespace, name string, definition *v1beta1.ValidatingAdmissionPolicy) error { - c.cachedPolicies = nil // invalidate cachedPolicies - - // Namespace for policydefinition is empty. - nn := getNamespaceName(namespace, name) - info, ok := c.definitionInfo[nn] - if !ok { - info = &definitionInfo{} - c.definitionInfo[nn] = info - // TODO(DangerOnTheRanger): add support for "warn" being a valid enforcementAction - celmetrics.Metrics.ObserveDefinition(context.TODO(), "active", "deny") - } - - // Skip reconcile if the spec of the definition is unchanged and had a - // successful previous sync - if info.configurationError == nil && info.lastReconciledValue != nil && definition != nil && - apiequality.Semantic.DeepEqual(info.lastReconciledValue.Spec, definition.Spec) { - return nil - } - - var paramSource *v1beta1.ParamKind - if definition != nil { - paramSource = definition.Spec.ParamKind - } - - // If param source has changed, remove definition as dependent of old params - // If there are no more dependents of old param, stop and clean up controller - if info.lastReconciledValue != nil && info.lastReconciledValue.Spec.ParamKind != nil { - oldParamSource := *info.lastReconciledValue.Spec.ParamKind - - // If we are: - // - switching from having a param to not having a param (includes deletion) - // - or from having a param to a different one - // we remove dependency on the controller. - if paramSource == nil || *paramSource != oldParamSource { - if oldParamInfo, ok := c.paramsCRDControllers[oldParamSource]; ok { - oldParamInfo.dependentDefinitions.Delete(nn) - if len(oldParamInfo.dependentDefinitions) == 0 { - oldParamInfo.stop() - delete(c.paramsCRDControllers, oldParamSource) - } - } - } - } - - // Reset all previously compiled evaluators in case something relevant in - // definition has changed. - for key := range c.definitionsToBindings[nn] { - bindingInfo := c.bindingInfos[key] - bindingInfo.validator = nil - c.bindingInfos[key] = bindingInfo - } - - if definition == nil { - delete(c.definitionInfo, nn) - return nil - } - - // Update definition info - info.lastReconciledValue = definition - info.configurationError = nil - - if paramSource == nil { - // Skip setting up controller for empty param type - return nil - } - // find GVR for params - // Parse param source into a GVK - - paramSourceGV, err := schema.ParseGroupVersion(paramSource.APIVersion) - if err != nil { - // Failed to resolve. Return error so we retry again (rate limited) - // Save a record of this definition with an evaluator that unconditionally - info.configurationError = fmt.Errorf("failed to parse apiVersion of paramKind '%v' with error: %w", paramSource.String(), err) - - // Return nil, since this error cannot be resolved by waiting more time - return nil - } - - paramsGVR, err := c.restMapper.RESTMapping(schema.GroupKind{ - Group: paramSourceGV.Group, - Kind: paramSource.Kind, - }, paramSourceGV.Version) - - if err != nil { - // Failed to resolve. Return error so we retry again (rate limited) - // Save a record of this definition with an evaluator that unconditionally - // - info.configurationError = fmt.Errorf("failed to find resource referenced by paramKind: '%v'", paramSourceGV.WithKind(paramSource.Kind)) - return info.configurationError - } - - paramInfo := c.ensureParamInfo(paramSource, paramsGVR) - paramInfo.dependentDefinitions.Insert(nn) - - return nil -} - -// Ensures that there is an informer started for the given GVK to be used as a -// param -func (c *policyController) ensureParamInfo(paramSource *v1beta1.ParamKind, mapping *meta.RESTMapping) *paramInfo { - if info, ok := c.paramsCRDControllers[*paramSource]; ok { - return info - } - - // We are not watching this param. Start an informer for it. - instanceContext, instanceCancel := context.WithCancel(c.context) - - var informer cache.SharedIndexInformer - - // Try to see if our provided informer factory has an informer for this type. - // We assume the informer is already started, and starts all types associated - // with it. - if genericInformer, err := c.informerFactory.ForResource(mapping.Resource); err == nil { - informer = genericInformer.Informer() - - // Ensure the informer is started - // Use policyController's context rather than the instance context. - // PolicyController context is expected to last until app shutdown - // This is due to behavior of informerFactory which would cause the - // informer to stop running once the context is cancelled, and - // never started again. - c.informerFactory.Start(c.context.Done()) - } else { - // Dynamic JSON informer fallback. - // Cannot use shared dynamic informer since it would be impossible - // to clean CRD informers properly with multiple dependents - // (cannot start ahead of time, and cannot track dependencies via stopCh) - informer = dynamicinformer.NewFilteredDynamicInformer( - c.dynamicClient, - mapping.Resource, - corev1.NamespaceAll, - // Use same interval as is used for k8s typed sharedInformerFactory - // https://github.com/kubernetes/kubernetes/blob/7e0923899fed622efbc8679cca6b000d43633e38/cmd/kube-apiserver/app/server.go#L430 - 10*time.Minute, - cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, - nil, - ).Informer() - go informer.Run(instanceContext.Done()) - } - - controller := generic.NewController( - generic.NewInformer[runtime.Object](informer), - c.reconcileParams, - generic.ControllerOptions{ - Workers: 1, - Name: paramSource.String() + "-controller", - }, - ) - - ret := ¶mInfo{ - controller: controller, - stop: instanceCancel, - scope: mapping.Scope, - dependentDefinitions: sets.New[namespacedName](), - } - c.paramsCRDControllers[*paramSource] = ret - - go controller.Run(instanceContext) - return ret - -} - -func (c *policyController) reconcilePolicyBinding(namespace, name string, binding *v1beta1.ValidatingAdmissionPolicyBinding) error { - c.mutex.Lock() - defer c.mutex.Unlock() - - c.cachedPolicies = nil // invalidate cachedPolicies - - // Namespace for PolicyBinding is empty. In the future a namespaced binding - // may be added - // https://github.com/kubernetes/enhancements/blob/bf5c3c81ea2081d60c1dc7c832faa98479e06209/keps/sig-api-machinery/3488-cel-admission-control/README.md?plain=1#L1042 - nn := getNamespaceName(namespace, name) - info, ok := c.bindingInfos[nn] - if !ok { - info = &bindingInfo{} - c.bindingInfos[nn] = info - } - - // Skip if the spec of the binding is unchanged. - if info.lastReconciledValue != nil && binding != nil && - apiequality.Semantic.DeepEqual(info.lastReconciledValue.Spec, binding.Spec) { - return nil - } - - var oldNamespacedDefinitionName namespacedName - if info.lastReconciledValue != nil { - // All validating policies are cluster-scoped so have empty namespace - oldNamespacedDefinitionName = getNamespaceName("", info.lastReconciledValue.Spec.PolicyName) - } - - var namespacedDefinitionName namespacedName - if binding != nil { - // All validating policies are cluster-scoped so have empty namespace - namespacedDefinitionName = getNamespaceName("", binding.Spec.PolicyName) - } - - // Remove record of binding from old definition if the referred policy - // has changed - if oldNamespacedDefinitionName != namespacedDefinitionName { - if dependentBindings, ok := c.definitionsToBindings[oldNamespacedDefinitionName]; ok { - dependentBindings.Delete(nn) - - // if there are no more dependent bindings, remove knowledge of the - // definition altogether - if len(dependentBindings) == 0 { - delete(c.definitionsToBindings, oldNamespacedDefinitionName) - } - } - } - - if binding == nil { - delete(c.bindingInfos, nn) - return nil - } - - // Add record of binding to new definition - if dependentBindings, ok := c.definitionsToBindings[namespacedDefinitionName]; ok { - dependentBindings.Insert(nn) - } else { - c.definitionsToBindings[namespacedDefinitionName] = sets.New(nn) - } - - // Remove compiled template for old binding - info.validator = nil - info.lastReconciledValue = binding - return nil -} - -func (c *policyController) reconcileParams(namespace, name string, params runtime.Object) error { - // Do nothing. - // When we add informational type checking we will need to compile in the - // reconcile loops instead of lazily so we can add compiler errors / type - // checker errors to the status of the resources. - return nil -} - -// Fetches the latest set of policy data or recalculates it if it has changed -// since it was last fetched -func (c *policyController) latestPolicyData() []policyData { - existing := func() []policyData { - c.mutex.RLock() - defer c.mutex.RUnlock() - - return c.cachedPolicies - }() - - if existing != nil { - return existing - } - - c.mutex.Lock() - defer c.mutex.Unlock() - - var res []policyData - for definitionNN, definitionInfo := range c.definitionInfo { - var bindingInfos []bindingInfo - for bindingNN := range c.definitionsToBindings[definitionNN] { - bindingInfo := c.bindingInfos[bindingNN] - if bindingInfo.validator == nil && definitionInfo.configurationError == nil { - hasParam := false - if definitionInfo.lastReconciledValue.Spec.ParamKind != nil { - hasParam = true - } - optionalVars := cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: true} - expressionOptionalVars := cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: false} - failurePolicy := convertv1beta1FailurePolicyTypeTov1FailurePolicyType(definitionInfo.lastReconciledValue.Spec.FailurePolicy) - var matcher matchconditions.Matcher = nil - matchConditions := definitionInfo.lastReconciledValue.Spec.MatchConditions - - filterCompiler := c.filterCompiler - if filterCompiler == nil { - compositedCompiler, err := cel.NewCompositedCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion())) - if err == nil { - filterCompiler = compositedCompiler - compositedCompiler.CompileAndStoreVariables(convertv1beta1Variables(definitionInfo.lastReconciledValue.Spec.Variables), optionalVars, environment.StoredExpressions) - } else { - utilruntime.HandleError(err) - } - } - if len(matchConditions) > 0 { - matchExpressionAccessors := make([]cel.ExpressionAccessor, len(matchConditions)) - for i := range matchConditions { - matchExpressionAccessors[i] = (*matchconditions.MatchCondition)(&matchConditions[i]) - } - matcher = matchconditions.NewMatcher(filterCompiler.Compile(matchExpressionAccessors, optionalVars, environment.StoredExpressions), failurePolicy, "policy", "validate", definitionInfo.lastReconciledValue.Name) - } - bindingInfo.validator = c.newValidator( - filterCompiler.Compile(convertv1beta1Validations(definitionInfo.lastReconciledValue.Spec.Validations), optionalVars, environment.StoredExpressions), - matcher, - filterCompiler.Compile(convertv1beta1AuditAnnotations(definitionInfo.lastReconciledValue.Spec.AuditAnnotations), optionalVars, environment.StoredExpressions), - filterCompiler.Compile(convertv1beta1MessageExpressions(definitionInfo.lastReconciledValue.Spec.Validations), expressionOptionalVars, environment.StoredExpressions), - failurePolicy, - ) - } - bindingInfos = append(bindingInfos, *bindingInfo) - } - - var pInfo paramInfo - if paramKind := definitionInfo.lastReconciledValue.Spec.ParamKind; paramKind != nil { - if info, ok := c.paramsCRDControllers[*paramKind]; ok { - pInfo = *info - } - } - - res = append(res, policyData{ - definitionInfo: *definitionInfo, - paramInfo: pInfo, - bindings: bindingInfos, - }) - } - - c.cachedPolicies = res - return res -} - -func convertv1beta1FailurePolicyTypeTov1FailurePolicyType(policyType *v1beta1.FailurePolicyType) *v1.FailurePolicyType { - if policyType == nil { - return nil - } - - var v1FailPolicy v1.FailurePolicyType - if *policyType == v1beta1.Fail { - v1FailPolicy = v1.Fail - } else if *policyType == v1beta1.Ignore { - v1FailPolicy = v1.Ignore - } - return &v1FailPolicy -} - -func convertv1beta1Validations(inputValidations []v1beta1.Validation) []cel.ExpressionAccessor { - celExpressionAccessor := make([]cel.ExpressionAccessor, len(inputValidations)) - for i, validation := range inputValidations { - validation := ValidationCondition{ - Expression: validation.Expression, - Message: validation.Message, - Reason: validation.Reason, - } - celExpressionAccessor[i] = &validation - } - return celExpressionAccessor -} - -func convertv1beta1MessageExpressions(inputValidations []v1beta1.Validation) []cel.ExpressionAccessor { - celExpressionAccessor := make([]cel.ExpressionAccessor, len(inputValidations)) - for i, validation := range inputValidations { - if validation.MessageExpression != "" { - condition := MessageExpressionCondition{ - MessageExpression: validation.MessageExpression, - } - celExpressionAccessor[i] = &condition - } - } - return celExpressionAccessor -} - -func convertv1beta1AuditAnnotations(inputValidations []v1beta1.AuditAnnotation) []cel.ExpressionAccessor { - celExpressionAccessor := make([]cel.ExpressionAccessor, len(inputValidations)) - for i, validation := range inputValidations { - validation := AuditAnnotationCondition{ - Key: validation.Key, - ValueExpression: validation.ValueExpression, - } - celExpressionAccessor[i] = &validation - } - return celExpressionAccessor -} - -func convertv1beta1Variables(variables []v1beta1.Variable) []cel.NamedExpressionAccessor { - namedExpressions := make([]cel.NamedExpressionAccessor, len(variables)) - for i, variable := range variables { - namedExpressions[i] = &Variable{Name: variable.Name, Expression: variable.Expression} - } - return namedExpressions -} - -func getNamespaceName(namespace, name string) namespacedName { - return namespacedName{ - namespace: namespace, - name: name, - } -} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/controller.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/dispatcher.go similarity index 71% rename from staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/controller.go rename to staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/dispatcher.go index c5c1ffd6063..5af6a3f3092 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/controller.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/dispatcher.go @@ -21,8 +21,6 @@ import ( "errors" "fmt" "strings" - "sync" - "sync/atomic" "time" "k8s.io/api/admissionregistration/v1beta1" @@ -34,47 +32,32 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" utiljson "k8s.io/apimachinery/pkg/util/json" utilruntime "k8s.io/apimachinery/pkg/util/runtime" - "k8s.io/apimachinery/pkg/util/sets" - "k8s.io/apimachinery/pkg/util/wait" "k8s.io/apiserver/pkg/admission" - "k8s.io/apiserver/pkg/admission/plugin/policy/internal/generic" - "k8s.io/apiserver/pkg/admission/plugin/policy/matching" + "k8s.io/apiserver/pkg/admission/plugin/policy/generic" celmetrics "k8s.io/apiserver/pkg/admission/plugin/policy/validating/metrics" celconfig "k8s.io/apiserver/pkg/apis/cel" "k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/warning" - "k8s.io/client-go/dynamic" "k8s.io/client-go/informers" - "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/cache" "k8s.io/klog/v2" ) -var _ CELPolicyEvaluator = &celAdmissionController{} - -// celAdmissionController is the top-level controller for admission control using CEL -// it is responsible for watching policy definitions, bindings, and config param CRDs -type celAdmissionController struct { - // Controller which manages book-keeping for the cluster's dynamic policy - // information. - policyController *policyController - - // atomic []policyData - // list of every known policy definition, and all informatoin required to - // validate its bindings against an object. - // A snapshot of the current policy configuration is synced with this field - // asynchronously - definitions atomic.Value - - authz authorizer.Authorizer +type dispatcher struct { + matcher Matcher + authz authorizer.Authorizer } -// Everything someone might need to validate a single ValidatingPolicyDefinition -// against all of its registered bindings. -type policyData struct { - definitionInfo - paramInfo - bindings []bindingInfo +var _ generic.Dispatcher[PolicyHook] = &dispatcher{} + +func NewDispatcher( + authorizer authorizer.Authorizer, + matcher Matcher, +) generic.Dispatcher[PolicyHook] { + return &dispatcher{ + matcher: matcher, + authz: authorizer, + } } // contains the cel PolicyDecisions along with the ValidatingAdmissionPolicy and ValidatingAdmissionPolicyBinding @@ -85,110 +68,8 @@ type policyDecisionWithMetadata struct { Binding *v1beta1.ValidatingAdmissionPolicyBinding } -// namespaceName is used as a key in definitionInfo and bindingInfos -type namespacedName struct { - namespace, name string -} - -type definitionInfo struct { - // Error about the state of the definition's configuration and the cluster - // preventing its enforcement or compilation. - // Reset every reconciliation - configurationError error - - // Last value seen by this controller to be used in policy enforcement - // May not be nil - lastReconciledValue *v1beta1.ValidatingAdmissionPolicy -} - -type bindingInfo struct { - // Compiled CEL expression turned into an validator - validator Validator - - // Last value seen by this controller to be used in policy enforcement - // May not be nil - lastReconciledValue *v1beta1.ValidatingAdmissionPolicyBinding -} - -type paramInfo struct { - // Controller which is watching this param CRD - controller generic.Controller[runtime.Object] - - // Function to call to stop the informer and clean up the controller - stop func() - - // Whether this param is cluster or namespace scoped - scope meta.RESTScope - - // Policy Definitions which refer to this param CRD - dependentDefinitions sets.Set[namespacedName] -} - -func NewAdmissionController( - // Injected Dependencies - informerFactory informers.SharedInformerFactory, - client kubernetes.Interface, - restMapper meta.RESTMapper, - dynamicClient dynamic.Interface, - authz authorizer.Authorizer, -) CELPolicyEvaluator { - return &celAdmissionController{ - definitions: atomic.Value{}, - policyController: newPolicyController( - restMapper, - client, - dynamicClient, - informerFactory, - nil, - NewMatcher(matching.NewMatcher(informerFactory.Core().V1().Namespaces().Lister(), client)), - generic.NewInformer[*v1beta1.ValidatingAdmissionPolicy]( - informerFactory.Admissionregistration().V1beta1().ValidatingAdmissionPolicies().Informer()), - generic.NewInformer[*v1beta1.ValidatingAdmissionPolicyBinding]( - informerFactory.Admissionregistration().V1beta1().ValidatingAdmissionPolicyBindings().Informer()), - ), - authz: authz, - } -} - -func (c *celAdmissionController) Run(stopCh <-chan struct{}) { - ctx, cancel := context.WithCancel(context.Background()) - wg := sync.WaitGroup{} - - wg.Add(1) - go func() { - defer wg.Done() - c.policyController.Run(ctx) - }() - - wg.Add(1) - go func() { - defer wg.Done() - - // Wait indefinitely until policies/bindings are listed & handled before - // allowing policies to be refreshed - if !cache.WaitForNamedCacheSync("cel-admission-controller", ctx.Done(), c.policyController.HasSynced) { - return - } - - // Loop every 1 second until context is cancelled, refreshing policies - wait.Until(c.refreshPolicies, 1*time.Second, ctx.Done()) - }() - - <-stopCh - cancel() - wg.Wait() -} - -const maxAuditAnnotationValueLength = 10 * 1024 - -func (c *celAdmissionController) Validate( - ctx context.Context, - a admission.Attributes, - o admission.ObjectInterfaces, -) (err error) { - if !c.HasSynced() { - return admission.NewForbidden(a, fmt.Errorf("not yet ready to handle request")) - } +// Dispatch implements generic.Dispatcher. +func (c *dispatcher) Dispatch(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces, hooks []PolicyHook) error { var deniedDecisions []policyDecisionWithMetadata @@ -232,19 +113,18 @@ func (c *celAdmissionController) Validate( }) } } - policyDatas := c.definitions.Load().([]policyData) authz := newCachingAuthorizer(c.authz) - for _, definitionInfo := range policyDatas { + for _, hook := range hooks { // versionedAttributes will be set to non-nil inside of the loop, but // is scoped outside of the param loop so we only convert once. We defer // conversion so that it is only performed when we know a policy matches, // saving the cost of converting non-matching requests. var versionedAttr *admission.VersionedAttributes - definition := definitionInfo.lastReconciledValue - matches, matchResource, matchKind, err := c.policyController.matcher.DefinitionMatches(a, o, definition) + definition := hook.Policy + matches, matchResource, matchKind, err := c.matcher.DefinitionMatches(a, o, definition) if err != nil { // Configuration error. addConfigError(err, definition, nil) @@ -253,18 +133,17 @@ func (c *celAdmissionController) Validate( if !matches { // Policy definition does not match request continue - } else if definitionInfo.configurationError != nil { + } else if hook.ConfigurationError != nil { // Configuration error. - addConfigError(definitionInfo.configurationError, definition, nil) + addConfigError(hook.ConfigurationError, definition, nil) continue } auditAnnotationCollector := newAuditAnnotationCollector() - for _, bindingInfo := range definitionInfo.bindings { + for _, binding := range hook.Bindings { // If the key is inside dependentBindings, there is guaranteed to // be a bindingInfo for it - binding := bindingInfo.lastReconciledValue - matches, err := c.policyController.matcher.BindingMatches(a, o, binding) + matches, err := c.matcher.BindingMatches(a, o, binding) if err != nil { // Configuration error. addConfigError(err, definition, binding) @@ -274,7 +153,14 @@ func (c *celAdmissionController) Validate( continue } - params, err := c.collectParams(definition.Spec.ParamKind, definitionInfo.paramInfo, binding.Spec.ParamRef, a.GetNamespace()) + params, err := c.collectParams( + definition.Spec.ParamKind, + hook.ParamInformer, + hook.ParamScope, + binding.Spec.ParamRef, + a.GetNamespace(), + ) + if err != nil { addConfigError(err, definition, binding) continue @@ -303,7 +189,7 @@ func (c *celAdmissionController) Validate( // if it is cluster scoped, namespaceName will be empty // Otherwise, get the Namespace resource. if namespaceName != "" { - namespace, err = c.policyController.matcher.GetNamespace(namespaceName) + namespace, err = c.matcher.GetNamespace(namespaceName) if err != nil { return err } @@ -323,7 +209,18 @@ func (c *celAdmissionController) Validate( nested: param, } } - validationResults = append(validationResults, bindingInfo.validator.Validate(ctx, matchResource, versionedAttr, p, namespace, celconfig.RuntimeCELCostBudget, authz)) + + validationResults = append(validationResults, + hook.Evaluator.Validate( + ctx, + matchResource, + versionedAttr, + p, + namespace, + celconfig.RuntimeCELCostBudget, + authz, + ), + ) } for _, validationResult := range validationResults { @@ -344,7 +241,7 @@ func (c *celAdmissionController) Validate( }) celmetrics.Metrics.ObserveRejection(ctx, decision.Elapsed, definition.Name, binding.Name, "active") case v1beta1.Audit: - c.publishValidationFailureAnnotation(binding, i, decision, versionedAttr) + publishValidationFailureAnnotation(binding, i, decision, versionedAttr) celmetrics.Metrics.ObserveAudit(ctx, decision.Elapsed, definition.Name, binding.Name, "active") case v1beta1.Warn: warning.AddWarning(ctx, "", fmt.Sprintf("Validation failed for ValidatingAdmissionPolicy '%s' with binding '%s': %s", definition.Name, binding.Name, decision.Message)) @@ -411,28 +308,30 @@ func (c *celAdmissionController) Validate( } // Returns objects to use to evaluate the policy -func (c *celAdmissionController) collectParams( +// Copied with minor modification to account for slightly different arguments +func (c *dispatcher) collectParams( paramKind *v1beta1.ParamKind, - info paramInfo, + paramInformer informers.GenericInformer, + paramScope meta.RESTScope, paramRef *v1beta1.ParamRef, namespace string, ) ([]runtime.Object, error) { // If definition has paramKind, paramRef is required in binding. // If definition has no paramKind, paramRef set in binding will be ignored. var params []runtime.Object - var paramStore generic.NamespacedLister[runtime.Object] + var paramStore cache.GenericNamespaceLister // Make sure the param kind is ready to use if paramKind != nil && paramRef != nil { - if info.controller == nil { + if paramInformer == nil { return nil, fmt.Errorf("paramKind kind `%v` not known", paramKind.String()) } // Set up cluster-scoped, or namespaced access to the params // "default" if not provided, and paramKind is namespaced - paramStore = info.controller.Informer() - if info.scope.Name() == meta.RESTScopeNameNamespace { + paramStore = paramInformer.Lister() + if paramScope.Name() == meta.RESTScopeNameNamespace { paramsNamespace := namespace if len(paramRef.Namespace) > 0 { paramsNamespace = paramRef.Namespace @@ -442,16 +341,16 @@ func (c *celAdmissionController) collectParams( return nil, fmt.Errorf("cannot use namespaced paramRef in policy binding that matches cluster-scoped resources") } - paramStore = info.controller.Informer().Namespaced(paramsNamespace) + paramStore = paramInformer.Lister().ByNamespace(paramsNamespace) } // If the param informer for this admission policy has not yet // had time to perform an initial listing, don't attempt to use // it. - timeoutCtx, cancel := context.WithTimeout(c.policyController.context, 1*time.Second) + timeoutCtx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() - if !cache.WaitForCacheSync(timeoutCtx.Done(), info.controller.HasSynced) { + if !cache.WaitForCacheSync(timeoutCtx.Done(), paramInformer.Informer().HasSynced) { return nil, fmt.Errorf("paramKind kind `%v` not yet synced to use for admission", paramKind.String()) } @@ -467,7 +366,7 @@ func (c *celAdmissionController) collectParams( // Policy ParamKind is set, but binding does not use it. // Validate with nil params return []runtime.Object{nil}, nil - case len(paramRef.Namespace) > 0 && info.scope.Name() == meta.RESTScopeRoot.Name(): + case len(paramRef.Namespace) > 0 && paramScope.Name() == meta.RESTScopeRoot.Name(): // Not allowed to set namespace for cluster-scoped param return nil, fmt.Errorf("paramRef.namespace must not be provided for a cluster-scoped `paramKind`") @@ -527,10 +426,10 @@ func (c *celAdmissionController) collectParams( return params, nil } -func (c *celAdmissionController) publishValidationFailureAnnotation(binding *v1beta1.ValidatingAdmissionPolicyBinding, expressionIndex int, decision PolicyDecision, attributes admission.Attributes) { +func publishValidationFailureAnnotation(binding *v1beta1.ValidatingAdmissionPolicyBinding, expressionIndex int, decision PolicyDecision, attributes admission.Attributes) { key := "validation.policy.admission.k8s.io/validation_failure" // Marshal to a list of failures since, in the future, we may need to support multiple failures - valueJson, err := utiljson.Marshal([]validationFailureValue{{ + valueJSON, err := utiljson.Marshal([]ValidationFailureValue{{ ExpressionIndex: expressionIndex, Message: decision.Message, ValidationActions: binding.Spec.ValidationActions, @@ -540,27 +439,17 @@ func (c *celAdmissionController) publishValidationFailureAnnotation(binding *v1b if err != nil { klog.Warningf("Failed to set admission audit annotation %s for ValidatingAdmissionPolicy %s and ValidatingAdmissionPolicyBinding %s: %v", key, binding.Spec.PolicyName, binding.Name, err) } - value := string(valueJson) + value := string(valueJSON) if err := attributes.AddAnnotation(key, value); err != nil { klog.Warningf("Failed to set admission audit annotation %s to %s for ValidatingAdmissionPolicy %s and ValidatingAdmissionPolicyBinding %s: %v", key, value, binding.Spec.PolicyName, binding.Name, err) } } -func (c *celAdmissionController) HasSynced() bool { - return c.policyController.HasSynced() && c.definitions.Load() != nil -} - -func (c *celAdmissionController) ValidateInitialization() error { - return c.policyController.matcher.ValidateInitialization() -} - -func (c *celAdmissionController) refreshPolicies() { - c.definitions.Store(c.policyController.latestPolicyData()) -} +const maxAuditAnnotationValueLength = 10 * 1024 // validationFailureValue defines the JSON format of a "validation.policy.admission.k8s.io/validation_failure" audit // annotation value. -type validationFailureValue struct { +type ValidationFailureValue struct { Message string `json:"message"` Policy string `json:"policy"` Binding string `json:"binding"` diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/plugin.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/plugin.go new file mode 100644 index 00000000000..65d44c4f51d --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/plugin.go @@ -0,0 +1,197 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package validating + +import ( + "context" + "io" + + v1 "k8s.io/api/admissionregistration/v1" + "k8s.io/api/admissionregistration/v1beta1" + "k8s.io/apimachinery/pkg/api/meta" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apiserver/pkg/admission" + "k8s.io/apiserver/pkg/admission/initializer" + "k8s.io/apiserver/pkg/admission/plugin/cel" + "k8s.io/apiserver/pkg/admission/plugin/policy/generic" + "k8s.io/apiserver/pkg/admission/plugin/policy/matching" + "k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions" + "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/apiserver/pkg/cel/environment" + "k8s.io/apiserver/pkg/features" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/informers" + "k8s.io/client-go/kubernetes" + "k8s.io/component-base/featuregate" +) + +const ( + // PluginName indicates the name of admission plug-in + PluginName = "ValidatingAdmissionPolicy" +) + +// Register registers a plugin +func Register(plugins *admission.Plugins) { + plugins.Register(PluginName, func(configFile io.Reader) (admission.Interface, error) { + return NewPlugin(configFile), nil + }) +} + +// Plugin is an implementation of admission.Interface. +type Policy = v1beta1.ValidatingAdmissionPolicy +type PolicyBinding = v1beta1.ValidatingAdmissionPolicyBinding +type PolicyEvaluator = Validator +type PolicyHook = generic.PolicyHook[*Policy, *PolicyBinding, PolicyEvaluator] + +type Plugin struct { + *generic.Plugin[PolicyHook] +} + +var _ admission.Interface = &Plugin{} +var _ admission.ValidationInterface = &Plugin{} +var _ initializer.WantsFeatures = &Plugin{} + +func NewPlugin(_ io.Reader) *Plugin { + handler := admission.NewHandler(admission.Connect, admission.Create, admission.Delete, admission.Update) + + return &Plugin{ + Plugin: generic.NewPlugin( + handler, + func(f informers.SharedInformerFactory, client kubernetes.Interface, dynamicClient dynamic.Interface, restMapper meta.RESTMapper) generic.Source[PolicyHook] { + return generic.NewPolicySource( + f.Admissionregistration().V1beta1().ValidatingAdmissionPolicies().Informer(), + f.Admissionregistration().V1beta1().ValidatingAdmissionPolicyBindings().Informer(), + NewValidatingAdmissionPolicyAccessor, + NewValidatingAdmissionPolicyBindingAccessor, + compilePolicy, + f, + dynamicClient, + restMapper, + ) + }, + func(a authorizer.Authorizer, m *matching.Matcher) generic.Dispatcher[PolicyHook] { + return NewDispatcher(a, NewMatcher(m)) + }, + ), + } +} + +// Validate makes an admission decision based on the request attributes. +func (a *Plugin) Validate(ctx context.Context, attr admission.Attributes, o admission.ObjectInterfaces) error { + return a.Plugin.Dispatch(ctx, attr, o) +} + +func (a *Plugin) InspectFeatureGates(featureGates featuregate.FeatureGate) { + a.Plugin.SetEnabled(featureGates.Enabled(features.ValidatingAdmissionPolicy)) +} + +func compilePolicy(policy *Policy) Validator { + hasParam := false + if policy.Spec.ParamKind != nil { + hasParam = true + } + optionalVars := cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: true} + expressionOptionalVars := cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: false} + failurePolicy := convertv1beta1FailurePolicyTypeTov1FailurePolicyType(policy.Spec.FailurePolicy) + var matcher matchconditions.Matcher = nil + matchConditions := policy.Spec.MatchConditions + + filterCompiler, err := cel.NewCompositedCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion())) + if err == nil { + filterCompiler.CompileAndStoreVariables(convertv1beta1Variables(policy.Spec.Variables), optionalVars, environment.StoredExpressions) + } else { + //!TODO: return a validator that always fails with internal error? + utilruntime.HandleError(err) + } + + if len(matchConditions) > 0 { + matchExpressionAccessors := make([]cel.ExpressionAccessor, len(matchConditions)) + for i := range matchConditions { + matchExpressionAccessors[i] = (*matchconditions.MatchCondition)(&matchConditions[i]) + } + matcher = matchconditions.NewMatcher(filterCompiler.Compile(matchExpressionAccessors, optionalVars, environment.StoredExpressions), failurePolicy, "policy", "validate", policy.Name) + } + res := NewValidator( + filterCompiler.Compile(convertv1beta1Validations(policy.Spec.Validations), optionalVars, environment.StoredExpressions), + matcher, + filterCompiler.Compile(convertv1beta1AuditAnnotations(policy.Spec.AuditAnnotations), optionalVars, environment.StoredExpressions), + filterCompiler.Compile(convertv1beta1MessageExpressions(policy.Spec.Validations), expressionOptionalVars, environment.StoredExpressions), + failurePolicy, + ) + + return res +} + +func convertv1beta1FailurePolicyTypeTov1FailurePolicyType(policyType *v1beta1.FailurePolicyType) *v1.FailurePolicyType { + if policyType == nil { + return nil + } + + var v1FailPolicy v1.FailurePolicyType + if *policyType == v1beta1.Fail { + v1FailPolicy = v1.Fail + } else if *policyType == v1beta1.Ignore { + v1FailPolicy = v1.Ignore + } + return &v1FailPolicy +} + +func convertv1beta1Validations(inputValidations []v1beta1.Validation) []cel.ExpressionAccessor { + celExpressionAccessor := make([]cel.ExpressionAccessor, len(inputValidations)) + for i, validation := range inputValidations { + validation := ValidationCondition{ + Expression: validation.Expression, + Message: validation.Message, + Reason: validation.Reason, + } + celExpressionAccessor[i] = &validation + } + return celExpressionAccessor +} + +func convertv1beta1MessageExpressions(inputValidations []v1beta1.Validation) []cel.ExpressionAccessor { + celExpressionAccessor := make([]cel.ExpressionAccessor, len(inputValidations)) + for i, validation := range inputValidations { + if validation.MessageExpression != "" { + condition := MessageExpressionCondition{ + MessageExpression: validation.MessageExpression, + } + celExpressionAccessor[i] = &condition + } + } + return celExpressionAccessor +} + +func convertv1beta1AuditAnnotations(inputValidations []v1beta1.AuditAnnotation) []cel.ExpressionAccessor { + celExpressionAccessor := make([]cel.ExpressionAccessor, len(inputValidations)) + for i, validation := range inputValidations { + validation := AuditAnnotationCondition{ + Key: validation.Key, + ValueExpression: validation.ValueExpression, + } + celExpressionAccessor[i] = &validation + } + return celExpressionAccessor +} + +func convertv1beta1Variables(variables []v1beta1.Variable) []cel.NamedExpressionAccessor { + namedExpressions := make([]cel.NamedExpressionAccessor, len(variables)) + for i, variable := range variables { + namedExpressions[i] = &Variable{Name: variable.Name, Expression: variable.Expression} + } + return namedExpressions +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/typechecking_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/typechecking_test.go index 95bf748cc48..31e9a129909 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/typechecking_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/typechecking_test.go @@ -26,11 +26,35 @@ import ( appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/cel/openapi/resolver" + "k8s.io/client-go/kubernetes/fake" "k8s.io/kube-openapi/pkg/validation/spec" ) +var ( + scheme *runtime.Scheme = func() *runtime.Scheme { + res := runtime.NewScheme() + if err := v1beta1.AddToScheme(res); err != nil { + panic(err) + } + + if err := fake.AddToScheme(res); err != nil { + panic(err) + } + + return res + }() +) + +func must3[T any, I any](val T, _ I, err error) T { + if err != nil { + panic(err) + } + return val +} + func TestExtractTypeNames(t *testing.T) { for _, tc := range []struct { name string