-
Notifications
You must be signed in to change notification settings - Fork 0
Watcher Interface
The Watcher interface in Controller Framework provides intelligent, automatic watch management for your Kubernetes controllers. It eliminates the need for manual .Owns() and .Watches() calls while providing efficient, metadata-only watching capabilities.
The Watcher interface enables your controller to automatically set up watches for resources it manages or depends on, without requiring explicit configuration during controller setup. This dynamic approach significantly reduces boilerplate code and makes controllers more maintainable.
type Watcher interface {
ctrl.Manager
// AddWatchSource adds a watch source to the cache
AddWatchSource(key WatchCacheKey)
// IsWatchingSource checks if the key is a watch source
IsWatchingSource(key WatchCacheKey) bool
// GetController returns the controller for the watch cache
GetController() controller.TypedController[reconcile.Request]
}To enable automatic watch management, your reconciler must implement the ReconcilerWithWatcher interface:
type ReconcilerWithWatcher[ControllerResourceType ControllerCustomResource] interface {
Reconciler[ControllerResourceType]
Watcher
}The WatchCache is the built-in implementation that provides efficient watch management:
type WatchCache struct {
cache map[WatchCacheKey]bool
controller controller.TypedController[reconcile.Request]
ctrl.Manager
}type MyAppReconciler struct {
client.Client
ctrlfwk.WatchCache // Add this!
instrument.Instrumenter
record.EventRecorder
RuntimeScheme *runtime.Scheme
}
// Ensure your reconciler implements ReconcilerWithWatcher
var _ ctrlfwk.ReconcilerWithWatcher[*myappv1.MyApp] = &MyAppReconciler{}func main() {
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: scheme,
// ... other options
})
if err != nil {
setupLog.Error(err, "unable to start manager")
os.Exit(1)
}
if err := (&controller.MyAppReconciler{
Client: mgr.GetClient(),
WatchCache: ctrlfwk.NewWatchCache(mgr), // Initialize here
Instrumenter: instrumenter,
EventRecorder: mgr.GetEventRecorderFor("myapp"),
RuntimeScheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller")
os.Exit(1)
}
}func (reconciler *MyAppReconciler) SetupWithManager(mgr ctrl.Manager) error {
ctrler, err := ctrl.NewControllerManagedBy(mgr).
For(&myappv1.MyApp{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})).
Named("myapp").
Build(reconciler) // Use Build() instead of Complete()
// IMPORTANT: Set the controller on WatchCache
reconciler.WatchCache.SetController(ctrler)
return err
}When you use the framework's built-in steps (NewResolveDynamicDependenciesStep and NewReconcileResourcesStep), the WatchCache automatically:
- Detects when your controller interacts with new resource types
- Creates unique identifiers for each resource type
- Sets up metadata-only watches automatically
- Handles watch creation and deduplication
The framework uses metav1.PartialObjectMetadata for efficient watching:
// Framework automatically creates watches like this:
var partialObject metav1.PartialObjectMetadata
partialObject.SetGroupVersionKind(resourceGVK)
err := controller.Watch(
source.Kind(
cache,
&partialObject, // Metadata-only watching
requestHandler,
ResourceVersionChangedPredicate{},
),
)Benefits of this approach:
- Reduced memory usage since only metadata is cached, not full resource specs
- Lower network traffic with smaller objects transferred from API server
- Better performance with faster cache operations and reconciliation triggers
The framework includes an optimized predicate that only triggers on meaningful changes:
type ResourceVersionChangedPredicate struct{}
func (ResourceVersionChangedPredicate) Update(e event.UpdateEvent) bool {
// Only trigger if ResourceVersion actually changed
return e.ObjectOld.GetResourceVersion() != e.ObjectNew.GetResourceVersion()
}
func (ResourceVersionChangedPredicate) Create(e event.CreateEvent) bool {
return false // Don't trigger on initial cache population
}
func (ResourceVersionChangedPredicate) Delete(e event.DeleteEvent) bool {
return true // Always trigger on deletion
}The framework automatically sets up different watch patterns based on resource type:
For managed resources (your controller creates):
// Uses owner reference handler
requestHandler := handler.EnqueueRequestForOwner(
scheme,
restMapper,
customResource,
)For dependencies (external resources you consume):
// Uses managed-by annotation handler
managedByHandler, err := GetManagedByReconcileRequests(customResource, scheme)
requestHandler := handler.EnqueueRequestsFromMapFunc(managedByHandler)The WatchCache prevents duplicate watches through intelligent caching:
watchSource := NewWatchKey(gvk, CacheTypeEnqueueForOwner)
if !reconciler.IsWatchingSource(watchSource) {
// Only set up watch if not already watching this resource type
err := reconciler.GetController().Watch(source)
reconciler.AddWatchSource(watchSource)
}Metadata-only watching reduces memory usage by 70-90% compared to full object caching. The framework only watches resource types actually used by your controller and handles automatic cleanup without manual watch lifecycle management.
Smaller objects are transferred from the API server, smart predicates create fewer unnecessary reconciliation triggers, and the system only responds to actual resource changes.
Metadata operations are significantly faster, better predicate logic reduces unnecessary work, and automatic deduplication prevents redundant watch setup.
func (r *MyAppReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&myappv1.MyApp{}).
Owns(&appsv1.Deployment{}). // Manual watch setup
Owns(&corev1.Service{}). // Manual watch setup
Watches(&corev1.Secret{}, // Manual watch setup
handler.EnqueueRequestsFromMapFunc(r.secretMapper)).
Named("myapp").
Complete(r)
}func (reconciler *MyAppReconciler) SetupWithManager(mgr ctrl.Manager) error {
ctrler, err := ctrl.NewControllerManagedBy(mgr).
For(&myappv1.MyApp{}).
Named("myapp").
Build(reconciler) // WatchCache handles everything automatically
reconciler.WatchCache.SetController(ctrler)
return err
}// Automatic watch setup with framework steps
stepper := ctrlfwk.NewStepper(logger,
ctrlfwk.WithStep(ctrlfwk.NewFindControllerCustomResourceStep(reconciler)),
ctrlfwk.WithStep(ctrlfwk.NewResolveDynamicDependenciesStep(reconciler)), // Sets up dependency watches
ctrlfwk.WithStep(ctrlfwk.NewReconcileResourcesStep(reconciler)), // Sets up resource watches
ctrlfwk.WithStep(ctrlfwk.NewEndStep(reconciler, nil)),
)// Initialize WatchCache in main.go
WatchCache: ctrlfwk.NewWatchCache(mgr),Don't initialize in SetupWithManager as it's too late in the process.
Always set the controller on WatchCache:
reconciler.WatchCache.SetController(ctrler)Forgetting this breaks watch functionality completely.
Use Build() to get manual controller access for WatchCache setup:
ctrler, err := ctrl.NewControllerManagedBy(mgr).
For(&myappv1.MyApp{}).
Build(reconciler)Complete() doesn't give you the controller reference needed for SetController().
Symptoms: Resource changes don't trigger reconciliation Solution:
- Ensure reconciler implements
ReconcilerWithWatcher - Verify
SetController()is called after controller creation - Check that you're using framework steps that trigger watch setup
Symptoms: High memory usage despite using WatchCache Solution:
- Verify you're not using
.Owns()or.Watches()manually - Check that
ResourceVersionChangedPredicateis being used - Ensure you're not storing full objects in custom caches
Symptoms: Controller reconciles too frequently Solution:
- Check predicate implementation
- Verify watch deduplication is working
- Review resource mutation logic for unnecessary updates
Here's a complete example showing proper WatchCache usage:
package controller
import (
"context"
ctrlfwk "github.com/u-ctf/controller-fwk"
"github.com/u-ctf/controller-fwk/instrument"
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/predicate"
"k8s.io/client-go/tools/record"
myappv1 "myapp/api/v1"
)
type MyAppReconciler struct {
client.Client
ctrlfwk.WatchCache // Enable automatic watch management
instrument.Instrumenter
record.EventRecorder
RuntimeScheme *runtime.Scheme
}
// Interface implementations
var _ ctrlfwk.Reconciler[*myappv1.MyApp] = &MyAppReconciler{}
var _ ctrlfwk.ReconcilerWithWatcher[*myappv1.MyApp] = &MyAppReconciler{}
var _ ctrlfwk.ReconcilerWithResources[*myappv1.MyApp, myappv1.MyAppContext] = &MyAppReconciler{}
func (MyAppReconciler) For(*myappv1.MyApp) {}
func (reconciler *MyAppReconciler) GetResources(ctx myappv1.MyAppContext, req ctrl.Request) ([]myappv1.MyAppResource, error) {
return []myappv1.MyAppResource{
resources.NewDeploymentResource(ctx, reconciler), // Automatically watched
resources.NewServiceResource(ctx, reconciler), // Automatically watched
}, nil
}
func (reconciler *MyAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
logger := logf.FromContext(ctx)
stepper := ctrlfwk.NewStepper(logger,
ctrlfwk.WithStep(ctrlfwk.NewFindControllerCustomResourceStep(reconciler)),
ctrlfwk.WithStep(ctrlfwk.NewReconcileResourcesStep(reconciler)), // Triggers automatic watch setup
ctrlfwk.WithStep(ctrlfwk.NewEndStep(reconciler, ctrlfwk.SetReadyCondition(reconciler))),
)
return stepper.Execute(ctx, req)
}
func (reconciler *MyAppReconciler) SetupWithManager(mgr ctrl.Manager) error {
ctrler, err := ctrl.NewControllerManagedBy(mgr).
For(&myappv1.MyApp{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})).
Named("myapp").
Build(reconciler) // Get controller reference
// Set controller on WatchCache for automatic watch management
reconciler.WatchCache.SetController(ctrler)
return err
}The Watcher interface provides automatic watch management with no manual .Owns() or .Watches() setup required. Memory efficiency improves with metadata-only watching that reduces usage by 70-90%. Performance optimization comes from smart predicates and watch deduplication. Code becomes simpler by eliminating boilerplate controller setup, and dynamic discovery automatically handles resource types at runtime.
By implementing the ReconcilerWithWatcher interface and using WatchCache, your controllers become more efficient, maintainable, and performant while requiring significantly less setup code.