Skip to content

Resources

Yewolf edited this page Nov 13, 2025 · 1 revision

Resources: Managed Object Creation

Resources in Controller Framework represent Kubernetes objects that your controller creates, owns, and manages as part of implementing your custom resource's desired state. Unlike dependencies which are external resources you consume, resources are objects your controller actively creates and maintains.

What are Resources?

Resources are Kubernetes objects that your controller creates and manages to achieve the desired state defined by your custom resource. The framework provides automatic resource reconciliation, ownership management, and lifecycle handling with built-in support for creation, updates, and deletion.

All resources implement the GenericResource interface:

type GenericResource[CustomResourceType, ContextType] interface {
    ID() string
    ObjectMetaGenerator() (obj client.Object, delete bool, err error)
    ShouldDeleteNow() bool
    GetMutator(obj client.Object) func() error
    Set(obj client.Object)
    Get() client.Object
    Kind() string
    IsReady(obj client.Object) bool
    RequiresManualDeletion(obj client.Object) bool

    // Lifecycle hooks
    BeforeReconcile(ctx ContextType) error
    AfterReconcile(ctx ContextType, resource client.Object) error
    OnCreate(ctx ContextType, resource client.Object) error
    OnUpdate(ctx ContextType, resource client.Object) error
    OnDelete(ctx ContextType, resource client.Object) error
    OnFinalize(ctx ContextType, resource client.Object) error
}

Your reconciler implements ReconcilerWithResources to provide resources:

type ReconcilerWithResources[ControllerResourceType, ContextType] interface {
    Reconciler[ControllerResourceType]
    
    GetResources(ctx ContextType, req ctrl.Request) ([]GenericResource[ControllerResourceType, ContextType], error)
}

Resource vs Dependency Distinction

Resources (objects your controller creates and manages):

  • Deployments for your application
  • Services exposing your application
  • ConfigMaps generated by your controller
  • Secrets with computed data
  • RBAC resources for your workloads
  • PersistentVolumeClaims for storage

Dependencies (external resources your controller consumes):

  • ConfigMaps with application configuration
  • Secrets containing credentials
  • Other custom resources providing services
  • Third-party operator resources

Creating Resources

Type Aliases

Before using resources, you'll typically create type aliases for cleaner code. In your API package (e.g., api/v1/types.go):

// MyAppContext is the context type for your controller
type MyAppContext = ctrlfwk.Context[*MyApp]

// MyAppResource is a type alias for resources in your controller
type MyAppResource = ctrlfwk.GenericResource[*MyApp, MyAppContext]

// MyAppDependency is a type alias for dependencies in your controller
type MyAppDependency = ctrlfwk.GenericDependency[*MyApp, MyAppContext]

Typed Resources

For resources with Go types available in your project:

func (r *MyAppReconciler) GetResources(ctx MyAppContext, req ctrl.Request) ([]MyAppResource, error) {
    app := ctx.GetCustomResource()
    
    return []MyAppResource{
        // Application deployment
        NewResourceBuilder(ctx, &appsv1.Deployment{}).
            WithKeyFunc(func() types.NamespacedName {
                return types.NamespacedName{
                    Name:      app.Name + "-deployment",
                    Namespace: app.Namespace,
                }
            }).
            WithMutator(func(deployment *appsv1.Deployment) error {
                deployment.Spec.Replicas = &app.Spec.Replicas
                deployment.Spec.Template.Spec.Containers = []corev1.Container{{
                    Name:  "app",
                    Image: app.Spec.Image,
                }}
                return controllerutil.SetOwnerReference(app, deployment, r.Scheme())
            }).
            WithOutput(ctx.Data.Deployment).
            Build(),
            
        // Service for the application
        NewResourceBuilder(ctx, &corev1.Service{}).
            WithKeyFunc(func() types.NamespacedName {
                return types.NamespacedName{
                    Name:      app.Name + "-service",
                    Namespace: app.Namespace,
                }
            }).
            WithMutator(func(service *corev1.Service) error {
                service.Spec.Selector = map[string]string{"app": app.Name}
                service.Spec.Ports = []corev1.ServicePort{{
                    Port:       80,
                    TargetPort: intstr.FromInt(8080),
                    Protocol:   corev1.ProtocolTCP,
                }}
                return controllerutil.SetOwnerReference(app, service, r.Scheme())
            }).
            Build(),
    }, nil
}

