Skip to content

Instantly share code, notes, and snippets.

@akutz
Last active Jun 8, 2022
Embed
What would you like to do?
Generic conditions, patch, simple reconciler packages in Controller-Runtime

Generic conditions, patch, simple reconciler packages

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

New packages

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

Build a simple controller

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:

  1. Watch Dog resources
  2. Get a Dog resource
  3. 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]

Build a more intelligent, simple controller

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 and Setter 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 the Condition 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:

  1. Watch Dog resources
  2. Get a Dog resource
  3. 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.

Conditions and patch helpers

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.

Testing

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
@embano1
Copy link

embano1 commented Jan 6, 2022

These functions are normally defined on a Reconciler as ReconcileDelete and ReconcileNormal. To allow people that use simple.Reconciler to provide their own implementations of this logic, I defined callback functions called OnDelete and OnNormal.

Thx, I always used one implementation/callback for both. But makes sense.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment