This document provides instructions for AI agents to develop controllers in the ORC project.
ORC is a Kubernetes operator that manages OpenStack resources declaratively. Each OpenStack resource (Flavor, Server, Network, etc.) has a corresponding Kubernetes Custom Resource and controller.
Key Principle: ORC objects only reference other ORC objects, never OpenStack resources directly. OpenStack resource IDs appear only in status fields.
openstack-resource-controller/
├── api/v1alpha1/ # CRD type definitions (*_types.go)
├── internal/
│ ├── controllers/ # Controller implementations
│ │ └── <resource>/ # Each controller in its own package
│ │ ├── controller.go # Setup, dependencies, SetupWithManager
│ │ ├── actuator.go # OpenStack CRUD operations
│ │ ├── status.go # Status writer implementation
│ │ ├── zz_generated.*.go # Generated code (DO NOT EDIT)
│ │ └── tests/ # KUTTL E2E tests
│ ├── osclients/ # OpenStack API client wrappers
│ ├── scope/ # Cloud credentials & client factory
│ └── util/ # Utilities (errors, dependency, tags)
├── cmd/
│ ├── manager/ # Main entry point
│ ├── resource-generator/ # Code generation
│ └── scaffold-controller/ # New controller scaffolding
└── website/docs/development/ # Detailed documentation
All controllers use a generic reconciler that handles the reconciliation loop. Controllers implement interfaces:
- CreateResourceActuator: Create and import operations
- DeleteResourceActuator: Delete operations
- ReconcileResourceActuator: Post-creation updates (optional)
- ResourceStatusWriter: Status and condition management
Controllers implement these methods (see internal/controllers/flavor/ for a simple example):
// Required by all actuators
GetResourceID(osResource) string
GetOSResourceByID(ctx, id) (*osResource, ReconcileStatus)
ListOSResourcesForAdoption(ctx, obj) (iterator, bool)
// For creation/import
ListOSResourcesForImport(ctx, obj, filter) (iterator, ReconcileStatus)
CreateResource(ctx, orcObject) (*osResource, ReconcileStatus)
// For deletion
DeleteResource(ctx, orcObject, osResource) ReconcileStatus
// Optional - for updates after creation
GetResourceReconcilers(ctx, obj, osResource) ([]ResourceReconciler, error)Every ORC object has these conditions:
-
Progressing
True: Spec doesn't match status; controller expects more reconcilesFalse: Either available OR terminal error (no more reconciles until spec changes)
-
Available
True: Resource is ready for use- Determined by
ResourceStatusWriter.ResourceAvailableStatus()
Methods return ReconcileStatus instead of error:
nil // Success, no reschedule
progress.WrapError(err) // Wrap error for handling
reconcileStatus.WithRequeue(5*time.Second) // Schedule reconcile after delay
reconcileStatus.WithProgressMessage("waiting...") // Add progress message- Transient errors (5xx, API unavailable): Default handling with exponential backoff
- Terminal errors (400, invalid config): Wrap with
orcerrors.Terminal()- no retry
// Terminal error example
if !orcerrors.IsRetryable(err) {
err = orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration,
"invalid configuration: "+err.Error(), err)
}
return nil, progress.WrapError(err)Dependencies are core to ORC - they ensure resources are created in order.
- Normal Dependency: Wait for object to exist and be available
- Deletion Guard Dependency: Normal + prevents deletion of dependency while in use
var projectDependency = dependency.NewDeletionGuardDependency[*orcv1alpha1.SecurityGroupList, *orcv1alpha1.Project](
"spec.resource.projectRef", // Field path for indexing
func(sg *orcv1alpha1.SecurityGroup) []string {
if sg.Spec.Resource != nil && sg.Spec.Resource.ProjectRef != nil {
return []string{string(*sg.Spec.Resource.ProjectRef)}
}
return nil
},
finalizer, externalObjectFieldOwner,
)project, reconcileStatus := projectDependency.GetDependency(
ctx, actuator.k8sClient, orcObject,
func(dep *orcv1alpha1.Project) bool {
return orcv1alpha1.IsAvailable(dep) && dep.Status.ID != nil
},
)
if needsReschedule, _ := reconcileStatus.NeedsReschedule(); needsReschedule {
return nil, reconcileStatus
}
// project is now guaranteed available
projectID := ptr.Deref(project.Status.ID, "")func getResourceName(orcObject *orcv1alpha1.Flavor) string {
if orcObject.Spec.Resource.Name != nil {
return *orcObject.Spec.Resource.Name
}
return orcObject.Name
}type (
osResourceT = flavors.Flavor
createResourceActuator = interfaces.CreateResourceActuator[orcObjectPT, orcObjectT, filterT, osResourceT]
deleteResourceActuator = interfaces.DeleteResourceActuator[orcObjectPT, orcObjectT, osResourceT]
helperFactory = interfaces.ResourceHelperFactory[orcObjectPT, orcObjectT, resourceSpecT, filterT, osResourceT]
)var _ createResourceActuator = flavorActuator{}
var _ deleteResourceActuator = flavorActuator{}import "k8s.io/utils/ptr"
ptr.Deref(optionalPtr, defaultValue) // Dereference with default
ptr.To(value) // Create pointer// Most resources have a mix of immutable and mutable fields.
// Immutability is typically applied per-field, not on the whole struct.
type ServerResourceSpec struct {
// +optional
Name *OpenStackName `json:"name,omitempty"`
// +required
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="imageRef is immutable"
ImageRef KubernetesNameRef `json:"imageRef,omitempty"`
// tags is mutable (no immutability validation)
// +optional
Tags []ServerTag `json:"tags,omitempty"`
}
// Some resources are fully immutable (rare - e.g., Flavor, ServerGroup)
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="FlavorResourceSpec is immutable"
type FlavorResourceSpec struct {
// ...
}// +kubebuilder:validation:MinProperties:=1
type FlavorFilter struct {
Name *OpenStackName `json:"name,omitempty"`
RAM *int32 `json:"ram,omitempty"`
}type FlavorResourceStatus struct {
Name string `json:"name,omitempty"`
RAM *int32 `json:"ram,omitempty"`
}import "github.com/k-orc/openstack-resource-controller/v2/internal/logging"
log.V(logging.Status).Info("...") // Always shown: startup, shutdown
log.V(logging.Info).Info("...") // Default: creation/deletion, reconcile complete
log.V(logging.Verbose).Info("...") // Admin: fires every reconcile
log.V(logging.Debug).Info("...") // Development: detailed debuggingmake generate # Generate all code (run after API type changes)
make build # Build manager binary
make lint # Run linters
make test # Run unit tests
make test-e2e # Run KUTTL E2E tests (requires E2E_OSCLOUDS)
make fmt # Format code- Simple:
internal/controllers/flavor/- No dependencies, immutable - With dependencies:
internal/controllers/securitygroup/- Project dependency, rules reconciliation - Complex:
internal/controllers/server/- Multiple dependencies, reconcilers
Detailed documentation in website/docs/development/:
scaffolding.md- Creating new controllerscontroller-implementation.md- Progressing condition, ReconcileStatusinterfaces.md- Detailed interface descriptionscoding-standards.md- Code style and conventionswriting-tests.md- Testing patterns