state

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

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

Go to latest
Published: Jun 11, 2026 License: Apache-2.0 Imports: 27 Imported by: 0

README

constate

constate (CONtroller STATE) is a universal state machine library for Kubernetes operators. It replaces the open-ended Reconcile loop from controller-runtime with a structured finite state machine (FSM): instead of writing one monolithic reconciler, you implement focused handlers — one per state — and constate routes each reconciliation to the right handler automatically.

Why

controller-runtime gives you a blank Reconcile(ctx, req) function. In practice, every operator ends up re-implementing the same patterns: detect whether a resource exists upstream, track whether it's being created or updated, manage finalizers during deletion. This leads to deeply nested conditionals and logic that is hard to follow and test in isolation.

constate models each managed resource as a CRUD state machine. Your code becomes a set of small, single-purpose functions, each responsible for one phase of the resource lifecycle.

State machine

Initial ──► Creating ──► Created ◄──► Updating ──► Updated
   │                                                   │
   └──► ImportRequested ──► Imported ◄─────────────────┘
                                                        │
any ──► DeletionRequested ──► Deleting ──► Deleted ◄───┘

States are stored as State and Ready status conditions on the resource, so they are visible via kubectl get and usable in wait expressions.

State Ready Meaning
Initial False Resource just appeared, never reconciled
Creating False Upstream resource creation in progress
Created True Resource exists and is settled
Updating False Upstream resource update in progress
Updated True Resource updated and settled
ImportRequested False External-id annotation detected, importing
Imported True Upstream resource adopted
DeletionRequested False deletionTimestamp set, clean-up starting
Deleting False Upstream deletion in progress
Deleted Terminal — finalizers removed, object gone

Usage

Implement the StateHandler[T] interface for your CRD type. Each method receives the fully fetched object and returns a Result carrying the next state.

type Result struct {
    reconcile.Result          // standard requeue/requeueAfter
    NextState state.ResourceState
    StateMsg  string
}
type MyResourceHandler struct {
    client client.Client
}

func (h *MyResourceHandler) HandleInitial(ctx context.Context, obj *MyResource) (state.Result, error) {
    if err := h.client.CreateUpstream(ctx, obj); err != nil {
        return state.Result{}, err
    }
    return state.Result{
        NextState: state.StateCreating,
        Result:    reconcile.Result{RequeueAfter: 5 * time.Second},
    }, nil
}

func (h *MyResourceHandler) HandleCreating(ctx context.Context, obj *MyResource) (state.Result, error) {
    ready, err := h.client.IsReady(ctx, obj)
    if err != nil {
        return state.Result{}, err
    }
    if !ready {
        return state.Result{
            NextState: state.StateCreating,
            Result:    reconcile.Result{RequeueAfter: 5 * time.Second},
        }, nil
    }
    return state.Result{NextState: state.StateCreated}, nil
}

// ... implement remaining handlers

Wire it into controller-runtime:

reconciler := state.NewStateReconciler(&MyResourceHandler{client: mgr.GetClient()})
if err := reconciler.SetupWithManager(mgr, controller.Options{}); err != nil {
    return err
}

Importing existing resources

If a resource arrives with a mongodb.com/external-<key> annotation, constate automatically transitions it to ImportRequested instead of Initial. Implement HandleImportRequested to adopt the upstream resource and transition to Imported.

Drift detection

ComputeStateTracker produces a stable hash of the resource's generation plus any referenced Secrets and ConfigMaps. Store it with Patcher.UpdateStateTracker(deps...) when the resource is settled and compare it on the next reconcile to detect drift without re-calling the upstream API on every loop.

Patching

Patcher wraps Kubernetes server-side apply for status and annotations:

NewPatcher(obj).
    UpdateStatus().
    UpdateStateTracker(secret, configMap).
    Patch(ctx, client)

Skipping reconciliation

Annotate a resource with mongodb.com/atlas-reconciliation-policy=skip to pause reconciliation without deleting it.

Install

go get github.com/crd2go/constate

Documentation

Index

Constants

View Source
const (
	AnnotationReapplyTimestamp = "mongodb.internal.com/reapply-timestamp"
	AnnotationStateTracker     = "mongodb.internal.com/state-tracker"
	AnnotationExternalID       = "mongodb.com/external-id"
)
View Source
const (
	ReadyReasonError   = "Error"
	ReadyReasonPending = "Pending"
	ReadyReasonSettled = "Settled"
)
View Source
const FieldOwner = "mongodb-atlas-kubernetes-resource-handler"

Variables

This section is empty.

Functions

func ComputeStateTracker

