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
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
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
Specsection 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
requeueAfterparameter 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
- Infinite Loops: Avoid creating reconciliation loops that trigger themselves
- Missing Error Handling: Always handle potential errors
- Resource Leaks: Ensure proper cleanup of resources
- Missing RBAC Configuration: Configure appropriate permissions
- Status Updates: Remember that status updates don't trigger reconciliation