Documentation
¶
Overview ¶
Package rekon is a library for building Kubernetes operators on top of controller-runtime. It provides three independent layers that can be adopted incrementally.
Layer 1 - Test simulator ¶
Simulator runs reconciler tests without a cluster. It intercepts Kubernetes API calls and lets you assert on them in sequence:
sim := rekon.NewSimulator(t, scheme, func(c client.Client) reconcile.Reconciler {
return myReconciler(c)
}, &myv1.Widget{})
sim.Create(&myv1.Widget{Spec: myv1.WidgetSpec{Replicas: 3}})
sim.ExpectCreate(&appsv1.Deployment{}, rekon.Matching(hasReplicas(3)))
sim.ExpectStatusUpdate(rekon.Matching(hasState("initialising")))
// ... further assertions ...
Layer 2 - Declarative resource ownership ¶
ResourceOwner manages child resources for a parent CR: creating them, patching them when specs drift, garbage-collecting orphans, and handling cross-namespace deletion via finalizers.
owned := rekon.OwnedResources[*myv1.Widget]{
rekon.For(&appsv1.Deployment{}, buildDeployment,
rekon.WithUpdate(updateDeployment),
rekon.WithReady(deploymentReady),
),
}
ro, err := rekon.NewResourceOwner(client, scheme, owned)
Layer 3 - State machine reconciler ¶
StateMachine replaces a monolithic Reconcile function with an explicit state graph. Each state declares transitions (evaluated in order, first match wins), an Enter hook (called once on entry), and a Reconcile hook (called every pass while the machine stays in that state).
sm, err := rekon.NewStateMachine(client, "initialising", rekon.States[*myv1.Widget]{
"initialising": {
Enter: createChildResources,
Transitions: []rekon.Transition[*myv1.Widget]{
{To: "running", When: allChildrenReady},
},
},
"running": {
Reconcile: ensureDesiredState,
Transitions: []rekon.Transition[*myv1.Widget]{
{To: "degraded", When: anyChildFailed},
},
},
})
Operational boilerplate ¶
Setup registers health and readiness endpoints with the manager. ForStateMachine enables Prometheus metrics for a StateMachine. DefaultManagerOptions returns sensible ctrl.Options defaults.
Version note ¶
This is v0 software. The API may change in minor releases. Breaking changes will be noted in the changelog.
Index ¶
- func DefaultManagerOptions(leaderElectionID string) ctrl.Options
- func Setup(mgr manager.Manager, opts ...SetupOption) error
- type Condition
- type MachineObject
- type MachineStatus
- type Option
- type OwnedResourceOption
- func WithFinalizer[CR client.Object, Child client.Object](finalizer string) OwnedResourceOption[CR, Child]
- func WithReady[CR client.Object, Child client.Object](fn func(Child) bool) OwnedResourceOption[CR, Child]
- func WithUpdate[CR client.Object, Child client.Object](fn func(CR, Child) Child) OwnedResourceOption[CR, Child]
- type OwnedResourceSpec
- type OwnedResources
- type ResourceOwner
- func (ro *ResourceOwner[CR]) AreAllReady(ctx context.Context, cr CR) (bool, error)
- func (ro *ResourceOwner[CR]) Client() client.Client
- func (ro *ResourceOwner[CR]) IsReady(ctx context.Context, cr CR, kind client.Object) (bool, error)
- func (ro *ResourceOwner[CR]) Reconcile(ctx context.Context, cr CR) (reconcile.Result, error)
- type SetupOption
- type Simulator
- func (s *Simulator) AssertComplete()
- func (s *Simulator) Create(obj client.Object)
- func (s *Simulator) ExpectCreate(typeHint client.Object, opts ...Option)
- func (s *Simulator) ExpectStatusUpdate(opts ...Option)
- func (s *Simulator) FakeClient() client.Client
- func (s *Simulator) Reconcile()
- func (s *Simulator) SimulateReady(typeHint client.Object, name string)
- func (s *Simulator) SimulateState(typeHint MachineObject, state string)
- type StateCondition
- type StateEvent
- type StateHandler
- type StateMachine
- type StateMachineOption
- type StateTransition
- type States
- type Transition
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func DefaultManagerOptions ¶
DefaultManagerOptions returns ctrl.Options with Rekon's recommended defaults: metrics server on :8080, health probes on :8081, leader election enabled.
leaderElectionID must be unique per operator (e.g. "myoperator.io"). Override individual fields before passing to ctrl.NewManager.
func Setup ¶
func Setup(mgr manager.Manager, opts ...SetupOption) error
Setup registers Rekon's operational boilerplate with mgr.
Always registers /healthz and /readyz probes. Pass SetupOptions - constructed with ForStateMachine - to enable per-CR-type metrics.
Call Setup after creating your StateMachine and ResourceOwner instances but before calling mgr.Start.
Types ¶
type Condition ¶
type Condition[CR MachineObject] func(ctx context.Context, cr CR) (bool, error)
Condition is a predicate evaluated at reconcile time to determine whether a transition should fire. Returning an error surfaces it as a reconcile error and suppresses the transition.
type MachineObject ¶
type MachineObject interface {
client.Object
GetMachineStatus() *MachineStatus
}
MachineObject is the constraint on CR types managed by a StateMachine. Implement it by returning a pointer to the MachineStatus embedded in your CR's status struct.
type MachineStatus ¶
type MachineStatus struct {
State string `json:"state,omitempty"`
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
Conditions []metav1.Condition `json:"conditions,omitempty"`
StateHistory []StateTransition `json:"stateHistory,omitempty"`
}
MachineStatus is embedded in a CR's status struct to participate in state machine lifecycle management. The State, ObservedGeneration, Conditions, and StateHistory fields are always visible in kubectl output and are written automatically by the framework.
Example embedding:
type WidgetStatus struct {
rekon.MachineStatus `json:",inline"`
}
func (w *Widget) GetMachineStatus() *rekon.MachineStatus {
return &w.Status.MachineStatus
}
type Option ¶
type Option interface {
// contains filtered or unexported methods
}
Option is a predicate applied to an object in an Expect* assertion. Construct with Matching.
type OwnedResourceOption ¶
type OwnedResourceOption[CR client.Object, Child client.Object] interface { // contains filtered or unexported methods }
OwnedResourceOption configures an OwnedResourceSpec. Construct with WithUpdate, WithReady, or WithFinalizer.
func WithFinalizer ¶
func WithFinalizer[CR client.Object, Child client.Object](finalizer string) OwnedResourceOption[CR, Child]
WithFinalizer marks this child type as requiring finalizer-based lifecycle management on the owner CR.
Required when the child is cluster-scoped or lives in a different namespace than the owner - Kubernetes owner references cannot span namespaces, so the framework manages deletion explicitly.
finalizer is the string added to the owner CR's finalizer list (e.g. "myoperator.io/cleanup"). It is installed on first reconcile and removed only after all children of this type are deleted.
func WithReady ¶
func WithReady[CR client.Object, Child client.Object](fn func(Child) bool) OwnedResourceOption[CR, Child]
WithReady configures the health predicate for a child resource type.
fn reports whether the child is in a ready state.
If omitted, the child type is considered always ready for the purposes of IsReady and AreAllReady.
func WithUpdate ¶
func WithUpdate[CR client.Object, Child client.Object](fn func(CR, Child) Child) OwnedResourceOption[CR, Child]
WithUpdate configures drift correction for a child resource type.
fn receives the owner CR and the existing child, and returns the desired state. The framework patches the existing child to match.
If omitted, the child is created when missing but never updated afterwards. This is a safe default - it avoids overwriting fields managed by other controllers.
type OwnedResourceSpec ¶
OwnedResourceSpec describes one type of child resource managed under a parent CR of type CR. Construct with For.
func For ¶
func For[CR client.Object, Child client.Object]( kind Child, build func(CR) []Child, opts ...OwnedResourceOption[CR, Child], ) OwnedResourceSpec[CR]
For constructs an OwnedResourceSpec with compile-time type safety.
kind is a zero-value instance of the child type used for GVK resolution and listing (e.g. &appsv1.Deployment{}).
build returns the desired set of children from the owner CR. Return nil or an empty slice to indicate that no children of this type should exist.
Type parameters CR and Child are usually inferred from kind and build.
type OwnedResources ¶
type OwnedResources[CR client.Object] []OwnedResourceSpec[CR]
OwnedResources is the declared set of child resource types for a parent CR of type CR.
type ResourceOwner ¶
ResourceOwner reconciles the child resources declared in OwnedResources.
It is not itself a reconcile.Reconciler - call Reconcile from your own reconciler after fetching the CR.
func NewResourceOwner ¶
func NewResourceOwner[CR client.Object]( c client.Client, scheme *runtime.Scheme, owned OwnedResources[CR], ) (*ResourceOwner[CR], error)
NewResourceOwner validates owned and returns a ResourceOwner ready to use.
Validation errors (duplicate kinds, unregistered types) are returned immediately so they surface at startup, not mid-reconciliation.
scheme must include all child resource types declared in owned.
func (*ResourceOwner[CR]) AreAllReady ¶
func (ro *ResourceOwner[CR]) AreAllReady(ctx context.Context, cr CR) (bool, error)
AreAllReady reports whether every declared child type reports ready for cr. A type with no Ready predicate is considered ready.
func (*ResourceOwner[CR]) Client ¶
func (ro *ResourceOwner[CR]) Client() client.Client
Client returns the underlying client.Client.
Use this to bypass ResourceOwner entirely for resources that need hand-rolled management - you are not locked into the abstraction.
func (*ResourceOwner[CR]) IsReady ¶
IsReady reports whether all desired children of the type identified by kind satisfy that type's Ready predicate.
"Desired" is determined by calling Build with the current cr. If no Ready predicate was configured for that type (WithReady was not passed to For), IsReady returns (true, nil) immediately - the child's existence is not checked. When a predicate is configured, a desired child that does not yet exist counts as not ready. Returns an error if kind was not declared in OwnedResources.
func (*ResourceOwner[CR]) Reconcile ¶
Reconcile drives the full ownership loop for cr.
On a normal reconcile it:
- installs finalizers on cr for any cross-namespace or cluster-scoped children
- calls Build for each declared spec (empty slice means no children desired)
- creates children that do not yet exist
- patches children that exist when Update is configured
- garbage-collects children no longer in the desired set
On deletion (cr has a non-nil DeletionTimestamp) it:
- explicitly deletes children managed via finalizers
- removes each finalizer from cr once its children are gone
- same-namespace children are handled by Kubernetes owner-reference GC
Returns reconcile.Result so it composes with your own result.
type SetupOption ¶
type SetupOption interface {
// contains filtered or unexported methods
}
SetupOption configures Setup. Construct with ForStateMachine.
func ForStateMachine ¶
func ForStateMachine[CR MachineObject](sm *StateMachine[CR]) SetupOption
ForStateMachine enables Prometheus metrics for sm.
Injects event-driven metric instruments into sm so that reconcile events (throughput, latency, errors, panics, transitions, dwell time) are recorded automatically - no changes to the reconciler are required.
Also registers cache-driven collectors for state distribution, generation lag, and deletion backlog, computed from the manager's informer cache on each scrape.
The CR type must be registered in the manager's scheme before Setup is called.
type Simulator ¶
type Simulator struct {
// contains filtered or unexported fields
}
Simulator intercepts Kubernetes API calls made by a reconciler and lets you assert on them without a cluster.
func NewSimulator ¶
func NewSimulator(t testing.TB, scheme *runtime.Scheme, newReconciler func(client.Client) reconcile.Reconciler, statusSubresource ...client.Object) *Simulator
NewSimulator creates a simulator backed by a fake client using scheme. newReconciler is called once with the simulator's intercepting client so that all Kubernetes writes made during reconciliation are recorded.
statusSubresource lists object types whose status field is managed as a separate subresource in the fake client (equivalent to WithStatusSubresource). Pass any type whose reconciler calls Status().Update() or Status().Patch().
AssertComplete is registered via t.Cleanup and runs automatically at test end.
func (*Simulator) AssertComplete ¶
func (s *Simulator) AssertComplete()
AssertComplete fails the test if there are unconsumed actions in the queue - actions the reconciler took that no Expect* call consumed. Also called automatically via t.Cleanup.
Must not be called concurrently with other simulator methods, or after the test goroutine has exited - doing so will cause t.Errorf to panic.
func (*Simulator) Create ¶
Create adds obj to the simulated store and triggers a reconcile. The object is seeded via the raw fake client (not the intercepting client) so that this setup call does not appear in the action queue. If namespace is empty, it is set to "default". Fails the test immediately if the reconcile returns an error.
func (*Simulator) ExpectCreate ¶
ExpectCreate asserts that the next recorded action is a Create of the same type as typeHint, satisfying all opts. Fails immediately if it does not match.
func (*Simulator) ExpectStatusUpdate ¶
ExpectStatusUpdate asserts that the next recorded action is a status subresource update satisfying all opts. Fails immediately if it does not match.
Note: only intercepts calls made via Status().Update() or Status().Patch(). Reconcilers that call SubResource("status") directly are not intercepted.
func (*Simulator) FakeClient ¶
FakeClient returns the underlying fake client for direct manipulation in tests. Use this when you need to set status fields that SimulateReady does not cover - for example, setting Deployment.Status.AvailableReplicas to simulate a rollout, or calling Delete on a CR that has finalizers to set its DeletionTimestamp.
func (*Simulator) Reconcile ¶
func (s *Simulator) Reconcile()
Reconcile triggers one reconcile for the primary CR without modifying any store state. Use this to simulate the watch-event-triggered reconcile that would occur in a real cluster after a status write.
func (*Simulator) SimulateReady ¶
SimulateReady marks the named resource of the same type as typeHint as ready by setting a Ready=True condition in its status, then triggers a reconcile for the primary CR. Uses the same namespace as the primary CR.
The condition is set at status.conditions using a JSON merge patch:
{"type":"Ready","status":"True","reason":"SimulatedReady"}
If the resource type does not store readiness at status.conditions, this method will not have the intended effect. Manipulate the fake store directly for non-standard readiness signals.
func (*Simulator) SimulateState ¶
func (s *Simulator) SimulateState(typeHint MachineObject, state string)
SimulateState directly sets the primary CR's state to state and triggers a reconcile, bypassing transition predicates and Enter hooks. Use this to test individual state handlers in isolation without constructing predecessor state.
typeHint must be a zero-value instance of the primary CR's type (e.g. &myv1.Widget{}) and must implement MachineObject.
type StateCondition ¶
type StateCondition struct {
// Type is the condition type, e.g. "Ready", "Progressing", "Degraded".
Type string
// Status is metav1.ConditionTrue, ConditionFalse, or ConditionUnknown.
Status metav1.ConditionStatus
// Reason is a CamelCase word explaining why this condition has this status.
// Required by the Kubernetes API - must be non-empty.
Reason string
// Message is an optional human-readable explanation surfaced in kubectl output.
Message string
}
StateCondition describes one metav1.Condition value to write when the machine enters a state. All fields except Message are required.
Use apimeta.FindStatusCondition to read conditions from MachineStatus.Conditions if you need to inspect them in a hook.
type StateEvent ¶
type StateEvent struct {
// Reason is a CamelCase word used in the event's Reason field.
// Required - must be non-empty.
Reason string
// Message is the human-readable string surfaced in kubectl describe.
Message string
}
StateEvent describes a Kubernetes event to emit on state entry or hook failure. Reason must be a non-empty CamelCase word.
type StateHandler ¶
type StateHandler[CR MachineObject] struct { // Conditions are written to MachineStatus.Conditions when the machine // enters this state. apimeta.SetStatusCondition is used, so LastTransitionTime // is only updated when Status actually changes. // // Condition types not listed here are left unchanged. Declaring all condition // types on every state is safe and avoids ambiguity about previous values. Conditions []StateCondition // Event is emitted as a Normal Kubernetes event after a successful state entry // status write. Nil means no event is emitted for this state. Event *StateEvent // FailureEvent overrides the default Warning event emitted when an Enter or // Reconcile hook returns an error. Nil means the framework uses a default // message: "rekon: <phase> hook for state "<state>" failed: <err>". FailureEvent *StateEvent // Enter is called once when the machine first transitions into this state. // Must be idempotent: if it returns an error the framework retries on the // next reconcile without re-recording the transition. Enter func(ctx context.Context, cr CR) (reconcile.Result, error) // Reconcile is called on every reconcile loop while the machine is in this // state, but only when no transition fires on that pass. Reconcile func(ctx context.Context, cr CR) (reconcile.Result, error) // Transitions is evaluated in order. The first condition that returns true // fires; subsequent conditions are not evaluated. Transitions []Transition[CR] }
StateHandler describes one state in the machine. Both Enter and Reconcile are optional; a state with neither is valid (e.g. a terminal state).
type StateMachine ¶
type StateMachine[CR MachineObject] struct { // contains filtered or unexported fields }
StateMachine drives CR lifecycle management based on an explicit state graph.
It is not itself a reconcile.Reconciler - call Reconcile from your own reconciler after fetching the CR. This mirrors the ResourceOwner pattern and composes with it naturally.
func NewStateMachine ¶
func NewStateMachine[CR MachineObject]( c client.Client, initial string, states States[CR], opts ...StateMachineOption, ) (*StateMachine[CR], error)
NewStateMachine validates states and returns a StateMachine ready to use.
Intended to be called at manager startup so validation errors surface immediately rather than at reconcile time. Returns a descriptive error naming the offending state or field if validation fails.
Validation checks:
- states must be non-empty
- initial must be a key in states
- every Transition.To must be a key in states
- every StateCondition must have a non-empty Type and Reason, and a Status of metav1.ConditionTrue, ConditionFalse, or ConditionUnknown
- every non-nil StateHandler.Event and FailureEvent must have a non-empty Reason
func (*StateMachine[CR]) Reconcile ¶
Reconcile runs one pass of the state machine for cr.
On each pass it does exactly one of:
- Transition to the initial state (if current state is empty).
- Fire the first transition whose condition returns true, calling Enter on the target state.
- Call the current state's Reconcile hook if no transition fired.
A status update is written whenever state changes. Enter and Reconcile are never called on the same pass.
type StateMachineOption ¶
type StateMachineOption func(*stateMachineConfig)
StateMachineOption configures a StateMachine.
func WithEventRecorder ¶
func WithEventRecorder(r record.EventRecorder) StateMachineOption
WithEventRecorder sets the event recorder used to publish Kubernetes events for state entries and hook failures. When ForStateMachine is used, the recorder is injected automatically; call WithEventRecorder directly only when you need a custom recorder or are not using ForStateMachine.
func WithMaxHistory ¶
func WithMaxHistory(n int) StateMachineOption
WithMaxHistory sets the number of StateTransition entries retained in MachineStatus.StateHistory. Oldest entries are evicted when the limit is exceeded. Default: 10.
type StateTransition ¶
type StateTransition struct {
From string `json:"from,omitempty"`
To string `json:"to"`
Reason string `json:"reason,omitempty"`
Timestamp metav1.Time `json:"timestamp"`
}
StateTransition records one state change. Written automatically by the framework whenever the machine transitions. Load-bearing for debuggability: kubectl get <resource> -o yaml shows the full transition trail.
type States ¶
type States[CR MachineObject] map[string]StateHandler[CR]
States is the full declaration of the state graph, keyed by state name.
type Transition ¶
type Transition[CR MachineObject] struct { // To is the target state name. Must be a key in States. To string // When is the predicate that triggers this transition. When Condition[CR] // Reason is written to StateTransition.Reason in the history. // Optional - leave empty if no reason is needed. Reason string }
Transition describes one possible state change out of a state. Transitions are evaluated in slice order; the first true condition fires.