-
Notifications
You must be signed in to change notification settings - Fork 0
Resources
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.
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)
}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
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]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
}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
}The resource builders provide a fluent API for configuring how resources are managed. Here are the key patterns:
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().WithKeyFunc(func() types.NamespacedName {
app := ctx.GetCustomResource()
return types.NamespacedName{
Name: app.Name + "-service",
Namespace: app.Namespace,
}
}).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)
}).WithOutput(ctx.Data.Deployment) // Store for later access.WithReadinessCondition(func(deployment *appsv1.Deployment) bool {
return deployment.Status.ReadyReplicas == *deployment.Spec.Replicas
}).WithAfterReconcile(func(ctx MyAppContext, deployment *appsv1.Deployment) error {
// Update custom resource status with deployment info
ctx.Data.DeploymentReady = deployment.Status.ReadyReplicas > 0
return nil
})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: trueResources 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 requiredThe 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
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()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()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()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()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(),
}
}.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
}).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)
}).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
})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()
},
}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
}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())
})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
})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
})Symptoms: Resources don't appear in cluster despite reconciliation success Solutions:
- Check
WithKeyFuncreturns correct namespace and name - Verify
WithMutatordoesn't return errors - Ensure RBAC permissions allow resource creation
- Check for validation errors in resource specifications
Symptoms: Constant reconciliation loops with resource updates Solutions:
- Verify
WithMutatorproduces 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
Symptoms: Resources not deleted when custom resource is deleted Solutions:
- Ensure
controllerutil.SetOwnerReferenceis called inWithMutator - Check that the scheme includes both resource types
- Verify RBAC permissions include finalizer management
- Confirm custom resource has proper UID and API version
Symptoms: Resources appear created but custom resource never becomes ready Solutions:
- Review
WithReadinessConditionlogic 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
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
}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.