Documentation
¶
Overview ¶
Package dynamiccontroller provides a flexible and efficient solution for managing multiple GroupVersionResources (GVRs) in a Kubernetes environment. It implements a single controller capable of dynamically handling various resource types concurrently, adapting to runtime changes without system restarts.
Key features and design considerations:
Multi GVR management: It handles multiple resource types concurrently, creating and managing separate workflows for each.
Dynamic informer management: Creates and deletes informers on the fly for new resource types, allowing real time adaptation to changes in the cluster.
Minimal disruption: Operations on one resource type do not affect the performance or functionality of others.
Minimalism: Unlike controller-runtime, this implementation is tailored specifically for kro's needs, avoiding unnecessary dependencies and overhead.
Future Extensibility: It allows for future enhancements such as sharding and CEL cost aware leader election, which are not readily achievable with k8s.io/controller-runtime.
Why not use k8s.io/controller-runtime:
Static nature: controller-runtime is optimized for statically defined controllers, however kro requires runtime creation and management of controllers for various GVRs.
Overhead reduction: by not including unused features like leader election and certain metrics, this implementation remains minimalistic and efficient.
Customization: this design allows for deep customization and optimization specific to kro's unique requirements for managing multiple GVRs dynamically.
This implementation aims to provide a reusable, efficient, and flexible solution for dynamic multi-GVR controller management in Kubernetes environments.
NOTE(a-hilaly): Potentially we might open source this package for broader use cases.
Index ¶
- type Config
- type DynamicController
- func (dc *DynamicController) Coordinator() *WatchCoordinator
- func (dc *DynamicController) Deregister(_ context.Context, parent schema.GroupVersionResource) error
- func (dc *DynamicController) Register(_ context.Context, parent schema.GroupVersionResource, instanceHandler Handler) error
- func (dc *DynamicController) Start(ctx context.Context) error
- type EnqueueFunc
- type Event
- type EventHandler
- type EventType
- type Handler
- type InstanceWatcher
- type NoopInstanceWatcher
- type ObjectIdentifiers
- type WatchCoordinator
- func (c *WatchCoordinator) ForInstance(parentGVR schema.GroupVersionResource, instance types.NamespacedName) InstanceWatcher
- func (c *WatchCoordinator) InstanceWatchCount() int
- func (c *WatchCoordinator) RemoveInstance(parentGVR schema.GroupVersionResource, instance types.NamespacedName)
- func (c *WatchCoordinator) RemoveParentGVR(parentGVR schema.GroupVersionResource)
- func (c *WatchCoordinator) RouteEvent(event Event)
- func (c *WatchCoordinator) WatchRequestCount() (scalar, collection int)
- type WatchManager
- func (m *WatchManager) ActiveWatchCount() int
- func (m *WatchManager) EnsureWatch(gvr schema.GroupVersionResource, ownerID string) error
- func (m *WatchManager) GetInformer(gvr schema.GroupVersionResource) cache.SharedIndexInformer
- func (m *WatchManager) ReleaseWatch(gvr schema.GroupVersionResource, ownerID string)
- func (m *WatchManager) Shutdown()
- type WatchRequest
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Config ¶
type Config struct {
// Workers specifies the number of workers processing items from the queue
Workers int
// ResyncPeriod defines the interval at which the controller will re list
// the resources, even if there haven't been any changes.
ResyncPeriod time.Duration
// QueueMaxRetries is the maximum number of retries for an item in the queue
// will be retried before being dropped.
//
// NOTE(a-hilaly): I'm not very sure how useful is this, i'm trying to avoid
// situations where reconcile errors exhaust the queue.
QueueMaxRetries int
// MinRetryDelay is the minimum delay before retrying an item in the queue
MinRetryDelay time.Duration
// MaxRetryDelay is the maximum delay before retrying an item in the queue
MaxRetryDelay time.Duration
// RateLimit is the maximum number of events processed per second
RateLimit int
// BurstLimit is the maximum number of events in a burst
BurstLimit int
// QueueShutdownTimeout is the maximum time to wait for the queue to drain before shutting down.
QueueShutdownTimeout time.Duration
}
Config holds the configuration for DynamicController
type DynamicController ¶
type DynamicController struct {
// contains filtered or unexported fields
}
DynamicController (DC) is a single controller capable of managing multiple different kubernetes resources (GVRs) in parallel. It can safely start watching new resources and stop watching others at runtime - hence the term "dynamic". This flexibility allows us to accept and manage various resources in a Kubernetes cluster without requiring restarts or pod redeployments.
It is mainly inspired by native Kubernetes controllers but designed for more flexible and lightweight operation. DC serves as the core component of kro's dynamic resource management system. Its primary purpose is to create and manage "micro" controllers for custom resources defined by users at runtime (via the ResourceGraphDefinition CRs).
The DynamicController uses a layered architecture:
WatchManager: manages informer lifecycle per GVR (owner-set based).
WatchCoordinator: aggregates watch requests from all instance reconcilers, maintains reverse indexes for event routing, and manages shared watches on the WatchManager.
DynamicController: orchestrates parent watches (one per RGD), the work queue, and handler dispatch. Child/external resource watches are handled by the coordinator based on requests from instance reconcilers.
func NewDynamicController ¶
func NewDynamicController( log logr.Logger, config Config, kubeClient k8smetadata.Interface, mapper meta.RESTMapper, ) *DynamicController
NewDynamicController creates a new DynamicController.
func (*DynamicController) Coordinator ¶ added in v0.9.0
func (dc *DynamicController) Coordinator() *WatchCoordinator
Coordinator returns the WatchCoordinator for use by instance controllers.
func (*DynamicController) Deregister ¶
func (dc *DynamicController) Deregister(_ context.Context, parent schema.GroupVersionResource) error
Deregister removes a parent GVR handler and cleans up coordinator state.
func (*DynamicController) Register ¶
func (dc *DynamicController) Register( _ context.Context, parent schema.GroupVersionResource, instanceHandler Handler, ) error
Register registers a parent GVR with a handler. The coordinator discovers child/external GVRs dynamically from Watch() calls made by instance reconcilers.
type EnqueueFunc ¶ added in v0.9.0
type EnqueueFunc func(parentGVR schema.GroupVersionResource, instance types.NamespacedName)
EnqueueFunc is called by the coordinator to enqueue an instance for re-reconciliation when one of its watched resources changes.
type Event ¶ added in v0.9.0
type Event struct {
Type EventType
GVR schema.GroupVersionResource
Name string
Namespace string
Labels map[string]string
// OldLabels holds the labels from the previous version of the object
// (populated only for update events). Used by collection watches to detect
// label changes that cause an object to enter or leave a selector match.
OldLabels map[string]string
}
Event is a normalized watch event emitted by the WatchManager. Consumers decide what to act on -- no old/new comparison or generation filtering is performed by the watch layer.
type EventHandler ¶ added in v0.9.0
type EventHandler func(event Event)
EventHandler processes a single watch event.
type EventType ¶ added in v0.9.0
type EventType string
EventType identifies the kind of change that triggered an event.
type Handler ¶
Handler is used to actually perform the reconciliation logic for an instance GVR and will operate on a single instance of the resource received from the queue
type InstanceWatcher ¶ added in v0.9.0
type InstanceWatcher interface {
// Watch requests that the instance be re-reconciled when the specified
// resource changes. Call this for every resource (managed or external)
// the instance cares about.
//
// For scalar resources: set Name + Namespace.
// For collections: set Selector + Namespace.
//
// Call Watch() BEFORE operating on the resource to avoid event gaps.
Watch(req WatchRequest) error
// Done signals that all Watch() calls for this reconciliation cycle
// are complete. Any watch requests from the previous cycle that were
// NOT re-requested are automatically cleaned up. If commit is false, the
// current cycle is discarded and the previously committed watch set stays
// active.
Done(commit bool)
}
InstanceWatcher is the interface the instance reconciler uses to request watches. It is scoped to a single instance and obtained via WatchCoordinator.ForInstance().
type NoopInstanceWatcher ¶ added in v0.9.0
type NoopInstanceWatcher struct{}
NoopInstanceWatcher is a no-op implementation of InstanceWatcher for use in tests or when no coordinator is available.
func (NoopInstanceWatcher) Done ¶ added in v0.9.0
func (NoopInstanceWatcher) Done(bool)
func (NoopInstanceWatcher) Watch ¶ added in v0.9.0
func (NoopInstanceWatcher) Watch(_ WatchRequest) error
type ObjectIdentifiers ¶
type ObjectIdentifiers struct {
types.NamespacedName
GVR schema.GroupVersionResource
}
ObjectIdentifiers holds the key and GVR of the object to reconcile.
type WatchCoordinator ¶ added in v0.9.0
type WatchCoordinator struct {
// contains filtered or unexported fields
}
WatchCoordinator aggregates watch requests from all instances, manages shared watches via WatchManager, and routes events back to the correct instances.
func NewWatchCoordinator ¶ added in v0.9.0
func NewWatchCoordinator(watches *WatchManager, enqueue EnqueueFunc, log logr.Logger) *WatchCoordinator
NewWatchCoordinator creates a new WatchCoordinator.
func (*WatchCoordinator) ForInstance ¶ added in v0.9.0
func (c *WatchCoordinator) ForInstance(parentGVR schema.GroupVersionResource, instance types.NamespacedName) InstanceWatcher
ForInstance returns a scoped InstanceWatcher handle for the given instance.
func (*WatchCoordinator) InstanceWatchCount ¶ added in v0.9.0
func (c *WatchCoordinator) InstanceWatchCount() int
InstanceWatchCount returns the number of tracked instances.
func (*WatchCoordinator) RemoveInstance ¶ added in v0.9.0
func (c *WatchCoordinator) RemoveInstance(parentGVR schema.GroupVersionResource, instance types.NamespacedName)
RemoveInstance removes all watch requests for a specific instance. Called when an instance is deleted.
func (*WatchCoordinator) RemoveParentGVR ¶ added in v0.9.0
func (c *WatchCoordinator) RemoveParentGVR(parentGVR schema.GroupVersionResource)
RemoveParentGVR removes all instances for a given parent GVR. Called when an RGD is deregistered.
func (*WatchCoordinator) RouteEvent ¶ added in v0.9.0
func (c *WatchCoordinator) RouteEvent(event Event)
RouteEvent routes a watch event to all matching instances. Called by the watch handler for every event.
func (*WatchCoordinator) WatchRequestCount ¶ added in v0.9.0
func (c *WatchCoordinator) WatchRequestCount() (scalar, collection int)
WatchRequestCount returns the total number of active watch requests.
type WatchManager ¶ added in v0.9.0
type WatchManager struct {
// SyncTimeout is the maximum time to wait for cache sync in EnsureWatch.
// Zero means use the default (30s).
SyncTimeout time.Duration
// contains filtered or unexported fields
}
WatchManager manages informer lifecycle per GVR. Informers start lazily on first use and stay alive until all owners release them or Shutdown() is called.
func NewWatchManager ¶ added in v0.9.0
func NewWatchManager(client metadata.Interface, resync time.Duration, onEvent EventHandler, log logr.Logger) *WatchManager
NewWatchManager creates a new WatchManager. The onEvent callback is invoked for every informer event across all GVRs.
func (*WatchManager) ActiveWatchCount ¶ added in v0.9.0
func (m *WatchManager) ActiveWatchCount() int
ActiveWatchCount returns the number of active watches.
func (*WatchManager) EnsureWatch ¶ added in v0.9.0
func (m *WatchManager) EnsureWatch(gvr schema.GroupVersionResource, ownerID string) error
EnsureWatch ensures a watch on the given GVR registered under a given owner. If the informer fails to start, the owner is removed.
func (*WatchManager) GetInformer ¶ added in v0.9.0
func (m *WatchManager) GetInformer(gvr schema.GroupVersionResource) cache.SharedIndexInformer
GetInformer returns the SharedIndexInformer for the given GVR, or nil if no watch exists.
func (*WatchManager) ReleaseWatch ¶ added in v0.9.0
func (m *WatchManager) ReleaseWatch(gvr schema.GroupVersionResource, ownerID string)
ReleaseWatch removes an owner from the GVR. If no owners remain, the informer is stopped automatically.
func (*WatchManager) Shutdown ¶ added in v0.9.0
func (m *WatchManager) Shutdown()
Shutdown stops all informers and clears state.
type WatchRequest ¶ added in v0.9.0
type WatchRequest struct {
// NodeID is the graph node ID (for debugging/metrics).
NodeID string
// GVR is the GroupVersionResource to watch.
GVR schema.GroupVersionResource
// Name is the specific resource name (scalar watches).
Name string
// Namespace is the resource namespace. Empty for cluster-scoped resources.
Namespace string
// Selector is a label selector for collection watches. nil means scalar watch.
Selector labels.Selector
}
WatchRequest describes a resource the instance reconciler wants to watch. For scalar resources, set Name + Namespace. For collections, set Selector + Namespace. Selector supports both matchLabels and matchExpressions (the full metav1.LabelSelector spec).