// Copyright 2020 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package controllerlib import ( "fmt" "sync" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/events" "k8s.io/client-go/util/workqueue" "go.pinniped.dev/internal/plog" ) type Option func(*controller) func WithMaxRetries(maxRetries int) Option { return func(c *controller) { c.maxRetries = maxRetries } } func WithInitialEvent(key Key) Option { return toNaiveRunOpt(func(c *controller) { c.queueWrapper.Add(key) }) } func WithRateLimiter(limiter workqueue.RateLimiter) Option { return func(c *controller) { c.queue = workqueue.NewNamedRateLimitingQueue(limiter, c.Name()) c.queueWrapper = &queueWrapper{queue: c.queue} } } func WithRecorder(recorder events.EventRecorder) Option { return func(c *controller) { c.recorder = recorder } } func WithInformer(getter InformerGetter, filter Filter, opt InformerOption) Option { informer := getter.Informer() // immediately signal that we intend to use this informer in case it is lazily initialized return toRunOpt(func(c *controller) { if opt.SkipSync && opt.SkipEvents { panic(die("cannot skip syncing and event handlers at the same time")) } if !opt.SkipSync { c.cacheSyncs = append(c.cacheSyncs, informer.HasSynced) } if opt.SkipEvents { return } _, _ = informer.AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { object := metaOrDie(obj) if filter.Add(object) { plog.Debug("handling add", "controller", c.Name(), "namespace", object.GetNamespace(), "name", object.GetName(), "selfLink", object.GetSelfLink(), // TODO: self link is deprecated so we need to extract the GVR in some other way (using a series of schemes?) "kind", fmt.Sprintf("%T", object), ) c.add(filter, object) } }, UpdateFunc: func(oldObj, newObj interface{}) { oldObject := metaOrDie(oldObj) newObject := metaOrDie(newObj) if filter.Update(oldObject, newObject) { plog.Debug("handling update", "controller", c.Name(), "namespace", newObject.GetNamespace(), "name", newObject.GetName(), "selfLink", newObject.GetSelfLink(), // TODO: self link is deprecated so we need to extract the GVR in some other way (using a series of schemes?) "kind", fmt.Sprintf("%T", newObject), ) c.add(filter, newObject) } }, DeleteFunc: func(obj interface{}) { accessor, err := meta.Accessor(obj) if err != nil { tombstone, ok := obj.(cache.DeletedFinalStateUnknown) if !ok { utilruntime.HandleError(fmt.Errorf("%s: could not get object from tombstone: %+v", c.Name(), obj)) return } accessor, err = meta.Accessor(tombstone.Obj) if err != nil { utilruntime.HandleError(fmt.Errorf("%s: tombstone contained object that is not an accessor: %+v", c.Name(), obj)) return } } if filter.Delete(accessor) { plog.Debug("handling delete", "controller", c.Name(), "namespace", accessor.GetNamespace(), "name", accessor.GetName(), "selfLink", accessor.GetSelfLink(), // TODO: self link is deprecated so we need to extract the GVR in some other way (using a series of schemes?) "kind", fmt.Sprintf("%T", accessor), ) c.add(filter, accessor) } }, }) }) } // toRunOpt guarantees that an Option only runs once on the first call to Run (and not New), even if a controller is stopped and restarted. func toRunOpt(opt Option) Option { return toOnceOpt(toNaiveRunOpt(opt)) } // toNaiveRunOpt guarantees that an Option only runs on calls to Run (and not New), even if a controller is stopped and restarted. func toNaiveRunOpt(opt Option) Option { return func(c *controller) { if c.run { opt(c) return } c.runOpts = append(c.runOpts, opt) } } // toOnceOpt guarantees that an Option only runs once. func toOnceOpt(opt Option) Option { var once sync.Once return func(c *controller) { once.Do(func() { opt(c) }) } } func metaOrDie(obj interface{}) metav1.Object { accessor, err := meta.Accessor(obj) if err != nil { panic(err) // this should never happen } return accessor }