Documentation
¶
Overview ¶
Package reconcile implements the core convergence loop: desired state in, observed state diffed against it, idempotent and level-triggered controllers converge the two. Reconcilers never assume they're continuing a previous partial operation: every call re-derives what to do from current observed state, which is what makes them safe to interrupt and safe to retry.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Condition ¶
type Condition struct {
Type string
Status ConditionStatus
Reason string
Message string
LastTransitionTime time.Time
}
Condition is a single named status a controller reports after each reconcile, always with a machine-readable Reason: every reconcile emits a status condition with a reason string.
type ConditionStatus ¶
type ConditionStatus string
ConditionStatus mirrors Kubernetes' tri-state convention deliberately: "Unknown" is a real, distinct state from "False": a controller that hasn't finished its first reconcile yet is not the same as one that has confirmed failure.
const ( ConditionTrue ConditionStatus = "True" ConditionFalse ConditionStatus = "False" ConditionUnknown ConditionStatus = "Unknown" )
The three values a Condition's Status can hold.
type Controller ¶
type Controller interface {
// Name identifies the controller in logs and status lookups. Stable
// across restarts.
Name() string
// Reconcile diffs desired against observed state and takes whatever
// action is needed to converge them. It returns the conditions
// resulting from this attempt even when it also returns an error;
// callers should record both.
Reconcile(ctx context.Context) (Result, error)
}
Controller manages one resource's convergence from desired to observed state. Reconcile must be idempotent: calling it repeatedly with nothing changed must be a cheap no-op, and calling it after a previous call was interrupted mid-operation must still converge correctly rather than double-apply or corrupt state.
type Engine ¶
type Engine struct {
// contains filtered or unexported fields
}
Engine runs a fixed set of controllers, triggered by Docker events with a periodic resync as a safety net for any event the stream missed. This push-primary/pull-as-safety-net shape is deliberate: Coolify's own v5 rearchitecture (see docs-local/research/prior-art-coolify.md) converged on the same pattern independently.
func NewEngine ¶
func NewEngine(logger *slog.Logger, controllers ...Controller) *Engine
NewEngine builds an Engine over the given controllers. Order is preserved for each ReconcileAll pass but controllers must not depend on each other's side effects within a single pass: that's what makes per-resource-type controllers safe to reason about independently.
func (*Engine) LastResult ¶
LastResult returns the most recent Result and error for a controller by name, for status reporting. The zero Result and a nil error are returned if the controller has never run.
func (*Engine) ReconcileAll ¶
ReconcileAll runs every controller once: first the fixed set NewEngine was built with, then whatever the Source currently reports, if one is set. A single controller failing does not stop the others from running: one broken resource should never block convergence of everything else. A Source error is logged and skipped for this pass rather than treated as fatal, since the next tick or event re-derives the set again anyway.
func (*Engine) Run ¶
func (e *Engine) Run(ctx context.Context, events <-chan docker.Event, resyncInterval time.Duration) error
Run reconciles once immediately, then keeps reconciling on every incoming Docker event and on every resyncInterval tick, until ctx is cancelled or the event stream closes. Interrupting Run at any point is safe: nothing here holds a lock across a reconcile, so the next Run (or a fresh process) picks up wherever observed state actually is.
type Result ¶
type Result struct {
Conditions []Condition
}
Result is what a Controller returns from one Reconcile call.
type Source ¶
type Source func(ctx context.Context) ([]Controller, error)
Source dynamically supplies the current set of controllers to reconcile, called once per pass immediately before those controllers run. A Source re-derives its controller set from live desired state every call, the same level-triggered principle every controller in this codebase already follows for its own resource: nothing about which apps or databases exist is remembered between calls, everything is re-derived from what's in the store right now. This is how the engine goes from a fixed set of controllers built once at startup to one controller per app and per database, appearing and disappearing as desired state changes, without a restart.
type Store ¶
type Store interface {
UpsertConditions(ctx context.Context, controllerName string, conditions []Condition) error
}
Store is optional persistence for reconcile status. Every reconcile is required to emit a status condition with a reason string, stored and shown in the UI; without a Store, Engine still tracks the latest result via LastResult, but only in memory, lost on restart. internal/store.DB satisfies this interface structurally, no import of internal/store needed here (or that would be a cycle, since internal/store already imports this package for the Condition type).
Directories
¶
| Path | Synopsis |
|---|---|
|
Package application implements the declarative app spec's service contract and TASKS.md 1.3's application controller: the reconcile.Controller that converges a real, store-backed desired service to a running container, replacing nginxdemo's hardcoded desired state with the real thing.
|
Package application implements the declarative app spec's service contract and TASKS.md 1.3's application controller: the reconcile.Controller that converges a real, store-backed desired service to a running container, replacing nginxdemo's hardcoded desired state with the real thing. |
|
Package cloudflaretunnel implements the reconcile.Controller that converges a single, platform-wide desired state (store.
|
Package cloudflaretunnel implements the reconcile.Controller that converges a single, platform-wide desired state (store. |
|
Package database implements TASKS.md 1.8's managed database controller: the reconcile.Controller that converges a store-backed store.DesiredDatabase to a running, volume-backed container, the same architectural pattern internal/reconcile/application already establishes (level-triggered, deterministic naming, a narrow store interface for testability), applied to a database instead of a built application image.
|
Package database implements TASKS.md 1.8's managed database controller: the reconcile.Controller that converges a store-backed store.DesiredDatabase to a running, volume-backed container, the same architectural pattern internal/reconcile/application already establishes (level-triggered, deterministic naming, a narrow store interface for testability), applied to a database instead of a built application image. |
|
Package ingress implements TASKS.md 1.6's ingress controller: the reconcile.Controller that keeps Caddy's config (internal/ingress, ADR 005) in sync with every service that declares domains.
|
Package ingress implements TASKS.md 1.6's ingress controller: the reconcile.Controller that keeps Caddy's config (internal/ingress, ADR 005) in sync with every service that declares domains. |
|
Package mesh implements TASKS.md 3.4's mesh controller: the reconcile.Controller that keeps the WireGuard mesh and the internal DNS zone converged on whatever the store currently says the fleet and its placements look like.
|
Package mesh implements TASKS.md 3.4's mesh controller: the reconcile.Controller that keeps the WireGuard mesh and the internal DNS zone converged on whatever the store currently says the fleet and its placements look like. |
|
Package nginxdemo is the Phase 0 exit criterion: one controller that keeps a single hardcoded nginx container running.
|
Package nginxdemo is the Phase 0 exit criterion: one controller that keeps a single hardcoded nginx container running. |
|
Package nodehealth implements TASKS.md 3.7's node health check: the reconcile.Controller that converges a node's observed heartbeat (internal/store's last_seen_at, kept fresh by internal/agent.Server's periodic touch loop while a node's gRPC session stays open) against its recorded Status, the same architectural pattern internal/reconcile/application and internal/reconcile/database already establish (level-triggered, a narrow store interface for testability, one controller instance per resource), applied to a node instead of a service or database.
|
Package nodehealth implements TASKS.md 3.7's node health check: the reconcile.Controller that converges a node's observed heartbeat (internal/store's last_seen_at, kept fresh by internal/agent.Server's periodic touch loop while a node's gRPC session stays open) against its recorded Status, the same architectural pattern internal/reconcile/application and internal/reconcile/database already establish (level-triggered, a narrow store interface for testability, one controller instance per resource), applied to a node instead of a service or database. |
|
Package registry implements the reconcile.Controller that converges a single, platform-wide desired state (store.RegistrySettings plus a generated password in internal/secrets) to a running or absent registry:2 container: Levelrail's own built-in image registry, so a multi-node deployment gets a BuildKit cache/distribution backend (internal/build's WithCacheRegistry) without an operator first signing up for an external one.
|
Package registry implements the reconcile.Controller that converges a single, platform-wide desired state (store.RegistrySettings plus a generated password in internal/secrets) to a running or absent registry:2 container: Levelrail's own built-in image registry, so a multi-node deployment gets a BuildKit cache/distribution backend (internal/build's WithCacheRegistry) without an operator first signing up for an external one. |