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 as they may have different initialisation requirements. It is especially for the delete flow to minimise any initialisation requirements. It is idiomatic in Kubernetes to attempt to resolve problems by deleting and recreating resources. We must consider that a user may have taken manual actions to attempt to work round some problem, and therefore objects may be an inconsistent or illegal state. We don't want to limit a user's options by refusing to run deletion because we are unable to perform actions which are not strictly necessary for deletion. In practise they may both be implemented on the same struct.
Note that both NewCreateActuator
and NewDeleteActuator
can return []progress.ProgressStatus
and error
. If it is not possible to initialise the actuator yet, one of these MUST be returned.
Another point worth emphasising is that although CreateResource
and DeleteResource
are permitted to perform actions other than a single Create or Delete API call, they MUST NOT perform any action after the relevant OpenStack Create or Delete call. In some circumstances it is not possible to fully initialise an OpenStack resource in single Create call. For example, it is not possible to set tags on most Neutron resources during creation; they must be set in a separate call after the resource has been created. However, we MUST NOT do this in CreateResource
, because we cannot perform any action after creating the Neutron resource. Instead we must implement this as a reconcile action.
The reason for this is idempotency. We call CreateResource
if the resource does not exist. We don't call CreateResource
if the resource exists. This means that if any action after resource creation fails, it will never be retried. The same is true for DeleteResource
: we stop calling DeleteResource
when we observe that the OpenStack resource is no longer present. Therefore any action after the Delete API call may never be retried.
For the same reason, it is also important to remember that both CreateResource
and DeleteResource
may be called many times until they finally succeed. Therefore any actions prior to resource creation must be idempotent. If CreateResource
takes any state-changing action prior to resource creation, calling CreateResource
again must not do it again.
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, even if it already exists. This enables implementation of:
- Post-creation initialisation (e.g. setting Neutron tags)
- Object mutability (responding to spec changes after creation)
Because it is an optional interface, ReconcileResourceActuator
not not have a separate constructor in ResourceHelperFactory
. If the create actuator implements this interface, its methods will be called.
GetResourceReconcilers
returns a set of functions implementing ResourceReconciler
. Without the type descriptors, the signature of a ResourceReconciler
looks like:
func resourceReconciler(ctx, orcObject, osResource) ([]progress.ProgressStatus, error)
This function will be passed the orcObject and osResource as they were when the first reconciler was called, so ideally multiple reconcilers should not make changes which might interfere with the operation of subsequent reconcilers.
Note that a reconciler returning an error does not prevent execution of subsequent reconcilers. The reason for this is to try to prevent an error in a single reconciler preventing the execution of other reconcilers. Instead we will always do as much work as possible. The progress.ProgressStatus
and error
returned by all reconcilers is aggregated after they have all executed, and the aggregated result returned.
The same is true for GetResourceReconcilers
itself. Note that this method returns an error. The intention here is that we may want to generate reconcilers dynamically. For example we might make a kubernetes or OpenStack API call and return arbitrarily many reconcilers based on the return (e.g. one each for a list of objects). Any reconcilers returned by GetResourceReconcilers
will be executed, even if GetResourceReconcilers
also returns an error. The error from GetResourceReconcilers
will be aggregated with any other errors before being returned.
Reconcilers execute prior to generating the resource status, but they cannot affect the observed state of the OpenStack resource. Consequently, if a reconciler makes any change to the OpenStack resource it MUST return a progress.ProgressStatus
to force a refresh.
ResourceStatusWriter
The Available
and Progressing
conditions are critical components of the ORC API. To ensure they are implemented consistently by all controllers they are substantially generated by code called by the generic controller. The ResourceStatusWriter
interface provides necessary resource-specific methods to populate:
- The
Available
condition - The
Progressing
condition - 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'. This means that 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 round this or save previous state. The state will be populated again when the controller is able to fetch the resource.