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