rekon

package module
v0.0.0-...-e16237d Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 21, 2026 License: Apache-2.0 Imports: 26 Imported by: 0

README

Armada logo

CI Go Reference

Rekon

A Go library for building Kubernetes operators that are correct by default, easy to test, and readable long after you wrote them.

Rekon sits on top of controller-runtime. It doesn't replace Kubebuilder, doesn't own your main(), and doesn't ask you to learn a new mental model for Kubernetes. It gives you three things:

  1. A state machine reconciler.
  2. Declarative resource ownership.
  3. A test simulator that runs without a cluster.

Use all three, or just the one that solves your immediate problem.

Why not just use controller-runtime directly?

You can - many teams do. The honest answer is controller-runtime gives you everything you need to write a correct operator, and nothing to stop you writing an incorrect one.

In practice, this means every operator eventually has a Reconcile() function that's grown to several hundred lines, a finalizer implementation that's mostly right, status conditions that are inconsistent across resources, and a test suite that takes 90 seconds to run because it spins up envtest. These aren't hard problems. They're just problems that every operator author solves slightly differently, usually under deadlines, usually with subtle bugs they haven't discovered yet.

Rekon is the version of those patterns written once, carefully, with tests for the edge cases you'll hit in production.

Who this is for

If you've built ten operators and have your own patterns that work, you probably don't need this. The test simulator might still be worth a look.

If you're building your first or second operator, Rekon will save you a few weeks and prevent bugs you don't know about yet.

If you have an existing operator and it's working, the case for migrating is weak, as you've already paid the cost of the boilerplate and a rewrite introduces risk without much upside. Again, the test simulator is the exception: it works with any controller-runtime reconciler and can be adopted without changing your operator's structure.

Installation

go get github.com/rekon-k8s/rekon

Requires Go 1.26+. Compatible with controller-runtime v0.22+. See Compatibility for the support policy and COMPATIBILITY.md for the version matrix.

The three pieces

1. State machine reconciler

Most operators have lifecycle states, such as provisioning, running, upgrading and degraded - even if the code doesn't make them explicit. Rekon makes them explicit.

Your CR's status struct embeds rekon.MachineStatus so that state is always visible in kubectl output:

type WidgetStatus struct {
    rekon.MachineStatus `json:",inline"`
}

func (w *Widget) GetMachineStatus() *rekon.MachineStatus {
    return &w.Status.MachineStatus
}

Then declare the state graph and construct the machine once at startup:

sm, err := rekon.NewStateMachine(client, "initialising",
    rekon.States[*myv1.Widget]{
        "initialising": {
            Enter: createChildResources,
            Transitions: []rekon.Transition[*myv1.Widget]{
                {To: "running",  When: allChildrenReady},
                {To: "degraded", When: anyChildFailed},
            },
        },
        "running": {
            Reconcile: ensureDesiredState,
            Transitions: []rekon.Transition[*myv1.Widget]{
                {To: "updating", When: specChanged},
                {To: "degraded",  When: anyChildFailed},
                {To: "deleting",  When: markedForDeletion},
            },
        },
        "degraded": {
            Reconcile: attemptRecovery,
            Transitions: []rekon.Transition[*myv1.Widget]{
                {To: "running", When: allChildrenReady},
            },
        },
        "deleting": {
            Enter: cleanupExternalResources,
        },
    },
)

Call it from your reconciler after fetching the CR:

func (r *WidgetReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) {
    cr := &myv1.Widget{}
    if err := r.client.Get(ctx, req.NamespacedName, cr); err != nil {
        return reconcile.Result{}, client.IgnoreNotFound(err)
    }
    return r.sm.Reconcile(ctx, cr)
}

The framework handles state transitions, status updates, event recording, and requeue scheduling. Each state handler is a plain Go function that you can test in isolation.

Current state and recent transitions are recorded in the resource's status automatically. When a resource gets stuck, kubectl get myresource -o yaml tells you where it is and why it didn't move.

If the state machine doesn't fit your use case, don't use it. The resource ownership and test simulator work with any controller-runtime reconciler.

