Merge pull request #118051 from A-Hilaly/api-server/webhooks/smart-reload

support `WebhookAccessors` smart reload
This commit is contained in:
Kubernetes Prow Robot 2023-07-14 09:15:48 -07:00 committed by GitHub
commit 4e9b487e7e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 530 additions and 26 deletions

View File

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

View File

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

View File

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

View File

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

View File

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