diff --git a/cmd/kube-controller-manager/app/validatingadmissionpolicystatus.go b/cmd/kube-controller-manager/app/validatingadmissionpolicystatus.go index 2fd14589f5f..a685a94cdb1 100644 --- a/cmd/kube-controller-manager/app/validatingadmissionpolicystatus.go +++ b/cmd/kube-controller-manager/app/validatingadmissionpolicystatus.go @@ -20,7 +20,7 @@ import ( "context" apiextensionsscheme "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/scheme" - pluginvalidatingadmissionpolicy "k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy" + pluginvalidatingadmissionpolicy "k8s.io/apiserver/pkg/admission/plugin/policy/validating" "k8s.io/apiserver/pkg/cel/openapi/resolver" genericfeatures "k8s.io/apiserver/pkg/features" k8sscheme "k8s.io/client-go/kubernetes/scheme" diff --git a/pkg/apis/admissionregistration/validation/validation.go b/pkg/apis/admissionregistration/validation/validation.go index b7cf40281c3..4695b29eea8 100644 --- a/pkg/apis/admissionregistration/validation/validation.go +++ b/pkg/apis/admissionregistration/validation/validation.go @@ -31,7 +31,7 @@ import ( utilvalidation "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation/field" plugincel "k8s.io/apiserver/pkg/admission/plugin/cel" - "k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy" + validatingadmissionpolicy "k8s.io/apiserver/pkg/admission/plugin/policy/validating" "k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions" "k8s.io/apiserver/pkg/cel" "k8s.io/apiserver/pkg/cel/environment" diff --git a/pkg/controller/validatingadmissionpolicystatus/controller.go b/pkg/controller/validatingadmissionpolicystatus/controller.go index db6593a0234..a1d7f40fc53 100644 --- a/pkg/controller/validatingadmissionpolicystatus/controller.go +++ b/pkg/controller/validatingadmissionpolicystatus/controller.go @@ -26,7 +26,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apimachinery/pkg/util/wait" - "k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy" + validatingadmissionpolicy "k8s.io/apiserver/pkg/admission/plugin/policy/validating" admissionregistrationv1beta1apply "k8s.io/client-go/applyconfigurations/admissionregistration/v1beta1" informerv1beta1 "k8s.io/client-go/informers/admissionregistration/v1beta1" admissionregistrationv1beta1 "k8s.io/client-go/kubernetes/typed/admissionregistration/v1beta1" diff --git a/pkg/controller/validatingadmissionpolicystatus/controller_test.go b/pkg/controller/validatingadmissionpolicystatus/controller_test.go index 74af1be10a7..005fa204c3c 100644 --- a/pkg/controller/validatingadmissionpolicystatus/controller_test.go +++ b/pkg/controller/validatingadmissionpolicystatus/controller_test.go @@ -27,7 +27,7 @@ import ( "k8s.io/apimachinery/pkg/api/meta/testrestmapper" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/wait" - "k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy" + validatingadmissionpolicy "k8s.io/apiserver/pkg/admission/plugin/policy/validating" "k8s.io/apiserver/pkg/cel/openapi/resolver" "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes/fake" diff --git a/pkg/kubeapiserver/options/plugins.go b/pkg/kubeapiserver/options/plugins.go index 0d4556c318f..fa2c9b5f426 100644 --- a/pkg/kubeapiserver/options/plugins.go +++ b/pkg/kubeapiserver/options/plugins.go @@ -20,7 +20,7 @@ package options // This should probably be part of some configuration fed into the build for a // given binary target. import ( - "k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy" + validatingadmissionpolicy "k8s.io/apiserver/pkg/admission/plugin/policy/validating" // Admission policies "k8s.io/kubernetes/plugin/pkg/admission/admit" "k8s.io/kubernetes/plugin/pkg/admission/alwayspullimages" diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/composition.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/composition.go index 2dbfa099164..9c449ecda2f 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/composition.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/composition.go @@ -54,13 +54,22 @@ func NewCompositedCompiler(envSet *environment.EnvSet) (*CompositedCompiler, err if err != nil { return nil, err } - compiler := NewCompiler(compositionContext.EnvSet) - filterCompiler := NewFilterCompiler(compositionContext.EnvSet) + return NewCompositedCompilerFromTemplate(compositionContext), nil +} + +func NewCompositedCompilerFromTemplate(context *CompositionEnv) *CompositedCompiler { + context = &CompositionEnv{ + MapType: context.MapType, + EnvSet: context.EnvSet, + CompiledVariables: map[string]CompilationResult{}, + } + compiler := NewCompiler(context.EnvSet) + filterCompiler := NewFilterCompiler(context.EnvSet) return &CompositedCompiler{ Compiler: compiler, FilterCompiler: filterCompiler, - CompositionEnv: compositionContext, - }, nil + CompositionEnv: context, + } } func (c *CompositedCompiler) CompileAndStoreVariables(variables []NamedExpressionAccessor, options OptionalVariableDeclarations, mode environment.Type) { diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/accessor.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/accessor.go new file mode 100644 index 00000000000..11fd8c24f2d --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/accessor.go @@ -0,0 +1,38 @@ +/* +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 generic + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" +) + +type PolicyAccessor interface { + GetName() string + GetNamespace() string + GetParamKind() *schema.GroupVersionKind +} + +type BindingAccessor interface { + GetName() string + GetNamespace() string + + // GetPolicyName returns the name of the (Validating/Mutating)AdmissionPolicy, + // which is cluster-scoped, so namespace is usually left blank. + // But we leave the door open to add a namespaced vesion in the future + GetPolicyName() types.NamespacedName +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/interfaces.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/interfaces.go new file mode 100644 index 00000000000..d4dbfb0aa52 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/interfaces.go @@ -0,0 +1,64 @@ +/* +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 generic + +import ( + "context" + + "k8s.io/apiserver/pkg/admission" +) + +// Hook represents a dynamic admission hook. The hook may be a webhook or a +// policy. For webhook, the Hook may describe how to contact the endpoint, expected +// cert, etc. For policies, the hook may describe a compiled policy-binding pair. +type Hook interface { + // All hooks are expected to contain zero or more match conditions, object + // selectors, namespace selectors to help the dispatcher decide when to apply + // the hook. + // + // Methods of matching logic is applied are specific to the hook and left up + // to the implementation. +} + +// Source can list dynamic admission plugins. +type Source[H Hook] interface { + // Hooks returns the list of currently known admission hooks. + Hooks() []H + + // Run the source. This method should be called only once at startup. + Run(ctx context.Context) error + + // HasSynced returns true if the source has completed its initial sync. + HasSynced() bool +} + +// Dispatcher dispatches evaluates an admission request against the currently +// active hooks returned by the source. +type Dispatcher[H Hook] interface { + // Dispatch a request to the policies. Dispatcher may choose not to + // call a hook, either because the rules of the hook does not match, or + // the namespaceSelector or the objectSelector of the hook does not + // match. A non-nil error means the request is rejected. + Dispatch(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces, hooks []H) error +} + +// An evaluator represents a compiled CEL expression that can be evaluated a +// given a set of inputs used by the generic PolicyHook for Mutating and +// ValidatingAdmissionPolicy. +// Mutating and Validating may have different forms of evaluators +type Evaluator interface { +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/plugin.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/plugin.go new file mode 100644 index 00000000000..e6db5df2ec0 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/plugin.go @@ -0,0 +1,206 @@ +/* +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 generic + +import ( + "context" + "errors" + "fmt" + + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime/schema" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apiserver/pkg/admission" + "k8s.io/apiserver/pkg/admission/initializer" + "k8s.io/apiserver/pkg/admission/plugin/policy/matching" + "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/informers" + "k8s.io/client-go/kubernetes" +) + +// H is the Hook type generated by the source and consumed by the dispatcher. +type sourceFactory[H any] func(informers.SharedInformerFactory, kubernetes.Interface, dynamic.Interface, meta.RESTMapper) Source[H] +type dispatcherFactory[H any] func(authorizer.Authorizer, *matching.Matcher) Dispatcher[H] + +type Invocation struct { + Resource schema.GroupVersionResource + Subresource string + Kind schema.GroupVersionKind +} + +// AdmissionPolicyManager is an abstract admission plugin with all the +// infrastructure to define Admit or Validate on-top. +type Plugin[H any] struct { + *admission.Handler + + sourceFactory sourceFactory[H] + dispatcherFactory dispatcherFactory[H] + + source Source[H] + dispatcher Dispatcher[H] + matcher *matching.Matcher + + informerFactory informers.SharedInformerFactory + client kubernetes.Interface + restMapper meta.RESTMapper + dynamicClient dynamic.Interface + stopCh <-chan struct{} + authorizer authorizer.Authorizer + enabled bool +} + +var ( + _ initializer.WantsExternalKubeInformerFactory = &Plugin[any]{} + _ initializer.WantsExternalKubeClientSet = &Plugin[any]{} + _ initializer.WantsRESTMapper = &Plugin[any]{} + _ initializer.WantsDynamicClient = &Plugin[any]{} + _ initializer.WantsDrainedNotification = &Plugin[any]{} + _ initializer.WantsAuthorizer = &Plugin[any]{} + _ admission.InitializationValidator = &Plugin[any]{} +) + +func NewPlugin[H any]( + handler *admission.Handler, + sourceFactory sourceFactory[H], + dispatcherFactory dispatcherFactory[H], +) *Plugin[H] { + return &Plugin[H]{ + Handler: handler, + sourceFactory: sourceFactory, + dispatcherFactory: dispatcherFactory, + } +} + +func (c *Plugin[H]) SetExternalKubeInformerFactory(f informers.SharedInformerFactory) { + c.informerFactory = f +} + +func (c *Plugin[H]) SetExternalKubeClientSet(client kubernetes.Interface) { + c.client = client +} + +func (c *Plugin[H]) SetRESTMapper(mapper meta.RESTMapper) { + c.restMapper = mapper +} + +func (c *Plugin[H]) SetDynamicClient(client dynamic.Interface) { + c.dynamicClient = client +} + +func (c *Plugin[H]) SetDrainedNotification(stopCh <-chan struct{}) { + c.stopCh = stopCh +} + +func (c *Plugin[H]) SetAuthorizer(authorizer authorizer.Authorizer) { + c.authorizer = authorizer +} + +func (c *Plugin[H]) SetMatcher(matcher *matching.Matcher) { + c.matcher = matcher +} + +func (c *Plugin[H]) SetEnabled(enabled bool) { + c.enabled = enabled +} + +// ValidateInitialization - once clientset and informer factory are provided, creates and starts the admission controller +func (c *Plugin[H]) ValidateInitialization() error { + // By default enabled is set to false. It is up to types which embed this + // struct to set it to true (if feature gate is enabled, or other conditions) + if !c.enabled { + return nil + } + if c.Handler == nil { + return errors.New("missing handler") + } + 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") + } + + // Use default matcher + namespaceInformer := c.informerFactory.Core().V1().Namespaces() + c.matcher = matching.NewMatcher(namespaceInformer.Lister(), c.client) + + if err := c.matcher.ValidateInitialization(); err != nil { + return err + } + + c.source = c.sourceFactory(c.informerFactory, c.client, c.dynamicClient, c.restMapper) + c.dispatcher = c.dispatcherFactory(c.authorizer, c.matcher) + + pluginContext, pluginContextCancel := context.WithCancel(context.Background()) + go func() { + defer pluginContextCancel() + <-c.stopCh + }() + + go func() { + err := c.source.Run(pluginContext) + if err != nil && !errors.Is(err, context.Canceled) { + utilruntime.HandleError(fmt.Errorf("policy source context unexpectedly closed: %v", err)) + } + }() + + c.SetReadyFunc(func() bool { + return namespaceInformer.Informer().HasSynced() && c.source.HasSynced() + }) + return nil +} + +func (c *Plugin[H]) Dispatch( + ctx context.Context, + a admission.Attributes, + o admission.ObjectInterfaces, +) (err error) { + if !c.enabled { + return nil + } else if isPolicyResource(a) { + return nil + } else if !c.WaitForReady() { + return admission.NewForbidden(a, fmt.Errorf("not yet ready to handle request")) + } + + return c.dispatcher.Dispatch(ctx, a, o, c.source.Hooks()) +} + +func isPolicyResource(attr admission.Attributes) bool { + gvk := attr.GetResource() + if gvk.Group == "admissionregistration.k8s.io" { + if gvk.Resource == "validatingadmissionpolicies" || gvk.Resource == "validatingadmissionpolicybindings" { + return true + } else if gvk.Resource == "mutatingadmissionpolicies" || gvk.Resource == "mutatingadmissionpolicybindings" { + return true + } + } + return false +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/policy_source.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/policy_source.go new file mode 100644 index 00000000000..4f52e406404 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/policy_source.go @@ -0,0 +1,461 @@ +/* +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 generic + +import ( + "context" + goerrors "errors" + "fmt" + "sync" + "sync/atomic" + "time" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/apiserver/pkg/admission/plugin/policy/internal/generic" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/dynamic/dynamicinformer" + "k8s.io/client-go/informers" + "k8s.io/client-go/tools/cache" + "k8s.io/klog/v2" +) + +type policySource[P runtime.Object, B runtime.Object, E Evaluator] struct { + ctx context.Context + policyInformer generic.Informer[P] + bindingInformer generic.Informer[B] + restMapper meta.RESTMapper + newPolicyAccessor func(P) PolicyAccessor + newBindingAccessor func(B) BindingAccessor + + informerFactory informers.SharedInformerFactory + dynamicClient dynamic.Interface + + compiler func(P) E + + // Currently compiled list of valid/active policy-binding pairs + policies atomic.Pointer[[]PolicyHook[P, B, E]] + // Whether the cache of policies is dirty and needs to be recompiled + policiesDirty atomic.Bool + + lock sync.Mutex + compiledPolicies map[types.NamespacedName]compiledPolicyEntry[E] + + // Temporary until we use the dynamic informer factory + paramsCRDControllers map[schema.GroupVersionKind]*paramInfo +} + +type paramInfo struct { + mapping meta.RESTMapping + + // When the param is changed, or the informer is done being used, the cancel + // func should be called to stop/cleanup the original informer + cancelFunc func() + + // The lister for this param + informer informers.GenericInformer +} + +type compiledPolicyEntry[E Evaluator] struct { + policyVersion string + evaluator E +} + +type PolicyHook[P runtime.Object, B runtime.Object, E Evaluator] struct { + Policy P + Bindings []B + ParamInformer informers.GenericInformer + ParamScope meta.RESTScope + + Evaluator E + ConfigurationError error +} + +var _ Source[PolicyHook[runtime.Object, runtime.Object, Evaluator]] = &policySource[runtime.Object, runtime.Object, Evaluator]{} + +func NewPolicySource[P runtime.Object, B runtime.Object, E Evaluator]( + policyInformer cache.SharedIndexInformer, + bindingInformer cache.SharedIndexInformer, + newPolicyAccessor func(P) PolicyAccessor, + newBindingAccessor func(B) BindingAccessor, + compiler func(P) E, + paramInformerFactory informers.SharedInformerFactory, + dynamicClient dynamic.Interface, + restMapper meta.RESTMapper, +) Source[PolicyHook[P, B, E]] { + res := &policySource[P, B, E]{ + compiler: compiler, + policyInformer: generic.NewInformer[P](policyInformer), + bindingInformer: generic.NewInformer[B](bindingInformer), + compiledPolicies: map[types.NamespacedName]compiledPolicyEntry[E]{}, + newPolicyAccessor: newPolicyAccessor, + newBindingAccessor: newBindingAccessor, + paramsCRDControllers: map[schema.GroupVersionKind]*paramInfo{}, + informerFactory: paramInformerFactory, + dynamicClient: dynamicClient, + restMapper: restMapper, + } + return res +} + +func (s *policySource[P, B, E]) Run(ctx context.Context) error { + if s.ctx != nil { + return fmt.Errorf("policy source already running") + } + + // Wait for initial cache sync of policies and informers before reconciling + // any + if !cache.WaitForNamedCacheSync(fmt.Sprintf("%T", s), ctx.Done(), s.UpstreamHasSynced) { + err := ctx.Err() + if err == nil { + err = fmt.Errorf("initial cache sync for %T failed", s) + } + return err + } + + s.ctx = ctx + + // Perform initial policy compilation after initial list has finished + s.notify() + s.refreshPolicies() + + notifyFuncs := cache.ResourceEventHandlerFuncs{ + AddFunc: func(_ interface{}) { + s.notify() + }, + UpdateFunc: func(_, _ interface{}) { + s.notify() + }, + DeleteFunc: func(_ interface{}) { + s.notify() + }, + } + handle, err := s.policyInformer.AddEventHandler(notifyFuncs) + if err != nil { + return err + } + defer func() { + if err := s.policyInformer.RemoveEventHandler(handle); err != nil { + utilruntime.HandleError(fmt.Errorf("failed to remove policy event handler: %v", err)) + } + }() + + bindingHandle, err := s.bindingInformer.AddEventHandler(notifyFuncs) + if err != nil { + return err + } + defer func() { + if err := s.bindingInformer.RemoveEventHandler(bindingHandle); err != nil { + utilruntime.HandleError(fmt.Errorf("failed to remove binding event handler: %v", err)) + } + }() + + // Start a worker that checks every second to see if policy data is dirty + // and needs to be recompiled + go func() { + // Loop every 1 second until context is cancelled, refreshing policies + wait.Until(s.refreshPolicies, 1*time.Second, ctx.Done()) + }() + + <-ctx.Done() + return nil +} + +func (s *policySource[P, B, E]) UpstreamHasSynced() bool { + return s.policyInformer.HasSynced() && s.bindingInformer.HasSynced() +} + +// HasSynced implements Source. +func (s *policySource[P, B, E]) HasSynced() bool { + // As an invariant we never store `nil` into the atomic list of processed + // policy hooks. If it is nil, then we haven't compiled all the policies + // and stored them yet. + return s.Hooks() != nil +} + +// Hooks implements Source. +func (s *policySource[P, B, E]) Hooks() []PolicyHook[P, B, E] { + res := s.policies.Load() + + // Error case should not happen since evaluation function never + // returns error + if res == nil { + // Not yet synced + return nil + } + + return *res +} + +func (s *policySource[P, B, E]) refreshPolicies() { + if !s.UpstreamHasSynced() { + return + } else if !s.policiesDirty.Swap(false) { + return + } + + // It is ok the cache gets marked dirty again between us clearing the + // flag and us calculating the policies. The dirty flag would be marked again, + // and we'd have a no-op after comparing resource versions on the next sync. + klog.Infof("refreshing policies") + policies, err := s.calculatePolicyData() + + // Intentionally store policy list regardless of error. There may be + // an error returned if there was a configuration error in one of the policies, + // but we would still want those policies evaluated + // (for instance to return error on failaction). Or if there was an error + // listing all policies at all, we would want to wipe the list. + s.policies.Store(&policies) + + if err != nil { + // An error was generated while syncing policies. Mark it as dirty again + // so we can retry later + utilruntime.HandleError(fmt.Errorf("encountered error syncing policies: %v. Rescheduling policy sync", err)) + s.notify() + } +} + +func (s *policySource[P, B, E]) notify() { + s.policiesDirty.Store(true) +} + +// calculatePolicyData calculates the list of policies and bindings for each +// policy. If there is an error in generation, it will return the error and +// the partial list of policies that were able to be generated. Policies that +// have an error will have a non-nil ConfigurationError field, but still be +// included in the result. +// +// This function caches the result of the intermediate compilations +func (s *policySource[P, B, E]) calculatePolicyData() ([]PolicyHook[P, B, E], error) { + if !s.UpstreamHasSynced() { + return nil, fmt.Errorf("cannot calculate policy data until upstream has synced") + } + + // Fat-fingered lock that can be made more fine-tuned if required + s.lock.Lock() + defer s.lock.Unlock() + + // Create a local copy of all policies and bindings + policiesToBindings := map[types.NamespacedName][]B{} + bindingList, err := s.bindingInformer.List(labels.Everything()) + if err != nil { + // This should never happen unless types are misconfigured + // (can't use meta.accessor on them) + return nil, err + } + + // Gather a list of all active policy bindings + for _, bindingSpec := range bindingList { + bindingAccessor := s.newBindingAccessor(bindingSpec) + policyKey := bindingAccessor.GetPolicyName() + + // Add this binding to the list of bindings for this policy + policiesToBindings[policyKey] = append(policiesToBindings[policyKey], bindingSpec) + } + + result := make([]PolicyHook[P, B, E], 0, len(bindingList)) + usedParams := map[schema.GroupVersionKind]struct{}{} + var errs []error + for policyKey, bindingSpecs := range policiesToBindings { + var inf generic.NamespacedLister[P] = s.policyInformer + if len(policyKey.Namespace) > 0 { + inf = s.policyInformer.Namespaced(policyKey.Namespace) + } + policySpec, err := inf.Get(policyKey.Name) + if errors.IsNotFound(err) { + // Policy for bindings doesn't exist. This can happen if the policy + // was deleted before the binding, or the binding was created first. + // + // Just skip bindings that refer to non-existent policies + // If the policy is recreated, the cache will be marked dirty and + // this function will run again. + continue + } else if err != nil { + // This should never happen since fetching from a cache should never + // fail and this function checks that the cache was synced before + // even getting to this point. + errs = append(errs, err) + continue + } + + policyAccessor := s.newPolicyAccessor(policySpec) + paramInformer, paramScope, configurationError := s.ensureParamsForPolicyLocked(policyAccessor.GetParamKind()) + result = append(result, PolicyHook[P, B, E]{ + Policy: policySpec, + Bindings: bindingSpecs, + Evaluator: s.compilePolicyLocked(policySpec), + ParamInformer: paramInformer, + ParamScope: paramScope, + ConfigurationError: configurationError, + }) + + // TEMPORARY UNTIL WE HAVE SHARED PARAM INFORMERS + if paramKind := policyAccessor.GetParamKind(); paramKind != nil { + usedParams[*paramKind] = struct{}{} + } + + // Should queue a re-sync for policy sync error. If our shared param + // informer can notify us when CRD discovery changes we can remove this + // and just rely on the informer to notify us when the CRDs change + if configurationError != nil { + errs = append(errs, configurationError) + } + } + + // Clean up orphaned policies by replacing the old cache of compiled policies + // (the map of used policies is updated by `compilePolicy`) + for policyKey := range s.compiledPolicies { + if _, wasSeen := policiesToBindings[policyKey]; !wasSeen { + delete(s.compiledPolicies, policyKey) + } + } + + // Clean up orphaned param informers + for paramKind, info := range s.paramsCRDControllers { + if _, wasSeen := usedParams[paramKind]; !wasSeen { + info.cancelFunc() + delete(s.paramsCRDControllers, paramKind) + } + } + + err = nil + if len(errs) > 0 { + err = goerrors.Join(errs...) + } + return result, err +} + +// ensureParamsForPolicyLocked ensures that the informer for the paramKind is +// started and returns the informer and the scope of the paramKind. +// +// Must be called under write lock +func (s *policySource[P, B, E]) ensureParamsForPolicyLocked(paramSource *schema.GroupVersionKind) (informers.GenericInformer, meta.RESTScope, error) { + if paramSource == nil { + return nil, nil, nil + } else if info, ok := s.paramsCRDControllers[*paramSource]; ok { + return info.informer, info.mapping.Scope, nil + } + + mapping, err := s.restMapper.RESTMapping(schema.GroupKind{ + Group: paramSource.Group, + Kind: paramSource.Kind, + }, paramSource.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 + return nil, nil, fmt.Errorf("failed to find resource referenced by paramKind: '%v'", *paramSource) + } + + // We are not watching this param. Start an informer for it. + instanceContext, instanceCancel := context.WithCancel(s.ctx) + + var informer informers.GenericInformer + + // 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 := s.informerFactory.ForResource(mapping.Resource); err == nil { + informer = genericInformer + + // Start the informer + s.informerFactory.Start(instanceContext.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( + s.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, + ) + go informer.Informer().Run(instanceContext.Done()) + } + + klog.Infof("informer started for %v", *paramSource) + ret := ¶mInfo{ + mapping: *mapping, + cancelFunc: instanceCancel, + informer: informer, + } + s.paramsCRDControllers[*paramSource] = ret + return ret.informer, mapping.Scope, nil +} + +// For testing +func (s *policySource[P, B, E]) getParamInformer(param schema.GroupVersionKind) (informers.GenericInformer, meta.RESTScope) { + s.lock.Lock() + defer s.lock.Unlock() + + if info, ok := s.paramsCRDControllers[param]; ok { + return info.informer, info.mapping.Scope + } + + return nil, nil +} + +// compilePolicyLocked compiles the policy and returns the evaluator for it. +// If the policy has not changed since the last compilation, it will return +// the cached evaluator. +// +// Must be called under write lock +func (s *policySource[P, B, E]) compilePolicyLocked(policySpec P) E { + policyMeta, err := meta.Accessor(policySpec) + if err != nil { + // This should not happen if P, and B have ObjectMeta, but + // unfortunately there is no way to express "able to call + // meta.Accessor" as a type constraint + utilruntime.HandleError(err) + var emptyEvaluator E + return emptyEvaluator + } + key := types.NamespacedName{ + Namespace: policyMeta.GetNamespace(), + Name: policyMeta.GetName(), + } + + compiledPolicy, wasCompiled := s.compiledPolicies[key] + + // If the policy or binding has changed since it was last compiled, + // and if there is no configuration error (like a missing param CRD) + // then we recompile + if !wasCompiled || + compiledPolicy.policyVersion != policyMeta.GetResourceVersion() { + + compiledPolicy = compiledPolicyEntry[E]{ + policyVersion: policyMeta.GetResourceVersion(), + evaluator: s.compiler(policySpec), + } + s.compiledPolicies[key] = compiledPolicy + } + + return compiledPolicy.evaluator +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/policy_source_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/policy_source_test.go new file mode 100644 index 00000000000..be17f9c602b --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/policy_source_test.go @@ -0,0 +1,233 @@ +/* +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 generic_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apiserver/pkg/admission/plugin/policy/generic" + "k8s.io/apiserver/pkg/admission/plugin/policy/matching" + "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/client-go/tools/cache" +) + +func makeTestDispatcher(authorizer.Authorizer, *matching.Matcher) generic.Dispatcher[generic.PolicyHook[*FakePolicy, *FakeBinding, generic.Evaluator]] { + return nil +} + +func TestPolicySourceHasSyncedEmpty(t *testing.T) { + testContext, testCancel, err := generic.NewPolicyTestContext( + func(fp *FakePolicy) generic.PolicyAccessor { return fp }, + func(fb *FakeBinding) generic.BindingAccessor { return fb }, + func(fp *FakePolicy) generic.Evaluator { return nil }, + makeTestDispatcher, + nil, + nil, + ) + require.NoError(t, err) + defer testCancel() + require.NoError(t, testContext.Start()) + + // Should be able to wait for cache sync + require.True(t, cache.WaitForCacheSync(testContext.Done(), testContext.Source.HasSynced), "cache should sync after informer running") +} + +func TestPolicySourceHasSyncedInitialList(t *testing.T) { + // Create a list of fake policies + initialObjects := []runtime.Object{ + &FakePolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "policy1", + }, + }, + &FakeBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding1", + }, + PolicyName: "policy1", + }, + } + + testContext, testCancel, err := generic.NewPolicyTestContext( + func(fp *FakePolicy) generic.PolicyAccessor { return fp }, + func(fb *FakeBinding) generic.BindingAccessor { return fb }, + func(fp *FakePolicy) generic.Evaluator { return nil }, + makeTestDispatcher, + initialObjects, + nil, + ) + require.NoError(t, err) + defer testCancel() + require.NoError(t, testContext.Start()) + // Should be able to wait for cache sync + require.Len(t, testContext.Source.Hooks(), 1, "should have one policy") + require.NoError(t, testContext.UpdateAndWait( + &FakePolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "policy2", + }, + }, + &FakeBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding2", + }, + PolicyName: "policy2", + }, + )) + require.Len(t, testContext.Source.Hooks(), 2, "should have two policies") + require.NoError(t, testContext.UpdateAndWait( + &FakePolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "policy3", + }, + }, + &FakeBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding3", + }, + PolicyName: "policy3", + }, + &FakePolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "policy2", + }, + ParamKind: &schema.GroupVersionKind{ + Group: "policy.example.com", + Version: "v1", + Kind: "FakeParam", + }, + }, + )) + require.Len(t, testContext.Source.Hooks(), 3, "should have 3 policies") + +} + +func TestPolicySourceBindsToPolicies(t *testing.T) { + // Create a list of fake policies + initialObjects := []runtime.Object{ + &FakePolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "policy1", + }, + }, + &FakeBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding1", + }, + PolicyName: "policy1", + }, + } + + testContext, testCancel, err := generic.NewPolicyTestContext( + func(fp *FakePolicy) generic.PolicyAccessor { return fp }, + func(fb *FakeBinding) generic.BindingAccessor { return fb }, + func(fp *FakePolicy) generic.Evaluator { return nil }, + makeTestDispatcher, + initialObjects, + nil, + ) + require.NoError(t, err) + defer testCancel() + require.NoError(t, testContext.Start()) + + require.Len(t, testContext.Source.Hooks(), 1, "should have one policy") + require.Len(t, testContext.Source.Hooks()[0].Bindings, 1, "should have one binding") + require.Equal(t, "binding1", testContext.Source.Hooks()[0].Bindings[0].GetName(), "should have one binding") + + // Change the binding to another policy (policies without bindings should + // be ignored, so it should remove the first + require.NoError(t, testContext.UpdateAndWait( + &FakePolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "policy2", + }, + }, + &FakeBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding1", + }, + PolicyName: "policy2", + })) + require.Len(t, testContext.Source.Hooks(), 1, "should have one policy") + require.Equal(t, "policy2", testContext.Source.Hooks()[0].Policy.GetName(), "policy name should be policy2") + require.Len(t, testContext.Source.Hooks()[0].Bindings, 1, "should have one binding") + require.Equal(t, "binding1", testContext.Source.Hooks()[0].Bindings[0].GetName(), "binding name should be binding1") + +} + +type FakePolicy struct { + metav1.TypeMeta + metav1.ObjectMeta + + ParamKind *schema.GroupVersionKind +} + +var _ generic.PolicyAccessor = &FakePolicy{} + +type FakeBinding struct { + metav1.TypeMeta + metav1.ObjectMeta + + PolicyName string +} + +var _ generic.BindingAccessor = &FakeBinding{} + +func (fp *FakePolicy) GetName() string { + return fp.Name +} + +func (fp *FakePolicy) GetNamespace() string { + return fp.Namespace +} + +func (fp *FakePolicy) GetParamKind() *schema.GroupVersionKind { + return fp.ParamKind +} + +func (fb *FakeBinding) GetName() string { + return fb.Name +} + +func (fb *FakeBinding) GetNamespace() string { + return fb.Namespace +} + +func (fb *FakeBinding) GetPolicyName() types.NamespacedName { + return types.NamespacedName{ + Name: fb.PolicyName, + } +} + +func (fp *FakePolicy) DeepCopyObject() runtime.Object { + // totally fudged deepcopy + newFP := &FakePolicy{} + *newFP = *fp + return newFP +} + +func (fb *FakeBinding) DeepCopyObject() runtime.Object { + // totally fudged deepcopy + newFB := &FakeBinding{} + *newFB = *fb + return newFB +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/policy_test_context.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/policy_test_context.go new file mode 100644 index 00000000000..f47c000dd21 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/generic/policy_test_context.go @@ -0,0 +1,608 @@ +/* +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 generic + +import ( + "context" + "fmt" + "time" + + corev1 "k8s.io/api/core/v1" + "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/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/uuid" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/dynamic" + dynamicfake "k8s.io/client-go/dynamic/fake" + "k8s.io/client-go/informers" + "k8s.io/client-go/kubernetes" + "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/apiserver/pkg/admission" + "k8s.io/apiserver/pkg/admission/initializer" + "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/apiserver/pkg/features" +) + +// PolicyTestContext is everything you need to unit test a policy plugin +type PolicyTestContext[P runtime.Object, B runtime.Object, E Evaluator] struct { + context.Context + Plugin *Plugin[PolicyHook[P, B, E]] + Source Source[PolicyHook[P, B, E]] + Start func() error + + scheme *runtime.Scheme + restMapper *meta.DefaultRESTMapper + policyGVR schema.GroupVersionResource + bindingGVR schema.GroupVersionResource + + policyGVK schema.GroupVersionKind + bindingGVK schema.GroupVersionKind + + nativeTracker clienttesting.ObjectTracker + policyAndBindingTracker clienttesting.ObjectTracker + unstructuredTracker clienttesting.ObjectTracker +} + +func NewPolicyTestContext[P, B runtime.Object, E Evaluator]( + newPolicyAccessor func(P) PolicyAccessor, + newBindingAccessor func(B) BindingAccessor, + compileFunc func(P) E, + dispatcher dispatcherFactory[PolicyHook[P, B, E]], + initialObjects []runtime.Object, + paramMappings []meta.RESTMapping, +) (*PolicyTestContext[P, B, E], func(), error) { + var Pexample P + var Bexample B + + // Create a fake resource and kind for the provided policy and binding types + fakePolicyGVR := schema.GroupVersionResource{ + Group: "policy.example.com", + Version: "v1", + Resource: "fakepolicies", + } + fakeBindingGVR := schema.GroupVersionResource{ + Group: "policy.example.com", + Version: "v1", + Resource: "fakebindings", + } + fakePolicyGVK := fakePolicyGVR.GroupVersion().WithKind("FakePolicy") + fakeBindingGVK := fakeBindingGVR.GroupVersion().WithKind("FakeBinding") + + policySourceTestScheme, err := func() (*runtime.Scheme, error) { + scheme := runtime.NewScheme() + + if err := fake.AddToScheme(scheme); err != nil { + return nil, err + } + + scheme.AddKnownTypeWithName(fakePolicyGVK, Pexample) + scheme.AddKnownTypeWithName(fakeBindingGVK, Bexample) + scheme.AddKnownTypeWithName(fakePolicyGVK.GroupVersion().WithKind(fakePolicyGVK.Kind+"List"), &FakeList[P]{}) + scheme.AddKnownTypeWithName(fakeBindingGVK.GroupVersion().WithKind(fakeBindingGVK.Kind+"List"), &FakeList[B]{}) + + for _, mapping := range paramMappings { + // Skip if it is in the scheme already + if scheme.Recognizes(mapping.GroupVersionKind) { + continue + } + scheme.AddKnownTypeWithName(mapping.GroupVersionKind, &unstructured.Unstructured{}) + scheme.AddKnownTypeWithName(mapping.GroupVersionKind.GroupVersion().WithKind(mapping.GroupVersionKind.Kind+"List"), &unstructured.UnstructuredList{}) + } + + return scheme, nil + }() + if err != nil { + return nil, nil, err + } + + fakeRestMapper := func() *meta.DefaultRESTMapper { + res := meta.NewDefaultRESTMapper([]schema.GroupVersion{ + { + Group: "", + Version: "v1", + }, + }) + + res.Add(fakePolicyGVK, meta.RESTScopeRoot) + res.Add(fakeBindingGVK, meta.RESTScopeRoot) + res.Add(corev1.SchemeGroupVersion.WithKind("ConfigMap"), meta.RESTScopeNamespace) + + for _, mapping := range paramMappings { + res.AddSpecific(mapping.GroupVersionKind, mapping.Resource, mapping.Resource, mapping.Scope) + } + + return res + }() + + nativeClient := fake.NewSimpleClientset() + dynamicClient := dynamicfake.NewSimpleDynamicClient(policySourceTestScheme) + fakeInformerFactory := informers.NewSharedInformerFactory(nativeClient, 30*time.Second) + + // Make an object tracker specifically for our policies and bindings + policiesAndBindingsTracker := clienttesting.NewObjectTracker( + policySourceTestScheme, + serializer.NewCodecFactory(policySourceTestScheme).UniversalDecoder()) + + // Make an informer for our policies and bindings + + policyInformer := cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { + return policiesAndBindingsTracker.List(fakePolicyGVR, fakePolicyGVK, "") + }, + WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { + return policiesAndBindingsTracker.Watch(fakePolicyGVR, "") + }, + }, + Pexample, + 30*time.Second, + cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, + ) + bindingInformer := cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { + return policiesAndBindingsTracker.List(fakeBindingGVR, fakeBindingGVK, "") + }, + WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { + return policiesAndBindingsTracker.Watch(fakeBindingGVR, "") + }, + }, + Bexample, + 30*time.Second, + cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, + ) + + var source Source[PolicyHook[P, B, E]] + plugin := NewPlugin[PolicyHook[P, B, E]]( + admission.NewHandler(admission.Connect, admission.Create, admission.Delete, admission.Update), + func(sif informers.SharedInformerFactory, i1 kubernetes.Interface, i2 dynamic.Interface, r meta.RESTMapper) Source[PolicyHook[P, B, E]] { + source = NewPolicySource[P, B, E]( + policyInformer, + bindingInformer, + newPolicyAccessor, + newBindingAccessor, + compileFunc, + sif, + i2, + r, + ) + return source + }, dispatcher) + plugin.SetEnabled(true) + + featureGate := featuregate.NewFeatureGate() + err = featureGate.Add(map[featuregate.Feature]featuregate.FeatureSpec{ + //!TODO: move this to validating specific tests + features.ValidatingAdmissionPolicy: { + Default: true, PreRelease: featuregate.Beta}}) + if err != nil { + return nil, nil, err + } + err = featureGate.SetFromMap(map[string]bool{string(features.ValidatingAdmissionPolicy): true}) + if err != nil { + return nil, nil, err + } + + testContext, testCancel := context.WithCancel(context.Background()) + genericInitializer := initializer.New( + nativeClient, + dynamicClient, + fakeInformerFactory, + fakeAuthorizer{}, + featureGate, + testContext.Done(), + ) + genericInitializer.Initialize(plugin) + plugin.SetRESTMapper(fakeRestMapper) + + if err := plugin.ValidateInitialization(); err != nil { + testCancel() + return nil, nil, err + } + + res := &PolicyTestContext[P, B, E]{ + Context: testContext, + Plugin: plugin, + Source: source, + + restMapper: fakeRestMapper, + scheme: policySourceTestScheme, + policyGVK: fakePolicyGVK, + bindingGVK: fakeBindingGVK, + policyGVR: fakePolicyGVR, + bindingGVR: fakeBindingGVR, + nativeTracker: nativeClient.Tracker(), + policyAndBindingTracker: policiesAndBindingsTracker, + unstructuredTracker: dynamicClient.Tracker(), + } + + for _, obj := range initialObjects { + err := res.updateOne(obj) + if err != nil { + testCancel() + return nil, nil, err + } + } + + res.Start = func() error { + fakeInformerFactory.Start(res.Done()) + go policyInformer.Run(res.Done()) + go bindingInformer.Run(res.Done()) + + if !cache.WaitForCacheSync(res.Done(), res.Source.HasSynced) { + return fmt.Errorf("timed out waiting for initial cache sync") + } + return nil + } + return res, testCancel, nil +} + +// UpdateAndWait updates the given object in the test, or creates it if it doesn't exist +// Depending upon object type, waits afterward until the object is synced +// by the policy source +// +// Be aware the UpdateAndWait will modify the ResourceVersion of the +// provided objects. +func (p *PolicyTestContext[P, B, E]) UpdateAndWait(objects ...runtime.Object) error { + return p.update(true, objects...) +} + +// Update updates the given object in the test, or creates it if it doesn't exist +// +// Be aware the Update will modify the ResourceVersion of the +// provided objects. +func (p *PolicyTestContext[P, B, E]) Update(objects ...runtime.Object) error { + return p.update(false, objects...) +} + +// Objects the given object in the test, or creates it if it doesn't exist +// Depending upon object type, waits afterward until the object is synced +// by the policy source +func (p *PolicyTestContext[P, B, E]) update(wait bool, objects ...runtime.Object) error { + for _, object := range objects { + if err := p.updateOne(object); err != nil { + return err + } + } + + if wait { + timeoutCtx, timeoutCancel := context.WithTimeout(p, 3*time.Second) + defer timeoutCancel() + + for _, object := range objects { + if err := p.WaitForReconcile(timeoutCtx, object); err != nil { + return fmt.Errorf("error waiting for reconcile of %v: %v", object, err) + } + } + } + return nil +} + +// Depending upon object type, waits afterward until the object is synced +// by the policy source. Note that policies that are not bound are skipped, +// so you should not try to wait for an unbound policy. Create both the binding +// and policy, then wait. +func (p *PolicyTestContext[P, B, E]) WaitForReconcile(timeoutCtx context.Context, object runtime.Object) error { + if !p.Source.HasSynced() { + return nil + } + + objectMeta, err := meta.Accessor(object) + if err != nil { + return err + } + + objectGVK := object.GetObjectKind().GroupVersionKind() + switch objectGVK { + case p.policyGVK: + return wait.PollUntilContextCancel(timeoutCtx, 100*time.Millisecond, true, func(ctx context.Context) (done bool, err error) { + policies := p.Source.Hooks() + for _, policy := range policies { + policyMeta, err := meta.Accessor(policy.Policy) + if err != nil { + return true, err + } else if policyMeta.GetName() == objectMeta.GetName() && policyMeta.GetResourceVersion() == objectMeta.GetResourceVersion() { + return true, nil + } + } + return false, nil + }) + case p.bindingGVK: + return wait.PollUntilContextCancel(timeoutCtx, 100*time.Millisecond, true, func(ctx context.Context) (done bool, err error) { + policies := p.Source.Hooks() + for _, policy := range policies { + for _, binding := range policy.Bindings { + bindingMeta, err := meta.Accessor(binding) + if err != nil { + return true, err + } else if bindingMeta.GetName() == objectMeta.GetName() && bindingMeta.GetResourceVersion() == objectMeta.GetResourceVersion() { + return true, nil + } + } + } + return false, nil + }) + + default: + // Do nothing, params are visible immediately + // Loop until one of the params is visible via get of the param informer + return wait.PollUntilContextCancel(timeoutCtx, 100*time.Millisecond, true, func(ctx context.Context) (done bool, err error) { + informer, scope := p.Source.(*policySource[P, B, E]).getParamInformer(objectGVK) + if informer == nil { + // Informer does not exist yet, keep waiting for sync + return false, nil + } + + if !cache.WaitForCacheSync(timeoutCtx.Done(), informer.Informer().HasSynced) { + return false, fmt.Errorf("timed out waiting for cache sync of param informer") + } + + var lister cache.GenericNamespaceLister = informer.Lister() + if scope == meta.RESTScopeNamespace { + lister = informer.Lister().ByNamespace(objectMeta.GetNamespace()) + } + + fetched, err := lister.Get(objectMeta.GetName()) + if err != nil { + if errors.IsNotFound(err) { + return false, nil + } + return true, err + } + + // Ensure RV matches + fetchedMeta, err := meta.Accessor(fetched) + if err != nil { + return true, err + } else if fetchedMeta.GetResourceVersion() != objectMeta.GetResourceVersion() { + return false, nil + } + + return true, nil + }) + } +} + +func (p *PolicyTestContext[P, B, E]) waitForDelete(ctx context.Context, objectGVK schema.GroupVersionKind, name types.NamespacedName) error { + srce := p.Source.(*policySource[P, B, E]) + + return wait.PollUntilContextCancel(ctx, 100*time.Millisecond, true, func(ctx context.Context) (done bool, err error) { + switch objectGVK { + case p.policyGVK: + for _, hook := range p.Source.Hooks() { + accessor := srce.newPolicyAccessor(hook.Policy) + if accessor.GetName() == name.Name && accessor.GetNamespace() == name.Namespace { + return false, nil + } + } + + return true, nil + case p.bindingGVK: + for _, hook := range p.Source.Hooks() { + for _, binding := range hook.Bindings { + accessor := srce.newBindingAccessor(binding) + if accessor.GetName() == name.Name && accessor.GetNamespace() == name.Namespace { + return false, nil + } + } + } + return true, nil + default: + // Do nothing, params are visible immediately + // Loop until one of the params is visible via get of the param informer + informer, scope := p.Source.(*policySource[P, B, E]).getParamInformer(objectGVK) + if informer == nil { + return true, nil + } + + var lister cache.GenericNamespaceLister = informer.Lister() + if scope == meta.RESTScopeNamespace { + lister = informer.Lister().ByNamespace(name.Namespace) + } + + _, err = lister.Get(name.Name) + if err != nil { + if errors.IsNotFound(err) { + return true, nil + } + return false, err + } + return false, nil + } + }) +} + +func (p *PolicyTestContext[P, B, E]) updateOne(object runtime.Object) error { + objectMeta, err := meta.Accessor(object) + if err != nil { + return err + } + objectMeta.SetResourceVersion(string(uuid.NewUUID())) + objectGVK := object.GetObjectKind().GroupVersionKind() + + if objectGVK.Empty() { + // If the object doesn't have a GVK, ask the schema for preferred GVK + knownKinds, _, err := p.scheme.ObjectKinds(object) + if err != nil { + return err + } else if len(knownKinds) == 0 { + return fmt.Errorf("no known GVKs for object in schema: %T", object) + } + toTake := 0 + + // Prefer GVK if it is our fake policy or binding + for i, knownKind := range knownKinds { + if knownKind == p.policyGVK || knownKind == p.bindingGVK { + toTake = i + break + } + } + + objectGVK = knownKinds[toTake] + object.GetObjectKind().SetGroupVersionKind(objectGVK) + } + + // Make sure GVK is known to the fake rest mapper. To prevent cryptic error + mapping, err := p.restMapper.RESTMapping(objectGVK.GroupKind(), objectGVK.Version) + if err != nil { + return err + } + + switch objectGVK { + case p.policyGVK: + err := p.policyAndBindingTracker.Update(p.policyGVR, object, objectMeta.GetNamespace()) + if errors.IsNotFound(err) { + err = p.policyAndBindingTracker.Create(p.policyGVR, object, objectMeta.GetNamespace()) + } + + return err + case p.bindingGVK: + err := p.policyAndBindingTracker.Update(p.bindingGVR, object, objectMeta.GetNamespace()) + if errors.IsNotFound(err) { + err = p.policyAndBindingTracker.Create(p.bindingGVR, object, objectMeta.GetNamespace()) + } + + return err + default: + if _, ok := object.(*unstructured.Unstructured); ok { + if err := p.unstructuredTracker.Create(mapping.Resource, object, objectMeta.GetNamespace()); err != nil { + if errors.IsAlreadyExists(err) { + return p.unstructuredTracker.Update(mapping.Resource, object, objectMeta.GetNamespace()) + } + return err + } + return nil + } else if err := p.nativeTracker.Create(mapping.Resource, object, objectMeta.GetNamespace()); err != nil { + if errors.IsAlreadyExists(err) { + return p.nativeTracker.Update(mapping.Resource, object, objectMeta.GetNamespace()) + } + } + return nil + } +} + +// Depending upon object type, waits afterward until the object is synced +// by the policy source +func (p *PolicyTestContext[P, B, E]) DeleteAndWait(object ...runtime.Object) error { + for _, object := range object { + if err := p.deleteOne(object); err != nil && !errors.IsNotFound(err) { + return err + } + } + + timeoutCtx, timeoutCancel := context.WithTimeout(p, 3*time.Second) + defer timeoutCancel() + + for _, object := range object { + accessor, err := meta.Accessor(object) + if err != nil { + return err + } + + if err := p.waitForDelete( + timeoutCtx, + object.GetObjectKind().GroupVersionKind(), + types.NamespacedName{Name: accessor.GetName(), Namespace: accessor.GetNamespace()}); err != nil { + return err + } + } + return nil +} + +func (p *PolicyTestContext[P, B, E]) deleteOne(object runtime.Object) error { + objectMeta, err := meta.Accessor(object) + if err != nil { + return err + } + objectMeta.SetResourceVersion(string(uuid.NewUUID())) + objectGVK := object.GetObjectKind().GroupVersionKind() + + if objectGVK.Empty() { + // If the object doesn't have a GVK, ask the schema for preferred GVK + knownKinds, _, err := p.scheme.ObjectKinds(object) + if err != nil { + return err + } else if len(knownKinds) == 0 { + return fmt.Errorf("no known GVKs for object in schema: %T", object) + } + toTake := 0 + + // Prefer GVK if it is our fake policy or binding + for i, knownKind := range knownKinds { + if knownKind == p.policyGVK || knownKind == p.bindingGVK { + toTake = i + break + } + } + + objectGVK = knownKinds[toTake] + object.GetObjectKind().SetGroupVersionKind(objectGVK) + } + + // Make sure GVK is known to the fake rest mapper. To prevent cryptic error + mapping, err := p.restMapper.RESTMapping(objectGVK.GroupKind(), objectGVK.Version) + if err != nil { + return err + } + + switch objectGVK { + case p.policyGVK: + return p.policyAndBindingTracker.Delete(p.policyGVR, objectMeta.GetNamespace(), objectMeta.GetName()) + case p.bindingGVK: + return p.policyAndBindingTracker.Delete(p.bindingGVR, objectMeta.GetNamespace(), objectMeta.GetName()) + default: + if _, ok := object.(*unstructured.Unstructured); ok { + return p.unstructuredTracker.Delete(mapping.Resource, objectMeta.GetNamespace(), objectMeta.GetName()) + } + return p.nativeTracker.Delete(mapping.Resource, objectMeta.GetNamespace(), objectMeta.GetName()) + } +} + +type FakeList[T runtime.Object] struct { + metav1.TypeMeta + metav1.ListMeta + Items []T +} + +func (fl *FakeList[P]) DeepCopyObject() runtime.Object { + copiedItems := make([]P, len(fl.Items)) + for i, item := range fl.Items { + copiedItems[i] = item.DeepCopyObject().(P) + } + return &FakeList[P]{ + TypeMeta: fl.TypeMeta, + ListMeta: fl.ListMeta, + Items: copiedItems, + } +} + +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/validatingadmissionpolicy/internal/generic/controller.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/internal/generic/controller.go similarity index 100% rename from staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/internal/generic/controller.go rename to staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/internal/generic/controller.go diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/internal/generic/controller_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/internal/generic/controller_test.go similarity index 99% rename from staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/internal/generic/controller_test.go rename to staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/internal/generic/controller_test.go index cfd805750c5..ce277ed9d2f 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/internal/generic/controller_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/internal/generic/controller_test.go @@ -39,7 +39,7 @@ import ( "k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/watch" - "k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/internal/generic" + "k8s.io/apiserver/pkg/admission/plugin/policy/internal/generic" clienttesting "k8s.io/client-go/testing" "k8s.io/client-go/tools/cache" diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/internal/generic/doc.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/internal/generic/doc.go similarity index 100% rename from staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/internal/generic/doc.go rename to staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/internal/generic/doc.go diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/internal/generic/informer.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/internal/generic/informer.go similarity index 79% rename from staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/internal/generic/informer.go rename to staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/internal/generic/informer.go index 3025aa19533..acb6316ec3a 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/internal/generic/informer.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/internal/generic/informer.go @@ -28,6 +28,10 @@ type informer[T runtime.Object] struct { lister[T] } +// Creates a generic informer around a type-erased cache.SharedIndexInformer +// It is incumbent on the caller to ensure that the generic type argument is +// consistent with the type of the objects stored inside the SharedIndexInformer +// as they will be casted. func NewInformer[T runtime.Object](informe cache.SharedIndexInformer) Informer[T] { return informer[T]{ SharedIndexInformer: informe, diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/internal/generic/interface.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/internal/generic/interface.go similarity index 100% rename from staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/internal/generic/interface.go rename to staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/internal/generic/interface.go diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/internal/generic/lister.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/internal/generic/lister.go similarity index 100% rename from staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/internal/generic/lister.go rename to staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/internal/generic/lister.go diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/matching/matching.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/matching/matching.go similarity index 100% rename from staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/matching/matching.go rename to staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/matching/matching.go diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/matching/matching_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/matching/matching_test.go similarity index 100% rename from staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/matching/matching_test.go rename to staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/matching/matching_test.go diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/OWNERS b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/OWNERS similarity index 100% rename from staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/OWNERS rename to staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/OWNERS 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/validatingadmissionpolicy/admission_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/admission_test.go similarity index 51% rename from staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/admission_test.go rename to staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/admission_test.go index f86b6553587..cc8ee5ccbe4 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/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 validatingadmissionpolicy +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/validatingadmissionpolicy/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/validatingadmissionpolicy/caching_authorizer.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/caching_authorizer.go similarity index 99% rename from staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/caching_authorizer.go rename to staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/caching_authorizer.go index a295cb30dc0..fbefd595e57 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/caching_authorizer.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/caching_authorizer.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package validatingadmissionpolicy +package validating import ( "context" diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/caching_authorizer_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/caching_authorizer_test.go similarity index 99% rename from staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/caching_authorizer_test.go rename to staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/caching_authorizer_test.go index 9cef35fe4af..da7f219fa1c 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/caching_authorizer_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/caching_authorizer_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package validatingadmissionpolicy +package validating import ( "context" diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/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/validatingadmissionpolicy/controller.go rename to staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/dispatcher.go index 46b76e06d5f..5af6a3f3092 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/controller.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/dispatcher.go @@ -14,15 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -package validatingadmissionpolicy +package validating import ( "context" "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" - celmetrics "k8s.io/apiserver/pkg/admission/cel" - "k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/internal/generic" - "k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/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/validatingadmissionpolicy/initializer.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/initializer.go similarity index 96% rename from staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/initializer.go rename to staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/initializer.go index 15b75798585..c7cb9c657ee 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/initializer.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/initializer.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package validatingadmissionpolicy +package validating import ( "context" diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/interface.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/interface.go similarity index 99% rename from staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/interface.go rename to staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/interface.go index 206fc137831..33cbcce6588 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/interface.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/interface.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package validatingadmissionpolicy +package validating import ( "context" diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/matcher.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/matcher.go similarity index 96% rename from staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/matcher.go rename to staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/matcher.go index 397f2c26714..9d667e46edf 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/matcher.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/matcher.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package validatingadmissionpolicy +package validating import ( "k8s.io/api/admissionregistration/v1beta1" @@ -23,7 +23,7 @@ import ( "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/admission" - "k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/matching" + "k8s.io/apiserver/pkg/admission/plugin/policy/matching" ) var _ matching.MatchCriteria = &matchCriteria{} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/message.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/message.go similarity index 96% rename from staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/message.go rename to staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/message.go index 772891e3c8b..8b7d973617d 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/message.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/message.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package validatingadmissionpolicy +package validating import ( celgo "github.com/google/cel-go/cel" diff --git a/staging/src/k8s.io/apiserver/pkg/admission/cel/metrics.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/metrics/metrics.go similarity index 100% rename from staging/src/k8s.io/apiserver/pkg/admission/cel/metrics.go rename to staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/metrics/metrics.go diff --git a/staging/src/k8s.io/apiserver/pkg/admission/cel/metrics_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/metrics/metrics_test.go similarity index 100% rename from staging/src/k8s.io/apiserver/pkg/admission/cel/metrics_test.go rename to staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/metrics/metrics_test.go 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..db371804495 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/plugin.go @@ -0,0 +1,202 @@ +/* +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" + "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" +) + +var ( + compositionEnvTemplate *cel.CompositionEnv = func() *cel.CompositionEnv { + compositionEnvTemplate, err := cel.NewCompositionEnv(cel.VariablesTypeName, environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion())) + if err != nil { + panic(err) + } + + return compositionEnvTemplate + }() +) + +// 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 := cel.NewCompositedCompilerFromTemplate(compositionEnvTemplate) + filterCompiler.CompileAndStoreVariables(convertv1beta1Variables(policy.Spec.Variables), optionalVars, environment.StoredExpressions) + + 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/validatingadmissionpolicy/policy_decision.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/policy_decision.go similarity index 98% rename from staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/policy_decision.go rename to staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/policy_decision.go index 939cbea70c0..3ea17038a39 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/policy_decision.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/policy_decision.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package validatingadmissionpolicy +package validating import ( "net/http" diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/typechecking.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/typechecking.go similarity index 99% rename from staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/typechecking.go rename to staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/typechecking.go index d4a1bf82556..d54d3b667ab 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/typechecking.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/typechecking.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package validatingadmissionpolicy +package validating import ( "errors" diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/typechecking_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/typechecking_test.go similarity index 97% rename from staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/typechecking_test.go rename to staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/typechecking_test.go index 67a0e98aec3..31e9a129909 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/typechecking_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/typechecking_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package validatingadmissionpolicy +package validating import ( "fmt" @@ -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 diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/validator.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/validator.go similarity index 99% rename from staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/validator.go rename to staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/validator.go index 9630a497471..64d5fdc476a 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/validator.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/validator.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package validatingadmissionpolicy +package validating import ( "context" diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/validator_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/validator_test.go similarity index 99% rename from staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/validator_test.go rename to staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/validator_test.go index d1e5753afe6..ba4f1ca86c7 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/validator_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/policy/validating/validator_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package validatingadmissionpolicy +package validating import ( "context" diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/admission.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/admission.go deleted file mode 100644 index e51bc6e7379..00000000000 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/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 validatingadmissionpolicy - -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/validatingadmissionpolicy/controller_reconcile.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/controller_reconcile.go deleted file mode 100644 index 9cd3c01aed3..00000000000 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/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 validatingadmissionpolicy - -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" - celmetrics "k8s.io/apiserver/pkg/admission/cel" - "k8s.io/apiserver/pkg/admission/plugin/cel" - "k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/internal/generic" - "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/server/options/admission.go b/staging/src/k8s.io/apiserver/pkg/server/options/admission.go index 6f4990a7e2c..3f65a0d1f3f 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/options/admission.go +++ b/staging/src/k8s.io/apiserver/pkg/server/options/admission.go @@ -29,7 +29,7 @@ import ( "k8s.io/apiserver/pkg/admission/initializer" admissionmetrics "k8s.io/apiserver/pkg/admission/metrics" "k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle" - "k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy" + validatingadmissionpolicy "k8s.io/apiserver/pkg/admission/plugin/policy/validating" mutatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/mutating" validatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/validating" apiserverapi "k8s.io/apiserver/pkg/apis/apiserver" diff --git a/staging/src/k8s.io/apiserver/pkg/server/plugins.go b/staging/src/k8s.io/apiserver/pkg/server/plugins.go index 25acf92dc87..2390446419c 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/plugins.go +++ b/staging/src/k8s.io/apiserver/pkg/server/plugins.go @@ -20,7 +20,7 @@ package server import ( "k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle" - "k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy" + validatingadmissionpolicy "k8s.io/apiserver/pkg/admission/plugin/policy/validating" mutatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/mutating" validatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/validating" ) diff --git a/vendor/modules.txt b/vendor/modules.txt index 6cd78471b49..448c2b8aeda 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1360,12 +1360,16 @@ k8s.io/apimachinery/third_party/forked/golang/reflect # k8s.io/apiserver v0.0.0 => ./staging/src/k8s.io/apiserver ## explicit; go 1.21 k8s.io/apiserver/pkg/admission -k8s.io/apiserver/pkg/admission/cel k8s.io/apiserver/pkg/admission/configuration k8s.io/apiserver/pkg/admission/initializer k8s.io/apiserver/pkg/admission/metrics k8s.io/apiserver/pkg/admission/plugin/cel k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle +k8s.io/apiserver/pkg/admission/plugin/policy/generic +k8s.io/apiserver/pkg/admission/plugin/policy/internal/generic +k8s.io/apiserver/pkg/admission/plugin/policy/matching +k8s.io/apiserver/pkg/admission/plugin/policy/validating +k8s.io/apiserver/pkg/admission/plugin/policy/validating/metrics k8s.io/apiserver/pkg/admission/plugin/resourcequota k8s.io/apiserver/pkg/admission/plugin/resourcequota/apis/resourcequota k8s.io/apiserver/pkg/admission/plugin/resourcequota/apis/resourcequota/install @@ -1373,9 +1377,6 @@ k8s.io/apiserver/pkg/admission/plugin/resourcequota/apis/resourcequota/v1 k8s.io/apiserver/pkg/admission/plugin/resourcequota/apis/resourcequota/v1alpha1 k8s.io/apiserver/pkg/admission/plugin/resourcequota/apis/resourcequota/v1beta1 k8s.io/apiserver/pkg/admission/plugin/resourcequota/apis/resourcequota/validation -k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy -k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/internal/generic -k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/matching k8s.io/apiserver/pkg/admission/plugin/webhook k8s.io/apiserver/pkg/admission/plugin/webhook/config k8s.io/apiserver/pkg/admission/plugin/webhook/config/apis/webhookadmission