1
0
Fork 0
mirror of https://github.com/imjasonh/client-go2 synced 2026-07-07 00:33:49 +00:00
client-go2/controller/README.md
Jason Hall 6c7409049e
Fix controller example to implement correct interface method (#6)
* Fix controller example to implement correct interface method

- Change ReconcileKind to Reconcile to match the Reconciler interface
- Add nil check for secretLister to handle initialization timing
- Fix OnError handler in e2e test to not fail immediately on expected errors

This allows the controller-owned-lister example to compile and run correctly.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Update examples/controller-owned-lister/main.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-28 19:57:12 +00:00

5 KiB

Controller Package

The controller package provides a generic Kubernetes controller framework inspired by Knative's controller implementation. It simplifies building Kubernetes controllers with automatic update detection, conflict resolution, and owner reference support.

Features

  • Type-safe generic controllers - Build controllers for any Kubernetes resource type
  • Simple reconciler interface - Just implement one method: Reconcile or pass a function.
  • Automatic update detection - The framework detects and persists changes to your objects
  • Built-in conflict resolution - Automatic retry with exponential backoff
  • Owner reference support - Automatically reconcile owners when owned resources change
  • Error handling patterns - Control reconciliation behavior with special error types
  • Context-aware logging - Integrated with clog for structured logging

Quick Start

import (
    "context"
    "github.com/imjasonh/client-go2/controller"
    "github.com/imjasonh/client-go2/generic"
    corev1 "k8s.io/api/core/v1"
)

// Create a reconciler function
reconciler := controller.ReconcilerFunc[*corev1.ConfigMap](
    func(ctx context.Context, cm *corev1.ConfigMap) error {
        // Your reconciliation logic here
        cm.Annotations["processed"] = "true"
        return nil
    })

// Create and run the controller
client, _ := generic.NewClient[*corev1.ConfigMap](config)
ctrl := controller.New(client, reconciler, nil)
ctrl.Run(ctx)

Reconciler Interface

The core interface has just one method:

type Reconciler[T runtime.Object] interface {
    Reconcile(ctx context.Context, obj T) error
}

You can implement this interface directly or use ReconcilerFunc for simple cases.

Automatic Updates

The controller automatically detects and persists changes made during reconciliation:

  • Metadata changes - Updates to annotations, labels, and finalizers
  • Status updates - Changes to the status subresource
  • Spec changes - Logged as warnings (specs should not be modified in reconcilers)

Example:

func (r *MyReconciler) Reconcile(ctx context.Context, pod *corev1.Pod) error {
    // These changes will be automatically persisted
    pod.Labels["processed"] = "true"
    pod.Status.Phase = corev1.PodRunning
    
    // Add a finalizer
    pod.Finalizers = append(pod.Finalizers, "example.com/finalizer")
    
    return nil
}

Error Handling

Control reconciliation behavior by returning special errors:

// Requeue after a specific duration
return controller.RequeueAfter(5 * time.Minute)

// Requeue immediately with rate limiting
return controller.RequeueImmediately()

// Don't retry this error
return controller.PermanentError(fmt.Errorf("unrecoverable error"))

// Normal errors are retried with exponential backoff
return fmt.Errorf("temporary error")

Controller Options

Configure the controller behavior:

opts := &controller.Options[*corev1.Pod]{
    // Watch only a specific namespace
    Namespace: "my-namespace",
    
    // Number of concurrent workers
    Concurrency: 5,
    
    // Custom deep copy function (default uses JSON)
    DeepCopyFunc: func(pod *corev1.Pod) *corev1.Pod {
        return pod.DeepCopy()
    },
    
    // Watch owned resources
    OwnedTypes: []controller.OwnedType{
        {
            Client:       secretClient,
            OwnerGVK:    schema.GroupVersionKind{Version: "v1", Kind: "Pod"},
            IsController: true,
        },
    },
}

ctrl := controller.New(client, reconciler, opts)

Owner References

The controller can watch owned resources and reconcile owners when owned resources change:

// In your reconciler, set owner references
func (r *MyReconciler) Reconcile(ctx context.Context, pod *corev1.Pod) error {
    // Create a owned secret
    secret := &corev1.Secret{...}
    
    // Set owner reference
    controller.SetOwnerReference(secret, pod, scheme.Scheme, true)
    
    // Create the secret...
    return nil
}

// Configure the controller to watch secrets
opts := &controller.Options[*corev1.Pod]{
    OwnedTypes: []controller.OwnedType{
        {
            Client:       secretClient,
            OwnerGVK:    schema.GroupVersionKind{Version: "v1", Kind: "Pod"},
            IsController: true, // Only watch controller references
        },
    },
}

Complete Example

See examples/controller/main.go for a complete example that validates ConfigMaps.

Best Practices

  1. Don't modify spec - Reconcilers should only update metadata and status
  2. Use finalizers carefully - Always check DeletionTimestamp before adding
  3. Handle conflicts - The framework handles most conflicts, but design for idempotency
  4. Log with context - Use clog for structured, context-aware logging
  5. Return appropriate errors - Use RequeueAfter for known delays, PermanentError for unrecoverable failures

Testing

The package includes comprehensive unit and e2e tests. See controller_test.go and e2e_test.go for examples of testing your controllers.