Skip to content

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:

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.