Status conventions
Every Omnia CRD reports its health the same way. One piece of tooling, whether a
dashboard, a kubectl wait or a script, then works across all of them without
knowing each kind’s private vocabulary.
Read Ready, and nothing else
Section titled “Read Ready, and nothing else”Every CRD that carries status.conditions sets a Ready condition. It is
the roll-up: True means the resource is usable, False means it is not.
kubectl wait --for=condition=Ready promptpack/my-pack --timeout=60sEvery such CRD also exposes it as a print column:
$ kubectl get providersNAME READY TYPE MODELanthropic True claude claude-sonnet-5openai False openai gpt-4oSub-conditions (SourceValid, CredentialConfigured, ToolsDiscovered and
others) explain why Ready holds the value it does. They help with diagnosis but
make a poor health check: they vary per kind, and a resource can carry a False
sub-condition while remaining usable.
Ready is written on every path that persists status, including early returns.
A roll-up refreshed only on success goes stale when a reconcile fails early, and
a stale Ready=True on a resource that failed validation reports it as
healthy.
The well-known condition types
Section titled “The well-known condition types”Omnia uses the kstatus/Flux vocabulary instead of inventing its own, so generic
tooling behaves as expected. The four shared types are declared once, in
api/v1alpha1/conditions.go:
Readyis the roll-up.Truemeans usable.ReconcilingisTruewhile the controller is still working toward the observed generation. It differs fromReady=False, which means settled and broken.Degradedhas negative polarity.Truemeans the resource is serving, but not as specified.Stalledhas negative polarity.Truemeans progress has stopped and a requeue will not help; only a spec change will.
Ready and Degraded answer different questions. A ToolRegistry serving three
of five tools is Ready=True, Degraded=True: usable, but incomplete.
status.phase cannot express that because it is a single word.
flowchart LR Reconciler[Reconciler] -->|SetConditionWithEvent| Conds[status.conditions] Reconciler -->|on transition| Events[Kubernetes Events] Conds --> Ready[Ready roll-up] Conds --> Sub[Sub-conditions<br/>SourceValid, CredentialConfigured] Conds --> Neg[Negative-polarity<br/>Degraded, Stalled, ContentDrift] Ready --> Col[READY print column] Ready --> Wait[kubectl wait]The reconciler writes every condition and its event in one call. Consumers read
Ready for health and the other conditions for the reason.
Polarity: True is not always good
Section titled “Polarity: True is not always good”Six condition types mean trouble when True:
DegradedStalledContentDrift, a PromptPack whose ConfigMap was edited under the published digestSuperseded, a newer version has been published on the channelDuplicateVersion, a PromptPack that lost an equal-precedence tiebreakInsecureEgressTestHosts, a SandboxClaim whose egress reaches into the cluster
A condition is only a type string plus True, False or Unknown, so nothing
in the data says which value is the bad one. Anything that derives a colour, an
icon or an event type from a condition must consult the registry:
if v1alpha1.IsNegativePolarity(cond.Type) { /* True is the bad state */ }import { isNegativePolarity } from "@/types/conditions";The TypeScript list mirrors the Go one, and TestConditionVocabularyMatchesTypeScript
compares them. Adding a negative-polarity type in Go without the TypeScript
counterpart fails go test ./....
Neither ContentDrift nor Superseded gates Ready. A superseded pack still
serves, and drift is loud but does not break the pack. Folding them in would mark
every superseded pack as failed.
Unknown is not a failure
Section titled “Unknown is not a failure”Unknown means the state has not been determined yet. A transient probe failure
must not take a Provider out of service, so controllers do not fail closed on it.
Consumers should render it as a warning and never as an error.
Phase and conditions, together
Section titled “Phase and conditions, together”Most CRDs also carry status.phase. The kinds use many different words, with
three synonyms for healthy (Ready, Active, Running) and two for broken
(Error, Failed). Colouring a UI from phase therefore needs a lookup table per
kind, and that is how a codebase ends up with divergent status renderers.
The convention splits the work. Ready means one thing everywhere, so it decides
whether a resource is healthy or broken. phase carries the kind-specific
vocabulary, such as Fetching, that a five-value enum would flatten, so it
supplies the label. An ArenaProject mid-fetch reads “Fetching”, coloured by
Ready. Phase serves display only and should never stand in for a health check.
Staleness
Section titled “Staleness”Every condition carries observedGeneration, the metadata.generation it was
computed against. When it is behind, the condition describes a spec the user has
already replaced:
condition.observedGeneration < metadata.generation -> staleWithout it, a stale condition reads as authoritative forever. SetCondition and
SetConditionWithEvent set it on every write, so a controller never has to
remember.
Conditions and events share one call
Section titled “Conditions and events share one call”A condition transition records its own Kubernetes event from the same call:
SetConditionWithEvent(r.Recorder, obj, &obj.Status.Conditions, obj.Generation, v1alpha1.ConditionReady, metav1.ConditionFalse, "CredentialInvalid", err.Error())The event’s reason is the condition’s reason, by construction. Setting a condition and emitting an event as separate steps leaves two surfaces to maintain independently, and they drift apart.
Three properties keep this usable on a real cluster:
- Events fire only on a transition, which is a change of
statusorreason. Reconcilers run constantly, and an unchanged condition does not re-emit. - A change of message alone is silent. Messages carry counts, durations and wrapped errors, so keying on them would emit events for a resource whose state never moved.
- Event type follows polarity. A condition arriving at its bad state is a
Warningand at its good state aNormal, so a drifted PromptPack does not record trouble as routine.
A nil recorder degrades to a plain SetCondition, so a reconciler without one
still writes correct conditions but emits no events. SetCondition remains for
internal conditions not worth an event.
Trade-offs
Section titled “Trade-offs”A single roll-up gives up detail. Consumers that want to know why must read sub-conditions, whose names differ by kind, and the convention accepts that cost to keep the health check uniform.
Keeping polarity in a registry rather than in the condition itself means each
consumer must consult it, and the Go and TypeScript copies must agree. The
alternative, renaming negative conditions so True is always good (NotDegraded),
would break the kstatus names that generic tooling already understands.
Related
Section titled “Related”- AgentRuntime reference lists its phases and conditions.
- PromptPack reference lists its phases and conditions.