From 20c4f7b1b15fcfc1fceec694ebd5bbbc71359d43 Mon Sep 17 00:00:00 2001 From: Danil Grigorev Date: Thu, 29 Feb 2024 08:39:11 +0100 Subject: [PATCH 1/7] Make event handler type aware Signed-off-by: Danil Grigorev --- pkg/builder/controller.go | 11 +- pkg/internal/source/event_handler.go | 24 ++-- pkg/internal/source/internal_test.go | 190 ++++++++++++++++++++++++--- pkg/internal/source/kind.go | 17 +-- pkg/source/source.go | 9 +- pkg/source/source_test.go | 15 +++ 6 files changed, 222 insertions(+), 44 deletions(-) diff --git a/pkg/builder/controller.go b/pkg/builder/controller.go index 1a115f2f7b..660a976fdc 100644 --- a/pkg/builder/controller.go +++ b/pkg/builder/controller.go @@ -135,7 +135,7 @@ type WatchesInput struct { // This is the equivalent of calling // WatchesRawSource(source.Kind(cache, object), eventHandler, opts...). func (blder *Builder) Watches(object client.Object, eventHandler handler.EventHandler, opts ...WatchesOption) *Builder { - src := source.Kind(blder.mgr.GetCache(), object) + src := source.ObjectKind(blder.mgr.GetCache(), object) return blder.WatchesRawSource(src, eventHandler, opts...) } @@ -176,6 +176,9 @@ func (blder *Builder) WatchesMetadata(object client.Object, eventHandler handler // // STOP! Consider using For(...), Owns(...), Watches(...), WatchesMetadata(...) instead. // This method is only exposed for more advanced use cases, most users should use one of the higher level functions. +// +// Example: +// WatchesRawSource(source.Kind(cache, &corev1.Pod{}), eventHandler, opts...) // ensure that source propagates only valid Pod objects. func (blder *Builder) WatchesRawSource(src source.Source, eventHandler handler.EventHandler, opts ...WatchesOption) *Builder { input := WatchesInput{src: src, eventHandler: eventHandler} for _, opt := range opts { @@ -272,7 +275,7 @@ func (blder *Builder) doWatch() error { if err != nil { return err } - src := source.Kind(blder.mgr.GetCache(), obj) + src := source.ObjectKind(blder.mgr.GetCache(), obj) hdler := &handler.EnqueueRequestForObject{} allPredicates := append([]predicate.Predicate(nil), blder.globalPredicates...) allPredicates = append(allPredicates, blder.forInput.predicates...) @@ -290,7 +293,7 @@ func (blder *Builder) doWatch() error { if err != nil { return err } - src := source.Kind(blder.mgr.GetCache(), obj) + src := source.ObjectKind(blder.mgr.GetCache(), obj) opts := []handler.OwnerOption{} if !own.matchEveryOwner { opts = append(opts, handler.OnlyControllerOwner()) @@ -313,7 +316,7 @@ func (blder *Builder) doWatch() error { } for _, w := range blder.watchesInput { // If the source of this watch is of type Kind, project it. - if srcKind, ok := w.src.(*internalsource.Kind); ok { + if srcKind, ok := w.src.(*internalsource.Kind[client.Object]); ok { typeForSrc, err := blder.project(srcKind.Type, w.objectProjection) if err != nil { return err diff --git a/pkg/internal/source/event_handler.go b/pkg/internal/source/event_handler.go index ae8404a1fa..5e309db391 100644 --- a/pkg/internal/source/event_handler.go +++ b/pkg/internal/source/event_handler.go @@ -33,8 +33,8 @@ import ( var log = logf.RuntimeLog.WithName("source").WithName("EventHandler") // NewEventHandler creates a new EventHandler. -func NewEventHandler(ctx context.Context, queue workqueue.RateLimitingInterface, handler handler.EventHandler, predicates []predicate.Predicate) *EventHandler { - return &EventHandler{ +func NewEventHandler[T client.Object](ctx context.Context, queue workqueue.RateLimitingInterface, handler handler.EventHandler, predicates []predicate.Predicate) *EventHandler[T] { + return &EventHandler[T]{ ctx: ctx, handler: handler, queue: queue, @@ -43,7 +43,7 @@ func NewEventHandler(ctx context.Context, queue workqueue.RateLimitingInterface, } // EventHandler adapts a handler.EventHandler interface to a cache.ResourceEventHandler interface. -type EventHandler struct { +type EventHandler[T client.Object] struct { // ctx stores the context that created the event handler // that is used to propagate cancellation signals to each handler function. ctx context.Context @@ -55,7 +55,7 @@ type EventHandler struct { // HandlerFuncs converts EventHandler to a ResourceEventHandlerFuncs // TODO: switch to ResourceEventHandlerDetailedFuncs with client-go 1.27 -func (e *EventHandler) HandlerFuncs() cache.ResourceEventHandlerFuncs { +func (e *EventHandler[T]) HandlerFuncs() cache.ResourceEventHandlerFuncs { return cache.ResourceEventHandlerFuncs{ AddFunc: e.OnAdd, UpdateFunc: e.OnUpdate, @@ -64,11 +64,11 @@ func (e *EventHandler) HandlerFuncs() cache.ResourceEventHandlerFuncs { } // OnAdd creates CreateEvent and calls Create on EventHandler. -func (e *EventHandler) OnAdd(obj interface{}) { +func (e *EventHandler[T]) OnAdd(obj interface{}) { c := event.CreateEvent{} // Pull Object out of the object - if o, ok := obj.(client.Object); ok { + if o, ok := obj.(T); ok { c.Object = o } else { log.Error(nil, "OnAdd missing Object", @@ -89,10 +89,10 @@ func (e *EventHandler) OnAdd(obj interface{}) { } // OnUpdate creates UpdateEvent and calls Update on EventHandler. -func (e *EventHandler) OnUpdate(oldObj, newObj interface{}) { +func (e *EventHandler[T]) OnUpdate(oldObj, newObj interface{}) { u := event.UpdateEvent{} - if o, ok := oldObj.(client.Object); ok { + if o, ok := oldObj.(T); ok { u.ObjectOld = o } else { log.Error(nil, "OnUpdate missing ObjectOld", @@ -101,7 +101,7 @@ func (e *EventHandler) OnUpdate(oldObj, newObj interface{}) { } // Pull Object out of the object - if o, ok := newObj.(client.Object); ok { + if o, ok := newObj.(T); ok { u.ObjectNew = o } else { log.Error(nil, "OnUpdate missing ObjectNew", @@ -122,7 +122,7 @@ func (e *EventHandler) OnUpdate(oldObj, newObj interface{}) { } // OnDelete creates DeleteEvent and calls Delete on EventHandler. -func (e *EventHandler) OnDelete(obj interface{}) { +func (e *EventHandler[T]) OnDelete(obj interface{}) { d := event.DeleteEvent{} // Deal with tombstone events by pulling the object out. Tombstone events wrap the object in a @@ -131,7 +131,7 @@ func (e *EventHandler) OnDelete(obj interface{}) { // This should never happen if we aren't missing events, which we have concluded that we are not // and made decisions off of this belief. Maybe this shouldn't be here? var ok bool - if _, ok = obj.(client.Object); !ok { + if _, ok = obj.(T); !ok { // If the object doesn't have Metadata, assume it is a tombstone object of type DeletedFinalStateUnknown tombstone, ok := obj.(cache.DeletedFinalStateUnknown) if !ok { @@ -149,7 +149,7 @@ func (e *EventHandler) OnDelete(obj interface{}) { } // Pull Object out of the object - if o, ok := obj.(client.Object); ok { + if o, ok := obj.(T); ok { d.Object = o } else { log.Error(nil, "OnDelete missing Object", diff --git a/pkg/internal/source/internal_test.go b/pkg/internal/source/internal_test.go index 0574f7180e..8e425cfd6f 100644 --- a/pkg/internal/source/internal_test.go +++ b/pkg/internal/source/internal_test.go @@ -37,7 +37,8 @@ import ( var _ = Describe("Internal", func() { var ctx = context.Background() - var instance *internal.EventHandler + var instance *internal.EventHandler[*corev1.Pod] + var partialMetadataInstance *internal.EventHandler[*metav1.PartialObjectMetadata] var funcs, setfuncs *handler.Funcs var set bool BeforeEach(func() { @@ -74,11 +75,13 @@ var _ = Describe("Internal", func() { set = true }, } - instance = internal.NewEventHandler(ctx, &controllertest.Queue{}, funcs, nil) + instance = internal.NewEventHandler[*corev1.Pod](ctx, &controllertest.Queue{}, funcs, nil) + partialMetadataInstance = internal.NewEventHandler[*metav1.PartialObjectMetadata](ctx, &controllertest.Queue{}, funcs, nil) }) Describe("EventHandler", func() { var pod, newPod *corev1.Pod + var pom, newPom *metav1.PartialObjectMetadata BeforeEach(func() { pod = &corev1.Pod{ @@ -88,6 +91,9 @@ var _ = Describe("Internal", func() { } newPod = pod.DeepCopy() newPod.Labels = map[string]string{"foo": "bar"} + pom = &metav1.PartialObjectMetadata{} + newPom = pom.DeepCopy() + newPom.Labels = map[string]string{"foo": "bar"} }) It("should create a CreateEvent", func() { @@ -99,7 +105,7 @@ var _ = Describe("Internal", func() { }) It("should used Predicates to filter CreateEvents", func() { - instance = internal.NewEventHandler(ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ + instance = internal.NewEventHandler[*corev1.Pod](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ predicate.Funcs{CreateFunc: func(event.CreateEvent) bool { return false }}, }) set = false @@ -107,14 +113,14 @@ var _ = Describe("Internal", func() { Expect(set).To(BeFalse()) set = false - instance = internal.NewEventHandler(ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ + instance = internal.NewEventHandler[*corev1.Pod](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ predicate.Funcs{CreateFunc: func(event.CreateEvent) bool { return true }}, }) instance.OnAdd(pod) Expect(set).To(BeTrue()) set = false - instance = internal.NewEventHandler(ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ + instance = internal.NewEventHandler[*corev1.Pod](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ predicate.Funcs{CreateFunc: func(event.CreateEvent) bool { return true }}, predicate.Funcs{CreateFunc: func(event.CreateEvent) bool { return false }}, }) @@ -122,7 +128,7 @@ var _ = Describe("Internal", func() { Expect(set).To(BeFalse()) set = false - instance = internal.NewEventHandler(ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ + instance = internal.NewEventHandler[*corev1.Pod](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ predicate.Funcs{CreateFunc: func(event.CreateEvent) bool { return false }}, predicate.Funcs{CreateFunc: func(event.CreateEvent) bool { return true }}, }) @@ -130,7 +136,7 @@ var _ = Describe("Internal", func() { Expect(set).To(BeFalse()) set = false - instance = internal.NewEventHandler(ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ + instance = internal.NewEventHandler[*corev1.Pod](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ predicate.Funcs{CreateFunc: func(event.CreateEvent) bool { return true }}, predicate.Funcs{CreateFunc: func(event.CreateEvent) bool { return true }}, }) @@ -138,14 +144,62 @@ var _ = Describe("Internal", func() { Expect(set).To(BeTrue()) }) + It("should use Predicates to filter CreateEvents on PartialObjectMetadata", func() { + partialMetadataInstance = internal.NewEventHandler[*metav1.PartialObjectMetadata](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ + predicate.Funcs{CreateFunc: func(event.CreateEvent) bool { return false }}, + }) + set = false + partialMetadataInstance.OnAdd(pom) + Expect(set).To(BeFalse()) + + set = false + partialMetadataInstance = internal.NewEventHandler[*metav1.PartialObjectMetadata](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ + predicate.Funcs{CreateFunc: func(event.CreateEvent) bool { return true }}, + }) + partialMetadataInstance.OnAdd(pom) + Expect(set).To(BeTrue()) + + set = false + partialMetadataInstance = internal.NewEventHandler[*metav1.PartialObjectMetadata](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ + predicate.Funcs{CreateFunc: func(event.CreateEvent) bool { return true }}, + predicate.Funcs{CreateFunc: func(event.CreateEvent) bool { return false }}, + }) + partialMetadataInstance.OnAdd(pom) + Expect(set).To(BeFalse()) + + set = false + partialMetadataInstance = internal.NewEventHandler[*metav1.PartialObjectMetadata](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ + predicate.Funcs{CreateFunc: func(event.CreateEvent) bool { return false }}, + predicate.Funcs{CreateFunc: func(event.CreateEvent) bool { return true }}, + }) + partialMetadataInstance.OnAdd(pom) + Expect(set).To(BeFalse()) + + set = false + partialMetadataInstance = internal.NewEventHandler[*metav1.PartialObjectMetadata](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ + predicate.Funcs{CreateFunc: func(event.CreateEvent) bool { return true }}, + predicate.Funcs{CreateFunc: func(event.CreateEvent) bool { return true }}, + }) + partialMetadataInstance.OnAdd(pom) + Expect(set).To(BeTrue()) + }) + It("should not call Create EventHandler if the object is not a runtime.Object", func() { instance.OnAdd(&metav1.ObjectMeta{}) }) + It("should not call Create EventHandler if an object is not 'that' object", func() { + instance.OnAdd(&corev1.Secret{}) + }) + It("should not call Create EventHandler if the object does not have metadata", func() { instance.OnAdd(FooRuntimeObject{}) }) + It("should not call Create EventHandler if an object is not a partial object metadata object", func() { + partialMetadataInstance.OnAdd(&corev1.Secret{}) + }) + It("should create an UpdateEvent", func() { funcs.UpdateFunc = func(ctx context.Context, evt event.UpdateEvent, q workqueue.RateLimitingInterface) { defer GinkgoRecover() @@ -157,21 +211,21 @@ var _ = Describe("Internal", func() { It("should used Predicates to filter UpdateEvents", func() { set = false - instance = internal.NewEventHandler(ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ + instance = internal.NewEventHandler[*corev1.Pod](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ predicate.Funcs{UpdateFunc: func(updateEvent event.UpdateEvent) bool { return false }}, }) instance.OnUpdate(pod, newPod) Expect(set).To(BeFalse()) set = false - instance = internal.NewEventHandler(ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ + instance = internal.NewEventHandler[*corev1.Pod](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ predicate.Funcs{UpdateFunc: func(event.UpdateEvent) bool { return true }}, }) instance.OnUpdate(pod, newPod) Expect(set).To(BeTrue()) set = false - instance = internal.NewEventHandler(ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ + instance = internal.NewEventHandler[*corev1.Pod](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ predicate.Funcs{UpdateFunc: func(event.UpdateEvent) bool { return true }}, predicate.Funcs{UpdateFunc: func(event.UpdateEvent) bool { return false }}, }) @@ -179,7 +233,7 @@ var _ = Describe("Internal", func() { Expect(set).To(BeFalse()) set = false - instance = internal.NewEventHandler(ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ + instance = internal.NewEventHandler[*corev1.Pod](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ predicate.Funcs{UpdateFunc: func(event.UpdateEvent) bool { return false }}, predicate.Funcs{UpdateFunc: func(event.UpdateEvent) bool { return true }}, }) @@ -187,7 +241,7 @@ var _ = Describe("Internal", func() { Expect(set).To(BeFalse()) set = false - instance = internal.NewEventHandler(ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ + instance = internal.NewEventHandler[*corev1.Pod](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ predicate.Funcs{CreateFunc: func(event.CreateEvent) bool { return true }}, predicate.Funcs{CreateFunc: func(event.CreateEvent) bool { return true }}, }) @@ -195,14 +249,66 @@ var _ = Describe("Internal", func() { Expect(set).To(BeTrue()) }) + It("should use Predicates to filter UpdateEvents on PartialObjectMetadata", func() { + set = false + partialMetadataInstance = internal.NewEventHandler[*metav1.PartialObjectMetadata](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ + predicate.Funcs{UpdateFunc: func(updateEvent event.UpdateEvent) bool { return false }}, + }) + partialMetadataInstance.OnUpdate(pom, newPom) + Expect(set).To(BeFalse()) + + set = false + partialMetadataInstance = internal.NewEventHandler[*metav1.PartialObjectMetadata](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ + predicate.Funcs{UpdateFunc: func(event.UpdateEvent) bool { return true }}, + }) + partialMetadataInstance.OnUpdate(pom, newPom) + Expect(set).To(BeTrue()) + + set = false + partialMetadataInstance = internal.NewEventHandler[*metav1.PartialObjectMetadata](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ + predicate.Funcs{UpdateFunc: func(event.UpdateEvent) bool { return true }}, + predicate.Funcs{UpdateFunc: func(event.UpdateEvent) bool { return false }}, + }) + partialMetadataInstance.OnUpdate(pom, newPom) + Expect(set).To(BeFalse()) + + set = false + partialMetadataInstance = internal.NewEventHandler[*metav1.PartialObjectMetadata](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ + predicate.Funcs{UpdateFunc: func(event.UpdateEvent) bool { return false }}, + predicate.Funcs{UpdateFunc: func(event.UpdateEvent) bool { return true }}, + }) + partialMetadataInstance.OnUpdate(pom, newPom) + Expect(set).To(BeFalse()) + + set = false + partialMetadataInstance = internal.NewEventHandler[*metav1.PartialObjectMetadata](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ + predicate.Funcs{CreateFunc: func(event.CreateEvent) bool { return true }}, + predicate.Funcs{CreateFunc: func(event.CreateEvent) bool { return true }}, + }) + partialMetadataInstance.OnUpdate(pom, newPom) + Expect(set).To(BeTrue()) + }) + It("should not call Update EventHandler if the object is not a runtime.Object", func() { instance.OnUpdate(&metav1.ObjectMeta{}, &corev1.Pod{}) instance.OnUpdate(&corev1.Pod{}, &metav1.ObjectMeta{}) }) + It("should not call Update EventHandler if an object is not 'that' object", func() { + instance.OnUpdate(&corev1.Secret{}, &corev1.Pod{}) + instance.OnUpdate(&corev1.Pod{}, &corev1.ConfigMap{}) + }) + + It("should not call Update EventHandler if an object is not a partial object metadata object", func() { + partialMetadataInstance.OnUpdate(&corev1.Secret{}, &corev1.Pod{}) + partialMetadataInstance.OnUpdate(&metav1.PartialObjectMetadata{}, &corev1.ConfigMap{}) + partialMetadataInstance.OnUpdate(&corev1.ConfigMap{}, &metav1.PartialObjectMetadata{}) + }) + It("should not call Update EventHandler if the object does not have metadata", func() { - instance.OnUpdate(FooRuntimeObject{}, &corev1.Pod{}) instance.OnUpdate(&corev1.Pod{}, FooRuntimeObject{}) + instance.OnUpdate(FooRuntimeObject{}, &corev1.Pod{}) + instance.OnUpdate(FooRuntimeObject{}, FooRuntimeObject{}) }) It("should create a DeleteEvent", func() { @@ -215,21 +321,21 @@ var _ = Describe("Internal", func() { It("should used Predicates to filter DeleteEvents", func() { set = false - instance = internal.NewEventHandler(ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ + instance = internal.NewEventHandler[*corev1.Pod](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ predicate.Funcs{DeleteFunc: func(event.DeleteEvent) bool { return false }}, }) instance.OnDelete(pod) Expect(set).To(BeFalse()) set = false - instance = internal.NewEventHandler(ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ + instance = internal.NewEventHandler[*corev1.Pod](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ predicate.Funcs{DeleteFunc: func(event.DeleteEvent) bool { return true }}, }) instance.OnDelete(pod) Expect(set).To(BeTrue()) set = false - instance = internal.NewEventHandler(ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ + instance = internal.NewEventHandler[*corev1.Pod](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ predicate.Funcs{DeleteFunc: func(event.DeleteEvent) bool { return true }}, predicate.Funcs{DeleteFunc: func(event.DeleteEvent) bool { return false }}, }) @@ -237,7 +343,7 @@ var _ = Describe("Internal", func() { Expect(set).To(BeFalse()) set = false - instance = internal.NewEventHandler(ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ + instance = internal.NewEventHandler[*corev1.Pod](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ predicate.Funcs{DeleteFunc: func(event.DeleteEvent) bool { return false }}, predicate.Funcs{DeleteFunc: func(event.DeleteEvent) bool { return true }}, }) @@ -245,7 +351,7 @@ var _ = Describe("Internal", func() { Expect(set).To(BeFalse()) set = false - instance = internal.NewEventHandler(ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ + instance = internal.NewEventHandler[*corev1.Pod](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ predicate.Funcs{DeleteFunc: func(event.DeleteEvent) bool { return true }}, predicate.Funcs{DeleteFunc: func(event.DeleteEvent) bool { return true }}, }) @@ -253,10 +359,54 @@ var _ = Describe("Internal", func() { Expect(set).To(BeTrue()) }) + It("should use Predicates to filter DeleteEvents", func() { + set = false + partialMetadataInstance = internal.NewEventHandler[*metav1.PartialObjectMetadata](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ + predicate.Funcs{DeleteFunc: func(event.DeleteEvent) bool { return false }}, + }) + partialMetadataInstance.OnDelete(pom) + Expect(set).To(BeFalse()) + + set = false + partialMetadataInstance = internal.NewEventHandler[*metav1.PartialObjectMetadata](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ + predicate.Funcs{DeleteFunc: func(event.DeleteEvent) bool { return true }}, + }) + partialMetadataInstance.OnDelete(pom) + Expect(set).To(BeTrue()) + + set = false + partialMetadataInstance = internal.NewEventHandler[*metav1.PartialObjectMetadata](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ + predicate.Funcs{DeleteFunc: func(event.DeleteEvent) bool { return true }}, + predicate.Funcs{DeleteFunc: func(event.DeleteEvent) bool { return false }}, + }) + partialMetadataInstance.OnDelete(pom) + Expect(set).To(BeFalse()) + + set = false + partialMetadataInstance = internal.NewEventHandler[*metav1.PartialObjectMetadata](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ + predicate.Funcs{DeleteFunc: func(event.DeleteEvent) bool { return false }}, + predicate.Funcs{DeleteFunc: func(event.DeleteEvent) bool { return true }}, + }) + partialMetadataInstance.OnDelete(pom) + Expect(set).To(BeFalse()) + + set = false + partialMetadataInstance = internal.NewEventHandler[*metav1.PartialObjectMetadata](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ + predicate.Funcs{DeleteFunc: func(event.DeleteEvent) bool { return true }}, + predicate.Funcs{DeleteFunc: func(event.DeleteEvent) bool { return true }}, + }) + partialMetadataInstance.OnDelete(pom) + Expect(set).To(BeTrue()) + }) + It("should not call Delete EventHandler if the object is not a runtime.Object", func() { instance.OnDelete(&metav1.ObjectMeta{}) }) + It("should not call Delete EventHandler if an object is not 'that' object", func() { + instance.OnDelete(&corev1.Secret{}) + }) + It("should not call Delete EventHandler if the object does not have metadata", func() { instance.OnDelete(FooRuntimeObject{}) }) @@ -275,6 +425,10 @@ var _ = Describe("Internal", func() { instance.OnDelete(tombstone) }) + It("should not call Delete EventHandler if an object is not a partial object metadata object", func() { + partialMetadataInstance.OnDelete(&corev1.Secret{}) + }) + It("should ignore tombstone objects without meta", func() { tombstone := cache.DeletedFinalStateUnknown{Obj: Foo{}} instance.OnDelete(tombstone) diff --git a/pkg/internal/source/kind.go b/pkg/internal/source/kind.go index b3a8227125..738749bc5a 100644 --- a/pkg/internal/source/kind.go +++ b/pkg/internal/source/kind.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "reflect" "time" "k8s.io/apimachinery/pkg/api/meta" @@ -17,9 +18,9 @@ import ( ) // Kind is used to provide a source of events originating inside the cluster from Watches (e.g. Pod Create). -type Kind struct { +type Kind[T client.Object] struct { // Type is the type of object to watch. e.g. &v1.Pod{} - Type client.Object + Type T // Cache used to watch APIs Cache cache.Cache @@ -32,9 +33,9 @@ type Kind struct { // Start is internal and should be called only by the Controller to register an EventHandler with the Informer // to enqueue reconcile.Requests. -func (ks *Kind) Start(ctx context.Context, handler handler.EventHandler, queue workqueue.RateLimitingInterface, +func (ks *Kind[T]) Start(ctx context.Context, handler handler.EventHandler, queue workqueue.RateLimitingInterface, prct ...predicate.Predicate) error { - if ks.Type == nil { + if reflect.DeepEqual(ks.Type, *new(T)) { return fmt.Errorf("must create Kind with a non-nil object") } if ks.Cache == nil { @@ -79,7 +80,7 @@ func (ks *Kind) Start(ctx context.Context, handler handler.EventHandler, queue w return } - _, err := i.AddEventHandler(NewEventHandler(ctx, queue, handler, prct).HandlerFuncs()) + _, err := i.AddEventHandler(NewEventHandler[T](ctx, queue, handler, prct).HandlerFuncs()) if err != nil { ks.started <- err return @@ -94,8 +95,8 @@ func (ks *Kind) Start(ctx context.Context, handler handler.EventHandler, queue w return nil } -func (ks *Kind) String() string { - if ks.Type != nil { +func (ks *Kind[T]) String() string { + if !reflect.DeepEqual(ks.Type, *new(T)) { return fmt.Sprintf("kind source: %T", ks.Type) } return "kind source: unknown type" @@ -103,7 +104,7 @@ func (ks *Kind) String() string { // WaitForSync implements SyncingSource to allow controllers to wait with starting // workers until the cache is synced. -func (ks *Kind) WaitForSync(ctx context.Context) error { +func (ks *Kind[T]) WaitForSync(ctx context.Context) error { select { case err := <-ks.started: return err diff --git a/pkg/source/source.go b/pkg/source/source.go index c0b9b1d9da..78d1d06c15 100644 --- a/pkg/source/source.go +++ b/pkg/source/source.go @@ -59,7 +59,12 @@ type SyncingSource interface { // Kind creates a KindSource with the given cache provider. func Kind(cache cache.Cache, object client.Object) SyncingSource { - return &internal.Kind{Type: object, Cache: cache} + return &internal.Kind[client.Object]{Type: object, Cache: cache} +} + +// ObjectKind creates a typed KindSource with the given cache provider. +func ObjectKind[T client.Object](cache cache.Cache, object T) SyncingSource { + return &internal.Kind[T]{Type: object, Cache: cache} } var _ Source = &Channel{} @@ -198,7 +203,7 @@ func (is *Informer) Start(ctx context.Context, handler handler.EventHandler, que return fmt.Errorf("must specify Informer.Informer") } - _, err := is.Informer.AddEventHandler(internal.NewEventHandler(ctx, queue, handler, prct).HandlerFuncs()) + _, err := is.Informer.AddEventHandler(internal.NewEventHandler[client.Object](ctx, queue, handler, prct).HandlerFuncs()) if err != nil { return err } diff --git a/pkg/source/source_test.go b/pkg/source/source_test.go index 16c365e8a2..012cca3bb8 100644 --- a/pkg/source/source_test.go +++ b/pkg/source/source_test.go @@ -25,6 +25,7 @@ import ( . "github.com/onsi/gomega" "sigs.k8s.io/controller-runtime/pkg/cache/informertest" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/predicate" @@ -186,6 +187,13 @@ var _ = Describe("Source", func() { Expect(err.Error()).To(ContainSubstring("must create Kind with a non-nil cache")) }) + It("should return an error from Start cache was not provided", func() { + instance := source.ObjectKind(nil, &corev1.Pod{}) + err := instance.Start(ctx, nil, nil) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("must create Kind with a non-nil cache")) + }) + It("should return an error from Start if a type was not provided", func() { instance := source.Kind(ic, nil) err := instance.Start(ctx, nil, nil) @@ -193,6 +201,13 @@ var _ = Describe("Source", func() { Expect(err.Error()).To(ContainSubstring("must create Kind with a non-nil object")) }) + It("should return an error from Start if a type was not provided", func() { + instance := source.ObjectKind[client.Object](ic, nil) + err := instance.Start(ctx, nil, nil) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("must create Kind with a non-nil object")) + }) + It("should return an error if syncing fails", func() { f := false instance := source.Kind(&informertest.FakeInformers{Synced: &f}, &corev1.Pod{}) From 204048d8bddf384e3e9f57cfc51e961df0aeaf55 Mon Sep 17 00:00:00 2001 From: Danil Grigorev Date: Thu, 29 Feb 2024 17:02:53 +0100 Subject: [PATCH 2/7] Add builder handlers for generic objects Signed-off-by: Danil Grigorev --- alias.go | 8 + example_test.go | 31 +++ pkg/builder/controller.go | 193 ++++++++++++++++--- pkg/builder/options.go | 44 +++++ pkg/cache/cache.go | 8 + pkg/handler/enqueue_mapped.go | 48 +---- pkg/handler/enqueue_mapped_typed.go | 134 +++++++++++++ pkg/handler/enqueue_typed.go | 115 ++++++++++++ pkg/handler/eventhandler.go | 147 +++++++++++++++ pkg/internal/source/event_handler.go | 39 ++-- pkg/internal/source/internal_test.go | 269 +++++++++++++-------------- pkg/internal/source/kind.go | 14 +- pkg/predicate/predicate.go | 256 +++++++++++++++++++++++++ pkg/source/source.go | 5 +- 14 files changed, 1081 insertions(+), 230 deletions(-) create mode 100644 pkg/handler/enqueue_mapped_typed.go create mode 100644 pkg/handler/enqueue_typed.go diff --git a/alias.go b/alias.go index 1f8092f4ae..9ba2d066de 100644 --- a/alias.go +++ b/alias.go @@ -20,6 +20,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/config" cfg "sigs.k8s.io/controller-runtime/pkg/config" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" @@ -33,6 +34,9 @@ import ( // Builder builds an Application ControllerManagedBy (e.g. Operator) and returns a manager.Manager to start it. type Builder = builder.Builder +// WatchObject defines an interface on the watch object with inherited type info. +type WatchObject = builder.WatchObject + // Request contains the information necessary to reconcile a Kubernetes object. This includes the // information to uniquely identify the object - its Name and Namespace. It does NOT contain information about // any specific Event or the object contents itself. @@ -157,3 +161,7 @@ var ( // SetLogger sets a concrete logging implementation for all deferred Loggers. SetLogger = log.SetLogger ) + +func Object[T client.Object](obj T) WatchObject { + return builder.Object(obj) +} diff --git a/example_test.go b/example_test.go index cbbf032b0f..29339452ab 100644 --- a/example_test.go +++ b/example_test.go @@ -69,6 +69,37 @@ func Example() { } } +// This example creates a generic application Controller that is configured for ReplicaSets and Pods. +// +// * Create a new application for ReplicaSets that manages Pods owned by the ReplicaSet and calls into +// ReplicaSetReconciler. +// +// * Start the application. +func GenericExample() { + log := ctrl.Log.WithName("builder-examples") + + manager, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{}) + if err != nil { + log.Error(err, "could not create manager") + os.Exit(1) + } + + err = ctrl. + NewControllerManagedBy(manager). // Create the Controller + With(ctrl.Object(&appsv1.ReplicaSet{})). // ReplicaSet is the Application API + Own(ctrl.Object(&corev1.Pod{})). // ReplicaSet owns Pods created by it + Complete(&ReplicaSetReconciler{Client: manager.GetClient()}) + if err != nil { + log.Error(err, "could not create controller") + os.Exit(1) + } + + if err := manager.Start(ctrl.SetupSignalHandler()); err != nil { + log.Error(err, "could not start manager") + os.Exit(1) + } +} + type ExampleCRDWithConfigMapRef struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` diff --git a/pkg/builder/controller.go b/pkg/builder/controller.go index 660a976fdc..abc80291ef 100644 --- a/pkg/builder/controller.go +++ b/pkg/builder/controller.go @@ -26,6 +26,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/klog/v2" + "sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/apiutil" "sigs.k8s.io/controller-runtime/pkg/controller" @@ -52,14 +53,58 @@ const ( projectAsMetadata ) +// WatchObject is an interface on an object wrapper with preserved type information +type WatchObject interface { + GetObject() client.Object + SetSource(cache.Cache) source.SyncingSource + // SetPredicates() +} + +type watchObject[T client.Object] struct { + object T + source source.SyncingSource +} + +// // SetPredicates implements WatchObject. +// func (w watchObject[T]) SetPredicates() { +// w.source.Start() +// } + +// SetSource returns inner client.Object +func (w watchObject[T]) GetObject() client.Object { + return w.object +} + +// SetSource sets and returns the source.SyncingSource on the object +func (w watchObject[T]) SetSource(cache cache.Cache) source.SyncingSource { + if w.source == nil { + w.source = source.Object(cache, w.object) + } + + return w.source +} + +// Object constructs a wrapper on a generic object with stored type information +func Object[T client.Object](obj T) WatchObject { + return watchObject[T]{object: obj} +} + +type State struct { + options []Option + mgr manager.Manager +} + // Builder builds a Controller. type Builder struct { forInput ForInput ownsInput []OwnsInput watchesInput []WatchesInput + blocks []func(State) + options []Option mgr manager.Manager globalPredicates []predicate.Predicate ctrl controller.Controller + ctrls []controller.ControllerConstraint ctrlOptions controller.Options name string } @@ -71,7 +116,7 @@ func ControllerManagedBy(m manager.Manager) *Builder { // ForInput represents the information set by the For method. type ForInput struct { - object client.Object + object WatchObject predicates []predicate.Predicate objectProjection objectProjection err error @@ -81,12 +126,22 @@ type ForInput struct { // update events by *reconciling the object*. // This is the equivalent of calling // Watches(&source.Kind{Type: apiType}, &handler.EnqueueRequestForObject{}). -func (blder *Builder) For(object client.Object, opts ...ForOption) *Builder { +func (blder *Builder) For(object client.Object, opts ...Option) *Builder { + return blder.With(Object(object), opts...) +} + +// With defines the type of Object being *reconciled*, and configures the ControllerManagedBy to respond to create / delete / +// update events by *reconciling the object*. +// This is the equivalent of calling +// Watches(&source.Kind{Type: apiType}, &handler.EnqueueRequestForObject{}). +// +// Unlike For, With ensures the type of client.Object returned by predicates or event handlers. +func (blder *Builder) With(watch WatchObject, opts ...Option) *Builder { if blder.forInput.object != nil { blder.forInput.err = fmt.Errorf("For(...) should only be called once, could not assign multiple objects for reconciliation") return blder } - input := ForInput{object: object} + input := ForInput{object: watch} for _, opt := range opts { opt.ApplyToFor(&input) } @@ -97,10 +152,11 @@ func (blder *Builder) For(object client.Object, opts ...ForOption) *Builder { // OwnsInput represents the information set by Owns method. type OwnsInput struct { - matchEveryOwner bool - object client.Object - predicates []predicate.Predicate - objectProjection objectProjection + matchEveryOwner bool + object WatchObject + predicates []predicate.Predicate + genericPredicates []predicate.PredicateConstraint + objectProjection objectProjection } // Owns defines types of Objects being *generated* by the ControllerManagedBy, and configures the ControllerManagedBy to respond to @@ -111,7 +167,21 @@ type OwnsInput struct { // // By default, this is the equivalent of calling // Watches(object, handler.EnqueueRequestForOwner([...], ownerType, OnlyControllerOwner())). -func (blder *Builder) Owns(object client.Object, opts ...OwnsOption) *Builder { +func (blder *Builder) Owns(object client.Object, opts ...Option) *Builder { + return blder.Own(Object(object), opts...) +} + +// Own defines types of Objects being *generated* by the ControllerManagedBy, and configures the ControllerManagedBy to respond to +// create / delete / update events by *reconciling the owner object*. +// +// The default behavior reconciles only the first controller-type OwnerReference of the given type. +// Use Owns(object, builder.MatchEveryOwner) to reconcile all owners. +// +// By default, this is the equivalent of calling +// Watches(object, handler.EnqueueRequestForOwner([...], ownerType, OnlyControllerOwner())). +// +// Unlike Owns, Own ensures the type of client.Object returned by predicates or event handlers. +func (blder *Builder) Own(object WatchObject, opts ...Option) *Builder { input := OwnsInput{object: object} for _, opt := range opts { opt.ApplyToOwns(&input) @@ -134,11 +204,72 @@ type WatchesInput struct { // // This is the equivalent of calling // WatchesRawSource(source.Kind(cache, object), eventHandler, opts...). -func (blder *Builder) Watches(object client.Object, eventHandler handler.EventHandler, opts ...WatchesOption) *Builder { - src := source.ObjectKind(blder.mgr.GetCache(), object) +func (blder *Builder) Watches(object client.Object, eventHandler handler.EventHandler, opts ...Option) *Builder { + return blder.Watch(Object(object), eventHandler, opts...) +} + +// Watch defines the type of Object to watch, and configures the ControllerManagedBy to respond to create / delete / +// update events by *reconciling the object* with the given EventHandler. +// +// This is the equivalent of calling +// WatchesRawSource(source.Kind(cache, object), eventHandler, opts...). +// Unlike Watches, Watch ensures the type of client.Object returned by predicates or event handlers. +func (blder *Builder) Watch(object WatchObject, eventHandler handler.EventHandler, opts ...Option) *Builder { + src := object.SetSource(blder.mgr.GetCache()) return blder.WatchesRawSource(src, eventHandler, opts...) } +func (blder *Builder) Add(w AddWatch) *Builder { + w.AddTo(blder) + return blder +} + +func (blder *Builder) AddBlock(f func(State)) { + blder.blocks = append(blder.blocks, f) +} + +type AddWatch interface { + SetSource(*Builder) + SetEventHandler(*Builder) + GetOptions() []Option + AddTo(*Builder) +} + +type Adder interface { + AddTo(*Builder) *Builder +} + +var _ AddWatch = &RawAdder[any]{} + +type RawAdder[T any] struct { + *Raw[T] +} + +func (r *RawAdder[T]) AddTo(b *Builder) { + b.options = append(b.options, r.GetOptions()...) // state + // Establishing watches, event handlers + r.SetSource(b) + r.SetEventHandler(b) +} + +func (r *RawAdder[T]) SetSource(b *Builder) {} + +func (r *RawAdder[T]) SetEventHandler(b *Builder) { + b.AddBlock(func(s State) { + allPredicates := append([]predicate.Predicate(nil), s.options) + allPredicates = append(allPredicates, own.predicates...) + if err := blder.ctrl.Watch(src, hdler, allPredicates...); err != nil { + return err + } + }) +} + +func (blder *RawAdder[T]) GetOptions() []Option { + return []Option{} +} + +var _ controller.ControllerConstraint + // WatchesMetadata is the same as Watches, but forces the internal cache to only watch PartialObjectMetadata. // // This is useful when watching lots of objects, really big objects, or objects for which you only know @@ -166,11 +297,18 @@ func (blder *Builder) Watches(object client.Object, eventHandler handler.EventHa // In the first case, controller-runtime will create another cache for the // concrete type on top of the metadata cache; this increases memory // consumption and leads to race conditions as caches are not in sync. -func (blder *Builder) WatchesMetadata(object client.Object, eventHandler handler.EventHandler, opts ...WatchesOption) *Builder { +func (blder *Builder) WatchesMetadata(object client.Object, eventHandler handler.EventHandler, opts ...Option) *Builder { opts = append(opts, OnlyMetadata) + _ = RawAdder[any]{} return blder.Watches(object, eventHandler, opts...) } +type Raw[T any] struct { + src source.ObjectSource[T] + eventHandler handler.ObjectHandler[T] + predicates []predicate.ObjectPredicate[T] +} + // WatchesRawSource exposes the lower-level ControllerManagedBy Watches functions through the builder. // Specified predicates are registered only for given source. // @@ -179,7 +317,7 @@ func (blder *Builder) WatchesMetadata(object client.Object, eventHandler handler // // Example: // WatchesRawSource(source.Kind(cache, &corev1.Pod{}), eventHandler, opts...) // ensure that source propagates only valid Pod objects. -func (blder *Builder) WatchesRawSource(src source.Source, eventHandler handler.EventHandler, opts ...WatchesOption) *Builder { +func (blder *Builder) WatchesRawSource(src source.Source, eventHandler handler.EventHandler, opts ...Option) *Builder { input := WatchesInput{src: src, eventHandler: eventHandler} for _, opt := range opts { opt.ApplyToWatches(&input) @@ -271,14 +409,22 @@ func (blder *Builder) project(obj client.Object, proj objectProjection) (client. func (blder *Builder) doWatch() error { // Reconcile type if blder.forInput.object != nil { - obj, err := blder.project(blder.forInput.object, blder.forInput.objectProjection) - if err != nil { - return err + src := blder.forInput.object.SetSource(blder.mgr.GetCache()) + if _, ok := blder.forInput.object.(watchObject[client.Object]); ok { + obj, err := blder.project(blder.forInput.object.GetObject(), blder.forInput.objectProjection) + if err != nil { + return err + } + src = source.Object(blder.mgr.GetCache(), obj) } - src := source.ObjectKind(blder.mgr.GetCache(), obj) hdler := &handler.EnqueueRequestForObject{} allPredicates := append([]predicate.Predicate(nil), blder.globalPredicates...) allPredicates = append(allPredicates, blder.forInput.predicates...) + for _, c := range blder.ctrls { + if err := c.DoWatch(); err != nil { + return err + } + } if err := blder.ctrl.Watch(src, hdler, allPredicates...); err != nil { return err } @@ -289,18 +435,21 @@ func (blder *Builder) doWatch() error { return errors.New("Owns() can only be used together with For()") } for _, own := range blder.ownsInput { - obj, err := blder.project(own.object, own.objectProjection) - if err != nil { - return err + src := own.object.SetSource(blder.mgr.GetCache()) + if _, ok := own.object.(watchObject[client.Object]); ok { + obj, err := blder.project(own.object.GetObject(), own.objectProjection) + if err != nil { + return err + } + src = source.Object(blder.mgr.GetCache(), obj) } - src := source.ObjectKind(blder.mgr.GetCache(), obj) opts := []handler.OwnerOption{} if !own.matchEveryOwner { opts = append(opts, handler.OnlyControllerOwner()) } hdler := handler.EnqueueRequestForOwner( blder.mgr.GetScheme(), blder.mgr.GetRESTMapper(), - blder.forInput.object, + blder.forInput.object.GetObject(), opts..., ) allPredicates := append([]predicate.Predicate(nil), blder.globalPredicates...) @@ -359,7 +508,7 @@ func (blder *Builder) doController(r reconcile.Reconciler) error { hasGVK := blder.forInput.object != nil if hasGVK { var err error - gvk, err = getGvk(blder.forInput.object, blder.mgr.GetScheme()) + gvk, err = getGvk(blder.forInput.object.GetObject(), blder.mgr.GetScheme()) if err != nil { return err } diff --git a/pkg/builder/options.go b/pkg/builder/options.go index 15f66b2a82..cf5158883f 100644 --- a/pkg/builder/options.go +++ b/pkg/builder/options.go @@ -40,6 +40,12 @@ type WatchesOption interface { ApplyToWatches(*WatchesInput) } +type Option interface { + ForOption + OwnsOption + WatchesOption +} + // }}} // {{{ Multi-Type Options @@ -75,6 +81,36 @@ var _ ForOption = &Predicates{} var _ OwnsOption = &Predicates{} var _ WatchesOption = &Predicates{} +// WithPredicates sets the given predicates list. +func WithObjectPredicates[T any](predicates ...predicate.ObjectPredicate[T]) ObjectPredicates[T] { + return ObjectPredicates[T]{ + predicates: predicates, + } +} + +type ObjectPredicates[T any] struct { + predicates []predicate.ObjectPredicate[T] +} + +// ApplyToOwns implements OwnsOption. +func (o *ObjectPredicates[T]) ApplyToOwns(*OwnsInput) { + panic("unimplemented") +} + +// ApplyToWatches implements WatchesOption. +func (o *ObjectPredicates[T]) ApplyToWatches(*WatchesInput) { + panic("unimplemented") +} + +// ApplyToFor implements ForOption. +func (o *ObjectPredicates[T]) ApplyToFor(*ForInput) { + panic("unimplemented") +} + +var _ ForOption = &ObjectPredicates[any]{} +var _ OwnsOption = &ObjectPredicates[any]{} +var _ WatchesOption = &ObjectPredicates[any]{} + // }}} // {{{ For & Owns Dual-Type options @@ -154,3 +190,11 @@ type matchEveryOwner struct{} func (o matchEveryOwner) ApplyToOwns(opts *OwnsInput) { opts.matchEveryOwner = true } + +// ApplyToFor applies this configuration to the given OwnsInput options. +func (o matchEveryOwner) ApplyToFor(opts *ForInput) { +} + +// ApplyToWatches applies this configuration to the given OwnsInput options. +func (o matchEveryOwner) ApplyToWatches(opts *WatchesInput) { +} diff --git a/pkg/cache/cache.go b/pkg/cache/cache.go index e23045bf40..c104168aa5 100644 --- a/pkg/cache/cache.go +++ b/pkg/cache/cache.go @@ -96,6 +96,14 @@ type Informers interface { client.FieldIndexer } +type ObjectInformers[T any] interface { + Informer + + // GetInformer fetches or constructs an informer for the given object that corresponds to a single + // API kind and resource. + GetInformer(ctx context.Context, obj T, opts ...InformerGetOption) (Informer, error) +} + // Informer allows you to interact with the underlying informer. type Informer interface { // AddEventHandler adds an event handler to the shared informer using the shared informer's resync diff --git a/pkg/handler/enqueue_mapped.go b/pkg/handler/enqueue_mapped.go index b55fdde6ba..c3a24089f7 100644 --- a/pkg/handler/enqueue_mapped.go +++ b/pkg/handler/enqueue_mapped.go @@ -19,9 +19,7 @@ package handler import ( "context" - "k8s.io/client-go/util/workqueue" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/reconcile" ) @@ -40,49 +38,7 @@ type MapFunc func(context.Context, client.Object) []reconcile.Request // For UpdateEvents which contain both a new and old object, the transformation function is run on both // objects and both sets of Requests are enqueue. func EnqueueRequestsFromMapFunc(fn MapFunc) EventHandler { - return &enqueueRequestsFromMapFunc{ - toRequests: fn, - } -} - -var _ EventHandler = &enqueueRequestsFromMapFunc{} - -type enqueueRequestsFromMapFunc struct { - // Mapper transforms the argument into a slice of keys to be reconciled - toRequests MapFunc -} - -// Create implements EventHandler. -func (e *enqueueRequestsFromMapFunc) Create(ctx context.Context, evt event.CreateEvent, q workqueue.RateLimitingInterface) { - reqs := map[reconcile.Request]empty{} - e.mapAndEnqueue(ctx, q, evt.Object, reqs) -} - -// Update implements EventHandler. -func (e *enqueueRequestsFromMapFunc) Update(ctx context.Context, evt event.UpdateEvent, q workqueue.RateLimitingInterface) { - reqs := map[reconcile.Request]empty{} - e.mapAndEnqueue(ctx, q, evt.ObjectOld, reqs) - e.mapAndEnqueue(ctx, q, evt.ObjectNew, reqs) -} - -// Delete implements EventHandler. -func (e *enqueueRequestsFromMapFunc) Delete(ctx context.Context, evt event.DeleteEvent, q workqueue.RateLimitingInterface) { - reqs := map[reconcile.Request]empty{} - e.mapAndEnqueue(ctx, q, evt.Object, reqs) -} - -// Generic implements EventHandler. -func (e *enqueueRequestsFromMapFunc) Generic(ctx context.Context, evt event.GenericEvent, q workqueue.RateLimitingInterface) { - reqs := map[reconcile.Request]empty{} - e.mapAndEnqueue(ctx, q, evt.Object, reqs) -} - -func (e *enqueueRequestsFromMapFunc) mapAndEnqueue(ctx context.Context, q workqueue.RateLimitingInterface, object client.Object, reqs map[reconcile.Request]empty) { - for _, req := range e.toRequests(ctx, object) { - _, ok := reqs[req] - if !ok { - q.Add(req) - reqs[req] = empty{} - } + return &enqueueRequestsFromObjectMapFunc[any]{ + toRequests: MapFuncAdapter(fn), } } diff --git a/pkg/handler/enqueue_mapped_typed.go b/pkg/handler/enqueue_mapped_typed.go new file mode 100644 index 0000000000..80f54bacef --- /dev/null +++ b/pkg/handler/enqueue_mapped_typed.go @@ -0,0 +1,134 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package handler + +import ( + "context" + + "k8s.io/client-go/util/workqueue" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +// ObjectMapFunc is the signature required for enqueueing requests from a generic function. +// This type is usually used with EnqueueRequestsFromTypeMapFunc when registering an event handler. +// Unlike MapFunc, a specific object type can be used to process and create mapping requests. +type ObjectMapFunc[T any] func(context.Context, T) []reconcile.Request + +func MapFuncAdapter(m MapFunc) ObjectMapFunc[any] { + return func(ctx context.Context, a any) (reqs []reconcile.Request) { + obj, ok := a.(client.Object) + if ok { + return m(ctx, obj) + } + + return []reconcile.Request{} + } +} + +// EnqueueRequestsFromObjectMapFunc enqueues Requests by running a transformation function that outputs a collection +// of reconcile.Requests on each Event. The reconcile.Requests may be for an arbitrary set of objects +// defined by some user specified transformation of the source Event. (e.g. trigger Reconciler for a set of objects +// in response to a cluster resize event caused by adding or deleting a Node) +// +// EnqueueRequestsFromObjectMapFunc is frequently used to fan-out updates from one object to one or more other +// objects of a differing type. +// +// For UpdateEvents which contain both a new and old object, the transformation function is run on both +// objects and both sets of Requests are enqueue. +func EnqueueRequestsFromObjectMapFunc[T any](fn ObjectMapFunc[T]) EventHandler { + return &enqueueRequestsFromObjectMapFunc[T]{ + toRequests: fn, + } +} + +var _ EventHandler = &enqueueRequestsFromObjectMapFunc[any]{} +var _ ObjectHandler[any] = &enqueueRequestsFromObjectMapFunc[any]{} + +type enqueueRequestsFromObjectMapFunc[T any] struct { + // Mapper transforms the argument into a slice of keys to be reconciled + toRequests ObjectMapFunc[T] +} + +// OnCreate implements ObjectHandler. +func (e *enqueueRequestsFromObjectMapFunc[T]) OnCreate(ctx context.Context, obj T, q workqueue.RateLimitingInterface) { + reqs := map[reconcile.Request]empty{} + e.mapAndEnqueue(ctx, q, obj, reqs) +} + +// OnDelete implements ObjectHandler. +func (e *enqueueRequestsFromObjectMapFunc[T]) OnDelete(ctx context.Context, obj T, q workqueue.RateLimitingInterface) { + reqs := map[reconcile.Request]empty{} + e.mapAndEnqueue(ctx, q, obj, reqs) +} + +// OnGeneric implements ObjectHandler. +func (e *enqueueRequestsFromObjectMapFunc[T]) OnGeneric(ctx context.Context, obj T, q workqueue.RateLimitingInterface) { + reqs := map[reconcile.Request]empty{} + e.mapAndEnqueue(ctx, q, obj, reqs) +} + +// OnUpdate implements ObjectHandler. +func (e *enqueueRequestsFromObjectMapFunc[T]) OnUpdate(ctx context.Context, old T, new T, q workqueue.RateLimitingInterface) { + reqs := map[reconcile.Request]empty{} + e.mapAndEnqueue(ctx, q, old, reqs) + e.mapAndEnqueue(ctx, q, new, reqs) +} + +// Create implements EventHandler. +func (e *enqueueRequestsFromObjectMapFunc[T]) Create(ctx context.Context, evt event.CreateEvent, q workqueue.RateLimitingInterface) { + obj, ok := evt.Object.(T) + if ok { + e.OnCreate(ctx, obj, q) + } +} + +// Update implements EventHandler. +func (e *enqueueRequestsFromObjectMapFunc[T]) Update(ctx context.Context, evt event.UpdateEvent, q workqueue.RateLimitingInterface) { + old, okOld := evt.ObjectOld.(T) + new, okNew := evt.ObjectNew.(T) + if okOld && okNew { + e.OnUpdate(ctx, old, new, q) + } +} + +// Delete implements EventHandler. +func (e *enqueueRequestsFromObjectMapFunc[T]) Delete(ctx context.Context, evt event.DeleteEvent, q workqueue.RateLimitingInterface) { + obj, ok := evt.Object.(T) + if ok { + e.OnDelete(ctx, obj, q) + } +} + +// Generic implements EventHandler. +func (e *enqueueRequestsFromObjectMapFunc[T]) Generic(ctx context.Context, evt event.GenericEvent, q workqueue.RateLimitingInterface) { + obj, ok := evt.Object.(T) + if ok { + e.OnGeneric(ctx, obj, q) + } +} + +func (e *enqueueRequestsFromObjectMapFunc[T]) mapAndEnqueue(ctx context.Context, q workqueue.RateLimitingInterface, object T, reqs map[reconcile.Request]empty) { + for _, req := range e.toRequests(ctx, object) { + _, ok := reqs[req] + if !ok { + q.Add(req) + reqs[req] = empty{} + } + } +} diff --git a/pkg/handler/enqueue_typed.go b/pkg/handler/enqueue_typed.go new file mode 100644 index 0000000000..2fe76338f7 --- /dev/null +++ b/pkg/handler/enqueue_typed.go @@ -0,0 +1,115 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package handler + +import ( + "context" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/util/workqueue" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +var _ EventHandler = &EnqueueRequest[metav1.Object]{} +var _ ObjectHandler[metav1.Object] = &EnqueueRequest[metav1.Object]{} + +type Request interface { + comparable + GetName() string + GetNamespace() string +} + +// EnqueueRequest enqueues a Request containing the Name and Namespace of the object that is the source of the Event. +// (e.g. the created / deleted / updated objects Name and Namespace). handler.EnqueueRequest is used by almost all +// Controllers that have associated Resources (e.g. CRDs) to reconcile the associated Resource. +type EnqueueRequest[T Request] struct{} + +// OnCreate implements ObjectHandler. +func (e *EnqueueRequest[T]) OnCreate(ctx context.Context, obj T, q workqueue.RateLimitingInterface) { + if obj != *new(T) { + q.Add(reconcile.Request{NamespacedName: types.NamespacedName{ + Name: obj.GetName(), + Namespace: obj.GetNamespace(), + }}) + } +} + +// OnDelete implements ObjectHandler. +func (e *EnqueueRequest[T]) OnDelete(ctx context.Context, obj T, q workqueue.RateLimitingInterface) { + if obj != *new(T) { + q.Add(reconcile.Request{NamespacedName: types.NamespacedName{ + Name: obj.GetName(), + Namespace: obj.GetNamespace(), + }}) + } +} + +// OnGeneric implements ObjectHandler. +func (e *EnqueueRequest[T]) OnGeneric(ctx context.Context, obj T, q workqueue.RateLimitingInterface) { + if obj != *new(T) { + q.Add(reconcile.Request{NamespacedName: types.NamespacedName{ + Name: obj.GetName(), + Namespace: obj.GetNamespace(), + }}) + } +} + +// OnUpdate implements ObjectHandler. +func (e *EnqueueRequest[T]) OnUpdate(ctx context.Context, oldObj T, newObj T, q workqueue.RateLimitingInterface) { + if oldObj != *new(T) { + q.Add(reconcile.Request{NamespacedName: types.NamespacedName{ + Name: oldObj.GetName(), + Namespace: oldObj.GetNamespace(), + }}) + } +} + +// Create implements EventHandler. +func (e *EnqueueRequest[T]) Create(ctx context.Context, evt event.CreateEvent, q workqueue.RateLimitingInterface) { + obj, ok := evt.Object.(T) + if ok { + e.OnCreate(ctx, obj, q) + } +} + +// Update implements EventHandler. +func (e *EnqueueRequest[T]) Update(ctx context.Context, evt event.UpdateEvent, q workqueue.RateLimitingInterface) { + objOld, okOld := evt.ObjectOld.(T) + objNew, okNew := evt.ObjectNew.(T) + + if okOld && okNew { + e.OnUpdate(ctx, objOld, objNew, q) + } +} + +// Delete implements EventHandler. +func (e *EnqueueRequest[T]) Delete(ctx context.Context, evt event.DeleteEvent, q workqueue.RateLimitingInterface) { + obj, ok := evt.Object.(T) + if ok { + e.OnDelete(ctx, obj, q) + } +} + +// Generic implements EventHandler. +func (e *EnqueueRequest[T]) Generic(ctx context.Context, evt event.GenericEvent, q workqueue.RateLimitingInterface) { + obj, ok := evt.Object.(T) + if ok { + e.OnGeneric(ctx, obj, q) + } +} diff --git a/pkg/handler/eventhandler.go b/pkg/handler/eventhandler.go index ff2f3e80b2..121e675b19 100644 --- a/pkg/handler/eventhandler.go +++ b/pkg/handler/eventhandler.go @@ -20,6 +20,7 @@ import ( "context" "k8s.io/client-go/util/workqueue" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/event" ) @@ -56,6 +57,21 @@ type EventHandler interface { Generic(context.Context, event.GenericEvent, workqueue.RateLimitingInterface) } +// ObjectHandler filters events for type before enqueuing the keys. +type ObjectHandler[T any] interface { + // Create returns true if the Create event should be processed + OnCreate(ctx context.Context, obj T, queue workqueue.RateLimitingInterface) + + // Delete returns true if the Delete event should be processed + OnDelete(ctx context.Context, obj T, queue workqueue.RateLimitingInterface) + + // Update returns true if the Update event should be processed + OnUpdate(ctx context.Context, old, new T, queue workqueue.RateLimitingInterface) + + // Generic returns true if the Generic event should be processed + OnGeneric(ctx context.Context, obj T, queue workqueue.RateLimitingInterface) +} + var _ EventHandler = Funcs{} // Funcs implements EventHandler. @@ -104,3 +120,134 @@ func (h Funcs) Generic(ctx context.Context, e event.GenericEvent, q workqueue.Ra h.GenericFunc(ctx, e, q) } } + +var _ EventHandler = Funcs{} +var _ EventHandler = ObjectFuncs[any]{} +var _ ObjectHandler[any] = ObjectFuncs[any]{} + +// Funcs is a function that implements Predicate. +type ObjectFuncs[T any] struct { + // Create is called in response to an add event. Defaults to no-op. + // RateLimitingInterface is used to enqueue reconcile.Requests. + CreateFunc func(ctx context.Context, obj T, queue workqueue.RateLimitingInterface) + + // Update is called in response to an update event. Defaults to no-op. + // RateLimitingInterface is used to enqueue reconcile.Requests. + UpdateFunc func(ctx context.Context, old, new T, queue workqueue.RateLimitingInterface) + + // Delete is called in response to a delete event. Defaults to no-op. + // RateLimitingInterface is used to enqueue reconcile.Requests. + DeleteFunc func(ctx context.Context, obj T, queue workqueue.RateLimitingInterface) + + // GenericFunc is called in response to a generic event. Defaults to no-op. + // RateLimitingInterface is used to enqueue reconcile.Requests. + GenericFunc func(ctx context.Context, obj T, queue workqueue.RateLimitingInterface) +} + +// Update implements Predicate. +func (p ObjectFuncs[T]) Update(ctx context.Context, e event.UpdateEvent, q workqueue.RateLimitingInterface) { + new, ok := e.ObjectNew.(T) + old, oldOk := e.ObjectOld.(T) + if ok && oldOk { + p.OnUpdate(ctx, old, new, q) + } +} + +// Generic implements Predicate. +func (p ObjectFuncs[T]) Generic(ctx context.Context, e event.GenericEvent, q workqueue.RateLimitingInterface) { + obj, ok := e.Object.(T) + if ok { + p.OnGeneric(ctx, obj, q) + } +} + +// Create implements Predicate. +func (p ObjectFuncs[T]) Create(ctx context.Context, e event.CreateEvent, q workqueue.RateLimitingInterface) { + obj, ok := e.Object.(T) + if ok { + p.OnCreate(ctx, obj, q) + } +} + +// Delete implements Predicate. +func (p ObjectFuncs[T]) Delete(ctx context.Context, e event.DeleteEvent, q workqueue.RateLimitingInterface) { + obj, ok := e.Object.(T) + if ok { + p.OnDelete(ctx, obj, q) + } +} + +// Update implements Predicate. +func (p ObjectFuncs[T]) OnUpdate(ctx context.Context, old, new T, q workqueue.RateLimitingInterface) { + if p.UpdateFunc != nil { + p.UpdateFunc(ctx, old, new, q) + } +} + +// Generic implements Predicate. +func (p ObjectFuncs[T]) OnGeneric(ctx context.Context, obj T, q workqueue.RateLimitingInterface) { + if p.GenericFunc != nil { + p.GenericFunc(ctx, obj, q) + } +} + +// Create implements Predicate. +func (p ObjectFuncs[T]) OnCreate(ctx context.Context, obj T, q workqueue.RateLimitingInterface) { + if p.CreateFunc != nil { + p.CreateFunc(ctx, obj, q) + } +} + +// Delete implements Predicate. +func (p ObjectFuncs[T]) OnDelete(ctx context.Context, obj T, q workqueue.RateLimitingInterface) { + if p.DeleteFunc != nil { + p.DeleteFunc(ctx, obj, q) + } +} + +func ObjectFuncAdapter[T client.Object](h EventHandler) ObjectHandler[T] { + return ObjectFuncs[T]{ + CreateFunc: func(ctx context.Context, obj T, queue workqueue.RateLimitingInterface) { + h.Create(ctx, event.CreateEvent{Object: obj}, queue) + }, + DeleteFunc: func(ctx context.Context, obj T, queue workqueue.RateLimitingInterface) { + h.Delete(ctx, event.DeleteEvent{Object: obj}, queue) + }, + GenericFunc: func(ctx context.Context, obj T, queue workqueue.RateLimitingInterface) { + h.Generic(ctx, event.GenericEvent{Object: obj}, queue) + }, + UpdateFunc: func(ctx context.Context, old, new T, queue workqueue.RateLimitingInterface) { + h.Update(ctx, event.UpdateEvent{ObjectOld: old, ObjectNew: new}, queue) + }, + } +} + +func EventHandlerAdapter[T client.Object](h ObjectHandler[T]) EventHandler { + return Funcs{ + CreateFunc: func(ctx context.Context, e event.CreateEvent, queue workqueue.RateLimitingInterface) { + obj, ok := e.Object.(T) + if ok { + h.OnCreate(ctx, obj, queue) + } + }, + DeleteFunc: func(ctx context.Context, e event.DeleteEvent, queue workqueue.RateLimitingInterface) { + obj, ok := e.Object.(T) + if ok { + h.OnDelete(ctx, obj, queue) + } + }, + GenericFunc: func(ctx context.Context, e event.GenericEvent, queue workqueue.RateLimitingInterface) { + obj, ok := e.Object.(T) + if ok { + h.OnGeneric(ctx, obj, queue) + } + }, + UpdateFunc: func(ctx context.Context, e event.UpdateEvent, queue workqueue.RateLimitingInterface) { + new, ok := e.ObjectNew.(T) + old, oldOk := e.ObjectOld.(T) + if ok && oldOk { + h.OnUpdate(ctx, old, new, queue) + } + }, + } +} diff --git a/pkg/internal/source/event_handler.go b/pkg/internal/source/event_handler.go index 5e309db391..d66c0fcacb 100644 --- a/pkg/internal/source/event_handler.go +++ b/pkg/internal/source/event_handler.go @@ -22,8 +22,6 @@ import ( "k8s.io/client-go/tools/cache" "k8s.io/client-go/util/workqueue" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/handler" logf "sigs.k8s.io/controller-runtime/pkg/internal/log" @@ -33,7 +31,7 @@ import ( var log = logf.RuntimeLog.WithName("source").WithName("EventHandler") // NewEventHandler creates a new EventHandler. -func NewEventHandler[T client.Object](ctx context.Context, queue workqueue.RateLimitingInterface, handler handler.EventHandler, predicates []predicate.Predicate) *EventHandler[T] { +func NewEventHandler[T any](ctx context.Context, queue workqueue.RateLimitingInterface, handler handler.ObjectHandler[T], predicates []predicate.ObjectPredicate[T]) *EventHandler[T] { return &EventHandler[T]{ ctx: ctx, handler: handler, @@ -43,14 +41,14 @@ func NewEventHandler[T client.Object](ctx context.Context, queue workqueue.RateL } // EventHandler adapts a handler.EventHandler interface to a cache.ResourceEventHandler interface. -type EventHandler[T client.Object] struct { +type EventHandler[T any] struct { // ctx stores the context that created the event handler // that is used to propagate cancellation signals to each handler function. ctx context.Context - handler handler.EventHandler + handler handler.ObjectHandler[T] queue workqueue.RateLimitingInterface - predicates []predicate.Predicate + predicates []predicate.ObjectPredicate[T] } // HandlerFuncs converts EventHandler to a ResourceEventHandlerFuncs @@ -65,11 +63,11 @@ func (e *EventHandler[T]) HandlerFuncs() cache.ResourceEventHandlerFuncs { // OnAdd creates CreateEvent and calls Create on EventHandler. func (e *EventHandler[T]) OnAdd(obj interface{}) { - c := event.CreateEvent{} + var object T // Pull Object out of the object if o, ok := obj.(T); ok { - c.Object = o + object = o } else { log.Error(nil, "OnAdd missing Object", "object", obj, "type", fmt.Sprintf("%T", obj)) @@ -77,7 +75,7 @@ func (e *EventHandler[T]) OnAdd(obj interface{}) { } for _, p := range e.predicates { - if !p.Create(c) { + if !p.OnCreate(object) { return } } @@ -85,15 +83,15 @@ func (e *EventHandler[T]) OnAdd(obj interface{}) { // Invoke create handler ctx, cancel := context.WithCancel(e.ctx) defer cancel() - e.handler.Create(ctx, c, e.queue) + e.handler.OnCreate(ctx, object, e.queue) } // OnUpdate creates UpdateEvent and calls Update on EventHandler. func (e *EventHandler[T]) OnUpdate(oldObj, newObj interface{}) { - u := event.UpdateEvent{} + var objOld, objNew T if o, ok := oldObj.(T); ok { - u.ObjectOld = o + objOld = o } else { log.Error(nil, "OnUpdate missing ObjectOld", "object", oldObj, "type", fmt.Sprintf("%T", oldObj)) @@ -102,7 +100,7 @@ func (e *EventHandler[T]) OnUpdate(oldObj, newObj interface{}) { // Pull Object out of the object if o, ok := newObj.(T); ok { - u.ObjectNew = o + objNew = o } else { log.Error(nil, "OnUpdate missing ObjectNew", "object", newObj, "type", fmt.Sprintf("%T", newObj)) @@ -110,7 +108,7 @@ func (e *EventHandler[T]) OnUpdate(oldObj, newObj interface{}) { } for _, p := range e.predicates { - if !p.Update(u) { + if !p.OnUpdate(objOld, objNew) { return } } @@ -118,12 +116,12 @@ func (e *EventHandler[T]) OnUpdate(oldObj, newObj interface{}) { // Invoke update handler ctx, cancel := context.WithCancel(e.ctx) defer cancel() - e.handler.Update(ctx, u, e.queue) + e.handler.OnUpdate(ctx, objOld, objNew, e.queue) } // OnDelete creates DeleteEvent and calls Delete on EventHandler. func (e *EventHandler[T]) OnDelete(obj interface{}) { - d := event.DeleteEvent{} + var object T // Deal with tombstone events by pulling the object out. Tombstone events wrap the object in a // DeleteFinalStateUnknown struct, so the object needs to be pulled out. @@ -141,16 +139,13 @@ func (e *EventHandler[T]) OnDelete(obj interface{}) { return } - // Set DeleteStateUnknown to true - d.DeleteStateUnknown = true - // Set obj to the tombstone obj obj = tombstone.Obj } // Pull Object out of the object if o, ok := obj.(T); ok { - d.Object = o + object = o } else { log.Error(nil, "OnDelete missing Object", "object", obj, "type", fmt.Sprintf("%T", obj)) @@ -158,7 +153,7 @@ func (e *EventHandler[T]) OnDelete(obj interface{}) { } for _, p := range e.predicates { - if !p.Delete(d) { + if !p.OnDelete(object) { return } } @@ -166,5 +161,5 @@ func (e *EventHandler[T]) OnDelete(obj interface{}) { // Invoke delete handler ctx, cancel := context.WithCancel(e.ctx) defer cancel() - e.handler.Delete(ctx, d, e.queue) + e.handler.OnDelete(ctx, object, e.queue) } diff --git a/pkg/internal/source/internal_test.go b/pkg/internal/source/internal_test.go index 8e425cfd6f..92d20fef4e 100644 --- a/pkg/internal/source/internal_test.go +++ b/pkg/internal/source/internal_test.go @@ -23,7 +23,7 @@ import ( . "github.com/onsi/gomega" "k8s.io/client-go/tools/cache" "k8s.io/client-go/util/workqueue" - "sigs.k8s.io/controller-runtime/pkg/event" + "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/handler" internal "sigs.k8s.io/controller-runtime/pkg/internal/source" @@ -35,48 +35,59 @@ import ( "sigs.k8s.io/controller-runtime/pkg/predicate" ) +func newFunc[T any](_ T) *handler.ObjectFuncs[T] { + return &handler.ObjectFuncs[T]{ + CreateFunc: func(context.Context, T, workqueue.RateLimitingInterface) { + defer GinkgoRecover() + Fail("Did not expect CreateEvent to be called.") + }, + DeleteFunc: func(context.Context, T, workqueue.RateLimitingInterface) { + defer GinkgoRecover() + Fail("Did not expect DeleteEvent to be called.") + }, + UpdateFunc: func(context.Context, T, T, workqueue.RateLimitingInterface) { + defer GinkgoRecover() + Fail("Did not expect UpdateEvent to be called.") + }, + GenericFunc: func(context.Context, T, workqueue.RateLimitingInterface) { + defer GinkgoRecover() + Fail("Did not expect GenericEvent to be called.") + }, + } +} + +func newSetFunc[T any](_ T, set *bool) *handler.ObjectFuncs[T] { + return &handler.ObjectFuncs[T]{ + CreateFunc: func(context.Context, T, workqueue.RateLimitingInterface) { + set = ptr.To(true) + }, + DeleteFunc: func(context.Context, T, workqueue.RateLimitingInterface) { + set = ptr.To(true) + }, + UpdateFunc: func(context.Context, T, T, workqueue.RateLimitingInterface) { + set = ptr.To(true) + }, + GenericFunc: func(context.Context, T, workqueue.RateLimitingInterface) { + set = ptr.To(true) + }, + } +} + var _ = Describe("Internal", func() { var ctx = context.Background() var instance *internal.EventHandler[*corev1.Pod] var partialMetadataInstance *internal.EventHandler[*metav1.PartialObjectMetadata] - var funcs, setfuncs *handler.Funcs + var funcs, setfuncs *handler.ObjectFuncs[*corev1.Pod] + var metafuncs, metasetfuncs *handler.ObjectFuncs[*metav1.PartialObjectMetadata] var set bool BeforeEach(func() { - funcs = &handler.Funcs{ - CreateFunc: func(context.Context, event.CreateEvent, workqueue.RateLimitingInterface) { - defer GinkgoRecover() - Fail("Did not expect CreateEvent to be called.") - }, - DeleteFunc: func(context.Context, event.DeleteEvent, workqueue.RateLimitingInterface) { - defer GinkgoRecover() - Fail("Did not expect DeleteEvent to be called.") - }, - UpdateFunc: func(context.Context, event.UpdateEvent, workqueue.RateLimitingInterface) { - defer GinkgoRecover() - Fail("Did not expect UpdateEvent to be called.") - }, - GenericFunc: func(context.Context, event.GenericEvent, workqueue.RateLimitingInterface) { - defer GinkgoRecover() - Fail("Did not expect GenericEvent to be called.") - }, - } - - setfuncs = &handler.Funcs{ - CreateFunc: func(context.Context, event.CreateEvent, workqueue.RateLimitingInterface) { - set = true - }, - DeleteFunc: func(context.Context, event.DeleteEvent, workqueue.RateLimitingInterface) { - set = true - }, - UpdateFunc: func(context.Context, event.UpdateEvent, workqueue.RateLimitingInterface) { - set = true - }, - GenericFunc: func(context.Context, event.GenericEvent, workqueue.RateLimitingInterface) { - set = true - }, - } - instance = internal.NewEventHandler[*corev1.Pod](ctx, &controllertest.Queue{}, funcs, nil) - partialMetadataInstance = internal.NewEventHandler[*metav1.PartialObjectMetadata](ctx, &controllertest.Queue{}, funcs, nil) + funcs = newFunc(&corev1.Pod{}) + setfuncs = newSetFunc(&corev1.Pod{}, &set) + metafuncs = newFunc(&metav1.PartialObjectMetadata{}) + metasetfuncs = newSetFunc(&metav1.PartialObjectMetadata{}, &set) + + instance = internal.NewEventHandler(ctx, &controllertest.Queue{}, funcs, nil) + partialMetadataInstance = internal.NewEventHandler(ctx, &controllertest.Queue{}, metafuncs, nil) }) Describe("EventHandler", func() { @@ -97,88 +108,88 @@ var _ = Describe("Internal", func() { }) It("should create a CreateEvent", func() { - funcs.CreateFunc = func(ctx context.Context, evt event.CreateEvent, q workqueue.RateLimitingInterface) { + funcs.CreateFunc = func(ctx context.Context, obj *corev1.Pod, q workqueue.RateLimitingInterface) { defer GinkgoRecover() - Expect(evt.Object).To(Equal(pod)) + Expect(obj).To(Equal(pod)) } instance.OnAdd(pod) }) It("should used Predicates to filter CreateEvents", func() { - instance = internal.NewEventHandler[*corev1.Pod](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ - predicate.Funcs{CreateFunc: func(event.CreateEvent) bool { return false }}, + instance = internal.NewEventHandler(ctx, &controllertest.Queue{}, setfuncs, []predicate.ObjectPredicate[*corev1.Pod]{ + predicate.ObjectFuncs[*corev1.Pod]{CreateFunc: func(*corev1.Pod) bool { return false }}, }) set = false instance.OnAdd(pod) Expect(set).To(BeFalse()) set = false - instance = internal.NewEventHandler[*corev1.Pod](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ - predicate.Funcs{CreateFunc: func(event.CreateEvent) bool { return true }}, + instance = internal.NewEventHandler(ctx, &controllertest.Queue{}, setfuncs, []predicate.ObjectPredicate[*corev1.Pod]{ + predicate.ObjectFuncs[*corev1.Pod]{CreateFunc: func(*corev1.Pod) bool { return true }}, }) instance.OnAdd(pod) Expect(set).To(BeTrue()) set = false - instance = internal.NewEventHandler[*corev1.Pod](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ - predicate.Funcs{CreateFunc: func(event.CreateEvent) bool { return true }}, - predicate.Funcs{CreateFunc: func(event.CreateEvent) bool { return false }}, + instance = internal.NewEventHandler(ctx, &controllertest.Queue{}, setfuncs, []predicate.ObjectPredicate[*corev1.Pod]{ + predicate.ObjectFuncs[*corev1.Pod]{CreateFunc: func(*corev1.Pod) bool { return true }}, + predicate.ObjectFuncs[*corev1.Pod]{CreateFunc: func(*corev1.Pod) bool { return false }}, }) instance.OnAdd(pod) Expect(set).To(BeFalse()) set = false - instance = internal.NewEventHandler[*corev1.Pod](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ - predicate.Funcs{CreateFunc: func(event.CreateEvent) bool { return false }}, - predicate.Funcs{CreateFunc: func(event.CreateEvent) bool { return true }}, + instance = internal.NewEventHandler(ctx, &controllertest.Queue{}, setfuncs, []predicate.ObjectPredicate[*corev1.Pod]{ + predicate.ObjectFuncs[*corev1.Pod]{CreateFunc: func(*corev1.Pod) bool { return false }}, + predicate.ObjectFuncs[*corev1.Pod]{CreateFunc: func(*corev1.Pod) bool { return true }}, }) instance.OnAdd(pod) Expect(set).To(BeFalse()) set = false - instance = internal.NewEventHandler[*corev1.Pod](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ - predicate.Funcs{CreateFunc: func(event.CreateEvent) bool { return true }}, - predicate.Funcs{CreateFunc: func(event.CreateEvent) bool { return true }}, + instance = internal.NewEventHandler(ctx, &controllertest.Queue{}, setfuncs, []predicate.ObjectPredicate[*corev1.Pod]{ + predicate.ObjectFuncs[*corev1.Pod]{CreateFunc: func(*corev1.Pod) bool { return true }}, + predicate.ObjectFuncs[*corev1.Pod]{CreateFunc: func(*corev1.Pod) bool { return true }}, }) instance.OnAdd(pod) Expect(set).To(BeTrue()) }) It("should use Predicates to filter CreateEvents on PartialObjectMetadata", func() { - partialMetadataInstance = internal.NewEventHandler[*metav1.PartialObjectMetadata](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ - predicate.Funcs{CreateFunc: func(event.CreateEvent) bool { return false }}, + partialMetadataInstance = internal.NewEventHandler(ctx, &controllertest.Queue{}, metasetfuncs, []predicate.ObjectPredicate[*metav1.PartialObjectMetadata]{ + predicate.ObjectFuncs[*metav1.PartialObjectMetadata]{CreateFunc: func(obj *metav1.PartialObjectMetadata) bool { return false }}, }) set = false partialMetadataInstance.OnAdd(pom) Expect(set).To(BeFalse()) set = false - partialMetadataInstance = internal.NewEventHandler[*metav1.PartialObjectMetadata](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ - predicate.Funcs{CreateFunc: func(event.CreateEvent) bool { return true }}, + partialMetadataInstance = internal.NewEventHandler(ctx, &controllertest.Queue{}, metasetfuncs, []predicate.ObjectPredicate[*metav1.PartialObjectMetadata]{ + predicate.ObjectFuncs[*metav1.PartialObjectMetadata]{CreateFunc: func(*metav1.PartialObjectMetadata) bool { return true }}, }) partialMetadataInstance.OnAdd(pom) Expect(set).To(BeTrue()) set = false - partialMetadataInstance = internal.NewEventHandler[*metav1.PartialObjectMetadata](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ - predicate.Funcs{CreateFunc: func(event.CreateEvent) bool { return true }}, - predicate.Funcs{CreateFunc: func(event.CreateEvent) bool { return false }}, + partialMetadataInstance = internal.NewEventHandler(ctx, &controllertest.Queue{}, metasetfuncs, []predicate.ObjectPredicate[*metav1.PartialObjectMetadata]{ + predicate.ObjectFuncs[*metav1.PartialObjectMetadata]{CreateFunc: func(*metav1.PartialObjectMetadata) bool { return true }}, + predicate.ObjectFuncs[*metav1.PartialObjectMetadata]{CreateFunc: func(*metav1.PartialObjectMetadata) bool { return false }}, }) partialMetadataInstance.OnAdd(pom) Expect(set).To(BeFalse()) set = false - partialMetadataInstance = internal.NewEventHandler[*metav1.PartialObjectMetadata](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ - predicate.Funcs{CreateFunc: func(event.CreateEvent) bool { return false }}, - predicate.Funcs{CreateFunc: func(event.CreateEvent) bool { return true }}, + partialMetadataInstance = internal.NewEventHandler(ctx, &controllertest.Queue{}, metasetfuncs, []predicate.ObjectPredicate[*metav1.PartialObjectMetadata]{ + predicate.ObjectFuncs[*metav1.PartialObjectMetadata]{CreateFunc: func(*metav1.PartialObjectMetadata) bool { return false }}, + predicate.ObjectFuncs[*metav1.PartialObjectMetadata]{CreateFunc: func(*metav1.PartialObjectMetadata) bool { return true }}, }) partialMetadataInstance.OnAdd(pom) Expect(set).To(BeFalse()) set = false - partialMetadataInstance = internal.NewEventHandler[*metav1.PartialObjectMetadata](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ - predicate.Funcs{CreateFunc: func(event.CreateEvent) bool { return true }}, - predicate.Funcs{CreateFunc: func(event.CreateEvent) bool { return true }}, + partialMetadataInstance = internal.NewEventHandler(ctx, &controllertest.Queue{}, metasetfuncs, []predicate.ObjectPredicate[*metav1.PartialObjectMetadata]{ + predicate.ObjectFuncs[*metav1.PartialObjectMetadata]{CreateFunc: func(*metav1.PartialObjectMetadata) bool { return true }}, + predicate.ObjectFuncs[*metav1.PartialObjectMetadata]{CreateFunc: func(*metav1.PartialObjectMetadata) bool { return true }}, }) partialMetadataInstance.OnAdd(pom) Expect(set).To(BeTrue()) @@ -201,49 +212,49 @@ var _ = Describe("Internal", func() { }) It("should create an UpdateEvent", func() { - funcs.UpdateFunc = func(ctx context.Context, evt event.UpdateEvent, q workqueue.RateLimitingInterface) { + funcs.UpdateFunc = func(ctx context.Context, old *corev1.Pod, new *corev1.Pod, q workqueue.RateLimitingInterface) { defer GinkgoRecover() - Expect(evt.ObjectOld).To(Equal(pod)) - Expect(evt.ObjectNew).To(Equal(newPod)) + Expect(old).To(Equal(pod)) + Expect(new).To(Equal(newPod)) } instance.OnUpdate(pod, newPod) }) It("should used Predicates to filter UpdateEvents", func() { set = false - instance = internal.NewEventHandler[*corev1.Pod](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ - predicate.Funcs{UpdateFunc: func(updateEvent event.UpdateEvent) bool { return false }}, + instance = internal.NewEventHandler(ctx, &controllertest.Queue{}, setfuncs, []predicate.ObjectPredicate[*corev1.Pod]{ + predicate.ObjectFuncs[*corev1.Pod]{UpdateFunc: func(old, new *corev1.Pod) bool { return false }}, }) instance.OnUpdate(pod, newPod) Expect(set).To(BeFalse()) set = false - instance = internal.NewEventHandler[*corev1.Pod](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ - predicate.Funcs{UpdateFunc: func(event.UpdateEvent) bool { return true }}, + instance = internal.NewEventHandler(ctx, &controllertest.Queue{}, setfuncs, []predicate.ObjectPredicate[*corev1.Pod]{ + predicate.ObjectFuncs[*corev1.Pod]{UpdateFunc: func(old, new *corev1.Pod) bool { return true }}, }) instance.OnUpdate(pod, newPod) Expect(set).To(BeTrue()) set = false - instance = internal.NewEventHandler[*corev1.Pod](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ - predicate.Funcs{UpdateFunc: func(event.UpdateEvent) bool { return true }}, - predicate.Funcs{UpdateFunc: func(event.UpdateEvent) bool { return false }}, + instance = internal.NewEventHandler(ctx, &controllertest.Queue{}, setfuncs, []predicate.ObjectPredicate[*corev1.Pod]{ + predicate.ObjectFuncs[*corev1.Pod]{UpdateFunc: func(old, new *corev1.Pod) bool { return true }}, + predicate.ObjectFuncs[*corev1.Pod]{UpdateFunc: func(old, new *corev1.Pod) bool { return false }}, }) instance.OnUpdate(pod, newPod) Expect(set).To(BeFalse()) set = false - instance = internal.NewEventHandler[*corev1.Pod](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ - predicate.Funcs{UpdateFunc: func(event.UpdateEvent) bool { return false }}, - predicate.Funcs{UpdateFunc: func(event.UpdateEvent) bool { return true }}, + instance = internal.NewEventHandler(ctx, &controllertest.Queue{}, setfuncs, []predicate.ObjectPredicate[*corev1.Pod]{ + predicate.ObjectFuncs[*corev1.Pod]{UpdateFunc: func(old, new *corev1.Pod) bool { return false }}, + predicate.ObjectFuncs[*corev1.Pod]{UpdateFunc: func(old, new *corev1.Pod) bool { return true }}, }) instance.OnUpdate(pod, newPod) Expect(set).To(BeFalse()) set = false - instance = internal.NewEventHandler[*corev1.Pod](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ - predicate.Funcs{CreateFunc: func(event.CreateEvent) bool { return true }}, - predicate.Funcs{CreateFunc: func(event.CreateEvent) bool { return true }}, + instance = internal.NewEventHandler(ctx, &controllertest.Queue{}, setfuncs, []predicate.ObjectPredicate[*corev1.Pod]{ + predicate.ObjectFuncs[*corev1.Pod]{CreateFunc: func(*corev1.Pod) bool { return true }}, + predicate.ObjectFuncs[*corev1.Pod]{CreateFunc: func(*corev1.Pod) bool { return true }}, }) instance.OnUpdate(pod, newPod) Expect(set).To(BeTrue()) @@ -251,39 +262,39 @@ var _ = Describe("Internal", func() { It("should use Predicates to filter UpdateEvents on PartialObjectMetadata", func() { set = false - partialMetadataInstance = internal.NewEventHandler[*metav1.PartialObjectMetadata](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ - predicate.Funcs{UpdateFunc: func(updateEvent event.UpdateEvent) bool { return false }}, + partialMetadataInstance = internal.NewEventHandler(ctx, &controllertest.Queue{}, metasetfuncs, []predicate.ObjectPredicate[*metav1.PartialObjectMetadata]{ + predicate.ObjectFuncs[*metav1.PartialObjectMetadata]{UpdateFunc: func(old, new *metav1.PartialObjectMetadata) bool { return false }}, }) partialMetadataInstance.OnUpdate(pom, newPom) Expect(set).To(BeFalse()) set = false - partialMetadataInstance = internal.NewEventHandler[*metav1.PartialObjectMetadata](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ - predicate.Funcs{UpdateFunc: func(event.UpdateEvent) bool { return true }}, + partialMetadataInstance = internal.NewEventHandler(ctx, &controllertest.Queue{}, metasetfuncs, []predicate.ObjectPredicate[*metav1.PartialObjectMetadata]{ + predicate.ObjectFuncs[*metav1.PartialObjectMetadata]{UpdateFunc: func(old, new *metav1.PartialObjectMetadata) bool { return true }}, }) partialMetadataInstance.OnUpdate(pom, newPom) Expect(set).To(BeTrue()) set = false - partialMetadataInstance = internal.NewEventHandler[*metav1.PartialObjectMetadata](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ - predicate.Funcs{UpdateFunc: func(event.UpdateEvent) bool { return true }}, - predicate.Funcs{UpdateFunc: func(event.UpdateEvent) bool { return false }}, + partialMetadataInstance = internal.NewEventHandler(ctx, &controllertest.Queue{}, metasetfuncs, []predicate.ObjectPredicate[*metav1.PartialObjectMetadata]{ + predicate.ObjectFuncs[*metav1.PartialObjectMetadata]{UpdateFunc: func(old, new *metav1.PartialObjectMetadata) bool { return true }}, + predicate.ObjectFuncs[*metav1.PartialObjectMetadata]{UpdateFunc: func(old, new *metav1.PartialObjectMetadata) bool { return false }}, }) partialMetadataInstance.OnUpdate(pom, newPom) Expect(set).To(BeFalse()) set = false - partialMetadataInstance = internal.NewEventHandler[*metav1.PartialObjectMetadata](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ - predicate.Funcs{UpdateFunc: func(event.UpdateEvent) bool { return false }}, - predicate.Funcs{UpdateFunc: func(event.UpdateEvent) bool { return true }}, + partialMetadataInstance = internal.NewEventHandler(ctx, &controllertest.Queue{}, metasetfuncs, []predicate.ObjectPredicate[*metav1.PartialObjectMetadata]{ + predicate.ObjectFuncs[*metav1.PartialObjectMetadata]{UpdateFunc: func(old, new *metav1.PartialObjectMetadata) bool { return false }}, + predicate.ObjectFuncs[*metav1.PartialObjectMetadata]{UpdateFunc: func(old, new *metav1.PartialObjectMetadata) bool { return true }}, }) partialMetadataInstance.OnUpdate(pom, newPom) Expect(set).To(BeFalse()) set = false - partialMetadataInstance = internal.NewEventHandler[*metav1.PartialObjectMetadata](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ - predicate.Funcs{CreateFunc: func(event.CreateEvent) bool { return true }}, - predicate.Funcs{CreateFunc: func(event.CreateEvent) bool { return true }}, + partialMetadataInstance = internal.NewEventHandler(ctx, &controllertest.Queue{}, metasetfuncs, []predicate.ObjectPredicate[*metav1.PartialObjectMetadata]{ + predicate.ObjectFuncs[*metav1.PartialObjectMetadata]{CreateFunc: func(obj *metav1.PartialObjectMetadata) bool { return true }}, + predicate.ObjectFuncs[*metav1.PartialObjectMetadata]{CreateFunc: func(obj *metav1.PartialObjectMetadata) bool { return true }}, }) partialMetadataInstance.OnUpdate(pom, newPom) Expect(set).To(BeTrue()) @@ -312,48 +323,48 @@ var _ = Describe("Internal", func() { }) It("should create a DeleteEvent", func() { - funcs.DeleteFunc = func(ctx context.Context, evt event.DeleteEvent, q workqueue.RateLimitingInterface) { + funcs.DeleteFunc = func(ctx context.Context, obj *corev1.Pod, q workqueue.RateLimitingInterface) { defer GinkgoRecover() - Expect(evt.Object).To(Equal(pod)) + Expect(obj).To(Equal(pod)) } instance.OnDelete(pod) }) It("should used Predicates to filter DeleteEvents", func() { set = false - instance = internal.NewEventHandler[*corev1.Pod](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ - predicate.Funcs{DeleteFunc: func(event.DeleteEvent) bool { return false }}, + instance = internal.NewEventHandler(ctx, &controllertest.Queue{}, setfuncs, []predicate.ObjectPredicate[*corev1.Pod]{ + predicate.ObjectFuncs[*corev1.Pod]{DeleteFunc: func(*corev1.Pod) bool { return false }}, }) instance.OnDelete(pod) Expect(set).To(BeFalse()) set = false - instance = internal.NewEventHandler[*corev1.Pod](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ - predicate.Funcs{DeleteFunc: func(event.DeleteEvent) bool { return true }}, + instance = internal.NewEventHandler(ctx, &controllertest.Queue{}, setfuncs, []predicate.ObjectPredicate[*corev1.Pod]{ + predicate.ObjectFuncs[*corev1.Pod]{DeleteFunc: func(*corev1.Pod) bool { return true }}, }) instance.OnDelete(pod) Expect(set).To(BeTrue()) set = false - instance = internal.NewEventHandler[*corev1.Pod](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ - predicate.Funcs{DeleteFunc: func(event.DeleteEvent) bool { return true }}, - predicate.Funcs{DeleteFunc: func(event.DeleteEvent) bool { return false }}, + instance = internal.NewEventHandler(ctx, &controllertest.Queue{}, setfuncs, []predicate.ObjectPredicate[*corev1.Pod]{ + predicate.ObjectFuncs[*corev1.Pod]{DeleteFunc: func(*corev1.Pod) bool { return true }}, + predicate.ObjectFuncs[*corev1.Pod]{DeleteFunc: func(*corev1.Pod) bool { return false }}, }) instance.OnDelete(pod) Expect(set).To(BeFalse()) set = false - instance = internal.NewEventHandler[*corev1.Pod](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ - predicate.Funcs{DeleteFunc: func(event.DeleteEvent) bool { return false }}, - predicate.Funcs{DeleteFunc: func(event.DeleteEvent) bool { return true }}, + instance = internal.NewEventHandler(ctx, &controllertest.Queue{}, setfuncs, []predicate.ObjectPredicate[*corev1.Pod]{ + predicate.ObjectFuncs[*corev1.Pod]{DeleteFunc: func(*corev1.Pod) bool { return false }}, + predicate.ObjectFuncs[*corev1.Pod]{DeleteFunc: func(*corev1.Pod) bool { return true }}, }) instance.OnDelete(pod) Expect(set).To(BeFalse()) set = false - instance = internal.NewEventHandler[*corev1.Pod](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ - predicate.Funcs{DeleteFunc: func(event.DeleteEvent) bool { return true }}, - predicate.Funcs{DeleteFunc: func(event.DeleteEvent) bool { return true }}, + instance = internal.NewEventHandler(ctx, &controllertest.Queue{}, setfuncs, []predicate.ObjectPredicate[*corev1.Pod]{ + predicate.ObjectFuncs[*corev1.Pod]{DeleteFunc: func(*corev1.Pod) bool { return true }}, + predicate.ObjectFuncs[*corev1.Pod]{DeleteFunc: func(*corev1.Pod) bool { return true }}, }) instance.OnDelete(pod) Expect(set).To(BeTrue()) @@ -361,39 +372,39 @@ var _ = Describe("Internal", func() { It("should use Predicates to filter DeleteEvents", func() { set = false - partialMetadataInstance = internal.NewEventHandler[*metav1.PartialObjectMetadata](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ - predicate.Funcs{DeleteFunc: func(event.DeleteEvent) bool { return false }}, + partialMetadataInstance = internal.NewEventHandler(ctx, &controllertest.Queue{}, metasetfuncs, []predicate.ObjectPredicate[*metav1.PartialObjectMetadata]{ + predicate.ObjectFuncs[*metav1.PartialObjectMetadata]{DeleteFunc: func(*metav1.PartialObjectMetadata) bool { return false }}, }) partialMetadataInstance.OnDelete(pom) Expect(set).To(BeFalse()) set = false - partialMetadataInstance = internal.NewEventHandler[*metav1.PartialObjectMetadata](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ - predicate.Funcs{DeleteFunc: func(event.DeleteEvent) bool { return true }}, + partialMetadataInstance = internal.NewEventHandler(ctx, &controllertest.Queue{}, metasetfuncs, []predicate.ObjectPredicate[*metav1.PartialObjectMetadata]{ + predicate.ObjectFuncs[*metav1.PartialObjectMetadata]{DeleteFunc: func(*metav1.PartialObjectMetadata) bool { return true }}, }) partialMetadataInstance.OnDelete(pom) Expect(set).To(BeTrue()) set = false - partialMetadataInstance = internal.NewEventHandler[*metav1.PartialObjectMetadata](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ - predicate.Funcs{DeleteFunc: func(event.DeleteEvent) bool { return true }}, - predicate.Funcs{DeleteFunc: func(event.DeleteEvent) bool { return false }}, + partialMetadataInstance = internal.NewEventHandler(ctx, &controllertest.Queue{}, metasetfuncs, []predicate.ObjectPredicate[*metav1.PartialObjectMetadata]{ + predicate.ObjectFuncs[*metav1.PartialObjectMetadata]{DeleteFunc: func(*metav1.PartialObjectMetadata) bool { return true }}, + predicate.ObjectFuncs[*metav1.PartialObjectMetadata]{DeleteFunc: func(*metav1.PartialObjectMetadata) bool { return false }}, }) partialMetadataInstance.OnDelete(pom) Expect(set).To(BeFalse()) set = false - partialMetadataInstance = internal.NewEventHandler[*metav1.PartialObjectMetadata](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ - predicate.Funcs{DeleteFunc: func(event.DeleteEvent) bool { return false }}, - predicate.Funcs{DeleteFunc: func(event.DeleteEvent) bool { return true }}, + partialMetadataInstance = internal.NewEventHandler(ctx, &controllertest.Queue{}, metasetfuncs, []predicate.ObjectPredicate[*metav1.PartialObjectMetadata]{ + predicate.ObjectFuncs[*metav1.PartialObjectMetadata]{DeleteFunc: func(*metav1.PartialObjectMetadata) bool { return false }}, + predicate.ObjectFuncs[*metav1.PartialObjectMetadata]{DeleteFunc: func(*metav1.PartialObjectMetadata) bool { return true }}, }) partialMetadataInstance.OnDelete(pom) Expect(set).To(BeFalse()) set = false - partialMetadataInstance = internal.NewEventHandler[*metav1.PartialObjectMetadata](ctx, &controllertest.Queue{}, setfuncs, []predicate.Predicate{ - predicate.Funcs{DeleteFunc: func(event.DeleteEvent) bool { return true }}, - predicate.Funcs{DeleteFunc: func(event.DeleteEvent) bool { return true }}, + partialMetadataInstance = internal.NewEventHandler(ctx, &controllertest.Queue{}, metasetfuncs, []predicate.ObjectPredicate[*metav1.PartialObjectMetadata]{ + predicate.ObjectFuncs[*metav1.PartialObjectMetadata]{DeleteFunc: func(*metav1.PartialObjectMetadata) bool { return true }}, + predicate.ObjectFuncs[*metav1.PartialObjectMetadata]{DeleteFunc: func(*metav1.PartialObjectMetadata) bool { return true }}, }) partialMetadataInstance.OnDelete(pom) Expect(set).To(BeTrue()) @@ -411,20 +422,6 @@ var _ = Describe("Internal", func() { instance.OnDelete(FooRuntimeObject{}) }) - It("should create a DeleteEvent from a tombstone", func() { - - tombstone := cache.DeletedFinalStateUnknown{ - Obj: pod, - } - funcs.DeleteFunc = func(ctx context.Context, evt event.DeleteEvent, q workqueue.RateLimitingInterface) { - defer GinkgoRecover() - Expect(evt.Object).To(Equal(pod)) - Expect(evt.DeleteStateUnknown).Should(BeTrue()) - } - - instance.OnDelete(tombstone) - }) - It("should not call Delete EventHandler if an object is not a partial object metadata object", func() { partialMetadataInstance.OnDelete(&corev1.Secret{}) }) diff --git a/pkg/internal/source/kind.go b/pkg/internal/source/kind.go index 738749bc5a..0aa42861a8 100644 --- a/pkg/internal/source/kind.go +++ b/pkg/internal/source/kind.go @@ -31,10 +31,20 @@ type Kind[T client.Object] struct { startCancel func() } +// SetPredicates implements source.SyncingSource. +func (ks *Kind[T]) SetPredicates(...predicate.PredicateConstraint) { + panic("unimplemented") +} + // Start is internal and should be called only by the Controller to register an EventHandler with the Informer // to enqueue reconcile.Requests. -func (ks *Kind[T]) Start(ctx context.Context, handler handler.EventHandler, queue workqueue.RateLimitingInterface, +func (ks *Kind[T]) Start(ctx context.Context, h handler.EventHandler, queue workqueue.RateLimitingInterface, prct ...predicate.Predicate) error { + return ks.Run(ctx, handler.ObjectFuncAdapter[T](h), queue, predicate.ObjectPredicatesAdapter[T](prct...)...) +} + +func (ks *Kind[T]) Run(ctx context.Context, handler handler.ObjectHandler[T], queue workqueue.RateLimitingInterface, + prct ...predicate.ObjectPredicate[T]) error { if reflect.DeepEqual(ks.Type, *new(T)) { return fmt.Errorf("must create Kind with a non-nil object") } @@ -80,7 +90,7 @@ func (ks *Kind[T]) Start(ctx context.Context, handler handler.EventHandler, queu return } - _, err := i.AddEventHandler(NewEventHandler[T](ctx, queue, handler, prct).HandlerFuncs()) + _, err := i.AddEventHandler(NewEventHandler(ctx, queue, handler, prct).HandlerFuncs()) if err != nil { ks.started <- err return diff --git a/pkg/predicate/predicate.go b/pkg/predicate/predicate.go index 3200313089..62e9965a0d 100644 --- a/pkg/predicate/predicate.go +++ b/pkg/predicate/predicate.go @@ -28,6 +28,10 @@ import ( var log = logf.RuntimeLog.WithName("predicate").WithName("eventFilters") +type PredicateConstraint interface { + Register() +} + // Predicate filters events before enqueuing the keys. type Predicate interface { // Create returns true if the Create event should be processed @@ -43,6 +47,21 @@ type Predicate interface { Generic(event.GenericEvent) bool } +// ObjectPredicate filters events for type before enqueuing the keys. +type ObjectPredicate[T any] interface { + // Create returns true if the Create event should be processed + OnCreate(obj T) bool + + // Delete returns true if the Delete event should be processed + OnDelete(obj T) bool + + // Update returns true if the Update event should be processed + OnUpdate(old, new T) bool + + // Generic returns true if the Generic event should be processed + OnGeneric(obj T) bool +} + var _ Predicate = Funcs{} var _ Predicate = ResourceVersionChangedPredicate{} var _ Predicate = GenerationChangedPredicate{} @@ -50,6 +69,9 @@ var _ Predicate = AnnotationChangedPredicate{} var _ Predicate = or{} var _ Predicate = and{} var _ Predicate = not{} +var _ Predicate = ObjectFuncs[any]{} +var _ ObjectPredicate[any] = ObjectFuncs[any]{} +var _ Predicate = ObjectFuncs[any]{} // Funcs is a function that implements Predicate. type Funcs struct { @@ -98,6 +120,66 @@ func (p Funcs) Generic(e event.GenericEvent) bool { return true } +// Funcs is a function that implements Predicate. +type ObjectFuncs[T any] struct { + // Create returns true if the Create event should be processed + CreateFunc func(obj T) bool + + // Delete returns true if the Delete event should be processed + DeleteFunc func(obj T) bool + + // Update returns true if the Update event should be processed + UpdateFunc func(old, new T) bool + + // Generic returns true if the Generic event should be processed + GenericFunc func(obj T) bool +} + +// Update implements Predicate. +func (p ObjectFuncs[T]) Update(e event.UpdateEvent) bool { + new, ok := e.ObjectNew.(T) + old, oldOk := e.ObjectOld.(T) + return ok && oldOk && p.OnUpdate(old, new) +} + +// Generic implements Predicate. +func (p ObjectFuncs[T]) Generic(e event.GenericEvent) bool { + obj, ok := e.Object.(T) + return ok && p.OnGeneric(obj) +} + +// Create implements Predicate. +func (p ObjectFuncs[T]) Create(e event.CreateEvent) bool { + obj, ok := e.Object.(T) + return ok && p.OnCreate(obj) +} + +// Delete implements Predicate. +func (p ObjectFuncs[T]) Delete(e event.DeleteEvent) bool { + obj, ok := e.Object.(T) + return ok && p.OnDelete(obj) +} + +// Update implements Predicate. +func (p ObjectFuncs[T]) OnUpdate(old, new T) bool { + return p.UpdateFunc == nil || p.UpdateFunc(old, new) +} + +// Generic implements Predicate. +func (p ObjectFuncs[T]) OnGeneric(obj T) bool { + return p.GenericFunc == nil || p.GenericFunc(obj) +} + +// Create implements Predicate. +func (p ObjectFuncs[T]) OnCreate(obj T) bool { + return p.CreateFunc == nil || p.CreateFunc(obj) +} + +// Delete implements Predicate. +func (p ObjectFuncs[T]) OnDelete(obj T) bool { + return p.DeleteFunc == nil || p.DeleteFunc(obj) +} + // NewPredicateFuncs returns a predicate funcs that applies the given filter function // on CREATE, UPDATE, DELETE and GENERIC events. For UPDATE events, the filter is applied // to the new object. @@ -118,6 +200,20 @@ func NewPredicateFuncs(filter func(object client.Object) bool) Funcs { } } +// NewPredicateFuncs returns a predicate funcs that applies the given filter function +// on CREATE, UPDATE, DELETE and GENERIC events. For UPDATE events, the filter is applied +// to the new object. +func NewObjectPredicateFuncs[T any](filter func(object T) bool) ObjectFuncs[T] { + return ObjectFuncs[T]{ + CreateFunc: filter, + DeleteFunc: filter, + GenericFunc: filter, + UpdateFunc: func(_, new T) bool { + return filter(new) + }, + } +} + // ResourceVersionChangedPredicate implements a default update predicate function on resource version change. type ResourceVersionChangedPredicate struct { Funcs @@ -277,6 +373,59 @@ func (a and) Generic(e event.GenericEvent) bool { return true } +// All returns a composite predicate that implements a logical AND of the predicates passed to it. +func All[T any](predicates ...ObjectPredicate[T]) ObjectPredicate[T] { + return all[T]{predicates} +} + +type all[T any] struct { + predicates []ObjectPredicate[T] +} + +// OnCreate implements ObjectPredicate. +func (a all[T]) OnCreate(obj T) bool { + for _, p := range a.predicates { + if !p.OnCreate(obj) { + return false + } + } + + return true +} + +// OnDelete implements ObjectPredicate. +func (a all[T]) OnDelete(obj T) bool { + for _, p := range a.predicates { + if !p.OnDelete(obj) { + return false + } + } + + return true +} + +// OnGeneric implements ObjectPredicate. +func (a all[T]) OnGeneric(obj T) bool { + for _, p := range a.predicates { + if !p.OnGeneric(obj) { + return false + } + } + + return true +} + +// OnUpdate implements ObjectPredicate. +func (a all[T]) OnUpdate(old, new T) bool { + for _, p := range a.predicates { + if !p.OnUpdate(old, new) { + return false + } + } + + return true +} + // Or returns a composite predicate that implements a logical OR of the predicates passed to it. func Or(predicates ...Predicate) Predicate { return or{predicates} @@ -322,6 +471,59 @@ func (o or) Generic(e event.GenericEvent) bool { return false } +// Any returns a composite predicate that implements a logical OR of the predicates passed to it. +func Any[T any](predicates ...ObjectPredicate[T]) ObjectPredicate[T] { + return anyOf[T]{predicates} +} + +type anyOf[T any] struct { + predicates []ObjectPredicate[T] +} + +// OnCreate implements ObjectPredicate. +func (a anyOf[T]) OnCreate(obj T) bool { + for _, p := range a.predicates { + if p.OnCreate(obj) { + return true + } + } + + return false +} + +// OnDelete implements ObjectPredicate. +func (a anyOf[T]) OnDelete(obj T) bool { + for _, p := range a.predicates { + if p.OnDelete(obj) { + return true + } + } + + return false +} + +// OnGeneric implements ObjectPredicate. +func (a anyOf[T]) OnGeneric(obj T) bool { + for _, p := range a.predicates { + if p.OnGeneric(obj) { + return true + } + } + + return false +} + +// OnUpdate implements ObjectPredicate. +func (a anyOf[T]) OnUpdate(old, new T) bool { + for _, p := range a.predicates { + if p.OnUpdate(old, new) { + return true + } + } + + return false +} + // Not returns a predicate that implements a logical NOT of the predicate passed to it. func Not(predicate Predicate) Predicate { return not{predicate} @@ -347,6 +549,35 @@ func (n not) Generic(e event.GenericEvent) bool { return !n.predicate.Generic(e) } +// Neg returns a predicate that implements a logical NOT of the predicate passed to it. +func Neg[T any](predicate ObjectPredicate[T]) ObjectPredicate[T] { + return neg[T]{predicate} +} + +type neg[T any] struct { + predicate ObjectPredicate[T] +} + +// OnCreate implements ObjectPredicate. +func (n neg[T]) OnCreate(obj T) bool { + return !n.predicate.OnCreate(obj) +} + +// OnDelete implements ObjectPredicate. +func (n neg[T]) OnDelete(obj T) bool { + return !n.predicate.OnDelete(obj) +} + +// OnGeneric implements ObjectPredicate. +func (n neg[T]) OnGeneric(obj T) bool { + return !n.predicate.OnGeneric(obj) +} + +// OnUpdate implements ObjectPredicate. +func (n neg[T]) OnUpdate(old T, new T) bool { + return !n.predicate.OnUpdate(old, new) +} + // LabelSelectorPredicate constructs a Predicate from a LabelSelector. // Only objects matching the LabelSelector will be admitted. func LabelSelectorPredicate(s metav1.LabelSelector) (Predicate, error) { @@ -358,3 +589,28 @@ func LabelSelectorPredicate(s metav1.LabelSelector) (Predicate, error) { return selector.Matches(labels.Set(o.GetLabels())) }), nil } + +func ObjectPredicateAdapter[T client.Object](h Predicate) ObjectPredicate[T] { + return ObjectFuncs[T]{ + CreateFunc: func(obj T) bool { + return h.Create(event.CreateEvent{Object: obj}) + }, + DeleteFunc: func(obj T) bool { + return h.Delete(event.DeleteEvent{Object: obj}) + }, + GenericFunc: func(obj T) bool { + return h.Generic(event.GenericEvent{Object: obj}) + }, + UpdateFunc: func(old, new T) bool { + return h.Update(event.UpdateEvent{ObjectOld: old, ObjectNew: new}) + }, + } +} + +func ObjectPredicatesAdapter[T client.Object](predicates ...Predicate) (prdt []ObjectPredicate[T]) { + for _, p := range predicates { + prdt = append(prdt, ObjectPredicatesAdapter[T](p)...) + } + + return +} diff --git a/pkg/source/source.go b/pkg/source/source.go index 78d1d06c15..0afc043963 100644 --- a/pkg/source/source.go +++ b/pkg/source/source.go @@ -48,6 +48,7 @@ type Source interface { // Start is internal and should be called only by the Controller to register an EventHandler with the Informer // to enqueue reconcile.Requests. Start(context.Context, handler.EventHandler, workqueue.RateLimitingInterface, ...predicate.Predicate) error + SetPredicates(...predicate.PredicateConstraint) } // SyncingSource is a source that needs syncing prior to being usable. The controller @@ -196,14 +197,14 @@ var _ Source = &Informer{} // Start is internal and should be called only by the Controller to register an EventHandler with the Informer // to enqueue reconcile.Requests. -func (is *Informer) Start(ctx context.Context, handler handler.EventHandler, queue workqueue.RateLimitingInterface, +func (is *Informer) Start(ctx context.Context, h handler.EventHandler, queue workqueue.RateLimitingInterface, prct ...predicate.Predicate) error { // Informer should have been specified by the user. if is.Informer == nil { return fmt.Errorf("must specify Informer.Informer") } - _, err := is.Informer.AddEventHandler(internal.NewEventHandler[client.Object](ctx, queue, handler, prct).HandlerFuncs()) + _, err := is.Informer.AddEventHandler(internal.NewEventHandler(ctx, queue, handler.ObjectFuncAdapter[client.Object](h), predicate.ObjectPredicatesAdapter[client.Object](prct...)).HandlerFuncs()) if err != nil { return err } From 3d2371d61098567bcdf42996e983c475327a59d4 Mon Sep 17 00:00:00 2001 From: Danil Grigorev Date: Tue, 2 Apr 2024 10:31:52 +0200 Subject: [PATCH 3/7] Add generics to source, split the interface, add builder methods Signed-off-by: Danil Grigorev --- alias.go | 8 - example_test.go | 9 +- examples/builtins/main.go | 9 +- pkg/builder/controller.go | 248 +++++------------- pkg/controller/controller.go | 4 +- pkg/controller/controller_integration_test.go | 11 +- pkg/controller/controller_test.go | 3 +- pkg/controller/example_test.go | 12 +- pkg/handler/enqueue_typed.go | 41 ++- pkg/handler/example_test.go | 99 ++++--- pkg/internal/controller/controller.go | 23 +- pkg/internal/controller/controller_test.go | 123 +++++---- .../recorder/recorder_integration_test.go | 4 +- pkg/internal/source/kind.go | 21 +- pkg/source/example_test.go | 12 +- pkg/source/source.go | 105 ++++++-- pkg/source/source_integration_test.go | 25 +- pkg/source/source_test.go | 113 ++++---- 18 files changed, 419 insertions(+), 451 deletions(-) diff --git a/alias.go b/alias.go index 9ba2d066de..1f8092f4ae 100644 --- a/alias.go +++ b/alias.go @@ -20,7 +20,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/controller-runtime/pkg/builder" - "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/config" cfg "sigs.k8s.io/controller-runtime/pkg/config" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" @@ -34,9 +33,6 @@ import ( // Builder builds an Application ControllerManagedBy (e.g. Operator) and returns a manager.Manager to start it. type Builder = builder.Builder -// WatchObject defines an interface on the watch object with inherited type info. -type WatchObject = builder.WatchObject - // Request contains the information necessary to reconcile a Kubernetes object. This includes the // information to uniquely identify the object - its Name and Namespace. It does NOT contain information about // any specific Event or the object contents itself. @@ -161,7 +157,3 @@ var ( // SetLogger sets a concrete logging implementation for all deferred Loggers. SetLogger = log.SetLogger ) - -func Object[T client.Object](obj T) WatchObject { - return builder.Object(obj) -} diff --git a/example_test.go b/example_test.go index 29339452ab..2890699d56 100644 --- a/example_test.go +++ b/example_test.go @@ -30,6 +30,7 @@ import ( "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/reconcile" @@ -84,10 +85,10 @@ func GenericExample() { os.Exit(1) } - err = ctrl. - NewControllerManagedBy(manager). // Create the Controller - With(ctrl.Object(&appsv1.ReplicaSet{})). // ReplicaSet is the Application API - Own(ctrl.Object(&corev1.Pod{})). // ReplicaSet owns Pods created by it + b := ctrl.NewControllerManagedBy(manager) // Create the Controller + // ReplicaSet is the Application API + b.Add(builder.For(b, &appsv1.ReplicaSet{})). + Add(builder.Owns(b, &appsv1.ReplicaSet{}, &corev1.Pod{})). // ReplicaSet owns Pods created by it Complete(&ReplicaSetReconciler{Client: manager.GetClient()}) if err != nil { log.Error(err, "could not create controller") diff --git a/examples/builtins/main.go b/examples/builtins/main.go index 8ea173b248..f83180a6cd 100644 --- a/examples/builtins/main.go +++ b/examples/builtins/main.go @@ -59,14 +59,17 @@ func main() { } // Watch ReplicaSets and enqueue ReplicaSet object key - if err := c.Watch(source.Kind(mgr.GetCache(), &appsv1.ReplicaSet{}), &handler.EnqueueRequestForObject{}); err != nil { + src := source.Kind(mgr.GetCache(), &appsv1.ReplicaSet{}) + src.Prepare(&handler.EnqueueRequestForObject{}) + if err := c.Watch(src); err != nil { entryLog.Error(err, "unable to watch ReplicaSets") os.Exit(1) } // Watch Pods and enqueue owning ReplicaSet key - if err := c.Watch(source.Kind(mgr.GetCache(), &corev1.Pod{}), - handler.EnqueueRequestForOwner(mgr.GetScheme(), mgr.GetRESTMapper(), &appsv1.ReplicaSet{}, handler.OnlyControllerOwner())); err != nil { + src = source.Kind(mgr.GetCache(), &corev1.Pod{}) + src.Prepare(handler.EnqueueRequestForOwner(mgr.GetScheme(), mgr.GetRESTMapper(), &appsv1.ReplicaSet{}, handler.OnlyControllerOwner())) + if err := c.Watch(src); err != nil { entryLog.Error(err, "unable to watch Pods") os.Exit(1) } diff --git a/pkg/builder/controller.go b/pkg/builder/controller.go index abc80291ef..a3b68da323 100644 --- a/pkg/builder/controller.go +++ b/pkg/builder/controller.go @@ -26,7 +26,6 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/klog/v2" - "sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/apiutil" "sigs.k8s.io/controller-runtime/pkg/controller" @@ -53,58 +52,15 @@ const ( projectAsMetadata ) -// WatchObject is an interface on an object wrapper with preserved type information -type WatchObject interface { - GetObject() client.Object - SetSource(cache.Cache) source.SyncingSource - // SetPredicates() -} - -type watchObject[T client.Object] struct { - object T - source source.SyncingSource -} - -// // SetPredicates implements WatchObject. -// func (w watchObject[T]) SetPredicates() { -// w.source.Start() -// } - -// SetSource returns inner client.Object -func (w watchObject[T]) GetObject() client.Object { - return w.object -} - -// SetSource sets and returns the source.SyncingSource on the object -func (w watchObject[T]) SetSource(cache cache.Cache) source.SyncingSource { - if w.source == nil { - w.source = source.Object(cache, w.object) - } - - return w.source -} - -// Object constructs a wrapper on a generic object with stored type information -func Object[T client.Object](obj T) WatchObject { - return watchObject[T]{object: obj} -} - -type State struct { - options []Option - mgr manager.Manager -} - // Builder builds a Controller. type Builder struct { forInput ForInput ownsInput []OwnsInput watchesInput []WatchesInput - blocks []func(State) - options []Option + rawWatches []source.Source mgr manager.Manager globalPredicates []predicate.Predicate ctrl controller.Controller - ctrls []controller.ControllerConstraint ctrlOptions controller.Options name string } @@ -116,7 +72,7 @@ func ControllerManagedBy(m manager.Manager) *Builder { // ForInput represents the information set by the For method. type ForInput struct { - object WatchObject + object client.Object predicates []predicate.Predicate objectProjection objectProjection err error @@ -126,22 +82,12 @@ type ForInput struct { // update events by *reconciling the object*. // This is the equivalent of calling // Watches(&source.Kind{Type: apiType}, &handler.EnqueueRequestForObject{}). -func (blder *Builder) For(object client.Object, opts ...Option) *Builder { - return blder.With(Object(object), opts...) -} - -// With defines the type of Object being *reconciled*, and configures the ControllerManagedBy to respond to create / delete / -// update events by *reconciling the object*. -// This is the equivalent of calling -// Watches(&source.Kind{Type: apiType}, &handler.EnqueueRequestForObject{}). -// -// Unlike For, With ensures the type of client.Object returned by predicates or event handlers. -func (blder *Builder) With(watch WatchObject, opts ...Option) *Builder { +func (blder *Builder) For(object client.Object, opts ...ForOption) *Builder { if blder.forInput.object != nil { blder.forInput.err = fmt.Errorf("For(...) should only be called once, could not assign multiple objects for reconciliation") return blder } - input := ForInput{object: watch} + input := ForInput{object: object} for _, opt := range opts { opt.ApplyToFor(&input) } @@ -152,11 +98,10 @@ func (blder *Builder) With(watch WatchObject, opts ...Option) *Builder { // OwnsInput represents the information set by Owns method. type OwnsInput struct { - matchEveryOwner bool - object WatchObject - predicates []predicate.Predicate - genericPredicates []predicate.PredicateConstraint - objectProjection objectProjection + matchEveryOwner bool + object client.Object + predicates []predicate.Predicate + objectProjection objectProjection } // Owns defines types of Objects being *generated* by the ControllerManagedBy, and configures the ControllerManagedBy to respond to @@ -167,21 +112,7 @@ type OwnsInput struct { // // By default, this is the equivalent of calling // Watches(object, handler.EnqueueRequestForOwner([...], ownerType, OnlyControllerOwner())). -func (blder *Builder) Owns(object client.Object, opts ...Option) *Builder { - return blder.Own(Object(object), opts...) -} - -// Own defines types of Objects being *generated* by the ControllerManagedBy, and configures the ControllerManagedBy to respond to -// create / delete / update events by *reconciling the owner object*. -// -// The default behavior reconciles only the first controller-type OwnerReference of the given type. -// Use Owns(object, builder.MatchEveryOwner) to reconcile all owners. -// -// By default, this is the equivalent of calling -// Watches(object, handler.EnqueueRequestForOwner([...], ownerType, OnlyControllerOwner())). -// -// Unlike Owns, Own ensures the type of client.Object returned by predicates or event handlers. -func (blder *Builder) Own(object WatchObject, opts ...Option) *Builder { +func (blder *Builder) Owns(object client.Object, opts ...OwnsOption) *Builder { input := OwnsInput{object: object} for _, opt := range opts { opt.ApplyToOwns(&input) @@ -193,7 +124,7 @@ func (blder *Builder) Own(object WatchObject, opts ...Option) *Builder { // WatchesInput represents the information set by Watches method. type WatchesInput struct { - src source.Source + src source.SourcePrepare eventHandler handler.EventHandler predicates []predicate.Predicate objectProjection objectProjection @@ -204,72 +135,11 @@ type WatchesInput struct { // // This is the equivalent of calling // WatchesRawSource(source.Kind(cache, object), eventHandler, opts...). -func (blder *Builder) Watches(object client.Object, eventHandler handler.EventHandler, opts ...Option) *Builder { - return blder.Watch(Object(object), eventHandler, opts...) -} - -// Watch defines the type of Object to watch, and configures the ControllerManagedBy to respond to create / delete / -// update events by *reconciling the object* with the given EventHandler. -// -// This is the equivalent of calling -// WatchesRawSource(source.Kind(cache, object), eventHandler, opts...). -// Unlike Watches, Watch ensures the type of client.Object returned by predicates or event handlers. -func (blder *Builder) Watch(object WatchObject, eventHandler handler.EventHandler, opts ...Option) *Builder { - src := object.SetSource(blder.mgr.GetCache()) +func (blder *Builder) Watches(object client.Object, eventHandler handler.EventHandler, opts ...WatchesOption) *Builder { + src := source.Kind(blder.mgr.GetCache(), object) return blder.WatchesRawSource(src, eventHandler, opts...) } -func (blder *Builder) Add(w AddWatch) *Builder { - w.AddTo(blder) - return blder -} - -func (blder *Builder) AddBlock(f func(State)) { - blder.blocks = append(blder.blocks, f) -} - -type AddWatch interface { - SetSource(*Builder) - SetEventHandler(*Builder) - GetOptions() []Option - AddTo(*Builder) -} - -type Adder interface { - AddTo(*Builder) *Builder -} - -var _ AddWatch = &RawAdder[any]{} - -type RawAdder[T any] struct { - *Raw[T] -} - -func (r *RawAdder[T]) AddTo(b *Builder) { - b.options = append(b.options, r.GetOptions()...) // state - // Establishing watches, event handlers - r.SetSource(b) - r.SetEventHandler(b) -} - -func (r *RawAdder[T]) SetSource(b *Builder) {} - -func (r *RawAdder[T]) SetEventHandler(b *Builder) { - b.AddBlock(func(s State) { - allPredicates := append([]predicate.Predicate(nil), s.options) - allPredicates = append(allPredicates, own.predicates...) - if err := blder.ctrl.Watch(src, hdler, allPredicates...); err != nil { - return err - } - }) -} - -func (blder *RawAdder[T]) GetOptions() []Option { - return []Option{} -} - -var _ controller.ControllerConstraint - // WatchesMetadata is the same as Watches, but forces the internal cache to only watch PartialObjectMetadata. // // This is useful when watching lots of objects, really big objects, or objects for which you only know @@ -297,27 +167,17 @@ var _ controller.ControllerConstraint // In the first case, controller-runtime will create another cache for the // concrete type on top of the metadata cache; this increases memory // consumption and leads to race conditions as caches are not in sync. -func (blder *Builder) WatchesMetadata(object client.Object, eventHandler handler.EventHandler, opts ...Option) *Builder { +func (blder *Builder) WatchesMetadata(object client.Object, eventHandler handler.EventHandler, opts ...WatchesOption) *Builder { opts = append(opts, OnlyMetadata) - _ = RawAdder[any]{} return blder.Watches(object, eventHandler, opts...) } -type Raw[T any] struct { - src source.ObjectSource[T] - eventHandler handler.ObjectHandler[T] - predicates []predicate.ObjectPredicate[T] -} - // WatchesRawSource exposes the lower-level ControllerManagedBy Watches functions through the builder. // Specified predicates are registered only for given source. // // STOP! Consider using For(...), Owns(...), Watches(...), WatchesMetadata(...) instead. // This method is only exposed for more advanced use cases, most users should use one of the higher level functions. -// -// Example: -// WatchesRawSource(source.Kind(cache, &corev1.Pod{}), eventHandler, opts...) // ensure that source propagates only valid Pod objects. -func (blder *Builder) WatchesRawSource(src source.Source, eventHandler handler.EventHandler, opts ...Option) *Builder { +func (blder *Builder) WatchesRawSource(src source.SourcePrepare, eventHandler handler.EventHandler, opts ...WatchesOption) *Builder { input := WatchesInput{src: src, eventHandler: eventHandler} for _, opt := range opts { opt.ApplyToWatches(&input) @@ -327,6 +187,37 @@ func (blder *Builder) WatchesRawSource(src source.Source, eventHandler handler.E return blder } +func For[T client.Object](blder *Builder, object T, prct ...predicate.ObjectPredicate[T]) source.Source { + src := source.ObjectKind(blder.mgr.GetCache(), object) + src.PrepareObject(&handler.EnqueueRequest[T]{}, prct...) + + return src +} + +func Owns[F, T client.Object](blder *Builder, owner F, owned T) source.Source { + src := source.ObjectKind(blder.mgr.GetCache(), owned) + + hdler := handler.EnqueueRequestForOwner( + blder.mgr.GetScheme(), blder.mgr.GetRESTMapper(), + owner, + ) + src.PrepareObject(handler.ObjectFuncAdapter[T](hdler)) + + return src +} + +func Watches[T client.Object](blder *Builder, object T, eventHandler handler.ObjectHandler[T], prct ...predicate.ObjectPredicate[T]) source.Source { + src := source.ObjectKind(blder.mgr.GetCache(), object) + src.PrepareObject(eventHandler, prct...) + + return src +} + +func (blder *Builder) Add(src source.Source) *Builder { + blder.rawWatches = append(blder.rawWatches, src) + return blder +} + // WithEventFilter sets the event filters, to filter which create/update/delete/generic events eventually // trigger reconciliations. For example, filtering on whether the resource version has changed. // Given predicate is added for all watched objects. @@ -409,23 +300,16 @@ func (blder *Builder) project(obj client.Object, proj objectProjection) (client. func (blder *Builder) doWatch() error { // Reconcile type if blder.forInput.object != nil { - src := blder.forInput.object.SetSource(blder.mgr.GetCache()) - if _, ok := blder.forInput.object.(watchObject[client.Object]); ok { - obj, err := blder.project(blder.forInput.object.GetObject(), blder.forInput.objectProjection) - if err != nil { - return err - } - src = source.Object(blder.mgr.GetCache(), obj) + obj, err := blder.project(blder.forInput.object, blder.forInput.objectProjection) + if err != nil { + return err } + src := source.Kind(blder.mgr.GetCache(), obj) hdler := &handler.EnqueueRequestForObject{} allPredicates := append([]predicate.Predicate(nil), blder.globalPredicates...) allPredicates = append(allPredicates, blder.forInput.predicates...) - for _, c := range blder.ctrls { - if err := c.DoWatch(); err != nil { - return err - } - } - if err := blder.ctrl.Watch(src, hdler, allPredicates...); err != nil { + src.Prepare(hdler, allPredicates...) + if err := blder.ctrl.Watch(src); err != nil { return err } } @@ -435,32 +319,30 @@ func (blder *Builder) doWatch() error { return errors.New("Owns() can only be used together with For()") } for _, own := range blder.ownsInput { - src := own.object.SetSource(blder.mgr.GetCache()) - if _, ok := own.object.(watchObject[client.Object]); ok { - obj, err := blder.project(own.object.GetObject(), own.objectProjection) - if err != nil { - return err - } - src = source.Object(blder.mgr.GetCache(), obj) + obj, err := blder.project(own.object, own.objectProjection) + if err != nil { + return err } + src := source.Kind(blder.mgr.GetCache(), obj) opts := []handler.OwnerOption{} if !own.matchEveryOwner { opts = append(opts, handler.OnlyControllerOwner()) } hdler := handler.EnqueueRequestForOwner( blder.mgr.GetScheme(), blder.mgr.GetRESTMapper(), - blder.forInput.object.GetObject(), + blder.forInput.object, opts..., ) allPredicates := append([]predicate.Predicate(nil), blder.globalPredicates...) allPredicates = append(allPredicates, own.predicates...) - if err := blder.ctrl.Watch(src, hdler, allPredicates...); err != nil { + src.Prepare(hdler, allPredicates...) + if err := blder.ctrl.Watch(src); err != nil { return err } } // Do the watch requests - if len(blder.watchesInput) == 0 && blder.forInput.object == nil { + if len(blder.watchesInput) == 0 && blder.forInput.object == nil && len(blder.rawWatches) == 0 { return errors.New("there are no watches configured, controller will never get triggered. Use For(), Owns() or Watches() to set them up") } for _, w := range blder.watchesInput { @@ -474,10 +356,18 @@ func (blder *Builder) doWatch() error { } allPredicates := append([]predicate.Predicate(nil), blder.globalPredicates...) allPredicates = append(allPredicates, w.predicates...) - if err := blder.ctrl.Watch(w.src, w.eventHandler, allPredicates...); err != nil { + w.src.Prepare(w.eventHandler, allPredicates...) + if err := blder.ctrl.Watch(w.src); err != nil { + return err + } + } + + for _, r := range blder.rawWatches { + if err := blder.ctrl.Watch(r); err != nil { return err } } + return nil } @@ -508,7 +398,7 @@ func (blder *Builder) doController(r reconcile.Reconciler) error { hasGVK := blder.forInput.object != nil if hasGVK { var err error - gvk, err = getGvk(blder.forInput.object.GetObject(), blder.mgr.GetScheme()) + gvk, err = getGvk(blder.forInput.object, blder.mgr.GetScheme()) if err != nil { return err } diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go index 4d6f6b8955..d143af33df 100644 --- a/pkg/controller/controller.go +++ b/pkg/controller/controller.go @@ -25,10 +25,8 @@ import ( "k8s.io/client-go/util/workqueue" "k8s.io/klog/v2" - "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/internal/controller" "sigs.k8s.io/controller-runtime/pkg/manager" - "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/ratelimiter" "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/source" @@ -90,7 +88,7 @@ type Controller interface { // Watch may be provided one or more Predicates to filter events before // they are given to the EventHandler. Events will be passed to the // EventHandler if all provided Predicates evaluate to true. - Watch(src source.Source, eventhandler handler.EventHandler, predicates ...predicate.Predicate) error + Watch(src source.Source) error // Start starts the controller. Start blocks until the context is closed or a // controller has an error starting. diff --git a/pkg/controller/controller_integration_test.go b/pkg/controller/controller_integration_test.go index 48facf1e94..cf95f57141 100644 --- a/pkg/controller/controller_integration_test.go +++ b/pkg/controller/controller_integration_test.go @@ -64,13 +64,14 @@ var _ = Describe("controller", func() { Expect(err).NotTo(HaveOccurred()) By("Watching Resources") - err = instance.Watch( - source.Kind(cm.GetCache(), &appsv1.ReplicaSet{}), - handler.EnqueueRequestForOwner(cm.GetScheme(), cm.GetRESTMapper(), &appsv1.Deployment{}), - ) + src := source.Kind(cm.GetCache(), &appsv1.ReplicaSet{}) + src.Prepare(handler.EnqueueRequestForOwner(cm.GetScheme(), cm.GetRESTMapper(), &appsv1.Deployment{})) + err = instance.Watch(src) Expect(err).NotTo(HaveOccurred()) - err = instance.Watch(source.Kind(cm.GetCache(), &appsv1.Deployment{}), &handler.EnqueueRequestForObject{}) + src = source.Kind(cm.GetCache(), &appsv1.Deployment{}) + src.Prepare(&handler.EnqueueRequestForObject{}) + err = instance.Watch(src) Expect(err).NotTo(HaveOccurred()) err = cm.GetClient().Get(ctx, types.NamespacedName{Name: "foo"}, &corev1.Namespace{}) diff --git a/pkg/controller/controller_test.go b/pkg/controller/controller_test.go index f1197827c5..f26416f0bc 100644 --- a/pkg/controller/controller_test.go +++ b/pkg/controller/controller_test.go @@ -101,7 +101,8 @@ var _ = Describe("controller.Controller", func() { Expect(err).NotTo(HaveOccurred()) c, err := controller.New("new-controller", m, controller.Options{Reconciler: rec}) - Expect(c.Watch(watch, &handler.EnqueueRequestForObject{})).To(Succeed()) + watch.Prepare(&handler.EnqueueRequestForObject{}) + Expect(c.Watch(watch)).To(Succeed()) Expect(err).NotTo(HaveOccurred()) go func() { diff --git a/pkg/controller/example_test.go b/pkg/controller/example_test.go index d4fa1aef0b..7a9ee367d7 100644 --- a/pkg/controller/example_test.go +++ b/pkg/controller/example_test.go @@ -71,7 +71,9 @@ func ExampleController() { } // Watch for Pod create / update / delete events and call Reconcile - err = c.Watch(source.Kind(mgr.GetCache(), &corev1.Pod{}), &handler.EnqueueRequestForObject{}) + src := source.Kind(mgr.GetCache(), &corev1.Pod{}) + src.Prepare(&handler.EnqueueRequestForObject{}) + err = c.Watch(src) if err != nil { log.Error(err, "unable to watch pods") os.Exit(1) @@ -108,7 +110,9 @@ func ExampleController_unstructured() { Version: "v1", }) // Watch for Pod create / update / delete events and call Reconcile - err = c.Watch(source.Kind(mgr.GetCache(), u), &handler.EnqueueRequestForObject{}) + src := source.Kind(mgr.GetCache(), u) + src.Prepare(&handler.EnqueueRequestForObject{}) + err = c.Watch(src) if err != nil { log.Error(err, "unable to watch pods") os.Exit(1) @@ -139,7 +143,9 @@ func ExampleNewUnmanaged() { os.Exit(1) } - if err := c.Watch(source.Kind(mgr.GetCache(), &corev1.Pod{}), &handler.EnqueueRequestForObject{}); err != nil { + src := source.Kind(mgr.GetCache(), &corev1.Pod{}) + src.Prepare(&handler.EnqueueRequestForObject{}) + if err := c.Watch(src); err != nil { log.Error(err, "unable to watch pods") os.Exit(1) } diff --git a/pkg/handler/enqueue_typed.go b/pkg/handler/enqueue_typed.go index 2fe76338f7..5801ee9f67 100644 --- a/pkg/handler/enqueue_typed.go +++ b/pkg/handler/enqueue_typed.go @@ -30,7 +30,6 @@ var _ EventHandler = &EnqueueRequest[metav1.Object]{} var _ ObjectHandler[metav1.Object] = &EnqueueRequest[metav1.Object]{} type Request interface { - comparable GetName() string GetNamespace() string } @@ -42,42 +41,34 @@ type EnqueueRequest[T Request] struct{} // OnCreate implements ObjectHandler. func (e *EnqueueRequest[T]) OnCreate(ctx context.Context, obj T, q workqueue.RateLimitingInterface) { - if obj != *new(T) { - q.Add(reconcile.Request{NamespacedName: types.NamespacedName{ - Name: obj.GetName(), - Namespace: obj.GetNamespace(), - }}) - } + q.Add(reconcile.Request{NamespacedName: types.NamespacedName{ + Name: obj.GetName(), + Namespace: obj.GetNamespace(), + }}) } // OnDelete implements ObjectHandler. func (e *EnqueueRequest[T]) OnDelete(ctx context.Context, obj T, q workqueue.RateLimitingInterface) { - if obj != *new(T) { - q.Add(reconcile.Request{NamespacedName: types.NamespacedName{ - Name: obj.GetName(), - Namespace: obj.GetNamespace(), - }}) - } + q.Add(reconcile.Request{NamespacedName: types.NamespacedName{ + Name: obj.GetName(), + Namespace: obj.GetNamespace(), + }}) } // OnGeneric implements ObjectHandler. func (e *EnqueueRequest[T]) OnGeneric(ctx context.Context, obj T, q workqueue.RateLimitingInterface) { - if obj != *new(T) { - q.Add(reconcile.Request{NamespacedName: types.NamespacedName{ - Name: obj.GetName(), - Namespace: obj.GetNamespace(), - }}) - } + q.Add(reconcile.Request{NamespacedName: types.NamespacedName{ + Name: obj.GetName(), + Namespace: obj.GetNamespace(), + }}) } // OnUpdate implements ObjectHandler. func (e *EnqueueRequest[T]) OnUpdate(ctx context.Context, oldObj T, newObj T, q workqueue.RateLimitingInterface) { - if oldObj != *new(T) { - q.Add(reconcile.Request{NamespacedName: types.NamespacedName{ - Name: oldObj.GetName(), - Namespace: oldObj.GetNamespace(), - }}) - } + q.Add(reconcile.Request{NamespacedName: types.NamespacedName{ + Name: oldObj.GetName(), + Namespace: oldObj.GetNamespace(), + }}) } // Create implements EventHandler. diff --git a/pkg/handler/example_test.go b/pkg/handler/example_test.go index 575ea05fca..273167ab6b 100644 --- a/pkg/handler/example_test.go +++ b/pkg/handler/example_test.go @@ -41,10 +41,9 @@ var ( // the Event (i.e. change caused by a Create, Update, Delete). func ExampleEnqueueRequestForObject() { // controller is a controller.controller - err := c.Watch( - source.Kind(mgr.GetCache(), &corev1.Pod{}), - &handler.EnqueueRequestForObject{}, - ) + src := source.Kind(mgr.GetCache(), &corev1.Pod{}) + src.Prepare(&handler.EnqueueRequestForObject{}) + err := c.Watch(src) if err != nil { // handle it } @@ -54,10 +53,9 @@ func ExampleEnqueueRequestForObject() { // owning (direct) Deployment responsible for the creation of the ReplicaSet. func ExampleEnqueueRequestForOwner() { // controller is a controller.controller - err := c.Watch( - source.Kind(mgr.GetCache(), &appsv1.ReplicaSet{}), - handler.EnqueueRequestForOwner(mgr.GetScheme(), mgr.GetRESTMapper(), &appsv1.Deployment{}, handler.OnlyControllerOwner()), - ) + src := source.Kind(mgr.GetCache(), &appsv1.ReplicaSet{}) + src.Prepare(handler.EnqueueRequestForOwner(mgr.GetScheme(), mgr.GetRESTMapper(), &appsv1.Deployment{}, handler.OnlyControllerOwner())) + err := c.Watch(src) if err != nil { // handle it } @@ -67,21 +65,20 @@ func ExampleEnqueueRequestForOwner() { // objects (of Type: MyKind) using a mapping function defined by the user. func ExampleEnqueueRequestsFromMapFunc() { // controller is a controller.controller - err := c.Watch( - source.Kind(mgr.GetCache(), &appsv1.Deployment{}), - handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, a client.Object) []reconcile.Request { - return []reconcile.Request{ - {NamespacedName: types.NamespacedName{ - Name: a.GetName() + "-1", - Namespace: a.GetNamespace(), - }}, - {NamespacedName: types.NamespacedName{ - Name: a.GetName() + "-2", - Namespace: a.GetNamespace(), - }}, - } - }), - ) + src := source.Kind(mgr.GetCache(), &appsv1.Deployment{}) + src.Prepare(handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, a client.Object) []reconcile.Request { + return []reconcile.Request{ + {NamespacedName: types.NamespacedName{ + Name: a.GetName() + "-1", + Namespace: a.GetNamespace(), + }}, + {NamespacedName: types.NamespacedName{ + Name: a.GetName() + "-2", + Namespace: a.GetNamespace(), + }}, + } + })) + err := c.Watch(src) if err != nil { // handle it } @@ -90,35 +87,35 @@ func ExampleEnqueueRequestsFromMapFunc() { // This example implements handler.EnqueueRequestForObject. func ExampleFuncs() { // controller is a controller.controller - err := c.Watch( - source.Kind(mgr.GetCache(), &corev1.Pod{}), - handler.Funcs{ - CreateFunc: func(ctx context.Context, e event.CreateEvent, q workqueue.RateLimitingInterface) { - q.Add(reconcile.Request{NamespacedName: types.NamespacedName{ - Name: e.Object.GetName(), - Namespace: e.Object.GetNamespace(), - }}) - }, - UpdateFunc: func(ctx context.Context, e event.UpdateEvent, q workqueue.RateLimitingInterface) { - q.Add(reconcile.Request{NamespacedName: types.NamespacedName{ - Name: e.ObjectNew.GetName(), - Namespace: e.ObjectNew.GetNamespace(), - }}) - }, - DeleteFunc: func(ctx context.Context, e event.DeleteEvent, q workqueue.RateLimitingInterface) { - q.Add(reconcile.Request{NamespacedName: types.NamespacedName{ - Name: e.Object.GetName(), - Namespace: e.Object.GetNamespace(), - }}) - }, - GenericFunc: func(ctx context.Context, e event.GenericEvent, q workqueue.RateLimitingInterface) { - q.Add(reconcile.Request{NamespacedName: types.NamespacedName{ - Name: e.Object.GetName(), - Namespace: e.Object.GetNamespace(), - }}) - }, + src := source.Kind(mgr.GetCache(), &corev1.Pod{}) + src.Prepare(handler.Funcs{ + CreateFunc: func(ctx context.Context, e event.CreateEvent, q workqueue.RateLimitingInterface) { + q.Add(reconcile.Request{NamespacedName: types.NamespacedName{ + Name: e.Object.GetName(), + Namespace: e.Object.GetNamespace(), + }}) }, - ) + UpdateFunc: func(ctx context.Context, e event.UpdateEvent, q workqueue.RateLimitingInterface) { + q.Add(reconcile.Request{NamespacedName: types.NamespacedName{ + Name: e.ObjectNew.GetName(), + Namespace: e.ObjectNew.GetNamespace(), + }}) + }, + DeleteFunc: func(ctx context.Context, e event.DeleteEvent, q workqueue.RateLimitingInterface) { + q.Add(reconcile.Request{NamespacedName: types.NamespacedName{ + Name: e.Object.GetName(), + Namespace: e.Object.GetNamespace(), + }}) + }, + GenericFunc: func(ctx context.Context, e event.GenericEvent, q workqueue.RateLimitingInterface) { + q.Add(reconcile.Request{NamespacedName: types.NamespacedName{ + Name: e.Object.GetName(), + Namespace: e.Object.GetNamespace(), + }}) + }, + }) + + err := c.Watch(src) if err != nil { // handle it } diff --git a/pkg/internal/controller/controller.go b/pkg/internal/controller/controller.go index 40ba0685d0..e3a96da7f1 100644 --- a/pkg/internal/controller/controller.go +++ b/pkg/internal/controller/controller.go @@ -29,10 +29,8 @@ import ( "k8s.io/apimachinery/pkg/util/uuid" "k8s.io/client-go/util/workqueue" - "sigs.k8s.io/controller-runtime/pkg/handler" ctrlmetrics "sigs.k8s.io/controller-runtime/pkg/internal/controller/metrics" logf "sigs.k8s.io/controller-runtime/pkg/log" - "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/ratelimiter" "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/source" @@ -81,7 +79,7 @@ type Controller struct { CacheSyncTimeout time.Duration // startWatches maintains a list of sources, handlers, and predicates to start when the controller is started. - startWatches []watchDescription + startWatches []source.Source // LogConstructor is used to construct a logger to then log messages to users during reconciliation, // or for example when a watch is started. @@ -96,13 +94,6 @@ type Controller struct { LeaderElected *bool } -// watchDescription contains all the information necessary to start a watch. -type watchDescription struct { - src source.Source - handler handler.EventHandler - predicates []predicate.Predicate -} - // Reconcile implements reconcile.Reconciler. func (c *Controller) Reconcile(ctx context.Context, req reconcile.Request) (_ reconcile.Result, err error) { defer func() { @@ -124,7 +115,7 @@ func (c *Controller) Reconcile(ctx context.Context, req reconcile.Request) (_ re } // Watch implements controller.Controller. -func (c *Controller) Watch(src source.Source, evthdler handler.EventHandler, prct ...predicate.Predicate) error { +func (c *Controller) Watch(src source.Source) error { c.mu.Lock() defer c.mu.Unlock() @@ -132,12 +123,12 @@ func (c *Controller) Watch(src source.Source, evthdler handler.EventHandler, prc // // These watches are going to be held on the controller struct until the manager or user calls Start(...). if !c.Started { - c.startWatches = append(c.startWatches, watchDescription{src: src, handler: evthdler, predicates: prct}) + c.startWatches = append(c.startWatches, src) return nil } c.LogConstructor(nil).Info("Starting EventSource", "source", src) - return src.Start(c.ctx, evthdler, c.Queue, prct...) + return src.Start(c.ctx, c.Queue) } // NeedLeaderElection implements the manager.LeaderElectionRunnable interface. @@ -179,9 +170,9 @@ func (c *Controller) Start(ctx context.Context) error { // caches to sync so that they have a chance to register their intendeded // caches. for _, watch := range c.startWatches { - c.LogConstructor(nil).Info("Starting EventSource", "source", fmt.Sprintf("%s", watch.src)) + c.LogConstructor(nil).Info("Starting EventSource", "source", fmt.Sprintf("%s", watch)) - if err := watch.src.Start(ctx, watch.handler, c.Queue, watch.predicates...); err != nil { + if err := watch.Start(ctx, c.Queue); err != nil { return err } } @@ -190,7 +181,7 @@ func (c *Controller) Start(ctx context.Context) error { c.LogConstructor(nil).Info("Starting Controller") for _, watch := range c.startWatches { - syncingSource, ok := watch.src.(source.SyncingSource) + syncingSource, ok := watch.(source.Syncing) if !ok { continue } diff --git a/pkg/internal/controller/controller_test.go b/pkg/internal/controller/controller_test.go index 96cd27e1e3..366f9e0ccc 100644 --- a/pkg/internal/controller/controller_test.go +++ b/pkg/internal/controller/controller_test.go @@ -42,7 +42,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/handler" ctrlmetrics "sigs.k8s.io/controller-runtime/pkg/internal/controller/metrics" "sigs.k8s.io/controller-runtime/pkg/internal/log" - "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/ratelimiter" "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/source" @@ -127,9 +126,9 @@ var _ = Describe("controller", func() { Describe("Start", func() { It("should return an error if there is an error waiting for the informers", func() { f := false - ctrl.startWatches = []watchDescription{{ - src: source.Kind(&informertest.FakeInformers{Synced: &f}, &corev1.Pod{}), - }} + ctrl.startWatches = []source.Source{ + source.Kind(&informertest.FakeInformers{Synced: &f}, &corev1.Pod{}), + } ctrl.Name = "foo" ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -145,9 +144,9 @@ var _ = Describe("controller", func() { Expect(err).NotTo(HaveOccurred()) c = &cacheWithIndefinitelyBlockingGetInformer{c} - ctrl.startWatches = []watchDescription{{ - src: source.Kind(c, &appsv1.Deployment{}), - }} + ctrl.startWatches = []source.Source{ + source.Kind(c, &appsv1.Deployment{}), + } ctrl.Name = "testcontroller" err = ctrl.Start(context.TODO()) @@ -162,12 +161,12 @@ var _ = Describe("controller", func() { c, err := cache.New(cfg, cache.Options{}) Expect(err).NotTo(HaveOccurred()) c = &cacheWithIndefinitelyBlockingGetInformer{c} - ctrl.startWatches = []watchDescription{{ - src: &singnallingSourceWrapper{ + ctrl.startWatches = []source.Source{ + &singnallingSourceWrapper{ SyncingSource: source.Kind(c, &appsv1.Deployment{}), cacheSyncDone: sourceSynced, }, - }} + } ctrl.Name = "testcontroller" ctx, cancel := context.WithCancel(context.TODO()) @@ -190,12 +189,12 @@ var _ = Describe("controller", func() { sourceSynced := make(chan struct{}) c, err := cache.New(cfg, cache.Options{}) Expect(err).NotTo(HaveOccurred()) - ctrl.startWatches = []watchDescription{{ - src: &singnallingSourceWrapper{ + ctrl.startWatches = []source.Source{ + &singnallingSourceWrapper{ SyncingSource: source.Kind(c, &appsv1.Deployment{}), cacheSyncDone: sourceSynced, }, - }} + } go func() { defer GinkgoRecover() @@ -228,20 +227,20 @@ var _ = Describe("controller", func() { } ins := &source.Channel{Source: ch} + ins.Prepare(handler.Funcs{ + GenericFunc: func(ctx context.Context, evt event.GenericEvent, q workqueue.RateLimitingInterface) { + defer GinkgoRecover() + close(processed) + }, + }) ins.DestBufferSize = 1 // send the event to the channel ch <- evt - ctrl.startWatches = []watchDescription{{ - src: ins, - handler: handler.Funcs{ - GenericFunc: func(ctx context.Context, evt event.GenericEvent, q workqueue.RateLimitingInterface) { - defer GinkgoRecover() - close(processed) - }, - }, - }} + ctrl.startWatches = []source.Source{ + ins, + } go func() { defer GinkgoRecover() @@ -255,52 +254,52 @@ var _ = Describe("controller", func() { defer cancel() ins := &source.Channel{} - ctrl.startWatches = []watchDescription{{ - src: ins, - }} + ctrl.startWatches = []source.Source{ + ins, + } e := ctrl.Start(ctx) Expect(e).To(HaveOccurred()) Expect(e.Error()).To(ContainSubstring("must specify Channel.Source")) }) - It("should call Start on sources with the appropriate EventHandler, Queue, and Predicates", func() { - pr1 := &predicate.Funcs{} - pr2 := &predicate.Funcs{} - evthdl := &handler.EnqueueRequestForObject{} - started := false - src := source.Func(func(ctx context.Context, e handler.EventHandler, q workqueue.RateLimitingInterface, p ...predicate.Predicate) error { - defer GinkgoRecover() - Expect(e).To(Equal(evthdl)) - Expect(q).To(Equal(ctrl.Queue)) - Expect(p).To(ConsistOf(pr1, pr2)) - - started = true - return nil - }) - Expect(ctrl.Watch(src, evthdl, pr1, pr2)).NotTo(HaveOccurred()) - - // Use a cancelled context so Start doesn't block - ctx, cancel := context.WithCancel(context.Background()) - cancel() - Expect(ctrl.Start(ctx)).To(Succeed()) - Expect(started).To(BeTrue()) - }) - - It("should return an error if there is an error starting sources", func() { - err := fmt.Errorf("Expected Error: could not start source") - src := source.Func(func(context.Context, handler.EventHandler, - workqueue.RateLimitingInterface, - ...predicate.Predicate) error { - defer GinkgoRecover() - return err - }) - Expect(ctrl.Watch(src, &handler.EnqueueRequestForObject{})).To(Succeed()) - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - Expect(ctrl.Start(ctx)).To(Equal(err)) - }) + // It("should call Start on sources with the appropriate EventHandler, Queue, and Predicates", func() { + // pr1 := &predicate.Funcs{} + // pr2 := &predicate.Funcs{} + // evthdl := &handler.EnqueueRequestForObject{} + // started := false + // src := source.Func(func(ctx context.Context, e handler.EventHandler, q workqueue.RateLimitingInterface, p ...predicate.Predicate) error { + // defer GinkgoRecover() + // Expect(e).To(Equal(evthdl)) + // Expect(q).To(Equal(ctrl.Queue)) + // Expect(p).To(ConsistOf(pr1, pr2)) + + // started = true + // return nil + // }) + // Expect(ctrl.Watch(src, evthdl, pr1, pr2)).NotTo(HaveOccurred()) + + // // Use a cancelled context so Start doesn't block + // ctx, cancel := context.WithCancel(context.Background()) + // cancel() + // Expect(ctrl.Start(ctx)).To(Succeed()) + // Expect(started).To(BeTrue()) + // }) + + // It("should return an error if there is an error starting sources", func() { + // err := fmt.Errorf("Expected Error: could not start source") + // src := source.Func(func(context.Context, handler.EventHandler, + // workqueue.RateLimitingInterface, + // ...predicate.Predicate) error { + // defer GinkgoRecover() + // return err + // }) + // Expect(ctrl.Watch(src, &handler.EnqueueRequestForObject{})).To(Succeed()) + + // ctx, cancel := context.WithCancel(context.Background()) + // defer cancel() + // Expect(ctrl.Start(ctx)).To(Equal(err)) + // }) It("should return an error if it gets started more than once", func() { // Use a cancelled context so Start doesn't block diff --git a/pkg/internal/recorder/recorder_integration_test.go b/pkg/internal/recorder/recorder_integration_test.go index 130a306053..b30eab6353 100644 --- a/pkg/internal/recorder/recorder_integration_test.go +++ b/pkg/internal/recorder/recorder_integration_test.go @@ -56,7 +56,9 @@ var _ = Describe("recorder", func() { Expect(err).NotTo(HaveOccurred()) By("Watching Resources") - err = instance.Watch(source.Kind(cm.GetCache(), &appsv1.Deployment{}), &handler.EnqueueRequestForObject{}) + src := source.Kind(cm.GetCache(), &appsv1.Deployment{}) + src.Prepare(&handler.EnqueueRequestForObject{}) + err = instance.Watch(src) Expect(err).NotTo(HaveOccurred()) By("Starting the Manager") diff --git a/pkg/internal/source/kind.go b/pkg/internal/source/kind.go index 0aa42861a8..adf8d31721 100644 --- a/pkg/internal/source/kind.go +++ b/pkg/internal/source/kind.go @@ -29,6 +29,9 @@ type Kind[T client.Object] struct { // contain an error, startup and syncing finished. started chan error startCancel func() + + predicates []predicate.ObjectPredicate[T] + handler handler.ObjectHandler[T] } // SetPredicates implements source.SyncingSource. @@ -36,11 +39,23 @@ func (ks *Kind[T]) SetPredicates(...predicate.PredicateConstraint) { panic("unimplemented") } +func (ks *Kind[T]) PrepareObject(h handler.ObjectHandler[T], prct ...predicate.ObjectPredicate[T]) { + ks.handler = h + ks.predicates = prct +} + +func (ks *Kind[T]) Prepare(h handler.EventHandler, prct ...predicate.Predicate) { + ks.handler = handler.ObjectFuncAdapter[T](h) + ks.predicates = predicate.ObjectPredicatesAdapter[T](prct...) +} + // Start is internal and should be called only by the Controller to register an EventHandler with the Informer // to enqueue reconcile.Requests. -func (ks *Kind[T]) Start(ctx context.Context, h handler.EventHandler, queue workqueue.RateLimitingInterface, - prct ...predicate.Predicate) error { - return ks.Run(ctx, handler.ObjectFuncAdapter[T](h), queue, predicate.ObjectPredicatesAdapter[T](prct...)...) +func (ks *Kind[T]) Start( + ctx context.Context, + queue workqueue.RateLimitingInterface, +) error { + return ks.Run(ctx, ks.handler, queue, ks.predicates...) } func (ks *Kind[T]) Run(ctx context.Context, handler handler.ObjectHandler[T], queue workqueue.RateLimitingInterface, diff --git a/pkg/source/example_test.go b/pkg/source/example_test.go index 77857729de..60cfd83c6b 100644 --- a/pkg/source/example_test.go +++ b/pkg/source/example_test.go @@ -31,7 +31,9 @@ var ctrl controller.Controller // This example Watches for Pod Events (e.g. Create / Update / Delete) and enqueues a reconcile.Request // with the Name and Namespace of the Pod. func ExampleKind() { - err := ctrl.Watch(source.Kind(mgr.GetCache(), &corev1.Pod{}), &handler.EnqueueRequestForObject{}) + instance := source.Kind(mgr.GetCache(), &corev1.Pod{}) + instance.Prepare(&handler.EnqueueRequestForObject{}) + err := ctrl.Watch(instance) if err != nil { // handle it } @@ -42,10 +44,10 @@ func ExampleKind() { func ExampleChannel() { events := make(chan event.GenericEvent) - err := ctrl.Watch( - &source.Channel{Source: events}, - &handler.EnqueueRequestForObject{}, - ) + ch := &source.Channel{Source: events} + ch.Prepare(&handler.EnqueueRequestForObject{}) + + err := ctrl.Watch(ch) if err != nil { // handle it } diff --git a/pkg/source/source.go b/pkg/source/source.go index 0afc043963..1a5d4f9ce7 100644 --- a/pkg/source/source.go +++ b/pkg/source/source.go @@ -47,28 +47,52 @@ const ( type Source interface { // Start is internal and should be called only by the Controller to register an EventHandler with the Informer // to enqueue reconcile.Requests. - Start(context.Context, handler.EventHandler, workqueue.RateLimitingInterface, ...predicate.Predicate) error - SetPredicates(...predicate.PredicateConstraint) + // Start(context.Context, handler.EventHandler, workqueue.RateLimitingInterface, ...predicate.Predicate) error + Start(context.Context, workqueue.RateLimitingInterface) error +} + +type SourcePrepare interface { + Source + Prepare(handler.EventHandler, ...predicate.Predicate) +} + +type ObjectSourcePrepare[T any] interface { + Source + PrepareObject(handler.ObjectHandler[T], ...predicate.ObjectPredicate[T]) } // SyncingSource is a source that needs syncing prior to being usable. The controller // will call its WaitForSync prior to starting workers. type SyncingSource interface { Source + Syncing +} + +type PrepareSyncing interface { + SyncingSource + SourcePrepare +} + +type ObjectPrepare[T any] interface { + SyncingSource + ObjectSourcePrepare[T] +} + +type Syncing interface { WaitForSync(ctx context.Context) error } // Kind creates a KindSource with the given cache provider. -func Kind(cache cache.Cache, object client.Object) SyncingSource { +func Kind(cache cache.Cache, object client.Object) PrepareSyncing { return &internal.Kind[client.Object]{Type: object, Cache: cache} } // ObjectKind creates a typed KindSource with the given cache provider. -func ObjectKind[T client.Object](cache cache.Cache, object T) SyncingSource { +func ObjectKind[T client.Object](cache cache.Cache, object T) ObjectPrepare[T] { return &internal.Kind[T]{Type: object, Cache: cache} } -var _ Source = &Channel{} +var _ SourcePrepare = &Channel{} // Channel is used to provide a source of events originating outside the cluster // (e.g. GitHub Webhook callback). Channel requires the user to wire the external @@ -89,23 +113,39 @@ type Channel struct { // destLock is to ensure the destination channels are safely added/removed destLock sync.Mutex + + predicates []predicate.Predicate + + handler handler.EventHandler } func (cs *Channel) String() string { return fmt.Sprintf("channel source: %p", cs) } +// Prepare implements Source preparation and should only be called when handler and predicates are available. +func (cs *Channel) Prepare( + handler handler.EventHandler, + prct ...predicate.Predicate, +) { + cs.predicates = prct + cs.handler = handler +} + // Start implements Source and should only be called by the Controller. func (cs *Channel) Start( ctx context.Context, - handler handler.EventHandler, queue workqueue.RateLimitingInterface, - prct ...predicate.Predicate) error { +) error { // Source should have been specified by the user. if cs.Source == nil { return fmt.Errorf("must specify Channel.Source") } + if cs.handler == nil { + return fmt.Errorf("must specify Channel.EventHandler") + } + // use default value if DestBufferSize not specified if cs.DestBufferSize == 0 { cs.DestBufferSize = defaultBufferSize @@ -125,7 +165,7 @@ func (cs *Channel) Start( go func() { for evt := range dst { shouldHandle := true - for _, p := range prct { + for _, p := range cs.predicates { if !p.Generic(evt) { shouldHandle = false break @@ -136,7 +176,7 @@ func (cs *Channel) Start( func() { ctx, cancel := context.WithCancel(ctx) defer cancel() - handler.Generic(ctx, evt, queue) + cs.handler.Generic(ctx, evt, queue) }() } } @@ -191,20 +231,36 @@ func (cs *Channel) syncLoop(ctx context.Context) { type Informer struct { // Informer is the controller-runtime Informer Informer cache.Informer + + predicates []predicate.Predicate + + handler handler.EventHandler } -var _ Source = &Informer{} +var _ SourcePrepare = &Informer{} + +func (is *Informer) Prepare( + h handler.EventHandler, + prct ...predicate.Predicate, +) { + is.handler = h + is.predicates = prct +} // Start is internal and should be called only by the Controller to register an EventHandler with the Informer // to enqueue reconcile.Requests. -func (is *Informer) Start(ctx context.Context, h handler.EventHandler, queue workqueue.RateLimitingInterface, - prct ...predicate.Predicate) error { +func (is *Informer) Start(ctx context.Context, queue workqueue.RateLimitingInterface) error { // Informer should have been specified by the user. if is.Informer == nil { return fmt.Errorf("must specify Informer.Informer") } - _, err := is.Informer.AddEventHandler(internal.NewEventHandler(ctx, queue, handler.ObjectFuncAdapter[client.Object](h), predicate.ObjectPredicatesAdapter[client.Object](prct...)).HandlerFuncs()) + // handler should have been specified by the user. + if is.handler == nil { + return fmt.Errorf("must specify Informer.handler with Prepare()") + } + + _, err := is.Informer.AddEventHandler(internal.NewEventHandler(ctx, queue, handler.ObjectFuncAdapter[client.Object](is.handler), predicate.ObjectPredicatesAdapter[client.Object](is.predicates...)).HandlerFuncs()) if err != nil { return err } @@ -215,17 +271,18 @@ func (is *Informer) String() string { return fmt.Sprintf("informer source: %p", is.Informer) } -var _ Source = Func(nil) +// // Func is no longer compatible +// var _ Source = Func(nil) -// Func is a function that implements Source. -type Func func(context.Context, handler.EventHandler, workqueue.RateLimitingInterface, ...predicate.Predicate) error +// // Func is a function that implements Source. +// type Func func(context.Context, handler.EventHandler, workqueue.RateLimitingInterface, ...predicate.Predicate) error -// Start implements Source. -func (f Func) Start(ctx context.Context, evt handler.EventHandler, queue workqueue.RateLimitingInterface, - pr ...predicate.Predicate) error { - return f(ctx, evt, queue, pr...) -} +// // Start implements Source. +// func (f Func) Start(ctx context.Context, evt handler.EventHandler, queue workqueue.RateLimitingInterface, +// pr ...predicate.Predicate) error { +// return f(ctx, evt, queue, pr...) +// } -func (f Func) String() string { - return fmt.Sprintf("func source: %p", f) -} +// func (f Func) String() string { +// return fmt.Sprintf("func source: %p", f) +// } diff --git a/pkg/source/source_integration_test.go b/pkg/source/source_integration_test.go index 594d3c9a9c..2d724b6508 100644 --- a/pkg/source/source_integration_test.go +++ b/pkg/source/source_integration_test.go @@ -37,7 +37,7 @@ import ( ) var _ = Describe("Source", func() { - var instance1, instance2 source.Source + var instance1, instance2 source.SourcePrepare var obj client.Object var q workqueue.RateLimitingInterface var c1, c2 chan interface{} @@ -124,8 +124,10 @@ var _ = Describe("Source", func() { handler2 := newHandler(c2) // Create 2 instances - Expect(instance1.Start(ctx, handler1, q)).To(Succeed()) - Expect(instance2.Start(ctx, handler2, q)).To(Succeed()) + instance1.Prepare(handler1) + instance2.Prepare(handler2) + Expect(instance1.Start(ctx, q)).To(Succeed()) + Expect(instance2.Start(ctx, q)).To(Succeed()) By("Creating a Deployment and expecting the CreateEvent.") created, err = client.Create(ctx, deployment, metav1.CreateOptions{}) @@ -242,7 +244,7 @@ var _ = Describe("Source", func() { q := workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "test") instance := &source.Informer{Informer: depInformer} - err := instance.Start(ctx, handler.Funcs{ + instance.Prepare(handler.Funcs{ CreateFunc: func(ctx context.Context, evt event.CreateEvent, q2 workqueue.RateLimitingInterface) { defer GinkgoRecover() var err error @@ -265,7 +267,8 @@ var _ = Describe("Source", func() { defer GinkgoRecover() Fail("Unexpected GenericEvent") }, - }, q) + }) + err := instance.Start(ctx, q) Expect(err).NotTo(HaveOccurred()) _, err = clientset.AppsV1().ReplicaSets("default").Create(ctx, rs, metav1.CreateOptions{}) @@ -283,7 +286,7 @@ var _ = Describe("Source", func() { q := workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "test") instance := &source.Informer{Informer: depInformer} - err = instance.Start(ctx, handler.Funcs{ + instance.Prepare(handler.Funcs{ CreateFunc: func(ctx context.Context, evt event.CreateEvent, q2 workqueue.RateLimitingInterface) { }, UpdateFunc: func(ctx context.Context, evt event.UpdateEvent, q2 workqueue.RateLimitingInterface) { @@ -307,7 +310,9 @@ var _ = Describe("Source", func() { defer GinkgoRecover() Fail("Unexpected GenericEvent") }, - }, q) + }) + + err = instance.Start(ctx, q) Expect(err).NotTo(HaveOccurred()) _, err = clientset.AppsV1().ReplicaSets("default").Update(ctx, rs2, metav1.UpdateOptions{}) @@ -320,7 +325,7 @@ var _ = Describe("Source", func() { q := workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "test") instance := &source.Informer{Informer: depInformer} - err := instance.Start(ctx, handler.Funcs{ + instance.Prepare(handler.Funcs{ CreateFunc: func(context.Context, event.CreateEvent, workqueue.RateLimitingInterface) { }, UpdateFunc: func(context.Context, event.UpdateEvent, workqueue.RateLimitingInterface) { @@ -335,7 +340,9 @@ var _ = Describe("Source", func() { defer GinkgoRecover() Fail("Unexpected GenericEvent") }, - }, q) + }) + + err := instance.Start(ctx, q) Expect(err).NotTo(HaveOccurred()) err = clientset.AppsV1().ReplicaSets("default").Delete(ctx, rs.Name, metav1.DeleteOptions{}) diff --git a/pkg/source/source_test.go b/pkg/source/source_test.go index 012cca3bb8..8d7cedd164 100644 --- a/pkg/source/source_test.go +++ b/pkg/source/source_test.go @@ -67,7 +67,7 @@ var _ = Describe("Source", func() { q := workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "test") instance := source.Kind(ic, &corev1.Pod{}) - err := instance.Start(ctx, handler.Funcs{ + instance.Prepare(handler.Funcs{ CreateFunc: func(ctx context.Context, evt event.CreateEvent, q2 workqueue.RateLimitingInterface) { defer GinkgoRecover() Expect(q2).To(Equal(q)) @@ -86,7 +86,8 @@ var _ = Describe("Source", func() { defer GinkgoRecover() Fail("Unexpected GenericEvent") }, - }, q) + }) + err := instance.Start(ctx, q) Expect(err).NotTo(HaveOccurred()) Expect(instance.WaitForSync(context.Background())).NotTo(HaveOccurred()) @@ -104,7 +105,7 @@ var _ = Describe("Source", func() { ic := &informertest.FakeInformers{} q := workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "test") instance := source.Kind(ic, &corev1.Pod{}) - err := instance.Start(ctx, handler.Funcs{ + instance.Prepare(handler.Funcs{ CreateFunc: func(ctx context.Context, evt event.CreateEvent, q2 workqueue.RateLimitingInterface) { defer GinkgoRecover() Fail("Unexpected CreateEvent") @@ -126,7 +127,8 @@ var _ = Describe("Source", func() { defer GinkgoRecover() Fail("Unexpected GenericEvent") }, - }, q) + }) + err := instance.Start(ctx, q) Expect(err).NotTo(HaveOccurred()) Expect(instance.WaitForSync(context.Background())).NotTo(HaveOccurred()) @@ -149,7 +151,7 @@ var _ = Describe("Source", func() { q := workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "test") instance := source.Kind(ic, &corev1.Pod{}) - err := instance.Start(ctx, handler.Funcs{ + instance.Prepare(handler.Funcs{ CreateFunc: func(context.Context, event.CreateEvent, workqueue.RateLimitingInterface) { defer GinkgoRecover() Fail("Unexpected DeleteEvent") @@ -168,7 +170,8 @@ var _ = Describe("Source", func() { defer GinkgoRecover() Fail("Unexpected GenericEvent") }, - }, q) + }) + err := instance.Start(ctx, q) Expect(err).NotTo(HaveOccurred()) Expect(instance.WaitForSync(context.Background())).NotTo(HaveOccurred()) @@ -182,28 +185,28 @@ var _ = Describe("Source", func() { It("should return an error from Start cache was not provided", func() { instance := source.Kind(nil, &corev1.Pod{}) - err := instance.Start(ctx, nil, nil) + err := instance.Start(ctx, nil) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("must create Kind with a non-nil cache")) }) It("should return an error from Start cache was not provided", func() { instance := source.ObjectKind(nil, &corev1.Pod{}) - err := instance.Start(ctx, nil, nil) + err := instance.Start(ctx, nil) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("must create Kind with a non-nil cache")) }) It("should return an error from Start if a type was not provided", func() { instance := source.Kind(ic, nil) - err := instance.Start(ctx, nil, nil) + err := instance.Start(ctx, nil) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("must create Kind with a non-nil object")) }) It("should return an error from Start if a type was not provided", func() { instance := source.ObjectKind[client.Object](ic, nil) - err := instance.Start(ctx, nil, nil) + err := instance.Start(ctx, nil) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("must create Kind with a non-nil object")) }) @@ -211,7 +214,7 @@ var _ = Describe("Source", func() { It("should return an error if syncing fails", func() { f := false instance := source.Kind(&informertest.FakeInformers{Synced: &f}, &corev1.Pod{}) - Expect(instance.Start(context.Background(), nil, nil)).NotTo(HaveOccurred()) + Expect(instance.Start(context.Background(), nil)).NotTo(HaveOccurred()) err := instance.WaitForSync(context.Background()) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(Equal("cache did not sync")) @@ -227,7 +230,8 @@ var _ = Describe("Source", func() { defer cancel() instance := source.Kind(ic, &corev1.Pod{}) - err := instance.Start(ctx, handler.Funcs{}, q) + instance.Prepare(handler.Funcs{}) + err := instance.Start(ctx, q) Expect(err).NotTo(HaveOccurred()) Eventually(instance.WaitForSync).WithArguments(context.Background()).Should(HaveOccurred()) }) @@ -236,7 +240,7 @@ var _ = Describe("Source", func() { It("should return an error if syncing fails", func() { f := false instance := source.Kind(&informertest.FakeInformers{Synced: &f}, &corev1.Pod{}) - Expect(instance.Start(context.Background(), nil, nil)).NotTo(HaveOccurred()) + Expect(instance.Start(context.Background(), nil)).NotTo(HaveOccurred()) err := instance.WaitForSync(context.Background()) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(Equal("cache did not sync")) @@ -244,29 +248,29 @@ var _ = Describe("Source", func() { }) }) - Describe("Func", func() { - It("should be called from Start", func() { - run := false - instance := source.Func(func( - context.Context, - handler.EventHandler, - workqueue.RateLimitingInterface, ...predicate.Predicate) error { - run = true - return nil - }) - Expect(instance.Start(ctx, nil, nil)).NotTo(HaveOccurred()) - Expect(run).To(BeTrue()) - - expected := fmt.Errorf("expected error: Func") - instance = source.Func(func( - context.Context, - handler.EventHandler, - workqueue.RateLimitingInterface, ...predicate.Predicate) error { - return expected - }) - Expect(instance.Start(ctx, nil, nil)).To(Equal(expected)) - }) - }) + // Describe("Func", func() { + // It("should be called from Start", func() { + // run := false + // instance := source.Func(func( + // context.Context, + // handler.EventHandler, + // workqueue.RateLimitingInterface, ...predicate.Predicate) error { + // run = true + // return nil + // }) + // Expect(instance.Start(ctx, nil, nil)).NotTo(HaveOccurred()) + // Expect(run).To(BeTrue()) + + // expected := fmt.Errorf("expected error: Func") + // instance = source.Func(func( + // context.Context, + // handler.EventHandler, + // workqueue.RateLimitingInterface, ...predicate.Predicate) error { + // return expected + // }) + // Expect(instance.Start(ctx, nil, nil)).To(Equal(expected)) + // }) + // }) Describe("Channel", func() { var ctx context.Context @@ -305,7 +309,7 @@ var _ = Describe("Source", func() { q := workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "test") instance := &source.Channel{Source: ch} - err := instance.Start(ctx, handler.Funcs{ + instance.Prepare(handler.Funcs{ CreateFunc: func(context.Context, event.CreateEvent, workqueue.RateLimitingInterface) { defer GinkgoRecover() Fail("Unexpected CreateEvent") @@ -326,7 +330,8 @@ var _ = Describe("Source", func() { Expect(evt.Object).To(Equal(p)) close(c) }, - }, q, prct) + }, prct) + err := instance.Start(ctx, q) Expect(err).NotTo(HaveOccurred()) ch <- invalidEvt @@ -344,7 +349,7 @@ var _ = Describe("Source", func() { // Add a handler to get distribution blocked instance := &source.Channel{Source: ch} instance.DestBufferSize = 1 - err := instance.Start(ctx, handler.Funcs{ + instance.Prepare(handler.Funcs{ CreateFunc: func(context.Context, event.CreateEvent, workqueue.RateLimitingInterface) { defer GinkgoRecover() Fail("Unexpected CreateEvent") @@ -369,7 +374,8 @@ var _ = Describe("Source", func() { close(processed) } }, - }, q) + }) + err := instance.Start(ctx, q) Expect(err).NotTo(HaveOccurred()) // Write 3 events into the source channel. @@ -401,7 +407,7 @@ var _ = Describe("Source", func() { instance := &source.Channel{Source: ch} instance.DestBufferSize = 1 - err := instance.Start(ctx, handler.Funcs{ + instance.Prepare(handler.Funcs{ CreateFunc: func(context.Context, event.CreateEvent, workqueue.RateLimitingInterface) { defer GinkgoRecover() Fail("Unexpected CreateEvent") @@ -419,7 +425,9 @@ var _ = Describe("Source", func() { close(processed) }, - }, q) + }) + + err := instance.Start(ctx, q) Expect(err).NotTo(HaveOccurred()) <-processed @@ -442,7 +450,7 @@ var _ = Describe("Source", func() { processed := make(chan struct{}) defer close(processed) - err := src.Start(ctx, handler.Funcs{ + src.Prepare(handler.Funcs{ CreateFunc: func(context.Context, event.CreateEvent, workqueue.RateLimitingInterface) { defer GinkgoRecover() Fail("Unexpected CreateEvent") @@ -460,7 +468,9 @@ var _ = Describe("Source", func() { processed <- struct{}{} }, - }, q) + }) + + err := src.Start(ctx, q) Expect(err).NotTo(HaveOccurred()) By("expecting to only get one event") @@ -470,7 +480,8 @@ var _ = Describe("Source", func() { It("should get error if no source specified", func() { q := workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "test") instance := &source.Channel{ /*no source specified*/ } - err := instance.Start(ctx, handler.Funcs{}, q) + instance.Prepare(handler.Funcs{}) + err := instance.Start(ctx, q) Expect(err).To(Equal(fmt.Errorf("must specify Channel.Source"))) }) }) @@ -490,7 +501,7 @@ var _ = Describe("Source", func() { q := workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "test") instance := &source.Channel{Source: ch} - err := instance.Start(ctx, handler.Funcs{ + instance.Prepare(handler.Funcs{ CreateFunc: func(context.Context, event.CreateEvent, workqueue.RateLimitingInterface) { defer GinkgoRecover() Fail("Unexpected CreateEvent") @@ -510,10 +521,12 @@ var _ = Describe("Source", func() { resEvent1 = evt close(c1) }, - }, q) + }) + + err := instance.Start(ctx, q) Expect(err).NotTo(HaveOccurred()) - err = instance.Start(ctx, handler.Funcs{ + instance.Prepare(handler.Funcs{ CreateFunc: func(context.Context, event.CreateEvent, workqueue.RateLimitingInterface) { defer GinkgoRecover() Fail("Unexpected CreateEvent") @@ -533,7 +546,9 @@ var _ = Describe("Source", func() { resEvent2 = evt close(c2) }, - }, q) + }) + + err = instance.Start(ctx, q) Expect(err).NotTo(HaveOccurred()) ch <- evt From 8c4d3df7b95c096af9ee15c3ae9f4ce36b728aa9 Mon Sep 17 00:00:00 2001 From: Danil Grigorev Date: Tue, 16 Apr 2024 18:02:19 +0200 Subject: [PATCH 4/7] Separate source into several interfaces - Add controller adapters Signed-off-by: Danil Grigorev --- examples/builtins/main.go | 8 +- pkg/builder/controller.go | 28 ++-- pkg/controller/controller.go | 17 +++ pkg/controller/controller_integration_test.go | 8 +- pkg/controller/controller_test.go | 3 +- pkg/controller/example_test.go | 10 +- pkg/handler/enqueue_mapped_typed.go | 16 +++ pkg/handler/example_test.go | 83 ++++++----- pkg/interfaces/source.go | 72 ++++++++++ .../recorder/recorder_integration_test.go | 3 +- pkg/internal/source/kind.go | 8 +- pkg/predicate/predicate.go | 2 +- pkg/source/example_test.go | 3 +- pkg/source/source.go | 73 ++++------ pkg/source/source_integration_test.go | 9 +- pkg/source/source_test.go | 131 +++++++++--------- 16 files changed, 267 insertions(+), 207 deletions(-) create mode 100644 pkg/interfaces/source.go diff --git a/examples/builtins/main.go b/examples/builtins/main.go index f83180a6cd..514f2663f1 100644 --- a/examples/builtins/main.go +++ b/examples/builtins/main.go @@ -59,17 +59,13 @@ func main() { } // Watch ReplicaSets and enqueue ReplicaSet object key - src := source.Kind(mgr.GetCache(), &appsv1.ReplicaSet{}) - src.Prepare(&handler.EnqueueRequestForObject{}) - if err := c.Watch(src); err != nil { + if err := c.Watch(source.Kind(mgr.GetCache(), &appsv1.ReplicaSet{}).Prepare(&handler.EnqueueRequestForObject{})); err != nil { entryLog.Error(err, "unable to watch ReplicaSets") os.Exit(1) } // Watch Pods and enqueue owning ReplicaSet key - src = source.Kind(mgr.GetCache(), &corev1.Pod{}) - src.Prepare(handler.EnqueueRequestForOwner(mgr.GetScheme(), mgr.GetRESTMapper(), &appsv1.ReplicaSet{}, handler.OnlyControllerOwner())) - if err := c.Watch(src); err != nil { + if err := c.Watch(source.Kind(mgr.GetCache(), &corev1.Pod{}).Prepare(handler.EnqueueRequestForOwner(mgr.GetScheme(), mgr.GetRESTMapper(), &appsv1.ReplicaSet{}, handler.OnlyControllerOwner()))); err != nil { entryLog.Error(err, "unable to watch Pods") os.Exit(1) } diff --git a/pkg/builder/controller.go b/pkg/builder/controller.go index a3b68da323..b23468ee15 100644 --- a/pkg/builder/controller.go +++ b/pkg/builder/controller.go @@ -124,7 +124,7 @@ func (blder *Builder) Owns(object client.Object, opts ...OwnsOption) *Builder { // WatchesInput represents the information set by Watches method. type WatchesInput struct { - src source.SourcePrepare + src source.PrepareSyncing eventHandler handler.EventHandler predicates []predicate.Predicate objectProjection objectProjection @@ -177,7 +177,7 @@ func (blder *Builder) WatchesMetadata(object client.Object, eventHandler handler // // STOP! Consider using For(...), Owns(...), Watches(...), WatchesMetadata(...) instead. // This method is only exposed for more advanced use cases, most users should use one of the higher level functions. -func (blder *Builder) WatchesRawSource(src source.SourcePrepare, eventHandler handler.EventHandler, opts ...WatchesOption) *Builder { +func (blder *Builder) WatchesRawSource(src source.PrepareSyncing, eventHandler handler.EventHandler, opts ...WatchesOption) *Builder { input := WatchesInput{src: src, eventHandler: eventHandler} for _, opt := range opts { opt.ApplyToWatches(&input) @@ -188,29 +188,24 @@ func (blder *Builder) WatchesRawSource(src source.SourcePrepare, eventHandler ha } func For[T client.Object](blder *Builder, object T, prct ...predicate.ObjectPredicate[T]) source.Source { - src := source.ObjectKind(blder.mgr.GetCache(), object) - src.PrepareObject(&handler.EnqueueRequest[T]{}, prct...) + blder.forInput = ForInput{object: object} - return src + return source.ObjectKind(blder.mgr.GetCache(), object).PrepareObject(&handler.EnqueueRequest[T]{}, prct...) } -func Owns[F, T client.Object](blder *Builder, owner F, owned T) source.Source { +func Owns[F, T client.Object](blder *Builder, owner F, owned T, prct ...predicate.ObjectPredicate[T]) source.Source { src := source.ObjectKind(blder.mgr.GetCache(), owned) hdler := handler.EnqueueRequestForOwner( blder.mgr.GetScheme(), blder.mgr.GetRESTMapper(), owner, ) - src.PrepareObject(handler.ObjectFuncAdapter[T](hdler)) - return src + return src.PrepareObject(handler.ObjectFuncAdapter[T](hdler), prct...) } func Watches[T client.Object](blder *Builder, object T, eventHandler handler.ObjectHandler[T], prct ...predicate.ObjectPredicate[T]) source.Source { - src := source.ObjectKind(blder.mgr.GetCache(), object) - src.PrepareObject(eventHandler, prct...) - - return src + return source.ObjectKind(blder.mgr.GetCache(), object).PrepareObject(eventHandler, prct...) } func (blder *Builder) Add(src source.Source) *Builder { @@ -308,8 +303,7 @@ func (blder *Builder) doWatch() error { hdler := &handler.EnqueueRequestForObject{} allPredicates := append([]predicate.Predicate(nil), blder.globalPredicates...) allPredicates = append(allPredicates, blder.forInput.predicates...) - src.Prepare(hdler, allPredicates...) - if err := blder.ctrl.Watch(src); err != nil { + if err := blder.ctrl.Watch(src.Prepare(hdler, allPredicates...)); err != nil { return err } } @@ -335,8 +329,7 @@ func (blder *Builder) doWatch() error { ) allPredicates := append([]predicate.Predicate(nil), blder.globalPredicates...) allPredicates = append(allPredicates, own.predicates...) - src.Prepare(hdler, allPredicates...) - if err := blder.ctrl.Watch(src); err != nil { + if err := blder.ctrl.Watch(src.Prepare(hdler, allPredicates...)); err != nil { return err } } @@ -356,8 +349,7 @@ func (blder *Builder) doWatch() error { } allPredicates := append([]predicate.Predicate(nil), blder.globalPredicates...) allPredicates = append(allPredicates, w.predicates...) - w.src.Prepare(w.eventHandler, allPredicates...) - if err := blder.ctrl.Watch(w.src); err != nil { + if err := blder.ctrl.Watch(w.src.Prepare(w.eventHandler, allPredicates...)); err != nil { return err } } diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go index d143af33df..72d15661af 100644 --- a/pkg/controller/controller.go +++ b/pkg/controller/controller.go @@ -25,8 +25,10 @@ import ( "k8s.io/client-go/util/workqueue" "k8s.io/klog/v2" + "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/internal/controller" "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/ratelimiter" "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/source" @@ -189,3 +191,18 @@ func NewUnmanaged(name string, mgr manager.Manager, options Options) (Controller // ReconcileIDFromContext gets the reconcileID from the current context. var ReconcileIDFromContext = controller.ReconcileIDFromContext + +// ControllerAdapter is an adapter for old controller implementations +type ControllerAdapter struct { + Controller +} + +// Watch implements old controller Watch interface +func (c *ControllerAdapter) Watch(src source.Source, handler handler.EventHandler, predicates ...predicate.Predicate) error { + source, ok := src.(source.PrepareSource) + if !ok { + return fmt.Errorf("expected source to fulfill SourcePrepare interface") + } + + return c.Controller.Watch(source.Prepare(handler, predicates...)) +} diff --git a/pkg/controller/controller_integration_test.go b/pkg/controller/controller_integration_test.go index cf95f57141..75ec77c9e4 100644 --- a/pkg/controller/controller_integration_test.go +++ b/pkg/controller/controller_integration_test.go @@ -64,14 +64,10 @@ var _ = Describe("controller", func() { Expect(err).NotTo(HaveOccurred()) By("Watching Resources") - src := source.Kind(cm.GetCache(), &appsv1.ReplicaSet{}) - src.Prepare(handler.EnqueueRequestForOwner(cm.GetScheme(), cm.GetRESTMapper(), &appsv1.Deployment{})) - err = instance.Watch(src) + err = instance.Watch(source.Kind(cm.GetCache(), &appsv1.ReplicaSet{}).Prepare(handler.EnqueueRequestForOwner(cm.GetScheme(), cm.GetRESTMapper(), &appsv1.Deployment{}))) Expect(err).NotTo(HaveOccurred()) - src = source.Kind(cm.GetCache(), &appsv1.Deployment{}) - src.Prepare(&handler.EnqueueRequestForObject{}) - err = instance.Watch(src) + err = instance.Watch(source.Kind(cm.GetCache(), &appsv1.Deployment{}).Prepare(&handler.EnqueueRequestForObject{})) Expect(err).NotTo(HaveOccurred()) err = cm.GetClient().Get(ctx, types.NamespacedName{Name: "foo"}, &corev1.Namespace{}) diff --git a/pkg/controller/controller_test.go b/pkg/controller/controller_test.go index f26416f0bc..7ee524ae19 100644 --- a/pkg/controller/controller_test.go +++ b/pkg/controller/controller_test.go @@ -101,8 +101,7 @@ var _ = Describe("controller.Controller", func() { Expect(err).NotTo(HaveOccurred()) c, err := controller.New("new-controller", m, controller.Options{Reconciler: rec}) - watch.Prepare(&handler.EnqueueRequestForObject{}) - Expect(c.Watch(watch)).To(Succeed()) + Expect(c.Watch(watch.Prepare(&handler.EnqueueRequestForObject{}))).To(Succeed()) Expect(err).NotTo(HaveOccurred()) go func() { diff --git a/pkg/controller/example_test.go b/pkg/controller/example_test.go index 7a9ee367d7..581abf8167 100644 --- a/pkg/controller/example_test.go +++ b/pkg/controller/example_test.go @@ -71,9 +71,7 @@ func ExampleController() { } // Watch for Pod create / update / delete events and call Reconcile - src := source.Kind(mgr.GetCache(), &corev1.Pod{}) - src.Prepare(&handler.EnqueueRequestForObject{}) - err = c.Watch(src) + err = c.Watch(source.Kind(mgr.GetCache(), &corev1.Pod{}).Prepare(&handler.EnqueueRequestForObject{})) if err != nil { log.Error(err, "unable to watch pods") os.Exit(1) @@ -111,8 +109,7 @@ func ExampleController_unstructured() { }) // Watch for Pod create / update / delete events and call Reconcile src := source.Kind(mgr.GetCache(), u) - src.Prepare(&handler.EnqueueRequestForObject{}) - err = c.Watch(src) + err = c.Watch(src.Prepare(&handler.EnqueueRequestForObject{})) if err != nil { log.Error(err, "unable to watch pods") os.Exit(1) @@ -144,8 +141,7 @@ func ExampleNewUnmanaged() { } src := source.Kind(mgr.GetCache(), &corev1.Pod{}) - src.Prepare(&handler.EnqueueRequestForObject{}) - if err := c.Watch(src); err != nil { + if err := c.Watch(src.Prepare(&handler.EnqueueRequestForObject{})); err != nil { log.Error(err, "unable to watch pods") os.Exit(1) } diff --git a/pkg/handler/enqueue_mapped_typed.go b/pkg/handler/enqueue_mapped_typed.go index 80f54bacef..98ffe8d27e 100644 --- a/pkg/handler/enqueue_mapped_typed.go +++ b/pkg/handler/enqueue_mapped_typed.go @@ -57,6 +57,22 @@ func EnqueueRequestsFromObjectMapFunc[T any](fn ObjectMapFunc[T]) EventHandler { } } +// EnqueueRequestsFromObjectMap enqueues Requests by running a transformation function that outputs a collection +// of reconcile.Requests on each Event. The reconcile.Requests may be for an arbitrary set of objects +// defined by some user specified transformation of the source Event. (e.g. trigger Reconciler for a set of objects +// in response to a cluster resize event caused by adding or deleting a Node) +// +// EnqueueRequestsFromObjectMap is frequently used to fan-out updates from one object to one or more other +// objects of a differing type. +// +// For UpdateEvents which contain both a new and old object, the transformation function is run on both +// objects and both sets of Requests are enqueue. +func EnqueueRequestsFromObjectMap[T any](fn ObjectMapFunc[T]) ObjectHandler[T] { + return &enqueueRequestsFromObjectMapFunc[T]{ + toRequests: fn, + } +} + var _ EventHandler = &enqueueRequestsFromObjectMapFunc[any]{} var _ ObjectHandler[any] = &enqueueRequestsFromObjectMapFunc[any]{} diff --git a/pkg/handler/example_test.go b/pkg/handler/example_test.go index 273167ab6b..38ae3a9f0f 100644 --- a/pkg/handler/example_test.go +++ b/pkg/handler/example_test.go @@ -42,8 +42,7 @@ var ( func ExampleEnqueueRequestForObject() { // controller is a controller.controller src := source.Kind(mgr.GetCache(), &corev1.Pod{}) - src.Prepare(&handler.EnqueueRequestForObject{}) - err := c.Watch(src) + err := c.Watch(src.Prepare(&handler.EnqueueRequestForObject{})) if err != nil { // handle it } @@ -65,19 +64,19 @@ func ExampleEnqueueRequestForOwner() { // objects (of Type: MyKind) using a mapping function defined by the user. func ExampleEnqueueRequestsFromMapFunc() { // controller is a controller.controller - src := source.Kind(mgr.GetCache(), &appsv1.Deployment{}) - src.Prepare(handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, a client.Object) []reconcile.Request { - return []reconcile.Request{ - {NamespacedName: types.NamespacedName{ - Name: a.GetName() + "-1", - Namespace: a.GetNamespace(), - }}, - {NamespacedName: types.NamespacedName{ - Name: a.GetName() + "-2", - Namespace: a.GetNamespace(), - }}, - } - })) + src := source.Kind(mgr.GetCache(), &appsv1.Deployment{}). + Prepare(handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, a client.Object) []reconcile.Request { + return []reconcile.Request{ + {NamespacedName: types.NamespacedName{ + Name: a.GetName() + "-1", + Namespace: a.GetNamespace(), + }}, + {NamespacedName: types.NamespacedName{ + Name: a.GetName() + "-2", + Namespace: a.GetNamespace(), + }}, + } + })) err := c.Watch(src) if err != nil { // handle it @@ -87,33 +86,33 @@ func ExampleEnqueueRequestsFromMapFunc() { // This example implements handler.EnqueueRequestForObject. func ExampleFuncs() { // controller is a controller.controller - src := source.Kind(mgr.GetCache(), &corev1.Pod{}) - src.Prepare(handler.Funcs{ - CreateFunc: func(ctx context.Context, e event.CreateEvent, q workqueue.RateLimitingInterface) { - q.Add(reconcile.Request{NamespacedName: types.NamespacedName{ - Name: e.Object.GetName(), - Namespace: e.Object.GetNamespace(), - }}) - }, - UpdateFunc: func(ctx context.Context, e event.UpdateEvent, q workqueue.RateLimitingInterface) { - q.Add(reconcile.Request{NamespacedName: types.NamespacedName{ - Name: e.ObjectNew.GetName(), - Namespace: e.ObjectNew.GetNamespace(), - }}) - }, - DeleteFunc: func(ctx context.Context, e event.DeleteEvent, q workqueue.RateLimitingInterface) { - q.Add(reconcile.Request{NamespacedName: types.NamespacedName{ - Name: e.Object.GetName(), - Namespace: e.Object.GetNamespace(), - }}) - }, - GenericFunc: func(ctx context.Context, e event.GenericEvent, q workqueue.RateLimitingInterface) { - q.Add(reconcile.Request{NamespacedName: types.NamespacedName{ - Name: e.Object.GetName(), - Namespace: e.Object.GetNamespace(), - }}) - }, - }) + src := source.Kind(mgr.GetCache(), &corev1.Pod{}). + Prepare(handler.Funcs{ + CreateFunc: func(ctx context.Context, e event.CreateEvent, q workqueue.RateLimitingInterface) { + q.Add(reconcile.Request{NamespacedName: types.NamespacedName{ + Name: e.Object.GetName(), + Namespace: e.Object.GetNamespace(), + }}) + }, + UpdateFunc: func(ctx context.Context, e event.UpdateEvent, q workqueue.RateLimitingInterface) { + q.Add(reconcile.Request{NamespacedName: types.NamespacedName{ + Name: e.ObjectNew.GetName(), + Namespace: e.ObjectNew.GetNamespace(), + }}) + }, + DeleteFunc: func(ctx context.Context, e event.DeleteEvent, q workqueue.RateLimitingInterface) { + q.Add(reconcile.Request{NamespacedName: types.NamespacedName{ + Name: e.Object.GetName(), + Namespace: e.Object.GetNamespace(), + }}) + }, + GenericFunc: func(ctx context.Context, e event.GenericEvent, q workqueue.RateLimitingInterface) { + q.Add(reconcile.Request{NamespacedName: types.NamespacedName{ + Name: e.Object.GetName(), + Namespace: e.Object.GetNamespace(), + }}) + }, + }) err := c.Watch(src) if err != nil { diff --git a/pkg/interfaces/source.go b/pkg/interfaces/source.go new file mode 100644 index 0000000000..bbb6a0b7d9 --- /dev/null +++ b/pkg/interfaces/source.go @@ -0,0 +1,72 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package interfaces + +import ( + "context" + + "k8s.io/client-go/util/workqueue" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/predicate" +) + +// Source is a source of events (e.g. Create, Update, Delete operations on Kubernetes Objects, Webhook callbacks, etc) +// which should be processed by event.EventHandlers to enqueue reconcile.Requests. +// +// * Use Kind for events originating in the cluster (e.g. Pod Create, Pod Update, Deployment Update). +// +// * Use Channel for events originating outside the cluster (e.g. GitHub Webhook callback, Polling external urls). +// +// Users may build their own Source implementations. +type Source interface { + // Start is internal and should be called only by the Controller to register an EventHandler with the Informer + // to enqueue reconcile.Requests. + Start(context.Context, workqueue.RateLimitingInterface) error +} + +// PrepareSource: Prepares a Source to be used with EventHandler and predicates +type PrepareSource interface { + Prepare(handler.EventHandler, ...predicate.Predicate) SyncingSource +} + +// PrepareSourceObject[T]: Prepares a Source preserving the object type +type PrepareSourceObject[T any] interface { + PrepareObject(handler.ObjectHandler[T], ...predicate.ObjectPredicate[T]) SyncingSource +} + +// Syncing allows to wait for synchronization with context +type Syncing interface { + WaitForSync(ctx context.Context) error +} + +// SyncingSource is a source that needs syncing prior to being usable. The controller +// will call its WaitForSync prior to starting workers. +type SyncingSource interface { + Source + Syncing +} + +// PrepareSyncing: A SyncingSource that also implements SourcePrepare and has WaitForSync method +type PrepareSyncing interface { + SyncingSource + PrepareSource +} + +// PrepareSyncingObject[T]: A SyncingSource that also implements PrepareSourceObject[T] and has WaitForSync method +type PrepareSyncingObject[T any] interface { + SyncingSource + PrepareSourceObject[T] +} diff --git a/pkg/internal/recorder/recorder_integration_test.go b/pkg/internal/recorder/recorder_integration_test.go index b30eab6353..477a35a594 100644 --- a/pkg/internal/recorder/recorder_integration_test.go +++ b/pkg/internal/recorder/recorder_integration_test.go @@ -56,8 +56,7 @@ var _ = Describe("recorder", func() { Expect(err).NotTo(HaveOccurred()) By("Watching Resources") - src := source.Kind(cm.GetCache(), &appsv1.Deployment{}) - src.Prepare(&handler.EnqueueRequestForObject{}) + src := source.Kind(cm.GetCache(), &appsv1.Deployment{}).Prepare(&handler.EnqueueRequestForObject{}) err = instance.Watch(src) Expect(err).NotTo(HaveOccurred()) diff --git a/pkg/internal/source/kind.go b/pkg/internal/source/kind.go index adf8d31721..d8537eeb80 100644 --- a/pkg/internal/source/kind.go +++ b/pkg/internal/source/kind.go @@ -14,6 +14,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/interfaces" "sigs.k8s.io/controller-runtime/pkg/predicate" ) @@ -39,14 +40,17 @@ func (ks *Kind[T]) SetPredicates(...predicate.PredicateConstraint) { panic("unimplemented") } -func (ks *Kind[T]) PrepareObject(h handler.ObjectHandler[T], prct ...predicate.ObjectPredicate[T]) { +func (ks *Kind[T]) PrepareObject(h handler.ObjectHandler[T], prct ...predicate.ObjectPredicate[T]) interfaces.SyncingSource { ks.handler = h ks.predicates = prct + + return ks } -func (ks *Kind[T]) Prepare(h handler.EventHandler, prct ...predicate.Predicate) { +func (ks *Kind[T]) Prepare(h handler.EventHandler, prct ...predicate.Predicate) interfaces.SyncingSource { ks.handler = handler.ObjectFuncAdapter[T](h) ks.predicates = predicate.ObjectPredicatesAdapter[T](prct...) + return ks } // Start is internal and should be called only by the Controller to register an EventHandler with the Informer diff --git a/pkg/predicate/predicate.go b/pkg/predicate/predicate.go index 62e9965a0d..2f45bfca66 100644 --- a/pkg/predicate/predicate.go +++ b/pkg/predicate/predicate.go @@ -609,7 +609,7 @@ func ObjectPredicateAdapter[T client.Object](h Predicate) ObjectPredicate[T] { func ObjectPredicatesAdapter[T client.Object](predicates ...Predicate) (prdt []ObjectPredicate[T]) { for _, p := range predicates { - prdt = append(prdt, ObjectPredicatesAdapter[T](p)...) + prdt = append(prdt, ObjectPredicateAdapter[T](p)) } return diff --git a/pkg/source/example_test.go b/pkg/source/example_test.go index 60cfd83c6b..9611aecb23 100644 --- a/pkg/source/example_test.go +++ b/pkg/source/example_test.go @@ -32,8 +32,7 @@ var ctrl controller.Controller // with the Name and Namespace of the Pod. func ExampleKind() { instance := source.Kind(mgr.GetCache(), &corev1.Pod{}) - instance.Prepare(&handler.EnqueueRequestForObject{}) - err := ctrl.Watch(instance) + err := ctrl.Watch(instance.Prepare(&handler.EnqueueRequestForObject{})) if err != nil { // handle it } diff --git a/pkg/source/source.go b/pkg/source/source.go index 1a5d4f9ce7..b4d96691b2 100644 --- a/pkg/source/source.go +++ b/pkg/source/source.go @@ -25,6 +25,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/interfaces" internal "sigs.k8s.io/controller-runtime/pkg/internal/source" "sigs.k8s.io/controller-runtime/pkg/cache" @@ -36,50 +37,14 @@ const ( defaultBufferSize = 1024 ) -// Source is a source of events (e.g. Create, Update, Delete operations on Kubernetes Objects, Webhook callbacks, etc) -// which should be processed by event.EventHandlers to enqueue reconcile.Requests. -// -// * Use Kind for events originating in the cluster (e.g. Pod Create, Pod Update, Deployment Update). -// -// * Use Channel for events originating outside the cluster (e.g. GitHub Webhook callback, Polling external urls). -// -// Users may build their own Source implementations. -type Source interface { - // Start is internal and should be called only by the Controller to register an EventHandler with the Informer - // to enqueue reconcile.Requests. - // Start(context.Context, handler.EventHandler, workqueue.RateLimitingInterface, ...predicate.Predicate) error - Start(context.Context, workqueue.RateLimitingInterface) error -} - -type SourcePrepare interface { - Source - Prepare(handler.EventHandler, ...predicate.Predicate) -} - -type ObjectSourcePrepare[T any] interface { - Source - PrepareObject(handler.ObjectHandler[T], ...predicate.ObjectPredicate[T]) -} +type Source = interfaces.Source +type Syncing = interfaces.Syncing +type SyncingSource = interfaces.SyncingSource +type PrepareSyncing = interfaces.PrepareSyncing +type PrepareSource = interfaces.PrepareSource -// SyncingSource is a source that needs syncing prior to being usable. The controller -// will call its WaitForSync prior to starting workers. -type SyncingSource interface { - Source - Syncing -} - -type PrepareSyncing interface { - SyncingSource - SourcePrepare -} - -type ObjectPrepare[T any] interface { - SyncingSource - ObjectSourcePrepare[T] -} - -type Syncing interface { - WaitForSync(ctx context.Context) error +type PrepareSyncingObject[T any] interface { + interfaces.PrepareSyncingObject[T] } // Kind creates a KindSource with the given cache provider. @@ -88,11 +53,11 @@ func Kind(cache cache.Cache, object client.Object) PrepareSyncing { } // ObjectKind creates a typed KindSource with the given cache provider. -func ObjectKind[T client.Object](cache cache.Cache, object T) ObjectPrepare[T] { +func ObjectKind[T client.Object](cache cache.Cache, object T) PrepareSyncingObject[T] { return &internal.Kind[T]{Type: object, Cache: cache} } -var _ SourcePrepare = &Channel{} +var _ PrepareSource = &Channel{} // Channel is used to provide a source of events originating outside the cluster // (e.g. GitHub Webhook callback). Channel requires the user to wire the external @@ -123,13 +88,19 @@ func (cs *Channel) String() string { return fmt.Sprintf("channel source: %p", cs) } +func (cs *Channel) WaitForSync(ctx context.Context) error { + return nil +} + // Prepare implements Source preparation and should only be called when handler and predicates are available. func (cs *Channel) Prepare( handler handler.EventHandler, prct ...predicate.Predicate, -) { +) SyncingSource { cs.predicates = prct cs.handler = handler + + return cs } // Start implements Source and should only be called by the Controller. @@ -237,14 +208,20 @@ type Informer struct { handler handler.EventHandler } -var _ SourcePrepare = &Informer{} +var _ PrepareSource = &Informer{} func (is *Informer) Prepare( h handler.EventHandler, prct ...predicate.Predicate, -) { +) SyncingSource { is.handler = h is.predicates = prct + + return is +} + +func (cs *Informer) WaitForSync(ctx context.Context) error { + return nil } // Start is internal and should be called only by the Controller to register an EventHandler with the Informer diff --git a/pkg/source/source_integration_test.go b/pkg/source/source_integration_test.go index 2d724b6508..a1d9872b61 100644 --- a/pkg/source/source_integration_test.go +++ b/pkg/source/source_integration_test.go @@ -24,6 +24,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/interfaces" "sigs.k8s.io/controller-runtime/pkg/source" . "github.com/onsi/ginkgo/v2" @@ -37,7 +38,7 @@ import ( ) var _ = Describe("Source", func() { - var instance1, instance2 source.SourcePrepare + var instance1, instance2 interfaces.PrepareSource var obj client.Object var q workqueue.RateLimitingInterface var c1, c2 chan interface{} @@ -124,10 +125,8 @@ var _ = Describe("Source", func() { handler2 := newHandler(c2) // Create 2 instances - instance1.Prepare(handler1) - instance2.Prepare(handler2) - Expect(instance1.Start(ctx, q)).To(Succeed()) - Expect(instance2.Start(ctx, q)).To(Succeed()) + Expect(instance1.Prepare(handler1).Start(ctx, q)).To(Succeed()) + Expect(instance2.Prepare(handler2).Start(ctx, q)).To(Succeed()) By("Creating a Deployment and expecting the CreateEvent.") created, err = client.Create(ctx, deployment, metav1.CreateOptions{}) diff --git a/pkg/source/source_test.go b/pkg/source/source_test.go index 8d7cedd164..11b05c751d 100644 --- a/pkg/source/source_test.go +++ b/pkg/source/source_test.go @@ -66,27 +66,27 @@ var _ = Describe("Source", func() { } q := workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "test") - instance := source.Kind(ic, &corev1.Pod{}) - instance.Prepare(handler.Funcs{ - CreateFunc: func(ctx context.Context, evt event.CreateEvent, q2 workqueue.RateLimitingInterface) { - defer GinkgoRecover() - Expect(q2).To(Equal(q)) - Expect(evt.Object).To(Equal(p)) - close(c) - }, - UpdateFunc: func(context.Context, event.UpdateEvent, workqueue.RateLimitingInterface) { - defer GinkgoRecover() - Fail("Unexpected UpdateEvent") - }, - DeleteFunc: func(context.Context, event.DeleteEvent, workqueue.RateLimitingInterface) { - defer GinkgoRecover() - Fail("Unexpected DeleteEvent") - }, - GenericFunc: func(context.Context, event.GenericEvent, workqueue.RateLimitingInterface) { - defer GinkgoRecover() - Fail("Unexpected GenericEvent") - }, - }) + instance := source.Kind(ic, &corev1.Pod{}). + Prepare(handler.Funcs{ + CreateFunc: func(ctx context.Context, evt event.CreateEvent, q2 workqueue.RateLimitingInterface) { + defer GinkgoRecover() + Expect(q2).To(Equal(q)) + Expect(evt.Object).To(Equal(p)) + close(c) + }, + UpdateFunc: func(context.Context, event.UpdateEvent, workqueue.RateLimitingInterface) { + defer GinkgoRecover() + Fail("Unexpected UpdateEvent") + }, + DeleteFunc: func(context.Context, event.DeleteEvent, workqueue.RateLimitingInterface) { + defer GinkgoRecover() + Fail("Unexpected DeleteEvent") + }, + GenericFunc: func(context.Context, event.GenericEvent, workqueue.RateLimitingInterface) { + defer GinkgoRecover() + Fail("Unexpected GenericEvent") + }, + }) err := instance.Start(ctx, q) Expect(err).NotTo(HaveOccurred()) Expect(instance.WaitForSync(context.Background())).NotTo(HaveOccurred()) @@ -104,30 +104,30 @@ var _ = Describe("Source", func() { ic := &informertest.FakeInformers{} q := workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "test") - instance := source.Kind(ic, &corev1.Pod{}) - instance.Prepare(handler.Funcs{ - CreateFunc: func(ctx context.Context, evt event.CreateEvent, q2 workqueue.RateLimitingInterface) { - defer GinkgoRecover() - Fail("Unexpected CreateEvent") - }, - UpdateFunc: func(ctx context.Context, evt event.UpdateEvent, q2 workqueue.RateLimitingInterface) { - defer GinkgoRecover() - Expect(q2).To(BeIdenticalTo(q)) - Expect(evt.ObjectOld).To(Equal(p)) + instance := source.Kind(ic, &corev1.Pod{}). + Prepare(handler.Funcs{ + CreateFunc: func(ctx context.Context, evt event.CreateEvent, q2 workqueue.RateLimitingInterface) { + defer GinkgoRecover() + Fail("Unexpected CreateEvent") + }, + UpdateFunc: func(ctx context.Context, evt event.UpdateEvent, q2 workqueue.RateLimitingInterface) { + defer GinkgoRecover() + Expect(q2).To(BeIdenticalTo(q)) + Expect(evt.ObjectOld).To(Equal(p)) - Expect(evt.ObjectNew).To(Equal(p2)) + Expect(evt.ObjectNew).To(Equal(p2)) - close(c) - }, - DeleteFunc: func(context.Context, event.DeleteEvent, workqueue.RateLimitingInterface) { - defer GinkgoRecover() - Fail("Unexpected DeleteEvent") - }, - GenericFunc: func(context.Context, event.GenericEvent, workqueue.RateLimitingInterface) { - defer GinkgoRecover() - Fail("Unexpected GenericEvent") - }, - }) + close(c) + }, + DeleteFunc: func(context.Context, event.DeleteEvent, workqueue.RateLimitingInterface) { + defer GinkgoRecover() + Fail("Unexpected DeleteEvent") + }, + GenericFunc: func(context.Context, event.GenericEvent, workqueue.RateLimitingInterface) { + defer GinkgoRecover() + Fail("Unexpected GenericEvent") + }, + }) err := instance.Start(ctx, q) Expect(err).NotTo(HaveOccurred()) Expect(instance.WaitForSync(context.Background())).NotTo(HaveOccurred()) @@ -150,27 +150,27 @@ var _ = Describe("Source", func() { } q := workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "test") - instance := source.Kind(ic, &corev1.Pod{}) - instance.Prepare(handler.Funcs{ - CreateFunc: func(context.Context, event.CreateEvent, workqueue.RateLimitingInterface) { - defer GinkgoRecover() - Fail("Unexpected DeleteEvent") - }, - UpdateFunc: func(context.Context, event.UpdateEvent, workqueue.RateLimitingInterface) { - defer GinkgoRecover() - Fail("Unexpected UpdateEvent") - }, - DeleteFunc: func(ctx context.Context, evt event.DeleteEvent, q2 workqueue.RateLimitingInterface) { - defer GinkgoRecover() - Expect(q2).To(BeIdenticalTo(q)) - Expect(evt.Object).To(Equal(p)) - close(c) - }, - GenericFunc: func(context.Context, event.GenericEvent, workqueue.RateLimitingInterface) { - defer GinkgoRecover() - Fail("Unexpected GenericEvent") - }, - }) + instance := source.Kind(ic, &corev1.Pod{}). + Prepare(handler.Funcs{ + CreateFunc: func(context.Context, event.CreateEvent, workqueue.RateLimitingInterface) { + defer GinkgoRecover() + Fail("Unexpected DeleteEvent") + }, + UpdateFunc: func(context.Context, event.UpdateEvent, workqueue.RateLimitingInterface) { + defer GinkgoRecover() + Fail("Unexpected UpdateEvent") + }, + DeleteFunc: func(ctx context.Context, evt event.DeleteEvent, q2 workqueue.RateLimitingInterface) { + defer GinkgoRecover() + Expect(q2).To(BeIdenticalTo(q)) + Expect(evt.Object).To(Equal(p)) + close(c) + }, + GenericFunc: func(context.Context, event.GenericEvent, workqueue.RateLimitingInterface) { + defer GinkgoRecover() + Fail("Unexpected GenericEvent") + }, + }) err := instance.Start(ctx, q) Expect(err).NotTo(HaveOccurred()) Expect(instance.WaitForSync(context.Background())).NotTo(HaveOccurred()) @@ -230,8 +230,7 @@ var _ = Describe("Source", func() { defer cancel() instance := source.Kind(ic, &corev1.Pod{}) - instance.Prepare(handler.Funcs{}) - err := instance.Start(ctx, q) + err := instance.Prepare(handler.Funcs{}).Start(ctx, q) Expect(err).NotTo(HaveOccurred()) Eventually(instance.WaitForSync).WithArguments(context.Background()).Should(HaveOccurred()) }) From 4d770047de3a10110f5ded3bce46c178d68d0500 Mon Sep 17 00:00:00 2001 From: Danil Grigorev Date: Wed, 17 Apr 2024 13:04:11 +0200 Subject: [PATCH 5/7] Cleanup Signed-off-by: Danil Grigorev --- pkg/builder/options.go | 44 ------------------------------------------ 1 file changed, 44 deletions(-) diff --git a/pkg/builder/options.go b/pkg/builder/options.go index cf5158883f..15f66b2a82 100644 --- a/pkg/builder/options.go +++ b/pkg/builder/options.go @@ -40,12 +40,6 @@ type WatchesOption interface { ApplyToWatches(*WatchesInput) } -type Option interface { - ForOption - OwnsOption - WatchesOption -} - // }}} // {{{ Multi-Type Options @@ -81,36 +75,6 @@ var _ ForOption = &Predicates{} var _ OwnsOption = &Predicates{} var _ WatchesOption = &Predicates{} -// WithPredicates sets the given predicates list. -func WithObjectPredicates[T any](predicates ...predicate.ObjectPredicate[T]) ObjectPredicates[T] { - return ObjectPredicates[T]{ - predicates: predicates, - } -} - -type ObjectPredicates[T any] struct { - predicates []predicate.ObjectPredicate[T] -} - -// ApplyToOwns implements OwnsOption. -func (o *ObjectPredicates[T]) ApplyToOwns(*OwnsInput) { - panic("unimplemented") -} - -// ApplyToWatches implements WatchesOption. -func (o *ObjectPredicates[T]) ApplyToWatches(*WatchesInput) { - panic("unimplemented") -} - -// ApplyToFor implements ForOption. -func (o *ObjectPredicates[T]) ApplyToFor(*ForInput) { - panic("unimplemented") -} - -var _ ForOption = &ObjectPredicates[any]{} -var _ OwnsOption = &ObjectPredicates[any]{} -var _ WatchesOption = &ObjectPredicates[any]{} - // }}} // {{{ For & Owns Dual-Type options @@ -190,11 +154,3 @@ type matchEveryOwner struct{} func (o matchEveryOwner) ApplyToOwns(opts *OwnsInput) { opts.matchEveryOwner = true } - -// ApplyToFor applies this configuration to the given OwnsInput options. -func (o matchEveryOwner) ApplyToFor(opts *ForInput) { -} - -// ApplyToWatches applies this configuration to the given OwnsInput options. -func (o matchEveryOwner) ApplyToWatches(opts *WatchesInput) { -} From 8984b304957169389c57b72d4cd2fcda983512d2 Mon Sep 17 00:00:00 2001 From: Danil Grigorev Date: Wed, 17 Apr 2024 14:47:11 +0200 Subject: [PATCH 6/7] Remove builder requirements from generic wrapper methods Signed-off-by: Danil Grigorev --- example_test.go | 56 +++++++++++++++++++++++++++++++++++++-- pkg/builder/controller.go | 16 +++++------ 2 files changed, 61 insertions(+), 11 deletions(-) diff --git a/example_test.go b/example_test.go index 2890699d56..e86719480f 100644 --- a/example_test.go +++ b/example_test.go @@ -87,8 +87,8 @@ func GenericExample() { b := ctrl.NewControllerManagedBy(manager) // Create the Controller // ReplicaSet is the Application API - b.Add(builder.For(b, &appsv1.ReplicaSet{})). - Add(builder.Owns(b, &appsv1.ReplicaSet{}, &corev1.Pod{})). // ReplicaSet owns Pods created by it + b.Add(builder.For(manager, &appsv1.ReplicaSet{})). + Add(builder.Owns(manager, &appsv1.ReplicaSet{}, &corev1.Pod{})). // ReplicaSet owns Pods created by it Complete(&ReplicaSetReconciler{Client: manager.GetClient()}) if err != nil { log.Error(err, "could not create controller") @@ -189,6 +189,58 @@ func Example_customHandler() { } } +// This example creates a simple application Controller that is configured for ExampleCRDWithConfigMapRef CRD. +// Any change in the configMap referenced in this Custom Resource will cause the re-reconcile of the parent ExampleCRDWithConfigMapRef +// due to the implementation of the .Watches method of "sigs.k8s.io/controller-runtime/pkg/builder".Builder. +func Example_generic_customHandler() { + log := ctrl.Log.WithName("builder-examples") + + manager, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{}) + if err != nil { + log.Error(err, "could not create manager") + os.Exit(1) + } + + err = ctrl. + NewControllerManagedBy(manager). + For(&ExampleCRDWithConfigMapRef{}). + Add(builder.Watches(manager, &corev1.ConfigMap{}, handler.EnqueueRequestsFromObjectMap(func(ctx context.Context, cm *corev1.ConfigMap) []ctrl.Request { + // map a change from referenced configMap to ExampleCRDWithConfigMapRef, which causes its re-reconcile + crList := &ExampleCRDWithConfigMapRefList{} + if err := manager.GetClient().List(ctx, crList); err != nil { + manager.GetLogger().Error(err, "while listing ExampleCRDWithConfigMapRefs") + return nil + } + + reqs := make([]ctrl.Request, 0, len(crList.Items)) + for _, item := range crList.Items { + if item.ConfigMapRef.Name == cm.Name && cm.Data["Namespace"] == item.GetNamespace() { + reqs = append(reqs, ctrl.Request{ + NamespacedName: types.NamespacedName{ + Namespace: item.GetNamespace(), + Name: item.GetName(), + }, + }) + } + } + + return reqs + }))). + Complete(reconcile.Func(func(ctx context.Context, r reconcile.Request) (reconcile.Result, error) { + // Your business logic to implement the API by creating, updating, deleting objects goes here. + return reconcile.Result{}, nil + })) + if err != nil { + log.Error(err, "could not create controller") + os.Exit(1) + } + + if err := manager.Start(ctrl.SetupSignalHandler()); err != nil { + log.Error(err, "could not start manager") + os.Exit(1) + } +} + // This example creates a simple application Controller that is configured for ReplicaSets and Pods. // This application controller will be running leader election with the provided configuration in the manager options. // If leader election configuration is not provided, controller runs leader election with default values. diff --git a/pkg/builder/controller.go b/pkg/builder/controller.go index b23468ee15..f75bfaaaec 100644 --- a/pkg/builder/controller.go +++ b/pkg/builder/controller.go @@ -187,25 +187,23 @@ func (blder *Builder) WatchesRawSource(src source.PrepareSyncing, eventHandler h return blder } -func For[T client.Object](blder *Builder, object T, prct ...predicate.ObjectPredicate[T]) source.Source { - blder.forInput = ForInput{object: object} - - return source.ObjectKind(blder.mgr.GetCache(), object).PrepareObject(&handler.EnqueueRequest[T]{}, prct...) +func For[T client.Object](mgr manager.Manager, object T, prct ...predicate.ObjectPredicate[T]) source.Source { + return source.ObjectKind(mgr.GetCache(), object).PrepareObject(&handler.EnqueueRequest[T]{}, prct...) } -func Owns[F, T client.Object](blder *Builder, owner F, owned T, prct ...predicate.ObjectPredicate[T]) source.Source { - src := source.ObjectKind(blder.mgr.GetCache(), owned) +func Owns[F, T client.Object](mgr manager.Manager, owner F, owned T, prct ...predicate.ObjectPredicate[T]) source.Source { + src := source.ObjectKind(mgr.GetCache(), owned) hdler := handler.EnqueueRequestForOwner( - blder.mgr.GetScheme(), blder.mgr.GetRESTMapper(), + mgr.GetScheme(), mgr.GetRESTMapper(), owner, ) return src.PrepareObject(handler.ObjectFuncAdapter[T](hdler), prct...) } -func Watches[T client.Object](blder *Builder, object T, eventHandler handler.ObjectHandler[T], prct ...predicate.ObjectPredicate[T]) source.Source { - return source.ObjectKind(blder.mgr.GetCache(), object).PrepareObject(eventHandler, prct...) +func Watches[T client.Object](mgr manager.Manager, object T, eventHandler handler.ObjectHandler[T], prct ...predicate.ObjectPredicate[T]) source.Source { + return source.ObjectKind(mgr.GetCache(), object).PrepareObject(eventHandler, prct...) } func (blder *Builder) Add(src source.Source) *Builder { From ebea1b0491108121d57978e63af9bfec7f4217ed Mon Sep 17 00:00:00 2001 From: Danil Grigorev Date: Wed, 17 Apr 2024 17:45:31 +0200 Subject: [PATCH 7/7] Cleanup and lint fixes Signed-off-by: Danil Grigorev --- example_test.go | 2 +- pkg/builder/controller.go | 4 ++++ pkg/cache/cache.go | 8 -------- pkg/controller/controller.go | 6 +++--- pkg/handler/enqueue_mapped.go | 6 ++++-- pkg/handler/enqueue_mapped_typed.go | 18 +++--------------- pkg/handler/enqueue_typed.go | 1 + pkg/handler/eventhandler.go | 28 +++++++++++++++------------- pkg/interfaces/source.go | 9 +++++---- pkg/internal/source/internal_test.go | 13 +++++++------ pkg/internal/source/kind.go | 11 ++++------- pkg/predicate/predicate.go | 24 +++++++++++------------- pkg/source/source.go | 23 ++++++++++++++++++++++- 13 files changed, 80 insertions(+), 73 deletions(-) diff --git a/example_test.go b/example_test.go index e86719480f..d3a113ec39 100644 --- a/example_test.go +++ b/example_test.go @@ -87,7 +87,7 @@ func GenericExample() { b := ctrl.NewControllerManagedBy(manager) // Create the Controller // ReplicaSet is the Application API - b.Add(builder.For(manager, &appsv1.ReplicaSet{})). + err = b.Add(builder.For(manager, &appsv1.ReplicaSet{})). Add(builder.Owns(manager, &appsv1.ReplicaSet{}, &corev1.Pod{})). // ReplicaSet owns Pods created by it Complete(&ReplicaSetReconciler{Client: manager.GetClient()}) if err != nil { diff --git a/pkg/builder/controller.go b/pkg/builder/controller.go index f75bfaaaec..e495749abc 100644 --- a/pkg/builder/controller.go +++ b/pkg/builder/controller.go @@ -187,10 +187,12 @@ func (blder *Builder) WatchesRawSource(src source.PrepareSyncing, eventHandler h return blder } +// For defines the type of Object being reconciled and allows to respond to object events inheriting the object type at all cases. func For[T client.Object](mgr manager.Manager, object T, prct ...predicate.ObjectPredicate[T]) source.Source { return source.ObjectKind(mgr.GetCache(), object).PrepareObject(&handler.EnqueueRequest[T]{}, prct...) } +// Owns defines the type of owner and owned objects to watch with predicates inheriting the owned object type applying to owned object. func Owns[F, T client.Object](mgr manager.Manager, owner F, owned T, prct ...predicate.ObjectPredicate[T]) source.Source { src := source.ObjectKind(mgr.GetCache(), owned) @@ -202,10 +204,12 @@ func Owns[F, T client.Object](mgr manager.Manager, owner F, owned T, prct ...pre return src.PrepareObject(handler.ObjectFuncAdapter[T](hdler), prct...) } +// Watches defines the type of object to watch with ObjectHandler and predicates inheriting the object type. func Watches[T client.Object](mgr manager.Manager, object T, eventHandler handler.ObjectHandler[T], prct ...predicate.ObjectPredicate[T]) source.Source { return source.ObjectKind(mgr.GetCache(), object).PrepareObject(eventHandler, prct...) } +// Add allows to pass a prepared source object with a fully defined event handler and predicates list. func (blder *Builder) Add(src source.Source) *Builder { blder.rawWatches = append(blder.rawWatches, src) return blder diff --git a/pkg/cache/cache.go b/pkg/cache/cache.go index c104168aa5..e23045bf40 100644 --- a/pkg/cache/cache.go +++ b/pkg/cache/cache.go @@ -96,14 +96,6 @@ type Informers interface { client.FieldIndexer } -type ObjectInformers[T any] interface { - Informer - - // GetInformer fetches or constructs an informer for the given object that corresponds to a single - // API kind and resource. - GetInformer(ctx context.Context, obj T, opts ...InformerGetOption) (Informer, error) -} - // Informer allows you to interact with the underlying informer. type Informer interface { // AddEventHandler adds an event handler to the shared informer using the shared informer's resync diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go index 72d15661af..3f91769c03 100644 --- a/pkg/controller/controller.go +++ b/pkg/controller/controller.go @@ -192,13 +192,13 @@ func NewUnmanaged(name string, mgr manager.Manager, options Options) (Controller // ReconcileIDFromContext gets the reconcileID from the current context. var ReconcileIDFromContext = controller.ReconcileIDFromContext -// ControllerAdapter is an adapter for old controller implementations -type ControllerAdapter struct { +// Adapter is an adapter for old controller implementations +type Adapter struct { Controller } // Watch implements old controller Watch interface -func (c *ControllerAdapter) Watch(src source.Source, handler handler.EventHandler, predicates ...predicate.Predicate) error { +func (c *Adapter) Watch(src source.Source, handler handler.EventHandler, predicates ...predicate.Predicate) error { source, ok := src.(source.PrepareSource) if !ok { return fmt.Errorf("expected source to fulfill SourcePrepare interface") diff --git a/pkg/handler/enqueue_mapped.go b/pkg/handler/enqueue_mapped.go index c3a24089f7..aab04792be 100644 --- a/pkg/handler/enqueue_mapped.go +++ b/pkg/handler/enqueue_mapped.go @@ -38,7 +38,9 @@ type MapFunc func(context.Context, client.Object) []reconcile.Request // For UpdateEvents which contain both a new and old object, the transformation function is run on both // objects and both sets of Requests are enqueue. func EnqueueRequestsFromMapFunc(fn MapFunc) EventHandler { - return &enqueueRequestsFromObjectMapFunc[any]{ - toRequests: MapFuncAdapter(fn), + return &enqueueRequestsFromObjectMapFunc[client.Object]{ + toRequests: func(ctx context.Context, obj client.Object) (reqs []reconcile.Request) { + return fn(ctx, obj) + }, } } diff --git a/pkg/handler/enqueue_mapped_typed.go b/pkg/handler/enqueue_mapped_typed.go index 98ffe8d27e..125c772e88 100644 --- a/pkg/handler/enqueue_mapped_typed.go +++ b/pkg/handler/enqueue_mapped_typed.go @@ -20,7 +20,6 @@ import ( "context" "k8s.io/client-go/util/workqueue" - "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/reconcile" ) @@ -30,17 +29,6 @@ import ( // Unlike MapFunc, a specific object type can be used to process and create mapping requests. type ObjectMapFunc[T any] func(context.Context, T) []reconcile.Request -func MapFuncAdapter(m MapFunc) ObjectMapFunc[any] { - return func(ctx context.Context, a any) (reqs []reconcile.Request) { - obj, ok := a.(client.Object) - if ok { - return m(ctx, obj) - } - - return []reconcile.Request{} - } -} - // EnqueueRequestsFromObjectMapFunc enqueues Requests by running a transformation function that outputs a collection // of reconcile.Requests on each Event. The reconcile.Requests may be for an arbitrary set of objects // defined by some user specified transformation of the source Event. (e.g. trigger Reconciler for a set of objects @@ -116,10 +104,10 @@ func (e *enqueueRequestsFromObjectMapFunc[T]) Create(ctx context.Context, evt ev // Update implements EventHandler. func (e *enqueueRequestsFromObjectMapFunc[T]) Update(ctx context.Context, evt event.UpdateEvent, q workqueue.RateLimitingInterface) { - old, okOld := evt.ObjectOld.(T) - new, okNew := evt.ObjectNew.(T) + oldObj, okOld := evt.ObjectOld.(T) + newObj, okNew := evt.ObjectNew.(T) if okOld && okNew { - e.OnUpdate(ctx, old, new, q) + e.OnUpdate(ctx, oldObj, newObj, q) } } diff --git a/pkg/handler/enqueue_typed.go b/pkg/handler/enqueue_typed.go index 5801ee9f67..b26905d2ff 100644 --- a/pkg/handler/enqueue_typed.go +++ b/pkg/handler/enqueue_typed.go @@ -29,6 +29,7 @@ import ( var _ EventHandler = &EnqueueRequest[metav1.Object]{} var _ ObjectHandler[metav1.Object] = &EnqueueRequest[metav1.Object]{} +// Request is a minimal subset of a client.Object interface, allowing to enact on non kubernetes resources. type Request interface { GetName() string GetNamespace() string diff --git a/pkg/handler/eventhandler.go b/pkg/handler/eventhandler.go index 121e675b19..cab0ef002e 100644 --- a/pkg/handler/eventhandler.go +++ b/pkg/handler/eventhandler.go @@ -125,7 +125,7 @@ var _ EventHandler = Funcs{} var _ EventHandler = ObjectFuncs[any]{} var _ ObjectHandler[any] = ObjectFuncs[any]{} -// Funcs is a function that implements Predicate. +// ObjectFuncs is a function that implements ObjectPredicate. type ObjectFuncs[T any] struct { // Create is called in response to an add event. Defaults to no-op. // RateLimitingInterface is used to enqueue reconcile.Requests. @@ -146,10 +146,10 @@ type ObjectFuncs[T any] struct { // Update implements Predicate. func (p ObjectFuncs[T]) Update(ctx context.Context, e event.UpdateEvent, q workqueue.RateLimitingInterface) { - new, ok := e.ObjectNew.(T) - old, oldOk := e.ObjectOld.(T) - if ok && oldOk { - p.OnUpdate(ctx, old, new, q) + objNew, newOk := e.ObjectNew.(T) + objOld, oldOk := e.ObjectOld.(T) + if newOk && oldOk { + p.OnUpdate(ctx, objOld, objNew, q) } } @@ -177,34 +177,35 @@ func (p ObjectFuncs[T]) Delete(ctx context.Context, e event.DeleteEvent, q workq } } -// Update implements Predicate. +// OnUpdate implements ObjectPredicate. func (p ObjectFuncs[T]) OnUpdate(ctx context.Context, old, new T, q workqueue.RateLimitingInterface) { if p.UpdateFunc != nil { p.UpdateFunc(ctx, old, new, q) } } -// Generic implements Predicate. +// OnGeneric implements ObjectPredicate. func (p ObjectFuncs[T]) OnGeneric(ctx context.Context, obj T, q workqueue.RateLimitingInterface) { if p.GenericFunc != nil { p.GenericFunc(ctx, obj, q) } } -// Create implements Predicate. +// OnCreate implements ObjectPredicate. func (p ObjectFuncs[T]) OnCreate(ctx context.Context, obj T, q workqueue.RateLimitingInterface) { if p.CreateFunc != nil { p.CreateFunc(ctx, obj, q) } } -// Delete implements Predicate. +// OnDelete implements ObjectPredicate. func (p ObjectFuncs[T]) OnDelete(ctx context.Context, obj T, q workqueue.RateLimitingInterface) { if p.DeleteFunc != nil { p.DeleteFunc(ctx, obj, q) } } +// ObjectFuncAdapter allows to reuse existing EventHandler for a typed ObjectHandler func ObjectFuncAdapter[T client.Object](h EventHandler) ObjectHandler[T] { return ObjectFuncs[T]{ CreateFunc: func(ctx context.Context, obj T, queue workqueue.RateLimitingInterface) { @@ -222,6 +223,7 @@ func ObjectFuncAdapter[T client.Object](h EventHandler) ObjectHandler[T] { } } +// EventHandlerAdapter allows to reuse existing typed event handler as EventHandler func EventHandlerAdapter[T client.Object](h ObjectHandler[T]) EventHandler { return Funcs{ CreateFunc: func(ctx context.Context, e event.CreateEvent, queue workqueue.RateLimitingInterface) { @@ -243,10 +245,10 @@ func EventHandlerAdapter[T client.Object](h ObjectHandler[T]) EventHandler { } }, UpdateFunc: func(ctx context.Context, e event.UpdateEvent, queue workqueue.RateLimitingInterface) { - new, ok := e.ObjectNew.(T) - old, oldOk := e.ObjectOld.(T) - if ok && oldOk { - h.OnUpdate(ctx, old, new, queue) + objNew, newOk := e.ObjectNew.(T) + objOld, oldOk := e.ObjectOld.(T) + if newOk && oldOk { + h.OnUpdate(ctx, objOld, objNew, queue) } }, } diff --git a/pkg/interfaces/source.go b/pkg/interfaces/source.go index bbb6a0b7d9..0e7f77f592 100644 --- a/pkg/interfaces/source.go +++ b/pkg/interfaces/source.go @@ -13,6 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ + package interfaces import ( @@ -37,12 +38,12 @@ type Source interface { Start(context.Context, workqueue.RateLimitingInterface) error } -// PrepareSource: Prepares a Source to be used with EventHandler and predicates +// PrepareSource - Prepares a Source to be used with EventHandler and predicates type PrepareSource interface { Prepare(handler.EventHandler, ...predicate.Predicate) SyncingSource } -// PrepareSourceObject[T]: Prepares a Source preserving the object type +// PrepareSourceObject - Prepares a Source preserving the object type type PrepareSourceObject[T any] interface { PrepareObject(handler.ObjectHandler[T], ...predicate.ObjectPredicate[T]) SyncingSource } @@ -59,13 +60,13 @@ type SyncingSource interface { Syncing } -// PrepareSyncing: A SyncingSource that also implements SourcePrepare and has WaitForSync method +// PrepareSyncing - a SyncingSource that also implements SourcePrepare and has WaitForSync method type PrepareSyncing interface { SyncingSource PrepareSource } -// PrepareSyncingObject[T]: A SyncingSource that also implements PrepareSourceObject[T] and has WaitForSync method +// PrepareSyncingObject - a SyncingSource that also implements PrepareSourceObject[T] and has WaitForSync method type PrepareSyncingObject[T any] interface { SyncingSource PrepareSourceObject[T] diff --git a/pkg/internal/source/internal_test.go b/pkg/internal/source/internal_test.go index 92d20fef4e..a299673625 100644 --- a/pkg/internal/source/internal_test.go +++ b/pkg/internal/source/internal_test.go @@ -23,7 +23,6 @@ import ( . "github.com/onsi/gomega" "k8s.io/client-go/tools/cache" "k8s.io/client-go/util/workqueue" - "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/handler" internal "sigs.k8s.io/controller-runtime/pkg/internal/source" @@ -59,16 +58,16 @@ func newFunc[T any](_ T) *handler.ObjectFuncs[T] { func newSetFunc[T any](_ T, set *bool) *handler.ObjectFuncs[T] { return &handler.ObjectFuncs[T]{ CreateFunc: func(context.Context, T, workqueue.RateLimitingInterface) { - set = ptr.To(true) + *set = true }, DeleteFunc: func(context.Context, T, workqueue.RateLimitingInterface) { - set = ptr.To(true) + *set = true }, UpdateFunc: func(context.Context, T, T, workqueue.RateLimitingInterface) { - set = ptr.To(true) + *set = true }, GenericFunc: func(context.Context, T, workqueue.RateLimitingInterface) { - set = ptr.To(true) + *set = true }, } } @@ -125,7 +124,9 @@ var _ = Describe("Internal", func() { set = false instance = internal.NewEventHandler(ctx, &controllertest.Queue{}, setfuncs, []predicate.ObjectPredicate[*corev1.Pod]{ - predicate.ObjectFuncs[*corev1.Pod]{CreateFunc: func(*corev1.Pod) bool { return true }}, + predicate.ObjectFuncs[*corev1.Pod]{CreateFunc: func(*corev1.Pod) bool { + return true + }}, }) instance.OnAdd(pod) Expect(set).To(BeTrue()) diff --git a/pkg/internal/source/kind.go b/pkg/internal/source/kind.go index d8537eeb80..73c2d5b3cb 100644 --- a/pkg/internal/source/kind.go +++ b/pkg/internal/source/kind.go @@ -35,11 +35,7 @@ type Kind[T client.Object] struct { handler handler.ObjectHandler[T] } -// SetPredicates implements source.SyncingSource. -func (ks *Kind[T]) SetPredicates(...predicate.PredicateConstraint) { - panic("unimplemented") -} - +// PrepareObject implements PrepareSyncingObject preparation and should only be called when handler and predicates are available. func (ks *Kind[T]) PrepareObject(h handler.ObjectHandler[T], prct ...predicate.ObjectPredicate[T]) interfaces.SyncingSource { ks.handler = h ks.predicates = prct @@ -47,6 +43,7 @@ func (ks *Kind[T]) PrepareObject(h handler.ObjectHandler[T], prct ...predicate.O return ks } +// Prepare implements Source preparation and should only be called when handler and predicates are available. func (ks *Kind[T]) Prepare(h handler.EventHandler, prct ...predicate.Predicate) interfaces.SyncingSource { ks.handler = handler.ObjectFuncAdapter[T](h) ks.predicates = predicate.ObjectPredicatesAdapter[T](prct...) @@ -59,10 +56,10 @@ func (ks *Kind[T]) Start( ctx context.Context, queue workqueue.RateLimitingInterface, ) error { - return ks.Run(ctx, ks.handler, queue, ks.predicates...) + return ks.run(ctx, ks.handler, queue, ks.predicates...) } -func (ks *Kind[T]) Run(ctx context.Context, handler handler.ObjectHandler[T], queue workqueue.RateLimitingInterface, +func (ks *Kind[T]) run(ctx context.Context, handler handler.ObjectHandler[T], queue workqueue.RateLimitingInterface, prct ...predicate.ObjectPredicate[T]) error { if reflect.DeepEqual(ks.Type, *new(T)) { return fmt.Errorf("must create Kind with a non-nil object") diff --git a/pkg/predicate/predicate.go b/pkg/predicate/predicate.go index 2f45bfca66..288493807e 100644 --- a/pkg/predicate/predicate.go +++ b/pkg/predicate/predicate.go @@ -28,10 +28,6 @@ import ( var log = logf.RuntimeLog.WithName("predicate").WithName("eventFilters") -type PredicateConstraint interface { - Register() -} - // Predicate filters events before enqueuing the keys. type Predicate interface { // Create returns true if the Create event should be processed @@ -120,7 +116,7 @@ func (p Funcs) Generic(e event.GenericEvent) bool { return true } -// Funcs is a function that implements Predicate. +// ObjectFuncs is a function that implements Predicate and ObjectPrediace. type ObjectFuncs[T any] struct { // Create returns true if the Create event should be processed CreateFunc func(obj T) bool @@ -137,9 +133,9 @@ type ObjectFuncs[T any] struct { // Update implements Predicate. func (p ObjectFuncs[T]) Update(e event.UpdateEvent) bool { - new, ok := e.ObjectNew.(T) - old, oldOk := e.ObjectOld.(T) - return ok && oldOk && p.OnUpdate(old, new) + newObj, newOk := e.ObjectNew.(T) + oldObj, oldOk := e.ObjectOld.(T) + return newOk && oldOk && p.OnUpdate(oldObj, newObj) } // Generic implements Predicate. @@ -160,22 +156,22 @@ func (p ObjectFuncs[T]) Delete(e event.DeleteEvent) bool { return ok && p.OnDelete(obj) } -// Update implements Predicate. +// OnUpdate implements ObjectPredicate. func (p ObjectFuncs[T]) OnUpdate(old, new T) bool { return p.UpdateFunc == nil || p.UpdateFunc(old, new) } -// Generic implements Predicate. +// OnGeneric implements ObjectPredicate. func (p ObjectFuncs[T]) OnGeneric(obj T) bool { return p.GenericFunc == nil || p.GenericFunc(obj) } -// Create implements Predicate. +// OnCreate implements ObjectPredicate. func (p ObjectFuncs[T]) OnCreate(obj T) bool { return p.CreateFunc == nil || p.CreateFunc(obj) } -// Delete implements Predicate. +// OnDelete implements ObjectPredicate. func (p ObjectFuncs[T]) OnDelete(obj T) bool { return p.DeleteFunc == nil || p.DeleteFunc(obj) } @@ -200,7 +196,7 @@ func NewPredicateFuncs(filter func(object client.Object) bool) Funcs { } } -// NewPredicateFuncs returns a predicate funcs that applies the given filter function +// NewObjectPredicateFuncs returns a typed predicate funcs that applies the given filter function // on CREATE, UPDATE, DELETE and GENERIC events. For UPDATE events, the filter is applied // to the new object. func NewObjectPredicateFuncs[T any](filter func(object T) bool) ObjectFuncs[T] { @@ -590,6 +586,7 @@ func LabelSelectorPredicate(s metav1.LabelSelector) (Predicate, error) { }), nil } +// ObjectPredicateAdapter allows to reuse existing predicate as a typed ObjectPredicate func ObjectPredicateAdapter[T client.Object](h Predicate) ObjectPredicate[T] { return ObjectFuncs[T]{ CreateFunc: func(obj T) bool { @@ -607,6 +604,7 @@ func ObjectPredicateAdapter[T client.Object](h Predicate) ObjectPredicate[T] { } } +// ObjectPredicatesAdapter allows to reuse existing set of predicates as ObjectPredicates func ObjectPredicatesAdapter[T client.Object](predicates ...Predicate) (prdt []ObjectPredicate[T]) { for _, p := range predicates { prdt = append(prdt, ObjectPredicateAdapter[T](p)) diff --git a/pkg/source/source.go b/pkg/source/source.go index b4d96691b2..c70a6dacae 100644 --- a/pkg/source/source.go +++ b/pkg/source/source.go @@ -37,12 +37,30 @@ const ( defaultBufferSize = 1024 ) +// Source is a source of events (e.g. Create, Update, Delete operations on Kubernetes Objects, Webhook callbacks, etc) +// which should be processed by event.EventHandlers to enqueue reconcile.Requests. +// +// * Use Kind for events originating in the cluster (e.g. Pod Create, Pod Update, Deployment Update). +// +// * Use Channel for events originating outside the cluster (e.g. GitHub Webhook callback, Polling external urls). +// +// Users may build their own Source implementations. type Source = interfaces.Source + +// Syncing allows to wait for synchronization with context type Syncing = interfaces.Syncing + +// SyncingSource is a source that needs syncing prior to being usable. The controller +// will call its WaitForSync prior to starting workers. type SyncingSource = interfaces.SyncingSource + +// PrepareSyncing - a SyncingSource that also implements SourcePrepare and has WaitForSync method type PrepareSyncing = interfaces.PrepareSyncing + +// PrepareSource - Prepares a Source to be used with EventHandler and predicates type PrepareSource = interfaces.PrepareSource +// PrepareSyncingObject - a SyncingSource that also implements PrepareSourceObject[T] and has WaitForSync method type PrepareSyncingObject[T any] interface { interfaces.PrepareSyncingObject[T] } @@ -88,6 +106,7 @@ func (cs *Channel) String() string { return fmt.Sprintf("channel source: %p", cs) } +// WaitForSync implements the source.SyncingSource interface func (cs *Channel) WaitForSync(ctx context.Context) error { return nil } @@ -210,6 +229,7 @@ type Informer struct { var _ PrepareSource = &Informer{} +// Prepare implements the source.PrepareSyncing interface func (is *Informer) Prepare( h handler.EventHandler, prct ...predicate.Predicate, @@ -220,7 +240,8 @@ func (is *Informer) Prepare( return is } -func (cs *Informer) WaitForSync(ctx context.Context) error { +// WaitForSync implements the source.SyncingSource interface +func (is *Informer) WaitForSync(ctx context.Context) error { return nil }