Skip to main content

Controllers

Controllers are the heart of your Kubernetes operator. They implement the reconciliation logic that ensures your custom resources are in the desired state.

Creating a Controller

To create a controller, create a class that implements IEntityController<TEntity> where TEntity is your custom entity type:

public class V1DemoEntityController(
ILogger<V1DemoEntityController> logger,
IKubernetesClient client) : IEntityController<V1DemoEntity>
{
public async Task ReconcileAsync(V1DemoEntity entity, CancellationToken token)
{
logger.LogInformation("Reconciling entity {Entity}.", entity);
// Implement your reconciliation logic here
}

public async Task DeletedAsync(V1DemoEntity entity, CancellationToken token)
{
logger.LogInformation("Deleting entity {Entity}.", entity);
// Implement your cleanup logic here
}
}

Resource Watcher

When you create a controller, KubeOps automatically creates a resource watcher (informer) for your entity type. This watcher:

  • Monitors the Kubernetes API for changes to your custom resources
  • Triggers reconciliation when resources are added, modified, or deleted
  • Maintains a local cache of resources to reduce API server load

Reconciliation Loop

The reconciliation loop is the core of your operator's functionality. It consists of two main methods:

ReconcileAsync

This method is called when:

  • A new resource is created
  • An existing resource is modified
  • The operator starts up and discovers existing resources
public async Task ReconcileAsync(V1DemoEntity entity, CancellationToken token)
{
// Check if required resources exist
var deployment = await client.GetAsync<V1Deployment>(
entity.Spec.DeploymentName,
entity.Namespace());

if (deployment == null)
{
// Create the deployment if it doesn't exist
await client.CreateAsync(new V1Deployment
{
Metadata = new V1ObjectMeta
{
Name = entity.Spec.DeploymentName,
NamespaceProperty = entity.Namespace()
},
Spec = new V1DeploymentSpec
{
Replicas = entity.Spec.Replicas,
// ... other deployment configuration
}
});
}

// Update status to reflect current state
entity.Status.LastReconciled = DateTime.UtcNow;
await client.UpdateStatusAsync(entity);
}

DeletedAsync

Important

The DeletedAsync method is purely informative and "fire and forget". It is called when a resource is deleted, but it cannot guarantee proper cleanup of resources. For reliable resource cleanup, you must use Finalizers.

This method is called when a resource is deleted, but should only be used for:

  • Logging deletion events
  • Triggering non-critical cleanup tasks
  • Updating external systems about the deletion
public async Task DeletedAsync(V1DemoEntity entity, CancellationToken token)
{
// Log the deletion event
logger.LogInformation("Entity {Entity} was deleted.", entity);

// Update external systems if needed
await NotifyExternalSystem(entity);
}

Important Considerations

Status Updates

  • Status updates do not trigger new reconciliation cycles
  • Only changes to the Spec section trigger reconciliation
  • Use status updates to track the current state of your resource

Race Conditions

  • If a reconciliation is currently running for a resource, new reconciliation requests for the same resource will be queued
  • This prevents race conditions and ensures consistent state management
  • The queue is processed in order, maintaining the sequence of changes

RBAC Requirements

Controllers need appropriate RBAC permissions to function. Use the [EntityRbac] attribute to specify required permissions:

[EntityRbac(typeof(V1DemoEntity), Verbs = RbacVerb.All)]
public class V1DemoEntityController(
ILogger<V1DemoEntityController> logger,
IKubernetesClient client) : IEntityController<V1DemoEntity>
{
// Controller implementation
}

For more details about RBAC configuration, see the RBAC documentation.

Best Practices

Idempotency

  • Make your reconciliation logic idempotent
  • The same reconciliation should be safe to run multiple times
  • Always check the current state before making changes
public async Task ReconcileAsync(V1DemoEntity entity, CancellationToken token)
{
// Check if required resources exist
if (await IsDesiredState(entity))
{
return;
}

// Only make changes if needed
await ApplyDesiredState(entity);
}

Error Handling

  • Handle errors gracefully
  • Log errors with appropriate context
  • Consider implementing retry logic for transient failures
public async Task ReconcileAsync(V1DemoEntity entity, CancellationToken token)
{
try
{
await ReconcileInternal(entity, token);
}
catch (Exception ex)
{
logger.LogError(ex, "Error reconciling entity {Entity}", entity);
// Update status to reflect the error
entity.Status.Error = ex.Message;
await client.UpdateStatusAsync(entity);
}
}

Resource Management

  • Clean up resources when entities are deleted
  • Use finalizers to ensure proper cleanup
  • Monitor resource usage and implement limits

Performance

  • Keep reconciliation logic efficient
  • Avoid long-running operations in the reconciliation loop
  • Use background tasks for time-consuming operations

Common Pitfalls

  1. Infinite Loops: Avoid creating reconciliation loops that trigger themselves
  2. Missing Error Handling: Always handle potential errors
  3. Resource Leaks: Ensure proper cleanup of resources
  4. Missing RBAC: Configure appropriate permissions
  5. Status Updates: Remember that status updates don't trigger reconciliation