Untyped Resources

For third-party resources without Go types:

func (r *MyAppReconciler) GetResources(ctx MyAppContext, req ctrl.Request) ([]MyAppResource, error) {
    app := ctx.GetCustomResource()
    
    // Create ServiceMonitor for Prometheus monitoring
    serviceMonitorGVK := schema.GroupVersionKind{
        Group:   "monitoring.coreos.com",
        Version: "v1",
        Kind:    "ServiceMonitor",
    }
    
    return []MyAppResource{
        NewUntypedResourceBuilder(ctx, serviceMonitorGVK).
            WithKeyFunc(func() types.NamespacedName {
                return types.NamespacedName{
                    Name:      app.Name + "-metrics",
                    Namespace: app.Namespace,
                }
            }).
            WithMutator(func(obj *unstructured.Unstructured) error {
                // Set selector to match our service
                selector := map[string]interface{}{
                    "matchLabels": map[string]interface{}{
                        "app": app.Name,
                    },
                }
                unstructured.SetNestedMap(obj.Object, selector, "spec", "selector")
                
                // Set endpoints
                endpoints := []interface{}{
                    map[string]interface{}{
                        "port": "metrics",
                        "path": "/metrics",
                    },
                }
                unstructured.SetNestedSlice(obj.Object, endpoints, "spec", "endpoints")
                
                return controllerutil.SetOwnerReference(app, obj, r.Scheme())
            }).
            Build(),
    }, nil
}

Builder Configuration

The resource builders provide a fluent API for configuring how resources are managed. Here are the key patterns:

Basic Setup

NewResourceBuilder(ctx, &appsv1.Deployment{}).
    WithKeyFunc(func() types.NamespacedName {
        return types.NamespacedName{Name: "my-app", Namespace: "default"}
    }).
    WithMutator(func(deployment *appsv1.Deployment) error {
        // Configure the deployment
        return controllerutil.SetOwnerReference(app, deployment, scheme)
    }).
    Build()

Resource Identification

.WithKeyFunc(func() types.NamespacedName {
    app := ctx.GetCustomResource()
    return types.NamespacedName{
        Name:      app.Name + "-service",
        Namespace: app.Namespace,
    }
})

Resource Configuration

.WithMutator(func(deployment *appsv1.Deployment) error {
    app := ctx.GetCustomResource()
    deployment.Spec.Replicas = &app.Spec.Replicas
    deployment.Spec.Template.Spec.Containers = []corev1.Container{{
        Name:  "app",
        Image: app.Spec.Image,
    }}
    return controllerutil.SetOwnerReference(app, deployment, scheme)
})

Data Access

.WithOutput(ctx.Data.Deployment) // Store for later access

Readiness Checking

.WithReadinessCondition(func(deployment *appsv1.Deployment) bool {
    return deployment.Status.ReadyReplicas == *deployment.Spec.Replicas
})

Lifecycle Processing

.WithAfterReconcile(func(ctx MyAppContext, deployment *appsv1.Deployment) error {
    // Update custom resource status with deployment info
    ctx.Data.DeploymentReady = deployment.Status.ReadyReplicas > 0
    return nil
})

Automatic Features

Owner Reference Management

Resources automatically set up owner references for garbage collection:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app-deployment
  ownerReferences:
    - apiVersion: myapp.example.com/v1
      kind: MyApp
      name: my-instance
      uid: 12345678-1234-1234-1234-123456789012
      controller: true
      blockOwnerDeletion: true

Watch Management

Resources automatically set up watches when used with ReconcilerWithWatcher:

type MyAppReconciler struct {
    client.Client
    ctrlfwk.WatchCache  // Enables automatic watch setup
    // ... other fields
}

// Resources automatically trigger controller reconciliation when they change
// No manual .Owns() setup required

Reconciliation Lifecycle

The framework automatically handles the complete resource lifecycle:

  • Creation when resources don't exist
  • Updates when the desired state changes
  • Deletion when resources are no longer needed
  • Finalizer handling for graceful cleanup