2. Declarative resource ownership

Define the child resources your operator manages. Rekon handles creation, drift correction, owner references, finalizers, and garbage collection.

owned := rekon.OwnedResources[*myv1.Widget]{
    rekon.For(&appsv1.Deployment{}, buildDeployment,
        rekon.WithUpdate(updateDeployment),
        rekon.WithReady(deploymentReady),
    ),
    rekon.For(&corev1.Service{}, buildService,
        rekon.WithReady(serviceReady),
    ),
}

// func buildDeployment(cr *myv1.Widget) []*appsv1.Deployment
// func updateDeployment(cr *myv1.Widget, existing *appsv1.Deployment) *appsv1.Deployment
// func deploymentReady(existing *appsv1.Deployment) bool
// func buildService(cr *myv1.Widget) []*corev1.Service
// func serviceReady(existing *corev1.Service) bool

Build, Update, and Ready are pure functions. They're easy to test, easy to read, and they compose well. The framework calls them at the right time; you don't need to think about when.

Build returns a slice - return nil or an empty slice to indicate that no children of that type should exist. Update returns the desired state; if omitted, the child is created when missing but never patched afterwards (safe default for fields managed by other controllers). Type parameters on OwnedResources and For are usually inferred from the function signatures.

3. Test simulator

The part that makes the most difference for most teams.

func TestWidget(t *testing.T) {
    sim := rekon.NewSimulator(t, scheme, func(c client.Client) reconcile.Reconciler {
        return NewWidgetReconciler(c)
    })

    sim.Create(&myv1.Widget{
        Spec: myv1.WidgetSpec{Replicas: 3},
    })

    sim.ExpectCreate(&appsv1.Deployment{},
        rekon.Matching(hasReplicas(3)),
    )
    sim.ExpectStatusUpdate(
        rekon.Matching(hasState("initialising")),
    )

    // Simulate a child becoming ready
    sim.SimulateReady(&appsv1.Deployment{}, "widget-deploy")

    sim.ExpectStatusUpdate(
        rekon.Matching(hasState("running")),
    )
}

No cluster; no envtest; no etcd. Tests run in milliseconds and are deterministic.

What the simulator doesn't cover: webhook validation, RBAC enforcement, and anything that depends on real controller behaviour (e.g. a Deployment creating Pods). For those, use envtest or a real cluster. The simulator replaces most reconciliation logic tests; it doesn't replace integration tests. See the testing guide for what falls into each category.

Built-in operational boilerplate

Every operator using Rekon gets, without configuration:

  • Prometheus metrics:
    • Reconciliation throughput, latency, and failures
    • State transition counts and per-state dwell time
    • Panic counts
    • Managed resource state distribution
    • Generation lag
    • Deletion backlog
  • Structured logging with reconciliation context on every log line
  • Health and readiness endpoints
  • Leader election (enabled by default, configurable)
  • Status conditions following the Kubernetes API conventions

Wire everything in one call:

mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), rekon.DefaultManagerOptions("myoperator.io"))
// ...
sm, err := rekon.NewStateMachine(mgr.GetClient(), "provisioning", states)
// ...
if err := rekon.Setup(mgr, rekon.ForStateMachine(sm)); err != nil {
    os.Exit(1)
}
mgr.Start(ctrl.SetupSignalHandler())

DefaultManagerOptions sets sensible defaults (metrics on :8080, health probes on :8081, leader election enabled) that you can override field by field. Setup always registers /healthz and /readyz; ForStateMachine adds the per-CR-type metrics and wires them into the state machine so reconcile events are recorded without any changes to your reconciler.

What Rekon is not

A replacement for Kubebuilder. Use Kubebuilder to scaffold your operator. Add Rekon as a dependency for the runtime logic. They're not in competition.

A replacement for Crossplane or kro. If your use case is composing existing Kubernetes resources declaratively, those tools are a better fit. Rekon is for operators that encode custom operational logic - lifecycle management, failure recovery, upgrade orchestration. The documentation has a longer treatment of this distinction.

