controller-runtime

repository·main·Indexed 23 days ago

https://github.com/kubernetes-sigs/controller-runtime

A set of Go libraries for building Kubernetes Controllers, serving as the core engine for Kubebuilder and Operator SDK. It provides essential components such as the Manager, Reconcilers, and Webhooks for managing both built-in Kubernetes resources and Custom Resource Definitions (CRDs). The project includes the setup-envtest tool for managing envtest binaries and utilities for object lifecycle management via CreateOrPatch and CreateOrUpdate.

Tokens
16.9K
Snippets
23
Records
89
Agent score
83%

What's inside controller-runtime

  1. Implement structured logging with controller-runtime

    main

    controller-runtime uses structured logging via the logr interface. Instead of using fmt.Sprintf to inject variables into a message string, you should use a constant message and provide variable data as key-value pairs. This makes logs searchable and machine-readable.

    To get started, use the root logger from sigs.k8s.io/controller-runtime/pkg/log. You can create named loggers using WithName or sub-loggers with fixed context using WithValues.

  2. Configure setup-envtest for offline or air-gapped environments

    main

    To use setup-envtest without internet access, use the following strategies:

    1. Use only installed binaries: Use the -i or --installed flag. You can automate this by setting the ENVTEST_INSTALLED_ONLY=true environment variable.
    2. Use KUBEBUILDER_ASSETS: If you have binaries at a specific location, use setup-envtest --use-env. This forces the tool to use the value in $KUBEBUILDER_ASSETS. You can automate this with ENVTEST_USE_ENV=true.
    3. Sideloading: Download tarballs manually and use setup-envtest sideload <version> <path-to-tarball> to add them to the local store.
    4. Custom Index: Point the tool to an internal HTTP index using --index <url>.
  3. Implement multi-cluster controllers using the Cluster interface

    main

    To build controllers that interact with multiple Kubernetes clusters, use the cluster.Cluster interface to manage cluster-specific dependencies (like Client, Cache, and Scheme) separately from the main manager.Manager. The manager.Manager now embeds the Cluster interface, allowing you to add additional clusters to a single manager.

    Key steps for multi-cluster setup:

    1. Create a primary manager for the main cluster.
    2. Create additional cluster.Cluster instances for secondary clusters.
    3. Add the secondary clusters to the manager using mgr.Add(clusterInstance).
    4. Use the specific cluster's methods (e.g., cluster.GetClient()) within your reconciler to perform actions on that specific cluster.
    // Example: Reconciler using two different clusters
    type secretMirrorReconciler struct {
    	referenceClusterClient, mirrorClusterClient client.Client
    }
    
    func NewSecretMirrorReconciler(mgr manager.Manager, mirrorCluster cluster.Cluster) error {
    	return ctrl.NewControllerManagedBy(mgr).
    		// Watch Secrets in the reference cluster (the one the manager is tied to)
    		For(&corev1.Secret{}).
    		// Watch Secrets in the mirror cluster using its specific cache
    		Watches(
    			source.NewKindWithCache(&corev1.Secret{}, mirrorCluster.GetCache()),
    			&handler.EnqueueRequestForObject{},
    		).
    		Complete(&secretMirrorReconciler{
    			referenceClusterClient: mgr.GetClient(),
    				mirrorClusterClient:    mirrorCluster.GetClient(),
    		})
    }
    
    func main() {
    	// 1. Setup primary manager
    	mgr, err := manager.New(cfg1, manager.Options{})
    	
    	// 2. Setup secondary cluster
    	mirrorCluster, err := cluster.New(cfg2)
    	
    	// 3. Add secondary cluster to manager
    	if err := mgr.Add(mirrorCluster); err != nil {
    		panic(err)
    	}
    
    	// 4. Initialize reconciler with both clients
    	if err := NewSecretMirrorReconciler(mgr, mirrorCluster); err != nil {
    		panic(err)
    	}
    
    	// 5. Start everything
    	if err := mgr.Start(signals.SetupSignalHandler()); err != nil {
    		panic(err)
    	}
    }
  4. Configure setup-envtest shell integration

    main

    Because setup-envtest is a binary and not a shell script, it cannot modify your current shell's environment variables directly. To allow setup-envtest use -p env to actually update your environment, you can add a wrapper function to your shell configuration (e.g., ~/.zshrc or ~/.bashrc).

    setup-envtest() {
        if (($@[(Ie)use])); then
            source <($GOPATH/bin/setup-envtest "$@" -p env)
        else
            $GOPATH/bin/setup-envtest "$@"
        fi
    }
  5. Handle stale cache data in controllers

    main

    Because controllers often read from a cache, the data might be slightly out of date. To handle this, use one of the following strategies:

    1. Optimistic Locking with Deterministic Names: Use deterministic names for objects you create. This allows the Kubernetes API server to return an error if the object already exists, preventing duplicate creation. (e.g., appending a specific number or hash to a name).
    2. Action Tracking: If you cannot use deterministic names (e.g., when using generateName), track the actions you have taken and assume they need to be repeated if they do not occur within a certain timeframe (using a requeue result).
    3. Full State Enforcement: Write your controller with the assumption that information will eventually be correct. Ensure the Reconcile function enforces the entire desired state of the world every time it runs.

    Note: Constructing a client that reads directly from the API server instead of the cache is a last resort and generally discouraged.

  6. Test controllers using envtest.Environment

    main

    While a fake client is available in sigs.k8s.io/controller-runtime/pkg/client/fake, it is generally recommended to use envtest.Environment to test against a real API server. Using fake clients often leads to complex, hard-to-maintain test code that attempts to re-implement API server behavior.

    Best practices for testing:

    • Use envtest.Environment to spin up a real API server.
    • Test state, not calls: Structure tests to verify that the state of the world is as expected, rather than verifying that a specific sequence of API calls was made. This makes tests more resilient to internal refactoring.
    • Account for latency: Remember that there may be a delay between a write operation and the subsequent reconcile triggered by the API server.
  7. Implement idempotent Reconcile logic

    main

    When writing a Reconcile function, do not implement different logic branches for specific event types (e.g., Create, Update, Delete). Instead, ensure your reconciler is idempotent.

    Your Reconcile method should:

    1. Read the current state of all necessary objects.
    2. Compare it to the desired state.
    3. Perform updates to reconcile the difference.

    This approach allows the controller to correctly handle coalesced events, skipped events, and application startup. It is your responsibility to ensure you have enough information to clean up state that is no longer referenced.

  8. Enable Warm Replicas for Controllers

    main

    To reduce recovery time during leader failover for controllers managing large caches, you can enable 'Warm Replicas'. This allows the manager to start event sources and populate caches while the instance is still on standby (before acquiring the leader lock).

    When EnableWarmup is true and NeedLeaderElection is true, the controller-runtime registers the controller as a warmup runnable. The Warmup phase launches event sources and cache sync logic but does not start worker goroutines. Once leader election is won, the controller's Start method simply spins up workers against the already-populated queues.

    Note: Enabling warmup on a controller that does not use leader election is a no-op.

    // Enable warmup for all controllers in the manager
    mgr, err := ctrl.NewManager(cfg, ctrl.Options{
    	Controller: config.Controller{
    		EnableWarmup: new(true),
    	},
    })
    
    // ...
    
    // Or override/enable warmup for a specific controller
    builder.ControllerManagedBy(mgr).
    	Named("slow-source").
    	WithOptions(controller.Options{
    		EnableWarmup: new(true),
    	}).
    	For(&examplev1.Example{}).
    	Complete(reconciler)
  9. Identify the canonical reference for Controller Runtime

    main
    When looking for the most accurate and up-to-date technical specifications, API documentation, or architectural details for Controller Runtime, always use the official GoDoc documentation. Design documents in the designs/ directory may be outdated and should not be treated as the primary source of truth for current implementation details.
  10. Initialize ctrl.Manager using ComponentConfig

    main

    You can initialize a ctrl.Manager using configuration loaded from a ConfigMap via the NewFromComponentConfig function. This allows for dynamic configuration of the manager without rebuilding the controller binary.

    Using the default configuration type

    1. Mount a ConfigMap containing the configuration into your controller pod.
    2. Initialize the manager using NewFromComponentConfig by providing the name of the ConfigMap and specifying the DefaultControllerManagerConfiguration type.
    3. Build your custom controller as usual.

    Using a custom configuration type

    1. Implement a custom ComponentConfig type.
    2. Embed the ControllerManagerConfiguration type within your custom type.
    3. Mount the ConfigMap containing your custom configuration into the controller pod.
    4. Initialize the manager using NewFromComponentConfig by providing the name of the ConfigMap and your custom ComponentConfig type.
    5. Build your custom controller as usual.