func ComputeStateTracker(obj metav1.Object, dependencies ...client.Object) string

func GetExternalID

func GetExternalID(obj metav1.Object) (string, error)

GetExternalID reads the AnnotationExternalID annotation from obj. Returns an error if the annotation is absent or empty.

func NewReadyCondition

func NewReadyCondition(result Result) metav1.Condition

func PatchReapplyTimestamp

func PatchReapplyTimestamp(ctx context.Context, kubeClient client.Client, obj client.Object) (time.Duration, error)

func ReapplyPeriod

func ReapplyPeriod(obj metav1.Object) (time.Duration, bool, error)

func ReapplyTimestamp

func ReapplyTimestamp(obj metav1.Object) (time.Time, bool, error)

func ShouldReapply

func ShouldReapply(obj metav1.Object) (bool, error)

func ShouldUpdate

func ShouldUpdate(obj metav1.Object, dependencies ...client.Object) (bool, error)

ShouldUpdate returns true if the object should be updated based on generation change, reapply period, or error status. Note: a generation change or error status will only be detected if the object implements StatusObject interface.

Returns an error if there is an issue checking the reapply period.

Types

type Patcher

type Patcher struct {
	// contains filtered or unexported fields
}

func NewPatcher

func NewPatcher(obj client.Object) *Patcher

func (*Patcher) Patch

func (p *Patcher) Patch(ctx context.Context, c client.Client) error

Patch applies the patches to the given object and updates both status and the annotations if they were modified.

func (*Patcher) UpdateConditions

func (p *Patcher) UpdateConditions(conditions []metav1.Condition) *Patcher

UpdateConditions updates the status conditions of the given object.

Note: this method only updates the "conditions" field in the status. To update remaining status fields, use UpdateStatus().

func (*Patcher) UpdateStateTracker

func (p *Patcher) UpdateStateTracker(dependencies ...client.Object) *Patcher

UpdateStateTracker updates the state tracker annotation on the given object.

func (*Patcher) UpdateStatus

func (p *Patcher) UpdateStatus() *Patcher

UpdateStatus updates the status of the given object.

Note: this method omits the "conditions" field from the status. To update conditions, use UpdateConditions().

func (*Patcher) WithFieldOwner

func (p *Patcher) WithFieldOwner(fieldOwner string) *Patcher

WithFieldOwner sets the field owner for the patch operation.

type Reconciler

type Reconciler[T any] struct {
	// contains filtered or unexported fields
}

func NewStateReconciler

func NewStateReconciler[T any](target StateHandler[T], options ...ReconcilerOptionFn[T]) *Reconciler[T]

func (*Reconciler[T]) For

func (r *Reconciler[T]) For() (client.Object, builder.Predicates)

func (*Reconciler[T]) Reconcile

func (r *Reconciler[T]) Reconcile(ctx context.Context, req ctrl.Request) (reconcile.Result, error)

func (*Reconciler[T]) ReconcileState

func (r *Reconciler[T]) ReconcileState(ctx context.Context, t *T) (Result, error)

func (*Reconciler[T]) SetupWithManager

func (r *Reconciler[T]) SetupWithManager(mgr ctrl.Manager, defaultOptions controller.Options) error

type ReconcilerOptionFn

type ReconcilerOptionFn[T any] func(*Reconciler[T])

func WithCluster

func WithCluster[T any](c cluster.Cluster) ReconcilerOptionFn[T]

func WithReapplySupport

func WithReapplySupport[T any](supportReapply bool) ReconcilerOptionFn[T]

type Result

type Result struct {
	reconcile.Result
	NextState state.ResourceState
	StateMsg  string
}

type StateHandler

type StateHandler[T any] interface {
	SetupWithManager(ctrl.Manager, reconcile.Reconciler, controller.Options) error
	For() (client.Object, builder.Predicates)
	HandleInitial(context.Context, *T) (Result, error)
	HandleImportRequested(context.Context, *T) (Result, error)
	HandleImported(context.Context, *T) (Result, error)
	HandleCreating(context.Context, *T) (Result, error)
	HandleCreated(context.Context, *T) (Result, error)
	HandleUpdating(context.Context, *T) (Result, error)
	HandleUpdated(context.Context, *T) (Result, error)
	HandleDeletionRequested(context.Context, *T) (Result, error)
	HandleDeleting(context.Context, *T) (Result, error)
}

type StatusObject

type StatusObject interface {
	GetConditions() []metav1.Condition
}

type UnstructuredStateReconciler

type UnstructuredStateReconciler = StateHandler[unstructured.Unstructured]

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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