Production-ready for every use case. Rekon is v0.x. The state machine and test simulator APIs are reasonably stable. The resource ownership API may still change. Anything marked experimental will move in a minor release. There's a full list of known limitations in LIMITATIONS.md.

Compatibility

Rekon depends on controller-runtime rather than the Kubernetes API directly. controller-runtime's own compatibility policy means a given version typically supports 3–4 Kubernetes minor versions. If your cluster version is supported by your controller-runtime version, it's supported by Rekon.

The supported version combinations are listed in COMPATIBILITY.md, updated with every release.

To check which controller-runtime version your project uses:

go list -m sigs.k8s.io/controller-runtime

Rekon follows controller-runtime minor releases promptly. When a new minor version is released, compatibility is tested and a Rekon patch or minor release follows within a few weeks.

Rekon does not test against Kubernetes versions that are end-of-life upstream. When a Kubernetes minor version reaches end-of-life, it is removed from the supported matrix in the next Rekon minor release.

Maintenance

Rekon is currently maintained by @mauriceyap. It's currently a small project, and you should factor that into your adoption decision.

What that means in practice: see MAINTENANCE.md for the compatibility commitment, release cadence, and what to do if the project goes quiet. The library is designed to be vendored and maintained independently - no code generation, no build-time magic, no central registry. If this project stops being maintained, your operator keeps working and the path to forking is straightforward.

If you're building something on Rekon and want to be involved, open a discussion. Please also consider adding your project or organisation to ADOPTERS.md.

Escape hatches

Rekon is a library that you import, not a framework that owns your operator. At every level, you can drop down to raw controller-runtime:

  • Use the test simulator with a hand-rolled Reconcile() function - it works with any reconcile.Reconciler
  • Use the resource ownership model without the state machine
  • Implement one state's Reconcile function as a raw controller-runtime reconciler if your logic doesn't fit the pattern
  • Access the underlying client.Client and manager.Manager directly at any point

If you find yourself fighting the abstractions, drop down a level. You're not locked in.

Example

The examples/ directory contains a web application operator - initialising, running, updating, degraded, deleting - built with Rekon. The same operator is implemented with vanilla Kubebuilder alongside it for comparison. The Kubebuilder example also includes simulator-based tests, showing that the test simulator works with any controller-runtime reconciler without adopting any other Rekon layer.

Contributing

Issues and PRs are welcome. See CONTRIBUTING.md. If you're planning something substantial, open an issue first.

Licence

Apache 2.0. See LICENSE.

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

Constants

This section is empty.

Variables

This section is empty.

Functions

func DefaultManagerOptions

func DefaultManagerOptions(leaderElectionID string) ctrl.Options

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.

func Matching

func Matching(fn func(client.Object) bool) Option

Matching returns an Option that passes when fn returns true for the object. Panics if fn is nil.

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

type OwnedResourceSpec[CR client.Object] struct {
	// contains filtered or unexported fields
}

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

type ResourceOwner[CR client.Object] struct {
	// contains filtered or unexported fields
}

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

func (ro *ResourceOwner[CR]) IsReady(ctx context.Context, cr CR, kind client.Object) (bool, error)

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

func (ro *ResourceOwner[CR]) Reconcile(ctx context.Context, cr CR) (reconcile.Result, error)

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

func (s *Simulator) Create(obj client.Object)

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

func (s *Simulator) ExpectCreate(typeHint client.Object, opts ...Option)

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

func (s *Simulator) ExpectStatusUpdate(opts ...Option)

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

func (s *Simulator) FakeClient() client.Client

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

func (s *Simulator) SimulateReady(typeHint client.Object, name string)

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

func (sm *StateMachine[CR]) Reconcile(ctx context.Context, cr CR) (reconcile.Result, error)

Reconcile runs one pass of the state machine for cr.

On each pass it does exactly one of:

  1. Transition to the initial state (if current state is empty).
  2. Fire the first transition whose condition returns true, calling Enter on the target state.
  3. 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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL