base

package
v0.4.1 Latest Latest
Warning

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

Go to latest
Published: Jun 11, 2026 License: AGPL-3.0 Imports: 13 Imported by: 0

Documentation

Overview

Package base provides the shared lifecycle harness every flate controller wraps around its per-resource reconcile body.

Each concrete controller (source, kustomization, helmrelease) embeds *base.Controller and contributes only the controller-specific dependencies (Fetchers, Helm client, Staging cache, ...) plus the reconcile function itself. Lifecycle wiring — the started gate, the unsubscriber slice, the per-id coalescer, the change filter, the Suspend/Filter pre-gate — lives here exactly once.

The package also owns the panic-recovery + status-transition harness (Recover, RunWithStatus) that surrounds individual reconcile bodies.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func DepFailed added in v0.3.4

func DepFailed(id manifest.NamedResource) func(depwait.Summary) error

DepFailed returns the canonical onFail closure that reports a dependency-wait failure as a *manifest.DependencyFailedError for id. The identical literal was rebuilt at every dependsOn/pre-render wait; single-sourcing it keeps the failure shape consistent across controllers.

func DispatchNode added in v0.3.4

func DispatchNode[T manifest.BaseManifest](
	ctx context.Context,
	c *Controller,
	id manifest.NamedResource,
	drainLevel int,
	suspended func(T) bool,
	reconcile func(context.Context, T) error,
) []manifest.NamedResource

DispatchNode runs id's reconcile body under the dag engine and reports the blocked dependency set (nil = terminalized). It performs the PreGate / preflight pre-checks, then RunWithStatusOutcome. drainLevel threads the scheduler's fixpoint level into Require via ctx. The orchestrator's Dispatcher calls this, routing by Kind, so no per-kind match check is needed here.

func Recover

func Recover(s *store.Store, id manifest.NamedResource, log *slog.Logger)

Recover catches a panic from the current goroutine and marks id StatusFailed with a "panic: <r>" message so the orchestrator surfaces it. log is the controller's pre-bound logger (Logger()); the panic is recorded under its controller=<name> attribute.

After recording status, re-raises the panic so the enclosing task.Service.Go increments its failures counter — a panicked reconcile MUST count against the orchestrator's failure gate, not silently masquerade as success when Service.Failures() is consulted for the final exit-code decision.

func RequireRefresh added in v0.3.4

func RequireRefresh[T manifest.BaseManifest](
	ctx context.Context,
	c *Controller,
	id manifest.NamedResource,
	timeout *metav1.Duration,
	deps []manifest.DependencyRef,
	pendingMsg string,
	onFail func(depwait.Summary) error,
) (T, bool, error)

RequireRefresh fuses Require with the load-bearing store re-read every dependency gate performs on success. A structural parent may have re-emitted this object with a mutated spec while it was parked, so the caller MUST reconcile against the refreshed object, not the stale snapshot it was dispatched with (see #102 and the dependsOn re-read comments at the call sites). Wait and re-read were two statements joined only by convention; fusing them means a future gate site can't call Require and silently forget the re-read.

func RunWithStatus

func RunWithStatus[T manifest.BaseManifest](
	ctx context.Context,
	s *store.Store,
	id manifest.NamedResource,
	log *slog.Logger,
	fn func(context.Context, T) error,
)

RunWithStatus is the standard reconcile body for controllers that (a) coalesce concurrent submits per-id and (b) want the recover → re-read → run → mark-Ready/Failed pattern. The re-read lets a coalesced re-run pick up patches a parent KS installed mid-flight rather than the stale payload from the original event. A missing object (deleted between coalescer enqueue and run) is treated as a no-op rather than a failure.

On success the terminal status write is conditional: if the current status already carries an informative Ready message (a soft-skip from --allow-missing-secrets, an MsgUnchanged from the change filter, an MsgSuspended from PreGate), the empty-message overwrite is suppressed so the informative message survives a short-circuited coalesced re-run that returns nil. Plain Ready (no message) and any non-Ready status get the standard "" Ready write so the controller's terminal contract is preserved.

func RunWithStatusOutcome added in v0.3.4

func RunWithStatusOutcome[T manifest.BaseManifest](
	ctx context.Context,
	s *store.Store,
	id manifest.NamedResource,
	log *slog.Logger,
	fn func(context.Context, T) error,
) []manifest.NamedResource

RunWithStatusOutcome is RunWithStatus that additionally reports the dag scheduler's outcome: the blocked dependency set, or nil when the body terminalized (Ready / Skipped / Failed). It intercepts a *depwait.ErrBlocked returned by the body BEFORE the generic Failed-status write, leaving the Pending status the body's Require wrote — so a blocked node stays non-Ready and re-runnable.

func WithDrainLevel added in v0.3.4

func WithDrainLevel(ctx context.Context, level int) context.Context

WithDrainLevel stamps the dag scheduler's current drain level onto ctx so the body's Require calls reach Classify with it. The orchestrator's Dispatcher sets it per-dispatch; absent ⇒ DrainNone (0).

Types

type Controller

type Controller struct {
	Store *store.Store
	Tasks *task.Service
	// contains filtered or unexported fields
}

Controller is the embeddable lifecycle harness. Construct via New, install reconcile-shaping config via SetFilter (panics if called after Start), then Start to register listeners.

All three concrete controllers carry the same lifecycle shape:

  • a started gate so Configure-after-Start is a hard error
  • a filter for changed-only mode
  • an unsubscriber list cleared by Close

Encoding it once means new pre-reconcile concerns (rate-limit, retries, debug-mode toggle) drop into one place and propagate.

The KS and HR controllers additionally share depwait configuration (Existence) and a pre-reconcile preflight check (Preflight, ParentOf). Configure these via SetDepwait / SetPreflight / SetParentOf before Start so reconcile bodies can call NewWaiter, PreflightError, and LookupParent without each controller duplicating the nil-check boilerplate.

func New

func New(s *store.Store, t *task.Service, name string) *Controller

New constructs a base controller with the given stable name (its controller-kind identity, surfaced in reconcile logs via Name()). Concrete controllers call this from their own constructor and embed the result.

func (*Controller) AddListener

func (c *Controller) AddListener(event store.EventKind, l store.Listener)

AddListener registers a store listener and records it so Close can unsubscribe. snapshot=true matches every concrete controller's needs (deliver the current store state on subscribe). Safe to call concurrently with Close — once Close has flipped the closed gate, any in-flight or later AddListener is refused and the registration is rolled back so the underlying store does not retain a listener the controller will never unsubscribe.

func (*Controller) Close

func (c *Controller) Close()

Close removes every registered listener and refuses any later AddListener so a late call from a shutdown-racing goroutine cannot leak a registration past us. Idempotent: a second Close is a no-op because the closed flag is set via Swap.

func (*Controller) Configure added in v0.3.4

func (c *Controller) Configure(opts Options)

Configure applies the common reconcile-shaping config. A concrete controller calls c.Controller.Configure(opts.Options) before assigning its own fields. The five setters are independent single-field writes, so their order is immaterial; each still panics if called after Start.

func (*Controller) Filter

func (c *Controller) Filter() *change.Filter

Filter returns the configured filter (may be nil-but-non-active).

func (*Controller) FingerprintDedup added in v0.3.4

func (c *Controller) FingerprintDedup(id manifest.NamedResource, fp string, emit func(docs []map[string]any)) (bool, error)

FingerprintDedup short-circuits a reconcile when id's cached rendered artifact carries a non-empty fingerprint equal to fp — the effective inputs are byte-identical, so the expensive render (kustomize/helm) is skipped. It still replays the cached docs through emit so the idempotent per-emission side-effects (keep-set extension + parent provenance) fire on every reconcile pass; emit is the controller's emitRenderedChildren(id, docs, publish=false) closure. The replay deliberately does NOT re-publish the children: they were already published byte-identically by the render that set this artifact, so re-AddObject-ing them would only re-fire listeners and re-submit already- settled resources — churn that can transiently un-Ready a parent and race quiescence (the "not ready" non-determinism, see #657–#660).

Returns (handled=true, err): the caller returns err. err is non-nil only when a preflight error was discovered mid-flight. Returns (false, nil) to render normally. Centralizes the byte-identical KS/HR dedup short-circuit.

func (*Controller) IsFileIndexed

func (c *Controller) IsFileIndexed(id manifest.NamedResource) bool

IsFileIndexed reports whether id is tracked by the file-existence index wired at Configure time. Returns false when no index is configured (offline / unit-test paths), which degrades safely by treating the resource as not-file-indexed.

func (*Controller) KeepEmitted

func (c *Controller) KeepEmitted(parent manifest.NamedResource, child manifest.BaseManifest)

KeepEmitted extends the change filter's keep set so render-emitted children pass the changed-only-mode PreGate check. Without this, a parent whose render emits a child that wasn't on disk at filter-build time (kustomize component+replacement KSes, charts that render source CRs) would silently drop that child from the diff comparison. Routed through Filter.AddEmitted so an ancestor-only parent doesn't cascade unrelated file-loaded children into keep (#204/#260/#308).

MUST be called BEFORE Store.AddObject so the listener that fires synchronously during AddObject sees the extended keep set.

func (*Controller) Logger added in v0.3.4

func (c *Controller) Logger() *slog.Logger

Logger returns the controller's slog logger, pre-bound with a controller=<name> attribute. Reconcile bodies (and the emit helper, via the controller they receive) log through it so the controller identity is a structured field rather than a per-call message prefix.

func (*Controller) LookupParent

LookupParent reports the structural parent KS of id via the configured resolver, or (zero, false) when no parent exists or no resolver was configured.

func (*Controller) Name added in v0.3.4

func (c *Controller) Name() string

Name returns the controller's stable identity (e.g. "helmrelease"). Set once at New.

func (*Controller) NewWaiter

func (c *Controller) NewWaiter(id manifest.NamedResource, timeout *metav1.Duration) *depwait.Waiter

NewWaiter constructs a depwait.Waiter pre-wired with the controller's Store and Existence lookup, parented to id and budgeted from timeout. HR and KS controllers call this rather than constructing their own Waiter literals so the Existence wiring is set once in Configure and flows through automatically.

func (*Controller) PreGate

func (c *Controller) PreGate(id manifest.NamedResource, suspended bool) bool

PreGate is the canonical Suspend/Filter pre-reconcile check. Returns true when the resource is gated out — caller MUST bail.

  • suspended → marks Ready "suspended", returns true
  • filter excludes the id → marks Ready "unchanged", returns true
  • otherwise → returns false, caller proceeds to Submit/reconcile

func (*Controller) PreflightError

func (c *Controller) PreflightError(id manifest.NamedResource) error

PreflightError returns an error wrapping the preflight failure message for id, or nil when no failure is recorded. Used at each yield point inside reconcile so a cycle detection or topology error published mid-flight aborts the current pass without waiting.

func (*Controller) PreflightFailure

func (c *Controller) PreflightFailure(id manifest.NamedResource) (string, bool)

PreflightFailure reports the pre-reconcile failure for id if the orchestrator detected a dependency-graph error. Returns ("", false) when no preflight check is configured or no failure was recorded.

func (*Controller) ReportRendered

func (c *Controller) ReportRendered(parent manifest.NamedResource, children []manifest.NamedResource)

ReportRendered reports parent→child render emissions to the configured RenderTracker; no-op when none is wired or there are no children. The emit loop accumulates every child it emits and flushes through this helper exactly once, holding the tracker's lock for one acquisition instead of N.

func (*Controller) Require added in v0.3.4

func (c *Controller) Require(
	ctx context.Context,
	id manifest.NamedResource,
	timeout *metav1.Duration,
	deps []manifest.DependencyRef,
	pendingMsg string,
	onFail func(depwait.Summary) error,
) error

Require gates the caller on deps WITHOUT blocking: it classifies each dep and returns one of:

  • nil — every dep satisfied; the body proceeds.
  • onFail(sum) (a *manifest.DependencyFailedError) — a dep is terminally Failed and none is still blockable; instant cascade, no grace.
  • *depwait.ErrBlocked — at least one dep is absent/Pending/ReadyExpr-pending; the body returns and the scheduler parks the node on those deps.

Blocked WINS over failed: if any dep is still producible (ClassBlocked) we park even when another dep already failed, discarding sum — the re-run re-derives the failure on a later pass once nothing is blocked. A failed-omittable ref is handled by the caller's onFail only once it is the sole remaining signal (see resolvePreRenderValuesFrom).

pendingMsg is written as an unconditional Pending status when non-empty (nothing when empty) so a dependent observing this node mid-gate sees a non-terminal status while it parks.

func (*Controller) SetDepwait

func (c *Controller) SetDepwait(existence depwait.ExistenceLookup)

SetDepwait installs the depwait resolution wires. Panics after Start.

func (*Controller) SetFilter

func (c *Controller) SetFilter(f *change.Filter)

SetFilter installs the change filter that gates reconciliation in changed-only mode. Panics if called after Start — the invariant is that reconcile-shaping config is frozen once dispatch begins.

func (*Controller) SetParentOf

func (c *Controller) SetParentOf(f func(manifest.NamedResource) (manifest.NamedResource, bool))

SetParentOf installs the structural parent resolver. Panics after Start.

func (*Controller) SetPendingUnlessReady added in v0.3.4

func (c *Controller) SetPendingUnlessReady(id manifest.NamedResource, msg string)

SetPendingUnlessReady writes a StatusPending progress message for id, UNLESS id is already StatusReady. A no-op re-reconcile (a parent render re-emitting the object with stamped ownership labels, a coalesced re-run) must not transiently downgrade Ready→Pending: a dependent's quiescence-bound depwait can re-read that transient Pending at a transient task-pool drain and give up ("not ready"), dropping the dependent nondeterministically.

Use for the progress writes that PRECEDE a reconcile's no-op (fingerprint / artifact) short-circuit. The genuine-work downgrade AFTER that check should stay an unconditional UpdateStatus so a real re-render re-gates dependents. Re-reading status per call is equivalent to capturing it once at reconcile entry: the coalescer serializes per-id, so nothing mutates an id's own status mid-reconcile. See #657 (kustomization) / #658 (source).

func (*Controller) SetPreflight

func (c *Controller) SetPreflight(f func(manifest.NamedResource) (string, bool))

SetPreflight installs the pre-reconcile failure reporter. Panics after Start.

func (*Controller) SetRenderTracker

func (c *Controller) SetRenderTracker(rt RenderTracker)

SetRenderTracker installs the render-emission tracker. Panics after Start — reconcile-shaping config is frozen once dispatch begins.

func (*Controller) StartLifecycle

func (c *Controller) StartLifecycle()

StartLifecycle flips the started gate, freezing reconcile-shaping config. Concrete controllers call this from their Start(ctx) before installing any listeners via AddListener.

type Options added in v0.3.4

type Options struct {
	// Filter narrows reconciliation to changed resources in changed-only mode.
	Filter *change.Filter
	// ParentOf maps a resource to its structural-parent Kustomization so
	// reconcile waits for the parent's Ready before rendering — any
	// parent-render-time spec mutation (replacements injecting
	// targetNamespace, postBuild substitutions, a re-emitted patched spec) is
	// then observable. Nil disables parent enforcement.
	ParentOf func(manifest.NamedResource) (manifest.NamedResource, bool)
	// RenderTracker receives every reconcilable child a render emits, feeding
	// the orchestrator's parent-provenance index (orphan detection, parent
	// resolution, attribution).
	RenderTracker RenderTracker
	// Existence is the file-existence lookup depwait uses to lazy-promote
	// file-indexed deps and to distinguish a render-only dep still in flight
	// from a typo'd one. See depwait.ExistenceLookup.
	Existence depwait.ExistenceLookup
	// PreflightFailure reports dependency-graph errors discovered before
	// reconcile; when set for an id the controller marks it Failed and renders
	// nothing.
	PreflightFailure func(manifest.NamedResource) (string, bool)
}

Options is the post-bootstrap, reconcile-shaping config common to every render controller (Kustomization, HelmRelease, ResourceSet). The orchestrator wires it once before Start; the source controller is fetch-only and configures just its filter directly, so it does NOT embed this. A concrete controller embeds Options in its own option struct and forwards it to Configure, then assigns its controller-specific fields.

type RenderTracker

type RenderTracker interface {
	MarkRenderedBatch(parent manifest.NamedResource, children []manifest.NamedResource)
}

RenderTracker is the seam a controller uses to report "this child id was emitted by THIS parent's render" to the orchestrator. Nil is OK — the controller no-ops.

The parent linkage feeds detectOrphans, the structural-parent resolver, and ResourceSet extension attribution for render-emitted resources. Both the KS and HR controllers report through it identically; MarkRenderedBatch records multiple children under a single lock acquisition so a render emitting N children pays one tracker round-trip rather than N.

Jump to

Keyboard shortcuts

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