Common Patterns

Application Deployment

NewResourceBuilder(ctx, &appsv1.Deployment{}).
    WithKeyFunc(func() types.NamespacedName {
        app := ctx.GetCustomResource()
        return types.NamespacedName{
            Name:      app.Name + "-deployment",
            Namespace: app.Namespace,
        }
    }).
    WithMutator(func(deployment *appsv1.Deployment) error {
        app := ctx.GetCustomResource()
        
        // Set basic deployment spec
        deployment.Spec.Replicas = &app.Spec.Replicas
        deployment.Spec.Selector = &metav1.LabelSelector{
            MatchLabels: map[string]string{"app": app.Name},
        }
        
        // Configure pod template
        deployment.Spec.Template.ObjectMeta.Labels = map[string]string{
            "app": app.Name,
        }
        deployment.Spec.Template.Spec.Containers = []corev1.Container{{
            Name:  "app",
            Image: app.Spec.Image,
            Ports: []corev1.ContainerPort{{
                ContainerPort: 8080,
                Name:          "http",
            }},
            Env: []corev1.EnvVar{{
                Name:  "APP_NAME",
                Value: app.Name,
            }},
        }}
        
        return controllerutil.SetOwnerReference(app, deployment, scheme)
    }).
    WithReadinessCondition(func(deployment *appsv1.Deployment) bool {
        // Consider ready when all replicas are available
        return deployment.Status.ReadyReplicas == *deployment.Spec.Replicas &&
               deployment.Status.UpdatedReplicas == *deployment.Spec.Replicas
    }).
    WithAfterReconcile(func(ctx MyAppContext, deployment *appsv1.Deployment) error {
        // Update custom resource status
        app := ctx.GetCustomResource()
        app.Status.DeploymentName = deployment.Name
        app.Status.ReadyReplicas = deployment.Status.ReadyReplicas
        return nil
    }).
    WithOutput(ctx.Data.Deployment).
    Build()

Service Configuration

NewResourceBuilder(ctx, &corev1.Service{}).
    WithKeyFunc(func() types.NamespacedName {
        app := ctx.GetCustomResource()
        return types.NamespacedName{
            Name:      app.Name + "-service",
            Namespace: app.Namespace,
        }
    }).
    WithMutator(func(service *corev1.Service) error {
        app := ctx.GetCustomResource()
        
        service.Spec.Selector = map[string]string{"app": app.Name}
        service.Spec.Type = corev1.ServiceTypeClusterIP
        
        // Configure ports based on app spec
        var ports []corev1.ServicePort
        for _, port := range app.Spec.Ports {
            ports = append(ports, corev1.ServicePort{
                Name:       port.Name,
                Port:       port.Port,
                TargetPort: intstr.FromInt(int(port.TargetPort)),
                Protocol:   corev1.ProtocolTCP,
            })
        }
        service.Spec.Ports = ports
        
        return controllerutil.SetOwnerReference(app, service, scheme)
    }).
    WithAfterReconcile(func(ctx MyAppContext, service *corev1.Service) error {
        // Store service details for other resources
        app := ctx.GetCustomResource()
        app.Status.ServiceName = service.Name
        app.Status.ClusterIP = service.Spec.ClusterIP
        return nil
    }).
    Build()

ConfigMap Generation

NewResourceBuilder(ctx, &corev1.ConfigMap{}).
    WithKeyFunc(func() types.NamespacedName {
        app := ctx.GetCustomResource()
        return types.NamespacedName{
            Name:      app.Name + "-config",
            Namespace: app.Namespace,
        }
    }).
    WithMutator(func(cm *corev1.ConfigMap) error {
        app := ctx.GetCustomResource()
        
        // Generate configuration from app spec
        config := map[string]string{
            "app-name":    app.Name,
            "namespace":   app.Namespace,
            "log-level":   app.Spec.LogLevel,
            "enable-tls":  strconv.FormatBool(app.Spec.TLS.Enabled),
        }
        
        // Add database configuration if dependency resolved
        if ctx.Data.DatabaseSecret != nil {
            config["database-host"] = string(ctx.Data.DatabaseSecret.Data["host"])
            config["database-port"] = string(ctx.Data.DatabaseSecret.Data["port"])
        }
        
        cm.Data = config
        return controllerutil.SetOwnerReference(app, cm, scheme)
    }).
    Build()

