Enable propagration of HasSynced
* Add tracker types and tests * Modify ResourceEventHandler interface's OnAdd member * Add additional ResourceEventHandlerDetailedFuncs struct * Fix SharedInformer to let users track HasSynced for their handlers * Fix in-tree controllers which weren't computing HasSynced correctly * Deprecate the cache.Pop function Kubernetes-commit: 8100efc7b3122ad119ee8fa4bbbedef3b90f2e0d
This commit is contained in:
parent
b973647620
commit
d053de6ca3
|
|
@ -36,11 +36,6 @@ type mutatingWebhookConfigurationManager struct {
|
||||||
configuration *atomic.Value
|
configuration *atomic.Value
|
||||||
lister admissionregistrationlisters.MutatingWebhookConfigurationLister
|
lister admissionregistrationlisters.MutatingWebhookConfigurationLister
|
||||||
hasSynced func() bool
|
hasSynced func() bool
|
||||||
// initialConfigurationSynced tracks if
|
|
||||||
// the existing webhook configs have been synced (honored) by the
|
|
||||||
// manager at startup-- the informer has synced and either has no items
|
|
||||||
// or has finished executing updateConfiguration() once.
|
|
||||||
initialConfigurationSynced *atomic.Bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ generic.Source = &mutatingWebhookConfigurationManager{}
|
var _ generic.Source = &mutatingWebhookConfigurationManager{}
|
||||||
|
|
@ -48,23 +43,25 @@ 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{
|
||||||
configuration: &atomic.Value{},
|
configuration: &atomic.Value{},
|
||||||
lister: informer.Lister(),
|
lister: informer.Lister(),
|
||||||
hasSynced: informer.Informer().HasSynced,
|
|
||||||
initialConfigurationSynced: &atomic.Bool{},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start with an empty list
|
// Start with an empty list
|
||||||
manager.configuration.Store([]webhook.WebhookAccessor{})
|
manager.configuration.Store([]webhook.WebhookAccessor{})
|
||||||
manager.initialConfigurationSynced.Store(false)
|
|
||||||
|
|
||||||
// On any change, rebuild the config
|
// On any change, rebuild the config
|
||||||
informer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
|
// TODO: the initial sync for this is N ^ 2, ideally we should make it N.
|
||||||
|
handler, _ := informer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
|
||||||
AddFunc: func(_ interface{}) { manager.updateConfiguration() },
|
AddFunc: func(_ interface{}) { manager.updateConfiguration() },
|
||||||
UpdateFunc: func(_, _ interface{}) { manager.updateConfiguration() },
|
UpdateFunc: func(_, _ interface{}) { manager.updateConfiguration() },
|
||||||
DeleteFunc: func(_ interface{}) { manager.updateConfiguration() },
|
DeleteFunc: func(_ interface{}) { manager.updateConfiguration() },
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Since our processing is synchronous, this is all we need to do to
|
||||||
|
// see if we have processed everything or not.
|
||||||
|
manager.hasSynced = handler.HasSynced
|
||||||
|
|
||||||
return manager
|
return manager
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -73,28 +70,9 @@ func (m *mutatingWebhookConfigurationManager) Webhooks() []webhook.WebhookAccess
|
||||||
return m.configuration.Load().([]webhook.WebhookAccessor)
|
return m.configuration.Load().([]webhook.WebhookAccessor)
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasSynced returns true when the manager is synced with existing webhookconfig
|
// HasSynced returns true if the initial set of mutating webhook configurations
|
||||||
// objects at startup-- which means the informer is synced and either has no items
|
// has been loaded.
|
||||||
// or updateConfiguration() has completed.
|
func (m *mutatingWebhookConfigurationManager) HasSynced() bool { return m.hasSynced() }
|
||||||
func (m *mutatingWebhookConfigurationManager) HasSynced() bool {
|
|
||||||
if !m.hasSynced() {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if m.initialConfigurationSynced.Load() {
|
|
||||||
// the informer has synced and configuration has been updated
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if configurations, err := m.lister.List(labels.Everything()); err == nil && len(configurations) == 0 {
|
|
||||||
// the empty list we initially stored is valid to use.
|
|
||||||
// Setting initialConfigurationSynced to true, so subsequent checks
|
|
||||||
// would be able to take the fast path on the atomic boolean in a
|
|
||||||
// cluster without any admission webhooks configured.
|
|
||||||
m.initialConfigurationSynced.Store(true)
|
|
||||||
// the informer has synced and we don't have any items
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mutatingWebhookConfigurationManager) updateConfiguration() {
|
func (m *mutatingWebhookConfigurationManager) updateConfiguration() {
|
||||||
configurations, err := m.lister.List(labels.Everything())
|
configurations, err := m.lister.List(labels.Everything())
|
||||||
|
|
@ -103,7 +81,6 @@ func (m *mutatingWebhookConfigurationManager) updateConfiguration() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
m.configuration.Store(mergeMutatingWebhookConfigurations(configurations))
|
m.configuration.Store(mergeMutatingWebhookConfigurations(configurations))
|
||||||
m.initialConfigurationSynced.Store(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func mergeMutatingWebhookConfigurations(configurations []*v1.MutatingWebhookConfiguration) []webhook.WebhookAccessor {
|
func mergeMutatingWebhookConfigurations(configurations []*v1.MutatingWebhookConfiguration) []webhook.WebhookAccessor {
|
||||||
|
|
|
||||||
|
|
@ -36,11 +36,6 @@ type validatingWebhookConfigurationManager struct {
|
||||||
configuration *atomic.Value
|
configuration *atomic.Value
|
||||||
lister admissionregistrationlisters.ValidatingWebhookConfigurationLister
|
lister admissionregistrationlisters.ValidatingWebhookConfigurationLister
|
||||||
hasSynced func() bool
|
hasSynced func() bool
|
||||||
// initialConfigurationSynced tracks if
|
|
||||||
// the existing webhook configs have been synced (honored) by the
|
|
||||||
// manager at startup-- the informer has synced and either has no items
|
|
||||||
// or has finished executing updateConfiguration() once.
|
|
||||||
initialConfigurationSynced *atomic.Bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ generic.Source = &validatingWebhookConfigurationManager{}
|
var _ generic.Source = &validatingWebhookConfigurationManager{}
|
||||||
|
|
@ -48,23 +43,25 @@ 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{
|
||||||
configuration: &atomic.Value{},
|
configuration: &atomic.Value{},
|
||||||
lister: informer.Lister(),
|
lister: informer.Lister(),
|
||||||
hasSynced: informer.Informer().HasSynced,
|
|
||||||
initialConfigurationSynced: &atomic.Bool{},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start with an empty list
|
// Start with an empty list
|
||||||
manager.configuration.Store([]webhook.WebhookAccessor{})
|
manager.configuration.Store([]webhook.WebhookAccessor{})
|
||||||
manager.initialConfigurationSynced.Store(false)
|
|
||||||
|
|
||||||
// On any change, rebuild the config
|
// On any change, rebuild the config
|
||||||
informer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
|
// TODO: the initial sync for this is N ^ 2, ideally we should make it N.
|
||||||
|
handle, _ := informer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
|
||||||
AddFunc: func(_ interface{}) { manager.updateConfiguration() },
|
AddFunc: func(_ interface{}) { manager.updateConfiguration() },
|
||||||
UpdateFunc: func(_, _ interface{}) { manager.updateConfiguration() },
|
UpdateFunc: func(_, _ interface{}) { manager.updateConfiguration() },
|
||||||
DeleteFunc: func(_ interface{}) { manager.updateConfiguration() },
|
DeleteFunc: func(_ interface{}) { manager.updateConfiguration() },
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Since our processing is synchronous, this is all we need to do to
|
||||||
|
// see if we have processed everything or not.
|
||||||
|
manager.hasSynced = handle.HasSynced
|
||||||
|
|
||||||
return manager
|
return manager
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -73,29 +70,9 @@ func (v *validatingWebhookConfigurationManager) Webhooks() []webhook.WebhookAcce
|
||||||
return v.configuration.Load().([]webhook.WebhookAccessor)
|
return v.configuration.Load().([]webhook.WebhookAccessor)
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasSynced returns true when the manager is synced with existing webhookconfig
|
// HasSynced returns true if the initial set of mutating webhook configurations
|
||||||
// objects at startup-- which means the informer is synced and either has no items
|
// has been loaded.
|
||||||
// or updateConfiguration() has completed.
|
func (v *validatingWebhookConfigurationManager) HasSynced() bool { return v.hasSynced() }
|
||||||
func (v *validatingWebhookConfigurationManager) HasSynced() bool {
|
|
||||||
if !v.hasSynced() {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if v.initialConfigurationSynced.Load() {
|
|
||||||
// the informer has synced and configuration has been updated
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if configurations, err := v.lister.List(labels.Everything()); err == nil && len(configurations) == 0 {
|
|
||||||
// the empty list we initially stored is valid to use.
|
|
||||||
// Setting initialConfigurationSynced to true, so subsequent checks
|
|
||||||
// would be able to take the fast path on the atomic boolean in a
|
|
||||||
// cluster without any admission webhooks configured.
|
|
||||||
v.initialConfigurationSynced.Store(true)
|
|
||||||
// the informer has synced and we don't have any items
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *validatingWebhookConfigurationManager) updateConfiguration() {
|
func (v *validatingWebhookConfigurationManager) updateConfiguration() {
|
||||||
configurations, err := v.lister.List(labels.Everything())
|
configurations, err := v.lister.List(labels.Everything())
|
||||||
|
|
@ -104,7 +81,6 @@ func (v *validatingWebhookConfigurationManager) updateConfiguration() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
v.configuration.Store(mergeValidatingWebhookConfigurations(configurations))
|
v.configuration.Store(mergeValidatingWebhookConfigurations(configurations))
|
||||||
v.initialConfigurationSynced.Store(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func mergeValidatingWebhookConfigurations(configurations []*v1.ValidatingWebhookConfiguration) []webhook.WebhookAccessor {
|
func mergeValidatingWebhookConfigurations(configurations []*v1.ValidatingWebhookConfiguration) []webhook.WebhookAccessor {
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ import (
|
||||||
"k8s.io/client-go/dynamic"
|
"k8s.io/client-go/dynamic"
|
||||||
"k8s.io/client-go/informers"
|
"k8s.io/client-go/informers"
|
||||||
"k8s.io/client-go/kubernetes"
|
"k8s.io/client-go/kubernetes"
|
||||||
|
"k8s.io/client-go/tools/cache"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ CELPolicyEvaluator = &celAdmissionController{}
|
var _ CELPolicyEvaluator = &celAdmissionController{}
|
||||||
|
|
@ -165,6 +166,7 @@ func NewAdmissionController(
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *celAdmissionController) Run(stopCh <-chan struct{}) {
|
func (c *celAdmissionController) Run(stopCh <-chan struct{}) {
|
||||||
|
// TODO: Doesn't this comparison need a lock?
|
||||||
if c.runningContext != nil {
|
if c.runningContext != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -302,9 +304,10 @@ func (c *celAdmissionController) Validate(
|
||||||
// If the param informer for this admission policy has not yet
|
// If the param informer for this admission policy has not yet
|
||||||
// had time to perform an initial listing, don't attempt to use
|
// had time to perform an initial listing, don't attempt to use
|
||||||
// it.
|
// it.
|
||||||
//!TOOD(alexzielenski): add a wait for a very short amount of
|
//!TODO(alexzielenski): Add a shorter timeout
|
||||||
// time for the cache to sync
|
// than "forever" to this wait.
|
||||||
if !paramInfo.controller.HasSynced() {
|
|
||||||
|
if !cache.WaitForCacheSync(c.runningContext.Done(), paramInfo.controller.HasSynced) {
|
||||||
addConfigError(fmt.Errorf("paramKind kind `%v` not yet synced to use for admission",
|
addConfigError(fmt.Errorf("paramKind kind `%v` not yet synced to use for admission",
|
||||||
paramKind.String()), definition, binding)
|
paramKind.String()), definition, binding)
|
||||||
continue
|
continue
|
||||||
|
|
|
||||||
|
|
@ -128,7 +128,7 @@ func (c *celAdmissionController) reconcilePolicyDefinition(namespace, name strin
|
||||||
c.dynamicClient,
|
c.dynamicClient,
|
||||||
paramsGVR.Resource,
|
paramsGVR.Resource,
|
||||||
corev1.NamespaceAll,
|
corev1.NamespaceAll,
|
||||||
30*time.Second,
|
30*time.Second, // TODO: do we really need to ever resync these?
|
||||||
cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc},
|
cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc},
|
||||||
nil,
|
nil,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
|
@ -30,6 +31,7 @@ import (
|
||||||
|
|
||||||
"k8s.io/apimachinery/pkg/util/wait"
|
"k8s.io/apimachinery/pkg/util/wait"
|
||||||
"k8s.io/client-go/tools/cache"
|
"k8s.io/client-go/tools/cache"
|
||||||
|
"k8s.io/client-go/tools/cache/synctrack"
|
||||||
"k8s.io/client-go/util/workqueue"
|
"k8s.io/client-go/util/workqueue"
|
||||||
"k8s.io/klog/v2"
|
"k8s.io/klog/v2"
|
||||||
)
|
)
|
||||||
|
|
@ -45,6 +47,11 @@ type controller[T runtime.Object] struct {
|
||||||
reconciler func(namespace, name string, newObj T) error
|
reconciler func(namespace, name string, newObj T) error
|
||||||
|
|
||||||
options ControllerOptions
|
options ControllerOptions
|
||||||
|
|
||||||
|
// must hold a func() bool or nil
|
||||||
|
notificationsDelivered atomic.Value
|
||||||
|
|
||||||
|
hasProcessed synctrack.AsyncTracker[string]
|
||||||
}
|
}
|
||||||
|
|
||||||
type ControllerOptions struct {
|
type ControllerOptions struct {
|
||||||
|
|
@ -69,12 +76,20 @@ func NewController[T runtime.Object](
|
||||||
options.Name = fmt.Sprintf("%T-controller", *new(T))
|
options.Name = fmt.Sprintf("%T-controller", *new(T))
|
||||||
}
|
}
|
||||||
|
|
||||||
return &controller[T]{
|
c := &controller[T]{
|
||||||
options: options,
|
options: options,
|
||||||
informer: informer,
|
informer: informer,
|
||||||
reconciler: reconciler,
|
reconciler: reconciler,
|
||||||
queue: nil,
|
queue: nil,
|
||||||
}
|
}
|
||||||
|
c.hasProcessed.UpstreamHasSynced = func() bool {
|
||||||
|
f := c.notificationsDelivered.Load()
|
||||||
|
if f == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return f.(func() bool)()
|
||||||
|
}
|
||||||
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
// Runs the controller and returns an error explaining why running was stopped.
|
// Runs the controller and returns an error explaining why running was stopped.
|
||||||
|
|
@ -92,20 +107,22 @@ func (c *controller[T]) Run(ctx context.Context) error {
|
||||||
// would never shut down the workqueue
|
// would never shut down the workqueue
|
||||||
defer c.queue.ShutDown()
|
defer c.queue.ShutDown()
|
||||||
|
|
||||||
enqueue := func(obj interface{}) {
|
enqueue := func(obj interface{}, isInInitialList bool) {
|
||||||
var key string
|
var key string
|
||||||
var err error
|
var err error
|
||||||
if key, err = cache.DeletionHandlingMetaNamespaceKeyFunc(obj); err != nil {
|
if key, err = cache.DeletionHandlingMetaNamespaceKeyFunc(obj); err != nil {
|
||||||
utilruntime.HandleError(err)
|
utilruntime.HandleError(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if isInInitialList {
|
||||||
|
c.hasProcessed.Start(key)
|
||||||
|
}
|
||||||
|
|
||||||
c.queue.Add(key)
|
c.queue.Add(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
registration, err := c.informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
|
registration, err := c.informer.AddEventHandler(cache.ResourceEventHandlerDetailedFuncs{
|
||||||
AddFunc: func(obj interface{}) {
|
AddFunc: enqueue,
|
||||||
enqueue(obj)
|
|
||||||
},
|
|
||||||
UpdateFunc: func(oldObj, newObj interface{}) {
|
UpdateFunc: func(oldObj, newObj interface{}) {
|
||||||
oldMeta, err1 := meta.Accessor(oldObj)
|
oldMeta, err1 := meta.Accessor(oldObj)
|
||||||
newMeta, err2 := meta.Accessor(newObj)
|
newMeta, err2 := meta.Accessor(newObj)
|
||||||
|
|
@ -126,13 +143,14 @@ func (c *controller[T]) Run(ctx context.Context) error {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
enqueue(newObj)
|
enqueue(newObj, false)
|
||||||
},
|
},
|
||||||
DeleteFunc: func(obj interface{}) {
|
DeleteFunc: func(obj interface{}) {
|
||||||
// Enqueue
|
// Enqueue
|
||||||
enqueue(obj)
|
enqueue(obj, false)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
c.notificationsDelivered.Store(registration.HasSynced)
|
||||||
|
|
||||||
// Error might be raised if informer was started and stopped already
|
// Error might be raised if informer was started and stopped already
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -142,6 +160,7 @@ func (c *controller[T]) Run(ctx context.Context) error {
|
||||||
// Make sure event handler is removed from informer in case return early from
|
// Make sure event handler is removed from informer in case return early from
|
||||||
// an error
|
// an error
|
||||||
defer func() {
|
defer func() {
|
||||||
|
c.notificationsDelivered.Store(func() bool { return false })
|
||||||
// Remove event handler and Handle Error here. Error should only be raised
|
// Remove event handler and Handle Error here. Error should only be raised
|
||||||
// for improper usage of event handler API.
|
// for improper usage of event handler API.
|
||||||
if err := c.informer.RemoveEventHandler(registration); err != nil {
|
if err := c.informer.RemoveEventHandler(registration); err != nil {
|
||||||
|
|
@ -188,7 +207,7 @@ func (c *controller[T]) Run(ctx context.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *controller[T]) HasSynced() bool {
|
func (c *controller[T]) HasSynced() bool {
|
||||||
return c.informer.HasSynced()
|
return c.hasProcessed.HasSynced()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *controller[T]) runWorker() {
|
func (c *controller[T]) runWorker() {
|
||||||
|
|
@ -220,6 +239,7 @@ func (c *controller[T]) runWorker() {
|
||||||
// but the key is invalid so there is no point in doing that)
|
// but the key is invalid so there is no point in doing that)
|
||||||
return fmt.Errorf("expected string in workqueue but got %#v", obj)
|
return fmt.Errorf("expected string in workqueue but got %#v", obj)
|
||||||
}
|
}
|
||||||
|
defer c.hasProcessed.Finished(key)
|
||||||
|
|
||||||
if err := c.reconcile(key); err != nil {
|
if err := c.reconcile(key); err != nil {
|
||||||
// Put the item back on the workqueue to handle any transient errors.
|
// Put the item back on the workqueue to handle any transient errors.
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,7 @@ func setupTest(ctx context.Context, customReconciler func(string, string, runtim
|
||||||
controller generic.Controller[*unstructured.Unstructured],
|
controller generic.Controller[*unstructured.Unstructured],
|
||||||
informer *testInformer,
|
informer *testInformer,
|
||||||
waitForReconcile func(runtime.Object) error,
|
waitForReconcile func(runtime.Object) error,
|
||||||
|
verifyNoMoreEvents func() bool,
|
||||||
) {
|
) {
|
||||||
tracker = clienttesting.NewObjectTracker(scheme, codecs.UniversalDecoder())
|
tracker = clienttesting.NewObjectTracker(scheme, codecs.UniversalDecoder())
|
||||||
reconciledObjects := make(chan runtime.Object)
|
reconciledObjects := make(chan runtime.Object)
|
||||||
|
|
@ -127,7 +128,11 @@ func setupTest(ctx context.Context, customReconciler func(string, string, runtim
|
||||||
if customReconciler != nil {
|
if customReconciler != nil {
|
||||||
err = customReconciler(namespace, name, newObj)
|
err = customReconciler(namespace, name, newObj)
|
||||||
}
|
}
|
||||||
reconciledObjects <- copied
|
select {
|
||||||
|
case reconciledObjects <- copied:
|
||||||
|
case <-ctx.Done():
|
||||||
|
panic("timed out attempting to deliver reconcile event")
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -149,23 +154,24 @@ func setupTest(ctx context.Context, customReconciler func(string, string, runtim
|
||||||
generic.ControllerOptions{},
|
generic.ControllerOptions{},
|
||||||
)
|
)
|
||||||
|
|
||||||
go func() {
|
verifyNoMoreEvents = func() bool {
|
||||||
<-ctx.Done()
|
close(reconciledObjects) // closing means that a future attempt to send will crash
|
||||||
close(reconciledObjects)
|
|
||||||
|
|
||||||
for leftover := range reconciledObjects {
|
for leftover := range reconciledObjects {
|
||||||
panic(fmt.Errorf("leftover object which was not anticipated by test: %v", leftover))
|
panic(fmt.Errorf("leftover object which was not anticipated by test: %v", leftover))
|
||||||
}
|
}
|
||||||
}()
|
// TODO(alexzielenski): this effectively doesn't test anything since the
|
||||||
|
// controller drops any pending events when it shuts down.
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
return tracker, myController, informer, waitForReconcile
|
return tracker, myController, informer, waitForReconcile, verifyNoMoreEvents
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReconcile(t *testing.T) {
|
func TestReconcile(t *testing.T) {
|
||||||
testContext, testCancel := context.WithTimeout(context.Background(), 2*time.Second)
|
testContext, testCancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
defer testCancel()
|
defer testCancel()
|
||||||
|
|
||||||
tracker, myController, informer, waitForReconcile := setupTest(testContext, nil)
|
tracker, myController, informer, waitForReconcile, verifyNoMoreEvents := setupTest(testContext, nil)
|
||||||
|
|
||||||
// Add object to informer
|
// Add object to informer
|
||||||
initialObject := &unstructured.Unstructured{}
|
initialObject := &unstructured.Unstructured{}
|
||||||
|
|
@ -196,11 +202,16 @@ func TestReconcile(t *testing.T) {
|
||||||
require.ErrorIs(t, stopReason, context.Canceled)
|
require.ErrorIs(t, stopReason, context.Canceled)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
require.True(t, cache.WaitForCacheSync(testContext.Done(), myController.HasSynced))
|
// The controller is blocked because the reconcile function sends on an
|
||||||
|
// unbuffered channel.
|
||||||
|
require.False(t, myController.HasSynced())
|
||||||
|
|
||||||
// Wait for all enqueued reconciliations
|
// Wait for all enqueued reconciliations
|
||||||
require.NoError(t, waitForReconcile(initialObject))
|
require.NoError(t, waitForReconcile(initialObject))
|
||||||
|
|
||||||
|
// Now it is safe to wait for it to Sync
|
||||||
|
require.True(t, cache.WaitForCacheSync(testContext.Done(), myController.HasSynced))
|
||||||
|
|
||||||
// Updated object
|
// Updated object
|
||||||
updatedObject := &unstructured.Unstructured{}
|
updatedObject := &unstructured.Unstructured{}
|
||||||
updatedObject.SetUnstructuredContent(map[string]interface{}{
|
updatedObject.SetUnstructuredContent(map[string]interface{}{
|
||||||
|
|
@ -220,13 +231,15 @@ func TestReconcile(t *testing.T) {
|
||||||
|
|
||||||
testCancel()
|
testCancel()
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
||||||
|
verifyNoMoreEvents()
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShutdown(t *testing.T) {
|
func TestShutdown(t *testing.T) {
|
||||||
testContext, testCancel := context.WithTimeout(context.Background(), 2*time.Second)
|
testContext, testCancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
defer testCancel()
|
defer testCancel()
|
||||||
|
|
||||||
_, myController, informer, _ := setupTest(testContext, nil)
|
_, myController, informer, _, verifyNoMoreEvents := setupTest(testContext, nil)
|
||||||
|
|
||||||
wg := sync.WaitGroup{}
|
wg := sync.WaitGroup{}
|
||||||
|
|
||||||
|
|
@ -256,6 +269,8 @@ func TestShutdown(t *testing.T) {
|
||||||
|
|
||||||
// Ensure the event handler was cleaned up
|
// Ensure the event handler was cleaned up
|
||||||
require.Empty(t, informer.registrations)
|
require.Empty(t, informer.registrations)
|
||||||
|
|
||||||
|
verifyNoMoreEvents()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show an error is thrown informer isn't started when the controller runs
|
// Show an error is thrown informer isn't started when the controller runs
|
||||||
|
|
@ -263,7 +278,7 @@ func TestInformerNeverStarts(t *testing.T) {
|
||||||
testContext, testCancel := context.WithTimeout(context.Background(), 400*time.Millisecond)
|
testContext, testCancel := context.WithTimeout(context.Background(), 400*time.Millisecond)
|
||||||
defer testCancel()
|
defer testCancel()
|
||||||
|
|
||||||
_, myController, informer, _ := setupTest(testContext, nil)
|
_, myController, informer, _, verifyNoMoreEvents := setupTest(testContext, nil)
|
||||||
|
|
||||||
wg := sync.WaitGroup{}
|
wg := sync.WaitGroup{}
|
||||||
|
|
||||||
|
|
@ -283,6 +298,8 @@ func TestInformerNeverStarts(t *testing.T) {
|
||||||
|
|
||||||
// Ensure there are no event handlers
|
// Ensure there are no event handlers
|
||||||
require.Empty(t, informer.registrations)
|
require.Empty(t, informer.registrations)
|
||||||
|
|
||||||
|
verifyNoMoreEvents()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shows that if RV does not change, the reconciler does not get called
|
// Shows that if RV does not change, the reconciler does not get called
|
||||||
|
|
@ -290,7 +307,7 @@ func TestIgnoredUpdate(t *testing.T) {
|
||||||
testContext, testCancel := context.WithTimeout(context.Background(), 2*time.Second)
|
testContext, testCancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
defer testCancel()
|
defer testCancel()
|
||||||
|
|
||||||
tracker, myController, informer, waitForReconcile := setupTest(testContext, nil)
|
tracker, myController, informer, waitForReconcile, verifyNoMoreEvents := setupTest(testContext, nil)
|
||||||
|
|
||||||
// Add object to informer
|
// Add object to informer
|
||||||
initialObject := &unstructured.Unstructured{}
|
initialObject := &unstructured.Unstructured{}
|
||||||
|
|
@ -321,11 +338,16 @@ func TestIgnoredUpdate(t *testing.T) {
|
||||||
require.ErrorIs(t, stopReason, context.Canceled)
|
require.ErrorIs(t, stopReason, context.Canceled)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
require.True(t, cache.WaitForCacheSync(testContext.Done(), myController.HasSynced))
|
// The controller is blocked because the reconcile function sends on an
|
||||||
|
// unbuffered channel.
|
||||||
|
require.False(t, myController.HasSynced())
|
||||||
|
|
||||||
// Wait for all enqueued reconciliations
|
// Wait for all enqueued reconciliations
|
||||||
require.NoError(t, waitForReconcile(initialObject))
|
require.NoError(t, waitForReconcile(initialObject))
|
||||||
|
|
||||||
|
// Now it is safe to wait for it to Sync
|
||||||
|
require.True(t, cache.WaitForCacheSync(testContext.Done(), myController.HasSynced))
|
||||||
|
|
||||||
// Send update with the same object
|
// Send update with the same object
|
||||||
require.NoError(t, tracker.Update(fakeGVR, initialObject, ""))
|
require.NoError(t, tracker.Update(fakeGVR, initialObject, ""))
|
||||||
|
|
||||||
|
|
@ -334,8 +356,9 @@ func TestIgnoredUpdate(t *testing.T) {
|
||||||
testCancel()
|
testCancel()
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
||||||
// Test infrastructure has logic to panic if there are any reconciled objects
|
// TODO(alexzielenski): Find a better way to test this since the
|
||||||
// that weren't "expected"
|
// controller drops any pending events when it shuts down.
|
||||||
|
verifyNoMoreEvents()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shows that an object which fails reconciliation will retry
|
// Shows that an object which fails reconciliation will retry
|
||||||
|
|
@ -345,7 +368,7 @@ func TestReconcileRetry(t *testing.T) {
|
||||||
|
|
||||||
calls := atomic.Uint64{}
|
calls := atomic.Uint64{}
|
||||||
success := atomic.Bool{}
|
success := atomic.Bool{}
|
||||||
tracker, myController, _, waitForReconcile := setupTest(testContext, func(s1, s2 string, o runtime.Object) error {
|
tracker, myController, _, waitForReconcile, verifyNoMoreEvents := setupTest(testContext, func(s1, s2 string, o runtime.Object) error {
|
||||||
|
|
||||||
if calls.Add(1) > 2 {
|
if calls.Add(1) > 2 {
|
||||||
// Suddenly start liking the object
|
// Suddenly start liking the object
|
||||||
|
|
@ -390,13 +413,14 @@ func TestReconcileRetry(t *testing.T) {
|
||||||
require.True(t, success.Load(), "last call to reconcile should return success")
|
require.True(t, success.Load(), "last call to reconcile should return success")
|
||||||
testCancel()
|
testCancel()
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
||||||
|
verifyNoMoreEvents()
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestInformerList(t *testing.T) {
|
func TestInformerList(t *testing.T) {
|
||||||
testContext, testCancel := context.WithTimeout(context.Background(), 2*time.Second)
|
testContext, testCancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
defer testCancel()
|
|
||||||
|
|
||||||
tracker, myController, _, _ := setupTest(testContext, nil)
|
tracker, myController, _, _, _ := setupTest(testContext, nil)
|
||||||
|
|
||||||
wg := sync.WaitGroup{}
|
wg := sync.WaitGroup{}
|
||||||
|
|
||||||
|
|
@ -406,7 +430,12 @@ func TestInformerList(t *testing.T) {
|
||||||
myController.Informer().Run(testContext.Done())
|
myController.Informer().Run(testContext.Done())
|
||||||
}()
|
}()
|
||||||
|
|
||||||
require.True(t, cache.WaitForCacheSync(testContext.Done(), myController.HasSynced))
|
defer func() {
|
||||||
|
testCancel()
|
||||||
|
wg.Wait()
|
||||||
|
}()
|
||||||
|
|
||||||
|
require.True(t, cache.WaitForCacheSync(testContext.Done(), myController.Informer().HasSynced))
|
||||||
|
|
||||||
object1 := &unstructured.Unstructured{}
|
object1 := &unstructured.Unstructured{}
|
||||||
object1.SetUnstructuredContent(map[string]interface{}{
|
object1.SetUnstructuredContent(map[string]interface{}{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue