refactor: create generic policy plugin type similar to webhook

This commit is contained in:
Alexander Zielenski 2024-01-22 17:31:34 -08:00
parent d697f43d73
commit a6366573d5
6 changed files with 1610 additions and 0 deletions

View File

@ -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
}

View File

@ -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 {
}

View File

@ -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
}

View File

@ -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 := &paramInfo{
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
}

View File

@ -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
}

View File

@ -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
}