The branch feature/generic-controller
leverages generics from Golang 1.18 to support the following enhancements to Controller-Runtime:
- Port the conditions and patch utilities from Cluster API into Controller-Runtime in such a way they are reusable with any API's types
- Introduce a new, simple reconciler type to make it even easier for people who want to write a controller using Controller-Runtime
There are four, new packages:
Package | Description |
---|---|
./pkg/conditions/ |
For getting, setting, and patching status conditions |
./pkg/patch/ |
For patching resources both with and without status conditions |
./pkg/reconcile/simple/ |
For building a simple reconciler that automatically gets the resource from the API server and patches the resource and its optional status subresource prior to returning from the Reconcile function |
./examples/simple/ |
A working example of all of the above that creates two controllers using simple reconcilers for a new Dog API and the built-in Node API |
Let's first look at the new, simple reconciler from ./pkg/reconcile/simple/
. Imagine for a moment there is a variable named mgr
that references a Controller-Runtime Manager
. Creating a new controller for the Dog API would now be as easy as the following:
ctrl.NewControllerManagedBy(mgr).
For(&Dog{}).
Complete(&simple.Reconciler[*Dog]{
Client: mgr.GetClient(),
OnNormal: func(
ctx context.Context,
obj *Dog) (ctrl.Result, error) {
// Update the object's spec, status,
// and even status conditions
return ctrl.Result{}, nil
},
},
})
Then above controller will:
- Watch
Dog
resources - Get a
Dog
resource - Patch the
Dog
resource and its status subresource, including conditions
The notation Reconciler[*Dog]
is how explicit type information is indicated to Go types that use generics. For example, the new simple.Reconciler
type is defined as:
type Reconciler[TObject client.object] struct
This means any object that can satisfy the constraint client.Object
may be used to instantiate the simple reconciler. Once compiled, the binary artifact would actually include the following type:
Reconciler[*Dog]
The ./pkg/reconcile/simple
package also provides a second type of reconciler:
type ReconcilerWithConditions[
TObject conditions.Setter[
TConditionType,
TConditionSeverity,
TCondition,
],
TConditionType,
TConditionSeverity,
TCondition[
TConditionType,
TConditionSeverity,
],
] struct
The ReconcilerWithConditions
type makes it possible to create an even more intelligent controller while still retaining the simplicity of the previous example. Imagine the following:
- the Dog API implements the
Getter
andSetter
interfaces from the new./pkg/conditions/
package - the Dog API includes the following types:
Type Description Condition
surfaced as part of the Dog API's status.conditions
field and satisfies theCondition
constraint from./pkg/conditions/
ConditionType
expresses the type for a Dog API's Condition and satisifies the Type
constraint from./pkg/conditions/
ConditionSeverity
expresses the severity for a Dog API's Condition and satisifies the Severity
constraint from./pkg/conditions/
With this knowledge it is possible to instantiate a new ReconcilerWithConditions
like so:
ctrl.NewControllerManagedBy(mgr).
For(&Dog{}).
Complete(&simple.ReconcilerWithConditions[
*Dog,
ConditionType,
ConditionSeverity,
*Condition,
]{
Client: mgr.GetClient(),
OnNormal: func(
ctx context.Context,
obj *Dog) (ctrl.Result, error) {
// Update the object's spec, status,
// and even status conditions
return ctrl.Result{}, nil
},
},
})
Much like the previous example, the above controller will:
- Watch
Dog
resources - Get a
Dog
resource - Patch the
Dog
resource and its status subresource, including conditions
However, the difference this time how the conditions are patched. This patchset ports the conditions and patch helper logic from Cluster API into Controller-Runtime. This was not possible in the past due to the Cluster API conditions and patch logic relying on a known Condition type. With Go generics it is possible to implement the same logic using constraints for expressing what is and is not a condition along with its type and severity.
With the new ./pkg/conditions/
and ./pkg/patch/
packages, anything that satifies the constraint conditions.Setter
will benefit from enhanced patch logic around a resource's status conditions, including:
- A three-way merge of a resource's status conditions
- Attempts to resolve conflicts automatically using merge priority based on severity
- The ability to prefer conditions owned/set by a controller
- And more!
Like the simple reconcilers, the patch helper comes in two variants:
Type | Description |
---|---|
patch.Helper |
Patches a resource and its status subresource, including conditions |
patch.HelperWithConditions |
Patches a resource and its status subresource, including conditions using the aforementioned patch logic |
The first helper may be used with any Kubernetes API, even the built-in types such as Node
. The patch.Helper
may also be used with APIs that are eligible for patch.HelperWithConditions
, but when doing so, the status conditions will be treated as any other field from the status subresource.
There is also ./examples/simple
-- a new example for how to use all of the above lessons to produce controllers for the built-in Node
resource and a new Dog
resource that leverages the enhanced conditions logic.
Finally, this patch also includes 124 tests to validate that everything works as it should:
$ git show | \
grep -c \
'\(func Test\)\|\(\(It\|Specify\)(\)\|\(name: \{1,\}"\)'
124
I'm confused by the use of generics for conditions - so I feel I'm missing some context