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<ReconciliationResult<V1DemoEntity>> ReconcileAsync(
V1DemoEntity entity,
CancellationToken cancellationToken)
{
logger.LogInformation("Reconciling entity {Entity}.", entity);
// Implement your reconciliation logic here
return ReconciliationResult<V1DemoEntity>.Success(entity);
}

public async Task<ReconciliationResult<V1DemoEntity>> DeletedAsync(
V1DemoEntity entity,
CancellationToken cancellationToken)
{
logger.LogInformation("Deleting entity {Entity}.", entity);
// Implement your cleanup logic here
return ReconciliationResult<V1DemoEntity>.Success(entity);
}
}

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<ReconciliationResult<V1DemoEntity>> ReconcileAsync(
V1DemoEntity entity,
CancellationToken cancellationToken)
{
// Check if required resources exist
var deployment = await client.GetAsync<V1Deployment>(
entity.Spec.DeploymentName,
entity.Namespace(),
cancellationToken);

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
}
},
cancellationToken);
}

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

return ReconciliationResult<V1DemoEntity>.Success(entity);
}

DeletedAsync

Important

The DeletedAsync method is informational only and executes asynchronously without guarantees. While it is called when a resource is deleted, it cannot ensure proper cleanup. For reliable resource cleanup, 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<ReconciliationResult<V1DemoEntity>> DeletedAsync(
V1DemoEntity entity,
CancellationToken cancellationToken)
{
// Log the deletion event
logger.LogInformation("Entity {Entity} was deleted.", entity);

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

return ReconciliationResult<V1DemoEntity>.Success(entity);
}

Reconciliation Results

All reconciliation methods must return a ReconciliationResult<TEntity>. This provides a standardized way to communicate the outcome of reconciliation operations.

Success Results

Return a success result when reconciliation completes without errors:

public async Task<ReconciliationResult<V1DemoEntity>> ReconcileAsync(
V1DemoEntity entity,
CancellationToken cancellationToken)
{
// Perform reconciliation
await ApplyDesiredState(entity, cancellationToken);

// Return success
return ReconciliationResult<V1DemoEntity>.Success(entity);
}

Failure Results

Return a failure result when reconciliation encounters an error:

public async Task<ReconciliationResult<V1DemoEntity>> ReconcileAsync(
V1DemoEntity entity,
CancellationToken cancellationToken)
{
try
{
await ApplyDesiredState(entity, cancellationToken);
return ReconciliationResult<V1DemoEntity>.Success(entity);
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to reconcile entity {Name}", entity.Name());
return ReconciliationResult<V1DemoEntity>.Failure(
entity,
"Failed to apply desired state",
ex);
}
}

Requeuing Entities

You can request automatic requeuing by specifying a requeueAfter parameter:

// Requeue after 5 minutes
return ReconciliationResult<V1DemoEntity>.Success(entity, TimeSpan.FromMinutes(5));

// Or set it after creation
var result = ReconciliationResult<V1DemoEntity>.Success(entity);
result.RequeueAfter = TimeSpan.FromSeconds(30);
return result;

This is useful for:

  • Polling external resources
  • Implementing retry logic with backoff
  • Periodic status checks
  • Waiting for external dependencies
Durable Requeue Mechanisms

By default, requeue requests are stored in memory and will be lost on operator restart. For production scenarios requiring persistence, see Advanced Configuration - Custom Requeue Mechanism to learn how to implement durable queues using Azure Service Bus, RabbitMQ, or other messaging systems.

Error Handling with Results

The ReconciliationResult provides structured error handling:

public async Task<ReconciliationResult<V1DemoEntity>> ReconcileAsync(
V1DemoEntity entity,
CancellationToken cancellationToken)
{
if (!await ValidateConfiguration(entity))
{
return ReconciliationResult<V1DemoEntity>.Failure(
entity,
"Configuration validation failed: Required field 'DeploymentName' is empty",
requeueAfter: TimeSpan.FromMinutes(1));
}

try
{
await ReconcileInternal(entity, cancellationToken);
return ReconciliationResult<V1DemoEntity>.Success(entity);
}
catch (KubernetesException ex) when (ex.Status.Code == 409)
{
// Conflict - retry after short delay
return ReconciliationResult<V1DemoEntity>.Failure(
entity,
"Resource conflict detected",
ex,
requeueAfter: TimeSpan.FromSeconds(5));
}
catch (Exception ex)
{
logger.LogError(ex, "Unexpected error during reconciliation");
return ReconciliationResult<V1DemoEntity>.Failure(
entity,
$"Reconciliation failed: {ex.Message}",
ex,
requeueAfter: TimeSpan.FromMinutes(5));
}
}

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<ReconciliationResult<V1DemoEntity>> ReconcileAsync(
V1DemoEntity entity,
CancellationToken cancellationToken)
{
// Check if required resources exist
if (await IsDesiredState(entity, cancellationToken))
{
return ReconciliationResult<V1DemoEntity>.Success(entity);
}

// Only make changes if needed
await ApplyDesiredState(entity, cancellationToken);
return ReconciliationResult<V1DemoEntity>.Success(entity);
}

Error Handling

  • Use ReconciliationResult.Failure() for errors
  • Include meaningful error messages
  • Use the requeueAfter parameter for retry logic
  • Preserve exception information
public async Task<ReconciliationResult<V1DemoEntity>> ReconcileAsync(
V1DemoEntity entity,
CancellationToken cancellationToken)
{
try
{
await ReconcileInternal(entity, cancellationToken);
return ReconciliationResult<V1DemoEntity>.Success(entity);
}
catch (Exception ex)
{
logger.LogError(ex, "Error reconciling entity {Name}", entity.Name());
return ReconciliationResult<V1DemoEntity>.Failure(
entity,
$"Reconciliation failed: {ex.Message}",
ex,
requeueAfter: TimeSpan.FromMinutes(1));
}
}

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 Configuration: Configure appropriate permissions
  5. Status Updates: Remember that status updates don't trigger reconciliation