RBAC Resources

NewResourceBuilder(ctx, &rbacv1.Role{}).
    WithKeyFunc(func() types.NamespacedName {
        app := ctx.GetCustomResource()
        return types.NamespacedName{
            Name:      app.Name + "-role",
            Namespace: app.Namespace,
        }
    }).
    WithMutator(func(role *rbacv1.Role) error {
        app := ctx.GetCustomResource()
        
        // Define required permissions
        role.Rules = []rbacv1.PolicyRule{
            {
                APIGroups: [""],
                Resources: ["configmaps", "secrets"],
                Verbs:     []string{"get", "list", "watch"},
            },
            {
                APIGroups: ["apps"],
                Resources: ["deployments"],
                Verbs:     []string{"get", "list", "watch", "create", "update", "patch"},
            },
        }
        
        return controllerutil.SetOwnerReference(app, role, scheme)
    }).
    Build()

Conditional Resources

func (r *MyAppReconciler) GetResources(ctx MyAppContext, req ctrl.Request) ([]MyAppResource, error) {
    app := ctx.GetCustomResource()
    resources := []MyAppResource{
        // Always create deployment and service
        r.createDeploymentResource(ctx),
        r.createServiceResource(ctx),
    }
    
    // Conditionally add resources based on spec
    if app.Spec.TLS.Enabled {
        resources = append(resources, r.createTLSResources(ctx)...)
    }
    
    if app.Spec.Monitoring.Enabled {
        resources = append(resources, r.createMonitoringResources(ctx)...)
    }
    
    if app.Spec.Storage.Enabled {
        resources = append(resources, r.createStorageResources(ctx)...)
    }
    
    return resources, nil
}

func (r *MyAppReconciler) createTLSResources(ctx MyAppContext) []MyAppResource {
    app := ctx.GetCustomResource()
    
    return []MyAppResource{
        // TLS certificate request
        NewUntypedResourceBuilder(ctx, schema.GroupVersionKind{
            Group:   "cert-manager.io",
            Version: "v1",
            Kind:    "Certificate",
        }).
            WithKeyFunc(func() types.NamespacedName {
                return types.NamespacedName{
                    Name:      app.Name + "-tls",
                    Namespace: app.Namespace,
                }
            }).
            WithMutator(func(cert *unstructured.Unstructured) error {
                // Configure certificate request
                spec := map[string]interface{}{
                    "secretName": app.Name + "-tls-secret",
                    "issuerRef": map[string]interface{}{
                        "name": app.Spec.TLS.IssuerRef.Name,
                        "kind": app.Spec.TLS.IssuerRef.Kind,
                    },
                    "dnsNames": app.Spec.TLS.DNSNames,
                }
                unstructured.SetNestedMap(cert.Object, spec, "spec")
                
                return controllerutil.SetOwnerReference(app, cert, scheme)
            }).
            Build(),
    }
}

Error Handling and Lifecycle Hooks

Creation and Update Hooks

.WithOnCreate(func(ctx MyAppContext, deployment *appsv1.Deployment) error {
    // Called only when resource is created for the first time
    logger := logf.FromContext(ctx)
    logger.Info("Deployment created successfully", "deployment", deployment.Name)
    
    // Record event
    r.EventRecorder.Event(ctx.GetCustomResource(), "Normal", "DeploymentCreated", 
                         "Deployment created successfully")
    return nil
}).
WithOnUpdate(func(ctx MyAppContext, deployment *appsv1.Deployment) error {
    // Called only when existing resource is updated
    logger := logf.FromContext(ctx)
    logger.Info("Deployment updated", "deployment", deployment.Name, 
               "generation", deployment.Generation)
    return nil
})

Deletion and Finalization

.WithOnDelete(func(ctx MyAppContext, deployment *appsv1.Deployment) error {
    // Called when resource is being deleted
    logger := logf.FromContext(ctx)
    logger.Info("Deployment is being deleted", "deployment", deployment.Name)
    return nil
}).
WithOnFinalize(func(ctx MyAppContext, deployment *appsv1.Deployment) error {
    // Called during finalization process
    // Perform cleanup operations here
    return r.cleanupExternalResources(deployment)
})

