Implementing resource-specific functionality
The majority of a resource controller's functionality is implemented in a few interfaces which are passed to the generic controller. Current controllers implement these in actuator.go in the controller's package directory, but feel free to split them up as makes most sense for your controller.
The required interfaces are described in detail by the godoc of the interfaces packages. Specific interfaces are linked below.
ResourceHelperFactory
The controller's entry point to these interfaces is ResourceHelperFactory, which is passed as an argument to reconciler.NewController in SetupWithManager. This interface is simply a set of constructors returning implementations of the other required interfaces. It is not expected to have any state, so is expected to be implemented as methods on an empty struct.
APIObjectAdapter
The APIObjectAdapter is an adapter interface which allows the generic controller to access fields of an API object which are common to all API types. For example, all API types have a status field which in turn has an ID field. However, the generic controller is not able to directly reference this concrete field as it is defined separately for every API type.
As the APIObjectAdapter covers only API fields which are automatically generated, it is itself automatically generated by the code generator and written to zz_generated.adapter.go in the controller's package directory. ResourceHelperFactory's NewAPIObjectAdapter method should simply return an instance of this struct.
Actuator
The actuator implements the majority of the resource-specific functionality. It is split into several interfaces:
BaseResourceActuator: methods required for all actuatorsCreateResourceActuator: methods required for creating or importing a resourceDeleteResourceActuator: methods required for deleting a resourceReconcileResourceActuator: methods required for reconciling a resource after creation
CreateResourceActuator and DeleteResourceActuator are separated because they may have different initialisation requirements. The delete flow should minimise initialisation requirements: users often try to fix problems by deleting resources, and we should not refuse deletion because a creation dependency is unhealthy. In practice, both may be implemented on the same struct.
Idempotency rules for CreateResource and DeleteResource
CreateResource and DeleteResource MUST NOT perform any action after the OpenStack Create or Delete API call. The generic controller calls CreateResource only when the resource does not exist, and DeleteResource only when it does. Any action after the API call would not be retried if it fails.
For example, Neutron tags cannot be set during resource creation. This must be done by a resource reconciler, not in CreateResource.
Both methods may be called multiple times until they succeed. Any actions before the API call must therefore be idempotent.
ReconcileResourceActuator
ReconcileResourceActuator is an optional interface which may be implemented by the object returned by NewCreateActuator. If implemented, its methods will be called every time the object is reconciled when the resource exists. This enables:
- Post-creation initialisation (e.g. setting Neutron tags)
- Object mutability (responding to spec changes after creation)
GetResourceReconcilers returns a list of ResourceReconciler functions. Each reconciler receives the orcObject and osResource and returns a ReconcileStatus. Key points:
- All returned reconcilers are executed in order, even if earlier ones fail. Errors and statuses are aggregated.
- If a reconciler makes any change to the OpenStack resource, it MUST return
progress.NeedsRefresh()to force a status refresh.
Reconcilers are only called for managed resources. CEL validation on the spec guarantees that spec.resource is set when managementPolicy is managed, so spec.resource is always non-nil when a reconciler runs.
Each reconciler should target exactly one OpenStack API. If an update operation uses a different API endpoint from the resource's Update API, it must be implemented as its own reconciler. This keeps each reconciler focused on a single concern, gives each operation independent error handling, and ensures a failure in one does not block the others.
There are two types of reconciler:
updateResource handles mutable fields via the resource's own Update API. It builds an UpdateOpts struct using handleXXXUpdate() helpers, checks whether any field changed, and makes a single Update API call. Examples: securitygroup, trunk, router, volumetype.
func (actuator myActuator) updateResource(
ctx context.Context, obj orcObjectPT, osResource *osResourceT,
) progress.ReconcileStatus {
resource := obj.Spec.Resource
updateOpts := things.UpdateOpts{}
handleNameUpdate(&updateOpts, obj, osResource)
handleDescriptionUpdate(&updateOpts, resource, osResource)
if !needsUpdate(updateOpts) {
return nil
}
_, err := actuator.osClient.UpdateThing(ctx, osResource.ID, updateOpts)
if err != nil {
if !orcerrors.IsRetryable(err) {
err = orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration,
"updating resource: "+err.Error(), err)
}
return progress.WrapError(err)
}
return progress.NeedsRefresh()
}
Single-concern reconcilers handle a specific aspect of the resource using a separate OpenStack API. For instance, security group rules use the rules API, trunk subports use the subports API, and flavor extra specs use the extra-specs API. Each is named with a descriptive verb+noun (e.g. reconcileExtraSpecs, reconcileSubports, updateRules). Examples: reconcileExtraSpecs (flavor), updateRules (securitygroup), reconcileSubports (trunk), reconcilePassword (user).
func (actuator myActuator) reconcileExtraSpecs(
ctx context.Context, obj orcObjectPT, osResource *osResourceT,
) progress.ReconcileStatus {
resource := obj.Spec.Resource
// Compute the diff between desired and observed state,
// then make API calls (creates, deletes, updates) as needed.
// ...
return progress.NeedsRefresh()
}
Some resources manage tags through a dedicated tags API rather than the resource's Update API (e.g. Neutron resources, Nova servers). Because this is so common, the internal/util/tags package provides a reusable reconciler:
tags.ReconcileTags[orcObjectPT, osResourceT](
orcObject.Spec.Resource.Tags,
osResource.Tags,
tags.NewNeutronTagReplacer(actuator.osClient, "security-groups", osResource.ID),
)
The package provides NewNeutronTagReplacer and NewServerTagReplacer out of the box. For resources whose tags are set via the standard Update API (e.g. block storage), tags can be handled inside updateResource instead.
Both types are returned together from GetResourceReconcilers:
func (actuator myActuator) GetResourceReconcilers(
ctx context.Context, orcObject orcObjectPT, osResource *osResourceT,
controller interfaces.ResourceController,
) ([]resourceReconciler, progress.ReconcileStatus) {
return []resourceReconciler{
tags.ReconcileTags[orcObjectPT, osResourceT](...),
actuator.updateRules, // single-concern: rules API
actuator.updateResource, // general fields: Update API
}, nil
}
ResourceStatusWriter
The Available and Progressing conditions are critical components of the ORC API. To ensure they are implemented consistently by all controllers they are largely generated by code called by the generic controller. The ResourceStatusWriter interface provides the resource-specific methods required to populate:
- The
Availablecondition - The
Progressingcondition - Resource-specific state in
status.resource
GetApplyConfig
This is a simple wrapper around the constructor for the relevant apply configuration. This apply configuration and its constructor will have been automatically generated by make generate. For example, in the Flavor controller this is a wrapper round pkg/clients/applyconfiguration/api/v1alpha1.Flavor.
ResourceAvailableStatus
This sets the value of the Available condition. This should not return true unless the resource is completely ready to be used.
ApplyResourceStatus
Writes the observed resource status to an apply configuration.
Note that object status is written in a single server-side apply 'transaction'. Meaning if, during a reconcile, the controller is not able to fetch the resource from OpenStack, it will not be able to add the resource status to the transaction and therefore status.resource will be unset. This is intentional behaviour, and you should not attempt to work around this or save the previous state. The state will be populated again when the controller is able to fetch the resource.