Merge pull request #118051 from A-Hilaly/api-server/webhooks/smart-reload
support `WebhookAccessors` smart reload
This commit is contained in:
		@@ -19,8 +19,9 @@ package configuration
 | 
				
			|||||||
import (
 | 
					import (
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"sort"
 | 
						"sort"
 | 
				
			||||||
 | 
						"sync"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"k8s.io/api/admissionregistration/v1"
 | 
						v1 "k8s.io/api/admissionregistration/v1"
 | 
				
			||||||
	"k8s.io/apimachinery/pkg/labels"
 | 
						"k8s.io/apimachinery/pkg/labels"
 | 
				
			||||||
	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
 | 
						utilruntime "k8s.io/apimachinery/pkg/util/runtime"
 | 
				
			||||||
	"k8s.io/apiserver/pkg/admission/plugin/webhook"
 | 
						"k8s.io/apiserver/pkg/admission/plugin/webhook"
 | 
				
			||||||
@@ -29,13 +30,22 @@ import (
 | 
				
			|||||||
	admissionregistrationlisters "k8s.io/client-go/listers/admissionregistration/v1"
 | 
						admissionregistrationlisters "k8s.io/client-go/listers/admissionregistration/v1"
 | 
				
			||||||
	"k8s.io/client-go/tools/cache"
 | 
						"k8s.io/client-go/tools/cache"
 | 
				
			||||||
	"k8s.io/client-go/tools/cache/synctrack"
 | 
						"k8s.io/client-go/tools/cache/synctrack"
 | 
				
			||||||
 | 
						"k8s.io/klog/v2"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Type for test injection.
 | 
				
			||||||
 | 
					type mutatingWebhookAccessorCreator func(uid string, configurationName string, h *v1.MutatingWebhook) webhook.WebhookAccessor
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// mutatingWebhookConfigurationManager collects the mutating webhook objects so that they can be called.
 | 
					// mutatingWebhookConfigurationManager collects the mutating webhook objects so that they can be called.
 | 
				
			||||||
type mutatingWebhookConfigurationManager struct {
 | 
					type mutatingWebhookConfigurationManager struct {
 | 
				
			||||||
	lister    admissionregistrationlisters.MutatingWebhookConfigurationLister
 | 
						lister              admissionregistrationlisters.MutatingWebhookConfigurationLister
 | 
				
			||||||
	hasSynced func() bool
 | 
						hasSynced           func() bool
 | 
				
			||||||
	lazy      synctrack.Lazy[[]webhook.WebhookAccessor]
 | 
						lazy                synctrack.Lazy[[]webhook.WebhookAccessor]
 | 
				
			||||||
 | 
						configurationsCache sync.Map
 | 
				
			||||||
 | 
						// createMutatingWebhookAccessor is used to instantiate webhook accessors.
 | 
				
			||||||
 | 
						// This function is defined as field instead of a struct method to allow injection
 | 
				
			||||||
 | 
						// during tests
 | 
				
			||||||
 | 
						createMutatingWebhookAccessor mutatingWebhookAccessorCreator
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var _ generic.Source = &mutatingWebhookConfigurationManager{}
 | 
					var _ generic.Source = &mutatingWebhookConfigurationManager{}
 | 
				
			||||||
@@ -43,14 +53,35 @@ var _ generic.Source = &mutatingWebhookConfigurationManager{}
 | 
				
			|||||||
func NewMutatingWebhookConfigurationManager(f informers.SharedInformerFactory) generic.Source {
 | 
					func NewMutatingWebhookConfigurationManager(f informers.SharedInformerFactory) generic.Source {
 | 
				
			||||||
	informer := f.Admissionregistration().V1().MutatingWebhookConfigurations()
 | 
						informer := f.Admissionregistration().V1().MutatingWebhookConfigurations()
 | 
				
			||||||
	manager := &mutatingWebhookConfigurationManager{
 | 
						manager := &mutatingWebhookConfigurationManager{
 | 
				
			||||||
		lister: informer.Lister(),
 | 
							lister:                        informer.Lister(),
 | 
				
			||||||
 | 
							createMutatingWebhookAccessor: webhook.NewMutatingWebhookAccessor,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	manager.lazy.Evaluate = manager.getConfiguration
 | 
						manager.lazy.Evaluate = manager.getConfiguration
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	handle, _ := informer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
 | 
						handle, _ := informer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
 | 
				
			||||||
		AddFunc:    func(_ interface{}) { manager.lazy.Notify() },
 | 
							AddFunc: func(_ interface{}) { manager.lazy.Notify() },
 | 
				
			||||||
		UpdateFunc: func(_, _ interface{}) { manager.lazy.Notify() },
 | 
							UpdateFunc: func(old, new interface{}) {
 | 
				
			||||||
		DeleteFunc: func(_ interface{}) { manager.lazy.Notify() },
 | 
								obj := new.(*v1.MutatingWebhookConfiguration)
 | 
				
			||||||
 | 
								manager.configurationsCache.Delete(obj.GetName())
 | 
				
			||||||
 | 
								manager.lazy.Notify()
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							DeleteFunc: func(obj interface{}) {
 | 
				
			||||||
 | 
								vwc, ok := obj.(*v1.MutatingWebhookConfiguration)
 | 
				
			||||||
 | 
								if !ok {
 | 
				
			||||||
 | 
									tombstone, ok := obj.(cache.DeletedFinalStateUnknown)
 | 
				
			||||||
 | 
									if !ok {
 | 
				
			||||||
 | 
										klog.V(2).Infof("Couldn't get object from tombstone %#v", obj)
 | 
				
			||||||
 | 
										return
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									vwc, ok = tombstone.Obj.(*v1.MutatingWebhookConfiguration)
 | 
				
			||||||
 | 
									if !ok {
 | 
				
			||||||
 | 
										klog.V(2).Infof("Tombstone contained object that is not expected %#v", obj)
 | 
				
			||||||
 | 
										return
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								manager.configurationsCache.Delete(vwc.Name)
 | 
				
			||||||
 | 
								manager.lazy.Notify()
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
	manager.hasSynced = handle.HasSynced
 | 
						manager.hasSynced = handle.HasSynced
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -75,25 +106,46 @@ func (m *mutatingWebhookConfigurationManager) getConfiguration() ([]webhook.Webh
 | 
				
			|||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return []webhook.WebhookAccessor{}, err
 | 
							return []webhook.WebhookAccessor{}, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return mergeMutatingWebhookConfigurations(configurations), nil
 | 
						return m.getMutatingWebhookConfigurations(configurations), nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func mergeMutatingWebhookConfigurations(configurations []*v1.MutatingWebhookConfiguration) []webhook.WebhookAccessor {
 | 
					// getMutatingWebhookConfigurations returns the webhook accessors for a given list of
 | 
				
			||||||
 | 
					// mutating webhook configurations.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// This function will, first, try to load the webhook accessors from the cache and avoid
 | 
				
			||||||
 | 
					// recreating them, which can be expessive (requiring CEL expression recompilation).
 | 
				
			||||||
 | 
					func (m *mutatingWebhookConfigurationManager) getMutatingWebhookConfigurations(configurations []*v1.MutatingWebhookConfiguration) []webhook.WebhookAccessor {
 | 
				
			||||||
	// The internal order of webhooks for each configuration is provided by the user
 | 
						// The internal order of webhooks for each configuration is provided by the user
 | 
				
			||||||
	// but configurations themselves can be in any order. As we are going to run these
 | 
						// but configurations themselves can be in any order. As we are going to run these
 | 
				
			||||||
	// webhooks in serial, they are sorted here to have a deterministic order.
 | 
						// webhooks in serial, they are sorted here to have a deterministic order.
 | 
				
			||||||
	sort.SliceStable(configurations, MutatingWebhookConfigurationSorter(configurations).ByName)
 | 
						sort.SliceStable(configurations, MutatingWebhookConfigurationSorter(configurations).ByName)
 | 
				
			||||||
	accessors := []webhook.WebhookAccessor{}
 | 
						size := 0
 | 
				
			||||||
 | 
						for _, cfg := range configurations {
 | 
				
			||||||
 | 
							size += len(cfg.Webhooks)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						accessors := make([]webhook.WebhookAccessor, 0, size)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for _, c := range configurations {
 | 
						for _, c := range configurations {
 | 
				
			||||||
 | 
							cachedConfigurationAccessors, ok := m.configurationsCache.Load(c.Name)
 | 
				
			||||||
 | 
							if ok {
 | 
				
			||||||
 | 
								// Pick an already cached webhookAccessor
 | 
				
			||||||
 | 
								accessors = append(accessors, cachedConfigurationAccessors.([]webhook.WebhookAccessor)...)
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// webhook names are not validated for uniqueness, so we check for duplicates and
 | 
							// webhook names are not validated for uniqueness, so we check for duplicates and
 | 
				
			||||||
		// add a int suffix to distinguish between them
 | 
							// add a int suffix to distinguish between them
 | 
				
			||||||
		names := map[string]int{}
 | 
							names := map[string]int{}
 | 
				
			||||||
 | 
							configurationAccessors := make([]webhook.WebhookAccessor, 0, len(c.Webhooks))
 | 
				
			||||||
		for i := range c.Webhooks {
 | 
							for i := range c.Webhooks {
 | 
				
			||||||
			n := c.Webhooks[i].Name
 | 
								n := c.Webhooks[i].Name
 | 
				
			||||||
			uid := fmt.Sprintf("%s/%s/%d", c.Name, n, names[n])
 | 
								uid := fmt.Sprintf("%s/%s/%d", c.Name, n, names[n])
 | 
				
			||||||
			names[n]++
 | 
								names[n]++
 | 
				
			||||||
			accessors = append(accessors, webhook.NewMutatingWebhookAccessor(uid, c.Name, &c.Webhooks[i]))
 | 
								configurationAccessor := m.createMutatingWebhookAccessor(uid, c.Name, &c.Webhooks[i])
 | 
				
			||||||
 | 
								configurationAccessors = append(configurationAccessors, configurationAccessor)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
							accessors = append(accessors, configurationAccessors...)
 | 
				
			||||||
 | 
							m.configurationsCache.Store(c.Name, configurationAccessors)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return accessors
 | 
						return accessors
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -24,6 +24,7 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	"k8s.io/api/admissionregistration/v1"
 | 
						"k8s.io/api/admissionregistration/v1"
 | 
				
			||||||
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 | 
						metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 | 
				
			||||||
 | 
						"k8s.io/apiserver/pkg/admission/plugin/webhook"
 | 
				
			||||||
	"k8s.io/client-go/informers"
 | 
						"k8s.io/client-go/informers"
 | 
				
			||||||
	"k8s.io/client-go/kubernetes/fake"
 | 
						"k8s.io/client-go/kubernetes/fake"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
@@ -81,3 +82,202 @@ func TestGetMutatingWebhookConfig(t *testing.T) {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// mockCreateMutatingWebhookAccessor is a struct used to compute how many times
 | 
				
			||||||
 | 
					// the function webhook.NewMutatingWebhookAccessor is being called when refreshing
 | 
				
			||||||
 | 
					// webhookAccessors.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// NOTE: Maybe there some testing help that we can import and reuse instead.
 | 
				
			||||||
 | 
					type mockCreateMutatingWebhookAccessor struct {
 | 
				
			||||||
 | 
						numberOfCalls int
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (mock *mockCreateMutatingWebhookAccessor) calledNTimes() int { return mock.numberOfCalls }
 | 
				
			||||||
 | 
					func (mock *mockCreateMutatingWebhookAccessor) resetCounter()     { mock.numberOfCalls = 0 }
 | 
				
			||||||
 | 
					func (mock *mockCreateMutatingWebhookAccessor) incrementCounter() { mock.numberOfCalls++ }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (mock *mockCreateMutatingWebhookAccessor) fn(uid string, configurationName string, h *v1.MutatingWebhook) webhook.WebhookAccessor {
 | 
				
			||||||
 | 
						mock.incrementCounter()
 | 
				
			||||||
 | 
						return webhook.NewMutatingWebhookAccessor(uid, configurationName, h)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func mutatingConfigurationTotalWebhooks(configurations []*v1.MutatingWebhookConfiguration) int {
 | 
				
			||||||
 | 
						total := 0
 | 
				
			||||||
 | 
						for _, configuration := range configurations {
 | 
				
			||||||
 | 
							total += len(configuration.Webhooks)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return total
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestGetMutatingWebhookConfigSmartReload(t *testing.T) {
 | 
				
			||||||
 | 
						type args struct {
 | 
				
			||||||
 | 
							createWebhookConfigurations []*v1.MutatingWebhookConfiguration
 | 
				
			||||||
 | 
							updateWebhookConfigurations []*v1.MutatingWebhookConfiguration
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						tests := []struct {
 | 
				
			||||||
 | 
							name              string
 | 
				
			||||||
 | 
							args              args
 | 
				
			||||||
 | 
							numberOfCreations int
 | 
				
			||||||
 | 
							// number of refreshes are number of times we recrated a webhook accessor
 | 
				
			||||||
 | 
							// instead of pulling from the cache.
 | 
				
			||||||
 | 
							numberOfRefreshes             int
 | 
				
			||||||
 | 
							finalNumberOfWebhookAccessors int
 | 
				
			||||||
 | 
						}{
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "no creations and no updates",
 | 
				
			||||||
 | 
								args: args{
 | 
				
			||||||
 | 
									nil,
 | 
				
			||||||
 | 
									nil,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								numberOfCreations:             0,
 | 
				
			||||||
 | 
								numberOfRefreshes:             0,
 | 
				
			||||||
 | 
								finalNumberOfWebhookAccessors: 0,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "create configurations and no updates",
 | 
				
			||||||
 | 
								args: args{
 | 
				
			||||||
 | 
									[]*v1.MutatingWebhookConfiguration{
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											ObjectMeta: metav1.ObjectMeta{Name: "webhook1"},
 | 
				
			||||||
 | 
											Webhooks:   []v1.MutatingWebhook{{Name: "webhook1.1"}, {Name: "webhook1.2"}},
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											ObjectMeta: metav1.ObjectMeta{Name: "webhook2"},
 | 
				
			||||||
 | 
											Webhooks:   []v1.MutatingWebhook{{Name: "webhook2.1"}, {Name: "webhook2.2"}},
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									nil,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								numberOfCreations:             4,
 | 
				
			||||||
 | 
								numberOfRefreshes:             0,
 | 
				
			||||||
 | 
								finalNumberOfWebhookAccessors: 4,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "create configurations and update some of them",
 | 
				
			||||||
 | 
								args: args{
 | 
				
			||||||
 | 
									[]*v1.MutatingWebhookConfiguration{
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											ObjectMeta: metav1.ObjectMeta{Name: "webhook3"},
 | 
				
			||||||
 | 
											Webhooks:   []v1.MutatingWebhook{{Name: "webhook3.1"}},
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											ObjectMeta: metav1.ObjectMeta{Name: "webhook4"},
 | 
				
			||||||
 | 
											Webhooks:   []v1.MutatingWebhook{{Name: "webhook4.1"}},
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									[]*v1.MutatingWebhookConfiguration{
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											ObjectMeta: metav1.ObjectMeta{Name: "webhook4"},
 | 
				
			||||||
 | 
											Webhooks:   []v1.MutatingWebhook{{Name: "webhook4.1-updated"}, {Name: "webhook4.2"}},
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								numberOfCreations:             2,
 | 
				
			||||||
 | 
								numberOfRefreshes:             2,
 | 
				
			||||||
 | 
								finalNumberOfWebhookAccessors: 3,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "create configuration and update moar of them",
 | 
				
			||||||
 | 
								args: args{
 | 
				
			||||||
 | 
									[]*v1.MutatingWebhookConfiguration{
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											ObjectMeta: metav1.ObjectMeta{Name: "webhook5"},
 | 
				
			||||||
 | 
											Webhooks:   []v1.MutatingWebhook{{Name: "webhook5.1"}, {Name: "webhook5.2"}},
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											ObjectMeta: metav1.ObjectMeta{Name: "webhook6"},
 | 
				
			||||||
 | 
											Webhooks:   []v1.MutatingWebhook{{Name: "webhook6.1"}},
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											ObjectMeta: metav1.ObjectMeta{Name: "webhook7"},
 | 
				
			||||||
 | 
											Webhooks:   []v1.MutatingWebhook{{Name: "webhook7.1"}},
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									[]*v1.MutatingWebhookConfiguration{
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											ObjectMeta: metav1.ObjectMeta{Name: "webhook6"},
 | 
				
			||||||
 | 
											Webhooks:   []v1.MutatingWebhook{{Name: "webhook6.1-updated"}},
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											ObjectMeta: metav1.ObjectMeta{Name: "webhook7"},
 | 
				
			||||||
 | 
											Webhooks:   []v1.MutatingWebhook{{Name: "webhook7.1-updated"}, {Name: "webhook7.2"}},
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								numberOfCreations:             4,
 | 
				
			||||||
 | 
								numberOfRefreshes:             3,
 | 
				
			||||||
 | 
								finalNumberOfWebhookAccessors: 5,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, tt := range tests {
 | 
				
			||||||
 | 
							t.Run(tt.name, func(t *testing.T) {
 | 
				
			||||||
 | 
								client := fake.NewSimpleClientset()
 | 
				
			||||||
 | 
								informerFactory := informers.NewSharedInformerFactory(client, 0)
 | 
				
			||||||
 | 
								stop := make(chan struct{})
 | 
				
			||||||
 | 
								defer close(stop)
 | 
				
			||||||
 | 
								manager := NewMutatingWebhookConfigurationManager(informerFactory)
 | 
				
			||||||
 | 
								managerStructPtr := manager.(*mutatingWebhookConfigurationManager)
 | 
				
			||||||
 | 
								fakeWebhookAccessorCreator := &mockCreateMutatingWebhookAccessor{}
 | 
				
			||||||
 | 
								managerStructPtr.createMutatingWebhookAccessor = fakeWebhookAccessorCreator.fn
 | 
				
			||||||
 | 
								informerFactory.Start(stop)
 | 
				
			||||||
 | 
								informerFactory.WaitForCacheSync(stop)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Create webhooks
 | 
				
			||||||
 | 
								for _, configurations := range tt.args.createWebhookConfigurations {
 | 
				
			||||||
 | 
									client.
 | 
				
			||||||
 | 
										AdmissionregistrationV1().
 | 
				
			||||||
 | 
										MutatingWebhookConfigurations().
 | 
				
			||||||
 | 
										Create(context.TODO(), configurations, metav1.CreateOptions{})
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								// TODO use channels to wait for manager.createMutatingWebhookAccessor
 | 
				
			||||||
 | 
								// to be called instead of using time.Sleep
 | 
				
			||||||
 | 
								time.Sleep(1 * time.Second)
 | 
				
			||||||
 | 
								webhooks := manager.Webhooks()
 | 
				
			||||||
 | 
								if mutatingConfigurationTotalWebhooks(tt.args.createWebhookConfigurations) != len(webhooks) {
 | 
				
			||||||
 | 
									t.Errorf("Expected number of webhooks %d received %d",
 | 
				
			||||||
 | 
										mutatingConfigurationTotalWebhooks(tt.args.createWebhookConfigurations),
 | 
				
			||||||
 | 
										len(webhooks),
 | 
				
			||||||
 | 
									)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								// assert creations
 | 
				
			||||||
 | 
								if tt.numberOfCreations != fakeWebhookAccessorCreator.calledNTimes() {
 | 
				
			||||||
 | 
									t.Errorf(
 | 
				
			||||||
 | 
										"Expected number of creations %d received %d",
 | 
				
			||||||
 | 
										tt.numberOfCreations, fakeWebhookAccessorCreator.calledNTimes(),
 | 
				
			||||||
 | 
									)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// reset mock counter
 | 
				
			||||||
 | 
								fakeWebhookAccessorCreator.resetCounter()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Update webhooks
 | 
				
			||||||
 | 
								for _, configurations := range tt.args.updateWebhookConfigurations {
 | 
				
			||||||
 | 
									client.
 | 
				
			||||||
 | 
										AdmissionregistrationV1().
 | 
				
			||||||
 | 
										MutatingWebhookConfigurations().
 | 
				
			||||||
 | 
										Update(context.TODO(), configurations, metav1.UpdateOptions{})
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								// TODO use channels to wait for manager.createMutatingWebhookAccessor
 | 
				
			||||||
 | 
								// to be called instead of using time.Sleep
 | 
				
			||||||
 | 
								time.Sleep(1 * time.Second)
 | 
				
			||||||
 | 
								webhooks = manager.Webhooks()
 | 
				
			||||||
 | 
								if tt.finalNumberOfWebhookAccessors != len(webhooks) {
 | 
				
			||||||
 | 
									t.Errorf("Expected final number of webhooks %d received %d",
 | 
				
			||||||
 | 
										tt.finalNumberOfWebhookAccessors,
 | 
				
			||||||
 | 
										len(webhooks),
 | 
				
			||||||
 | 
									)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// assert updates
 | 
				
			||||||
 | 
								if tt.numberOfRefreshes != fakeWebhookAccessorCreator.calledNTimes() {
 | 
				
			||||||
 | 
									t.Errorf(
 | 
				
			||||||
 | 
										"Expected number of refreshes %d received %d",
 | 
				
			||||||
 | 
										tt.numberOfRefreshes, fakeWebhookAccessorCreator.calledNTimes(),
 | 
				
			||||||
 | 
									)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								// reset mock counter for the next test cases
 | 
				
			||||||
 | 
								fakeWebhookAccessorCreator.resetCounter()
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,8 +19,9 @@ package configuration
 | 
				
			|||||||
import (
 | 
					import (
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"sort"
 | 
						"sort"
 | 
				
			||||||
 | 
						"sync"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"k8s.io/api/admissionregistration/v1"
 | 
						v1 "k8s.io/api/admissionregistration/v1"
 | 
				
			||||||
	"k8s.io/apimachinery/pkg/labels"
 | 
						"k8s.io/apimachinery/pkg/labels"
 | 
				
			||||||
	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
 | 
						utilruntime "k8s.io/apimachinery/pkg/util/runtime"
 | 
				
			||||||
	"k8s.io/apiserver/pkg/admission/plugin/webhook"
 | 
						"k8s.io/apiserver/pkg/admission/plugin/webhook"
 | 
				
			||||||
@@ -29,13 +30,22 @@ import (
 | 
				
			|||||||
	admissionregistrationlisters "k8s.io/client-go/listers/admissionregistration/v1"
 | 
						admissionregistrationlisters "k8s.io/client-go/listers/admissionregistration/v1"
 | 
				
			||||||
	"k8s.io/client-go/tools/cache"
 | 
						"k8s.io/client-go/tools/cache"
 | 
				
			||||||
	"k8s.io/client-go/tools/cache/synctrack"
 | 
						"k8s.io/client-go/tools/cache/synctrack"
 | 
				
			||||||
 | 
						"k8s.io/klog/v2"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Type for test injection.
 | 
				
			||||||
 | 
					type validatingWebhookAccessorCreator func(uid string, configurationName string, h *v1.ValidatingWebhook) webhook.WebhookAccessor
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// validatingWebhookConfigurationManager collects the validating webhook objects so that they can be called.
 | 
					// validatingWebhookConfigurationManager collects the validating webhook objects so that they can be called.
 | 
				
			||||||
type validatingWebhookConfigurationManager struct {
 | 
					type validatingWebhookConfigurationManager struct {
 | 
				
			||||||
	lister    admissionregistrationlisters.ValidatingWebhookConfigurationLister
 | 
						lister              admissionregistrationlisters.ValidatingWebhookConfigurationLister
 | 
				
			||||||
	hasSynced func() bool
 | 
						hasSynced           func() bool
 | 
				
			||||||
	lazy      synctrack.Lazy[[]webhook.WebhookAccessor]
 | 
						lazy                synctrack.Lazy[[]webhook.WebhookAccessor]
 | 
				
			||||||
 | 
						configurationsCache sync.Map
 | 
				
			||||||
 | 
						// createValidatingWebhookAccessor is used to instantiate webhook accessors.
 | 
				
			||||||
 | 
						// This function is defined as field instead of a struct method to allow injection
 | 
				
			||||||
 | 
						// during tests
 | 
				
			||||||
 | 
						createValidatingWebhookAccessor validatingWebhookAccessorCreator
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var _ generic.Source = &validatingWebhookConfigurationManager{}
 | 
					var _ generic.Source = &validatingWebhookConfigurationManager{}
 | 
				
			||||||
@@ -43,14 +53,35 @@ var _ generic.Source = &validatingWebhookConfigurationManager{}
 | 
				
			|||||||
func NewValidatingWebhookConfigurationManager(f informers.SharedInformerFactory) generic.Source {
 | 
					func NewValidatingWebhookConfigurationManager(f informers.SharedInformerFactory) generic.Source {
 | 
				
			||||||
	informer := f.Admissionregistration().V1().ValidatingWebhookConfigurations()
 | 
						informer := f.Admissionregistration().V1().ValidatingWebhookConfigurations()
 | 
				
			||||||
	manager := &validatingWebhookConfigurationManager{
 | 
						manager := &validatingWebhookConfigurationManager{
 | 
				
			||||||
		lister: informer.Lister(),
 | 
							lister:                          informer.Lister(),
 | 
				
			||||||
 | 
							createValidatingWebhookAccessor: webhook.NewValidatingWebhookAccessor,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	manager.lazy.Evaluate = manager.getConfiguration
 | 
						manager.lazy.Evaluate = manager.getConfiguration
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	handle, _ := informer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
 | 
						handle, _ := informer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
 | 
				
			||||||
		AddFunc:    func(_ interface{}) { manager.lazy.Notify() },
 | 
							AddFunc: func(_ interface{}) { manager.lazy.Notify() },
 | 
				
			||||||
		UpdateFunc: func(_, _ interface{}) { manager.lazy.Notify() },
 | 
							UpdateFunc: func(old, new interface{}) {
 | 
				
			||||||
		DeleteFunc: func(_ interface{}) { manager.lazy.Notify() },
 | 
								obj := new.(*v1.ValidatingWebhookConfiguration)
 | 
				
			||||||
 | 
								manager.configurationsCache.Delete(obj.GetName())
 | 
				
			||||||
 | 
								manager.lazy.Notify()
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							DeleteFunc: func(obj interface{}) {
 | 
				
			||||||
 | 
								vwc, ok := obj.(*v1.ValidatingWebhookConfiguration)
 | 
				
			||||||
 | 
								if !ok {
 | 
				
			||||||
 | 
									tombstone, ok := obj.(cache.DeletedFinalStateUnknown)
 | 
				
			||||||
 | 
									if !ok {
 | 
				
			||||||
 | 
										klog.V(2).Infof("Couldn't get object from tombstone %#v", obj)
 | 
				
			||||||
 | 
										return
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									vwc, ok = tombstone.Obj.(*v1.ValidatingWebhookConfiguration)
 | 
				
			||||||
 | 
									if !ok {
 | 
				
			||||||
 | 
										klog.V(2).Infof("Tombstone contained object that is not expected %#v", obj)
 | 
				
			||||||
 | 
										return
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								manager.configurationsCache.Delete(vwc.Name)
 | 
				
			||||||
 | 
								manager.lazy.Notify()
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
	manager.hasSynced = handle.HasSynced
 | 
						manager.hasSynced = handle.HasSynced
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -66,7 +97,7 @@ func (v *validatingWebhookConfigurationManager) Webhooks() []webhook.WebhookAcce
 | 
				
			|||||||
	return out
 | 
						return out
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// HasSynced returns true if the initial set of mutating webhook configurations
 | 
					// HasSynced returns true if the initial set of validating webhook configurations
 | 
				
			||||||
// has been loaded.
 | 
					// has been loaded.
 | 
				
			||||||
func (v *validatingWebhookConfigurationManager) HasSynced() bool { return v.hasSynced() }
 | 
					func (v *validatingWebhookConfigurationManager) HasSynced() bool { return v.hasSynced() }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -75,23 +106,45 @@ func (v *validatingWebhookConfigurationManager) getConfiguration() ([]webhook.We
 | 
				
			|||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return []webhook.WebhookAccessor{}, err
 | 
							return []webhook.WebhookAccessor{}, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return mergeValidatingWebhookConfigurations(configurations), nil
 | 
						return v.getValidatingWebhookConfigurations(configurations), nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func mergeValidatingWebhookConfigurations(configurations []*v1.ValidatingWebhookConfiguration) []webhook.WebhookAccessor {
 | 
					// getMutatingWebhookConfigurations returns the webhook accessors for a given list of
 | 
				
			||||||
 | 
					// mutating webhook configurations.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// This function will, first, try to load the webhook accessors from the cache and avoid
 | 
				
			||||||
 | 
					// recreating them, which can be expessive (requiring CEL expression recompilation).
 | 
				
			||||||
 | 
					func (v *validatingWebhookConfigurationManager) getValidatingWebhookConfigurations(configurations []*v1.ValidatingWebhookConfiguration) []webhook.WebhookAccessor {
 | 
				
			||||||
	sort.SliceStable(configurations, ValidatingWebhookConfigurationSorter(configurations).ByName)
 | 
						sort.SliceStable(configurations, ValidatingWebhookConfigurationSorter(configurations).ByName)
 | 
				
			||||||
	accessors := []webhook.WebhookAccessor{}
 | 
						size := 0
 | 
				
			||||||
 | 
						for _, cfg := range configurations {
 | 
				
			||||||
 | 
							size += len(cfg.Webhooks)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						accessors := make([]webhook.WebhookAccessor, 0, size)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for _, c := range configurations {
 | 
						for _, c := range configurations {
 | 
				
			||||||
 | 
							cachedConfigurationAccessors, ok := v.configurationsCache.Load(c.Name)
 | 
				
			||||||
 | 
							if ok {
 | 
				
			||||||
 | 
								// Pick an already cached webhookAccessor
 | 
				
			||||||
 | 
								accessors = append(accessors, cachedConfigurationAccessors.([]webhook.WebhookAccessor)...)
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// webhook names are not validated for uniqueness, so we check for duplicates and
 | 
							// webhook names are not validated for uniqueness, so we check for duplicates and
 | 
				
			||||||
		// add a int suffix to distinguish between them
 | 
							// add a int suffix to distinguish between them
 | 
				
			||||||
		names := map[string]int{}
 | 
							names := map[string]int{}
 | 
				
			||||||
 | 
							configurationAccessors := make([]webhook.WebhookAccessor, 0, len(c.Webhooks))
 | 
				
			||||||
		for i := range c.Webhooks {
 | 
							for i := range c.Webhooks {
 | 
				
			||||||
			n := c.Webhooks[i].Name
 | 
								n := c.Webhooks[i].Name
 | 
				
			||||||
			uid := fmt.Sprintf("%s/%s/%d", c.Name, n, names[n])
 | 
								uid := fmt.Sprintf("%s/%s/%d", c.Name, n, names[n])
 | 
				
			||||||
			names[n]++
 | 
								names[n]++
 | 
				
			||||||
			accessors = append(accessors, webhook.NewValidatingWebhookAccessor(uid, c.Name, &c.Webhooks[i]))
 | 
								configurationAccessor := v.createValidatingWebhookAccessor(uid, c.Name, &c.Webhooks[i])
 | 
				
			||||||
 | 
								configurationAccessors = append(configurationAccessors, configurationAccessor)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
							accessors = append(accessors, configurationAccessors...)
 | 
				
			||||||
 | 
							v.configurationsCache.Store(c.Name, configurationAccessors)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return accessors
 | 
						return accessors
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -24,6 +24,7 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	"k8s.io/api/admissionregistration/v1"
 | 
						"k8s.io/api/admissionregistration/v1"
 | 
				
			||||||
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 | 
						metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 | 
				
			||||||
 | 
						"k8s.io/apiserver/pkg/admission/plugin/webhook"
 | 
				
			||||||
	"k8s.io/client-go/informers"
 | 
						"k8s.io/client-go/informers"
 | 
				
			||||||
	"k8s.io/client-go/kubernetes/fake"
 | 
						"k8s.io/client-go/kubernetes/fake"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
@@ -81,3 +82,202 @@ func TestGetValidatingWebhookConfig(t *testing.T) {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// mockCreateValidatingWebhookAccessor is a struct used to compute how many times
 | 
				
			||||||
 | 
					// the function webhook.NewValidatingWebhookAccessor is being called when refreshing
 | 
				
			||||||
 | 
					// webhookAccessors.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// NOTE: Maybe there some testing help that we can import and reuse instead.
 | 
				
			||||||
 | 
					type mockCreateValidatingWebhookAccessor struct {
 | 
				
			||||||
 | 
						numberOfCalls int
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (mock *mockCreateValidatingWebhookAccessor) calledNTimes() int { return mock.numberOfCalls }
 | 
				
			||||||
 | 
					func (mock *mockCreateValidatingWebhookAccessor) resetCounter()     { mock.numberOfCalls = 0 }
 | 
				
			||||||
 | 
					func (mock *mockCreateValidatingWebhookAccessor) incrementCounter() { mock.numberOfCalls++ }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (mock *mockCreateValidatingWebhookAccessor) fn(uid string, configurationName string, h *v1.ValidatingWebhook) webhook.WebhookAccessor {
 | 
				
			||||||
 | 
						mock.incrementCounter()
 | 
				
			||||||
 | 
						return webhook.NewValidatingWebhookAccessor(uid, configurationName, h)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func configurationTotalWebhooks(configurations []*v1.ValidatingWebhookConfiguration) int {
 | 
				
			||||||
 | 
						total := 0
 | 
				
			||||||
 | 
						for _, configuration := range configurations {
 | 
				
			||||||
 | 
							total += len(configuration.Webhooks)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return total
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestGetValidatingWebhookConfigSmartReload(t *testing.T) {
 | 
				
			||||||
 | 
						type args struct {
 | 
				
			||||||
 | 
							createWebhookConfigurations []*v1.ValidatingWebhookConfiguration
 | 
				
			||||||
 | 
							updateWebhookConfigurations []*v1.ValidatingWebhookConfiguration
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						tests := []struct {
 | 
				
			||||||
 | 
							name              string
 | 
				
			||||||
 | 
							args              args
 | 
				
			||||||
 | 
							numberOfCreations int
 | 
				
			||||||
 | 
							// number of refreshes are number of times we recrated a webhook accessor
 | 
				
			||||||
 | 
							// instead of pulling from the cache.
 | 
				
			||||||
 | 
							numberOfRefreshes             int
 | 
				
			||||||
 | 
							finalNumberOfWebhookAccessors int
 | 
				
			||||||
 | 
						}{
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "no creations and no updates",
 | 
				
			||||||
 | 
								args: args{
 | 
				
			||||||
 | 
									nil,
 | 
				
			||||||
 | 
									nil,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								numberOfCreations:             0,
 | 
				
			||||||
 | 
								numberOfRefreshes:             0,
 | 
				
			||||||
 | 
								finalNumberOfWebhookAccessors: 0,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "create configurations and no updates",
 | 
				
			||||||
 | 
								args: args{
 | 
				
			||||||
 | 
									[]*v1.ValidatingWebhookConfiguration{
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											ObjectMeta: metav1.ObjectMeta{Name: "webhook1"},
 | 
				
			||||||
 | 
											Webhooks:   []v1.ValidatingWebhook{{Name: "webhook1.1"}},
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											ObjectMeta: metav1.ObjectMeta{Name: "webhook2"},
 | 
				
			||||||
 | 
											Webhooks:   []v1.ValidatingWebhook{{Name: "webhook2.1"}},
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									nil,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								numberOfCreations:             2,
 | 
				
			||||||
 | 
								numberOfRefreshes:             0,
 | 
				
			||||||
 | 
								finalNumberOfWebhookAccessors: 2,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "create configurations and update some of them",
 | 
				
			||||||
 | 
								args: args{
 | 
				
			||||||
 | 
									[]*v1.ValidatingWebhookConfiguration{
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											ObjectMeta: metav1.ObjectMeta{Name: "webhook3"},
 | 
				
			||||||
 | 
											Webhooks:   []v1.ValidatingWebhook{{Name: "webhook3.1"}},
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											ObjectMeta: metav1.ObjectMeta{Name: "webhook4"},
 | 
				
			||||||
 | 
											Webhooks:   []v1.ValidatingWebhook{{Name: "webhook4.1"}},
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									[]*v1.ValidatingWebhookConfiguration{
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											ObjectMeta: metav1.ObjectMeta{Name: "webhook3"},
 | 
				
			||||||
 | 
											Webhooks:   []v1.ValidatingWebhook{{Name: "webhook3.1-updated"}},
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								numberOfCreations:             2,
 | 
				
			||||||
 | 
								numberOfRefreshes:             1,
 | 
				
			||||||
 | 
								finalNumberOfWebhookAccessors: 2,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "create configuration and update moar of them",
 | 
				
			||||||
 | 
								args: args{
 | 
				
			||||||
 | 
									[]*v1.ValidatingWebhookConfiguration{
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											ObjectMeta: metav1.ObjectMeta{Name: "webhook5"},
 | 
				
			||||||
 | 
											Webhooks:   []v1.ValidatingWebhook{{Name: "webhook5.1"}, {Name: "webhook5.2"}},
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											ObjectMeta: metav1.ObjectMeta{Name: "webhook6"},
 | 
				
			||||||
 | 
											Webhooks:   []v1.ValidatingWebhook{{Name: "webhook6.1"}},
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											ObjectMeta: metav1.ObjectMeta{Name: "webhook7"},
 | 
				
			||||||
 | 
											Webhooks:   []v1.ValidatingWebhook{{Name: "webhook7.1"}, {Name: "webhook7.1"}},
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									[]*v1.ValidatingWebhookConfiguration{
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											ObjectMeta: metav1.ObjectMeta{Name: "webhook5"},
 | 
				
			||||||
 | 
											Webhooks:   []v1.ValidatingWebhook{{Name: "webhook5.1-updated"}},
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											ObjectMeta: metav1.ObjectMeta{Name: "webhook7"},
 | 
				
			||||||
 | 
											Webhooks:   []v1.ValidatingWebhook{{Name: "webhook7.1-updated"}, {Name: "webhook7.2-updated"}, {Name: "webhook7.3"}},
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								numberOfCreations:             5,
 | 
				
			||||||
 | 
								numberOfRefreshes:             4,
 | 
				
			||||||
 | 
								finalNumberOfWebhookAccessors: 5,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, tt := range tests {
 | 
				
			||||||
 | 
							t.Run(tt.name, func(t *testing.T) {
 | 
				
			||||||
 | 
								client := fake.NewSimpleClientset()
 | 
				
			||||||
 | 
								informerFactory := informers.NewSharedInformerFactory(client, 0)
 | 
				
			||||||
 | 
								stop := make(chan struct{})
 | 
				
			||||||
 | 
								defer close(stop)
 | 
				
			||||||
 | 
								manager := NewValidatingWebhookConfigurationManager(informerFactory)
 | 
				
			||||||
 | 
								managerStructPtr := manager.(*validatingWebhookConfigurationManager)
 | 
				
			||||||
 | 
								fakeWebhookAccessorCreator := &mockCreateValidatingWebhookAccessor{}
 | 
				
			||||||
 | 
								managerStructPtr.createValidatingWebhookAccessor = fakeWebhookAccessorCreator.fn
 | 
				
			||||||
 | 
								informerFactory.Start(stop)
 | 
				
			||||||
 | 
								informerFactory.WaitForCacheSync(stop)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Create webhooks
 | 
				
			||||||
 | 
								for _, configurations := range tt.args.createWebhookConfigurations {
 | 
				
			||||||
 | 
									client.
 | 
				
			||||||
 | 
										AdmissionregistrationV1().
 | 
				
			||||||
 | 
										ValidatingWebhookConfigurations().
 | 
				
			||||||
 | 
										Create(context.TODO(), configurations, metav1.CreateOptions{})
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								// TODO use channels to wait for manager.createValidatingWebhookAccessor
 | 
				
			||||||
 | 
								// to be called instead of using time.Sleep
 | 
				
			||||||
 | 
								time.Sleep(1 * time.Second)
 | 
				
			||||||
 | 
								webhooks := manager.Webhooks()
 | 
				
			||||||
 | 
								if configurationTotalWebhooks(tt.args.createWebhookConfigurations) != len(webhooks) {
 | 
				
			||||||
 | 
									t.Errorf("Expected number of webhooks %d received %d",
 | 
				
			||||||
 | 
										configurationTotalWebhooks(tt.args.createWebhookConfigurations),
 | 
				
			||||||
 | 
										len(webhooks),
 | 
				
			||||||
 | 
									)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								// assert creations
 | 
				
			||||||
 | 
								if tt.numberOfCreations != fakeWebhookAccessorCreator.calledNTimes() {
 | 
				
			||||||
 | 
									t.Errorf(
 | 
				
			||||||
 | 
										"Expected number of creations %d received %d",
 | 
				
			||||||
 | 
										tt.numberOfCreations, fakeWebhookAccessorCreator.calledNTimes(),
 | 
				
			||||||
 | 
									)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// reset mock counter
 | 
				
			||||||
 | 
								fakeWebhookAccessorCreator.resetCounter()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Update webhooks
 | 
				
			||||||
 | 
								for _, configurations := range tt.args.updateWebhookConfigurations {
 | 
				
			||||||
 | 
									client.
 | 
				
			||||||
 | 
										AdmissionregistrationV1().
 | 
				
			||||||
 | 
										ValidatingWebhookConfigurations().
 | 
				
			||||||
 | 
										Update(context.TODO(), configurations, metav1.UpdateOptions{})
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								// TODO use channels to wait for manager.createValidatingWebhookAccessor
 | 
				
			||||||
 | 
								// to be called instead of using time.Sleep
 | 
				
			||||||
 | 
								time.Sleep(1 * time.Second)
 | 
				
			||||||
 | 
								webhooks = manager.Webhooks()
 | 
				
			||||||
 | 
								if tt.finalNumberOfWebhookAccessors != len(webhooks) {
 | 
				
			||||||
 | 
									t.Errorf("Expected final number of webhooks %d received %d",
 | 
				
			||||||
 | 
										tt.finalNumberOfWebhookAccessors,
 | 
				
			||||||
 | 
										len(webhooks),
 | 
				
			||||||
 | 
									)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// assert updates
 | 
				
			||||||
 | 
								if tt.numberOfRefreshes != fakeWebhookAccessorCreator.calledNTimes() {
 | 
				
			||||||
 | 
									t.Errorf(
 | 
				
			||||||
 | 
										"Expected number of refreshes %d received %d",
 | 
				
			||||||
 | 
										tt.numberOfRefreshes, fakeWebhookAccessorCreator.calledNTimes(),
 | 
				
			||||||
 | 
									)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								// reset mock counter for the next test cases
 | 
				
			||||||
 | 
								fakeWebhookAccessorCreator.resetCounter()
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -123,7 +123,6 @@ func (m *mutatingWebhookAccessor) GetRESTClient(clientManager *webhookutil.Clien
 | 
				
			|||||||
	return m.client, m.clientErr
 | 
						return m.client, m.clientErr
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// TODO: graduation to beta: resolve the fact that we rebuild ALL items whenever ANY config changes in NewMutatingWebhookConfigurationManager and NewValidatingWebhookConfigurationManager ... now that we're doing CEL compilation, we probably want to avoid that
 | 
					 | 
				
			||||||
func (m *mutatingWebhookAccessor) GetCompiledMatcher(compiler cel.FilterCompiler) matchconditions.Matcher {
 | 
					func (m *mutatingWebhookAccessor) GetCompiledMatcher(compiler cel.FilterCompiler) matchconditions.Matcher {
 | 
				
			||||||
	m.compileMatcher.Do(func() {
 | 
						m.compileMatcher.Do(func() {
 | 
				
			||||||
		expressions := make([]cel.ExpressionAccessor, len(m.MutatingWebhook.MatchConditions))
 | 
							expressions := make([]cel.ExpressionAccessor, len(m.MutatingWebhook.MatchConditions))
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user