refactor: create generic policy plugin type similar to webhook
This commit is contained in:
		@@ -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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -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 {
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user