Error Recovery

.WithAfterReconcile(func(ctx MyAppContext, deployment *appsv1.Deployment) error {
    // Validate deployment state after reconciliation
    if deployment.Status.UnavailableReplicas > 0 {
        app := ctx.GetCustomResource()
        app.Status.Phase = "Degraded"
        app.Status.Conditions = append(app.Status.Conditions, metav1.Condition{
            Type:    "DeploymentReady",
            Status:  metav1.ConditionFalse,
            Reason:  "UnavailableReplicas",
            Message: fmt.Sprintf("%d replicas unavailable", deployment.Status.UnavailableReplicas),
        })
        return ctx.PatchStatus()
    }
    
    return nil
})

Integration with Reconciliation Steps

Resources are automatically reconciled when you use NewReconcileResourcesStep:

func (r *MyAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    logger := logf.FromContext(ctx)
    
    stepper := ctrlfwk.NewStepper(logger,
        ctrlfwk.WithStep(ctrlfwk.NewFindControllerCustomResourceStep(r)),
        ctrlfwk.WithStep(ctrlfwk.NewResolveDynamicDependenciesStep(r)), // Resolve dependencies first
        ctrlfwk.WithStep(ctrlfwk.NewReconcileResourcesStep(r)),         // Then reconcile resources
        ctrlfwk.WithStep(ctrlfwk.NewEndStep(r, nil)),
    )
    
    return stepper.Execute(ctx, req)
}

For custom resource reconciliation logic, you can create specialized steps:

// Custom step that only reconciles storage resources
storageStep := ctrlfwk.Step[*MyApp, MyAppContext]{
    Name: "reconcile-storage-resources",
    Step: func(ctx MyAppContext, logger logr.Logger, req ctrl.Request) ctrlfwk.StepResult {
        // Only reconcile storage-related resources
        resources := getStorageResources(ctx, req)
        for _, resource := range resources {
            subStep := ctrlfwk.NewReconcileResourceStep(ctx, r, resource)
            result := subStep.Step(ctx, logger, req)
            if result.ShouldReturn() {
                return result
            }
        }
        return ctrlfwk.ResultSuccess()
    },
}

Best Practices

Design Your Context Data Structure

Structure your context data to hold reconciled resources:

type MyAppData struct {
    // Reconciled resources
    Deployment     *appsv1.Deployment
    Service        *corev1.Service
    ConfigMap      *corev1.ConfigMap
    TLSSecret      *corev1.Secret
    
    // Dependencies
    DatabaseSecret *corev1.Secret
    
    // Computed state
    DeploymentReady bool
    ServiceReady    bool
    TLSEnabled      bool
    
    // Status information
    ClusterIP      string
    LoadBalancerIP string
    ReadyReplicas  int32
}

Use Owner References for Cleanup

Always set owner references for proper garbage collection:

.WithMutator(func(deployment *appsv1.Deployment) error {
    app := ctx.GetCustomResource()
    // ... configure deployment ...
    
    // CRITICAL: Set owner reference for garbage collection
    return controllerutil.SetOwnerReference(app, deployment, r.Scheme())
})

Implement Meaningful Readiness Checks

Don't just check existence - validate actual operational state:

.WithReadinessCondition(func(deployment *appsv1.Deployment) bool {
    // Check deployment is fully ready and updated
    return deployment.Status.ReadyReplicas == *deployment.Spec.Replicas &&
           deployment.Status.UpdatedReplicas == *deployment.Spec.Replicas &&
           deployment.Status.ObservedGeneration == deployment.Generation
})

Handle Resource Dependencies

When resources depend on each other, use hooks to coordinate:

.WithAfterReconcile(func(ctx MyAppContext, service *corev1.Service) error {
    // Store service info for other resources that need it
    ctx.Data.ServiceName = service.Name
    ctx.Data.ClusterIP = service.Spec.ClusterIP
    
    // Update custom resource status
    app := ctx.GetCustomResource()
    app.Status.ServiceEndpoint = fmt.Sprintf("%s:%d", 
                                           service.Spec.ClusterIP, 
                                           service.Spec.Ports[0].Port)
    return nil
})

Troubleshooting

Problem: Resources Not Being Created

Symptoms: Resources don't appear in cluster despite reconciliation success Solutions:

  • Check WithKeyFunc returns correct namespace and name
  • Verify WithMutator doesn't return errors
  • Ensure RBAC permissions allow resource creation
  • Check for validation errors in resource specifications

Problem: Resources Keep Getting Updated

Symptoms: Constant reconciliation loops with resource updates Solutions:

  • Verify WithMutator produces consistent output
  • Check if external controllers are modifying your resources
  • Ensure computed fields aren't included in desired state
  • Review resource comparison logic for unnecessary changes

Problem: Owner References Not Working

Symptoms: Resources not deleted when custom resource is deleted Solutions:

  • Ensure controllerutil.SetOwnerReference is called in WithMutator
  • Check that the scheme includes both resource types
  • Verify RBAC permissions include finalizer management
  • Confirm custom resource has proper UID and API version

Problem: Readiness Conditions Never Met

Symptoms: Resources appear created but custom resource never becomes ready Solutions:

  • Review WithReadinessCondition logic for correctness
  • Check if resource status is actually being populated by Kubernetes
  • Verify resource specifications allow reaching desired state
  • Add logging to understand what conditions are failing

Complete Example

Here's a comprehensive example showing resource usage:

type MyAppData struct {
    Deployment    *appsv1.Deployment
    Service       *corev1.Service
    ConfigMap     *corev1.ConfigMap
    Ingress       *networkingv1.Ingress
    
    ServiceReady    bool
    DeploymentReady bool
    IngressReady    bool
}

func (r *MyAppReconciler) GetResources(ctx MyAppContext, req ctrl.Request) ([]MyAppResource, error) {
    app := ctx.GetCustomResource()
    
    resources := []MyAppResource{
        // Application ConfigMap
        NewResourceBuilder(ctx, &corev1.ConfigMap{}).
            WithKeyFunc(func() types.NamespacedName {
                return types.NamespacedName{
                    Name:      app.Name + "-config",
                    Namespace: app.Namespace,
                }
            }).
            WithMutator(func(cm *corev1.ConfigMap) error {
                cm.Data = map[string]string{
                    "app.yaml": generateAppConfig(app, ctx),
                    "log-level": app.Spec.LogLevel,
                }
                return controllerutil.SetOwnerReference(app, cm, r.Scheme())
            }).
            WithAfterReconcile(func(ctx MyAppContext, cm *corev1.ConfigMap) error {
                ctx.Data.ConfigMapName = cm.Name
                return nil
            }).
            WithOutput(ctx.Data.ConfigMap).
            Build(),
            
        // Application Deployment
        NewResourceBuilder(ctx, &appsv1.Deployment{}).
            WithKeyFunc(func() types.NamespacedName {
                return types.NamespacedName{
                    Name:      app.Name + "-deployment",
                    Namespace: app.Namespace,
                }
            }).
            WithMutator(func(deployment *appsv1.Deployment) error {
                deployment.Spec.Replicas = &app.Spec.Replicas
                deployment.Spec.Selector = &metav1.LabelSelector{
                    MatchLabels: map[string]string{"app": app.Name},
                }
                deployment.Spec.Template = corev1.PodTemplateSpec{
                    ObjectMeta: metav1.ObjectMeta{
                        Labels: map[string]string{"app": app.Name},
                    },
                    Spec: corev1.PodSpec{
                        Containers: []corev1.Container{{
                            Name:  "app",
                            Image: app.Spec.Image,
                            Ports: []corev1.ContainerPort{{
                                ContainerPort: 8080,
                                Name:          "http",
                            }},
                            VolumeMounts: []corev1.VolumeMount{{
                                Name:      "config",
                                MountPath: "/etc/config",
                            }},
                        }},
                        Volumes: []corev1.Volume{{
                            Name: "config",
                            VolumeSource: corev1.VolumeSource{
                                ConfigMap: &corev1.ConfigMapVolumeSource{
                                    LocalObjectReference: corev1.LocalObjectReference{
                                        Name: app.Name + "-config",
                                    },
                                },
                            },
                        }},
                    },
                }
                return controllerutil.SetOwnerReference(app, deployment, r.Scheme())
            }).
            WithReadinessCondition(func(deployment *appsv1.Deployment) bool {
                return deployment.Status.ReadyReplicas == *deployment.Spec.Replicas
            }).
            WithOnCreate(func(ctx MyAppContext, deployment *appsv1.Deployment) error {
                r.EventRecorder.Event(app, "Normal", "DeploymentCreated", 
                                     "Application deployment created")
                return nil
            }).
            WithAfterReconcile(func(ctx MyAppContext, deployment *appsv1.Deployment) error {
                ctx.Data.DeploymentReady = deployment.Status.ReadyReplicas > 0
                app.Status.ReadyReplicas = deployment.Status.ReadyReplicas
                return nil
            }).
            WithOutput(ctx.Data.Deployment).
            Build(),
            
        // Application Service
        NewResourceBuilder(ctx, &corev1.Service{}).
            WithKeyFunc(func() types.NamespacedName {
                return types.NamespacedName{
                    Name:      app.Name + "-service",
                    Namespace: app.Namespace,
                }
            }).
            WithMutator(func(service *corev1.Service) error {
                service.Spec.Selector = map[string]string{"app": app.Name}
                service.Spec.Ports = []corev1.ServicePort{{
                    Name:       "http",
                    Port:       80,
                    TargetPort: intstr.FromInt(8080),
                    Protocol:   corev1.ProtocolTCP,
                }}
                return controllerutil.SetOwnerReference(app, service, r.Scheme())
            }).
            WithAfterReconcile(func(ctx MyAppContext, service *corev1.Service) error {
                ctx.Data.ServiceReady = true
                app.Status.ServiceName = service.Name
                return nil
            }).
            WithOutput(ctx.Data.Service).
            Build(),
    }
    
    // Conditionally add Ingress if enabled
    if app.Spec.Ingress.Enabled {
        ingress := NewResourceBuilder(ctx, &networkingv1.Ingress{}).
            WithKeyFunc(func() types.NamespacedName {
                return types.NamespacedName{
                    Name:      app.Name + "-ingress",
                    Namespace: app.Namespace,
                }
            }).
            WithMutator(func(ingress *networkingv1.Ingress) error {
                pathType := networkingv1.PathTypePrefix
                ingress.Spec.Rules = []networkingv1.IngressRule{{
                    Host: app.Spec.Ingress.Host,
                    IngressRuleValue: networkingv1.IngressRuleValue{
                        HTTP: &networkingv1.HTTPIngressRuleValue{
                            Paths: []networkingv1.HTTPIngressPath{{
                                Path:     "/",
                                PathType: &pathType,
                                Backend: networkingv1.IngressBackend{
                                    Service: &networkingv1.IngressServiceBackend{
                                        Name: app.Name + "-service",
                                        Port: networkingv1.ServiceBackendPort{
                                            Number: 80,
                                        },
                                    },
                                },
                            }},
                        },
                    },
                }}
                return controllerutil.SetOwnerReference(app, ingress, r.Scheme())
            }).
            WithAfterReconcile(func(ctx MyAppContext, ingress *networkingv1.Ingress) error {
                ctx.Data.IngressReady = len(ingress.Status.LoadBalancer.Ingress) > 0
                if len(ingress.Status.LoadBalancer.Ingress) > 0 {
                    app.Status.IngressIP = ingress.Status.LoadBalancer.Ingress[0].IP
                }
                return nil
            }).
            Build()
            
        resources = append(resources, ingress)
    }
    
    return resources, nil
}

Summary

Resources provide automatic creation and management of Kubernetes objects that your controller owns. Key features include automatic owner reference management for garbage collection, watch setup for change detection, complete lifecycle management (create, update, delete), readiness validation with custom logic, and integration with the framework's step-based reconciliation process.

Use type-safe builders when possible for compile-time safety, always set owner references for proper cleanup, implement meaningful readiness checks beyond simple existence, store reconciled resources in context data for access across reconciliation steps, and handle errors gracefully with clear messaging.

Resources eliminate the need for manual object management while providing robust error handling and lifecycle management for all objects your controller creates and maintains.

Clone this wiki locally