Skip to content

Add kstatus functionality to CRD statuses - #585

Open
zainawaisn wants to merge 5 commits into
mainfrom
zain/kstatus
Open

Add kstatus functionality to CRD statuses#585
zainawaisn wants to merge 5 commits into
mainfrom
zain/kstatus

Conversation

@zainawaisn

@zainawaisn zainawaisn commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

What was changed

The controller can now tell deployment tools the difference between a rollout that
is still working and one that has failed and will never finish.

  • WorkerDeployment and WorkerResourceTemplate now set two extra status
    conditions, Stalled and Reconciling. These are the exact names
    kstatus
    matches on — the library behind Helm 4's --wait and Flux's health checks. It
    never reads our existing Progressing condition, which is why we could not just
    keep using that one.
  • Both resources also now record which version of your spec they last looked at,
    even when they stopped because of an error. kstatus checks that before it reads
    any condition, so without it the two new conditions are ignored entirely and the
    resource just reports "still working".
  • Only failures that cannot fix themselves are reported as failed: invalid spec,
    or a connection type this controller is not set up to read. A missing Connection
    or missing credentials still report "still working", because when you install
    everything at once those can genuinely be absent for a second or two through no
    fault of yours. Failing a deploy that was about to succeed is worse than one that
    takes a while.
  • Ready and Progressing are unchanged, so existing dashboards and alerts keep
    working.
  • Connection, ClusterConnection and the two deprecated resource types are left
    exactly as they are.

One extra change: When you point a WorkerDeployment at a different Connection, the controller has to take its "in use" tag off the old one, or that old Connection can never be deleted. It used to skip that cleanup whenever the spec looked unchanged since the last run. Recording the spec version on error paths makes the spec look unchanged in exactly the case where the cleanup is needed, so the check now runs every time.

Why?

Resolves #478.

Our resources did not follow the conventions kstatus expects, so nothing downstream could tell a broken rollout from a slow one. Argo users had to write a custom Lua script to read our conditions, and Helm and Flux had to wait out the full timeout on a rollout that does not work.

kstatus reads five things: metadata.deletionTimestamp, status.observedGeneration, and the conditions Reconciling, Stalled and Ready. Everything this PR adds is one of those five.

Open Questions

Three implementation decisions. Open to changing these.

  1. Four of the six resource types were left alone on purpose. Connection and
    ClusterConnection have no controller behind them, so there is nothing to wait
    for. kstatus reports them as current the moment they exist, the same treatment
    it gives ConfigMap and Secret. A bad connection is still reported, on the
    WorkerDeployment that uses it.

  2. The two deprecated resource types were also left alone. Never reporting ready for these types
    is deliberate and already documented. kstatus reads them as "still working" for
    as long as they exist. That is fine in practice because the documented migration
    deletes them as it goes, and kstatus reports a resource being deleted as
    terminating instead.

  3. A missing Connection or missing credentials count as "still working", not
    "failed".
    This matches what the controller does today.

Checklist

  1. Closes Ensure that all CRD statuses are compatible with kstatus (used by Argo and Helm) #478

  2. How was this tested:

New unit tests and new integration tests, both of which run the real kstatus
library over our resources and assert the verdict it reaches.

Integration tests run against a real API server and a real
Temporal server, then read each resource back before handing it to kstatus.

Covered: every rollout stage, a failure that can fix itself, a failure that cannot,
recovery afterwards, deletion, a spec edit the controller has not seen yet, and all
six resource types.

  1. Any docs updates needed?

Done, in this PR:

  • docs/cd-rollouts.md: described two conditions, now four. Explains that Ready
    and Progressing are the ones to read yourself while Stalled and Reconciling
    exist for kstatus.
  • docs/migration-crd-rename.md: one note that the old resource types read as
    not-ready to condition-based health checks until you finish migrating.

Note: the suggested Argo health check script in cd-rollouts.md changed. It now reads Stalled and Reconciling instead of Progressing, so ArgoCD agrees with Helm and Flux about what counts as a failure. The old version would show a resource as broken while it was waiting for its Connection to appear. Anyone still using the old script keeps the old behaviour.

zainawaisn and others added 5 commits September 10, 2026 00:31
…neration

Adds the API surface needed for kstatus (sigs.k8s.io/cli-utils) to tell a
failed rollout from a slow one. No behaviour change; nothing sets these yet.

The condition names are the exact literals kstatus string-matches when
assessing a custom resource, which is why they belong in the API package
alongside Ready and Progressing rather than in the controller.

Stalled and Reconciling must never both be True on the same object. kstatus
scans status.conditions in array order and returns on the first match, so an
object carrying both would get a verdict decided by insertion order — which
could differ across controller restarts. The constants document this; the
controller enforces it by removing one whenever it sets the other.

WorkerResourceTemplateStatus gains a top-level observedGeneration. kstatus
compares it against metadata.generation before it reads any condition, so
without it a WRT could never report "I have not seen your latest edit yet".
The per-condition observedGeneration the type already carried is not read by
kstatus; only the top-level field is.

Refs #478

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A blocked rollout previously reported Ready=False and Progressing=False.
kstatus does not read Progressing, so it fell back to Ready=False and
concluded "still working" — meaning a rollout that could never succeed looked
identical to a slow one, and Helm --wait or Flux would sit until timeout
instead of failing with a reason.

WorkerDeployment and WorkerResourceTemplate now set Stalled=True on terminal
failures and Reconciling=True while work is genuinely in flight, with exactly
one of the two ever set. Successful reconciles remove both, so a resource that
recovers stops reporting Failed — meta.SetStatusCondition only upserts, so
without an explicit removal Stalled would have been permanent.

A failure is treated as terminal only when it is decidable from information
already in hand, which for WorkerDeployment means InvalidSpec and
ClusterConnectionUnsupported (see stalledReasons). Deliberately excluded are
failures waiting on another object to exist — a missing Connection or
credential Secret — because applying a WorkerDeployment alongside those in one
release gives no ordering guarantee, so a missing reference is often a normal
few-second gap rather than a mistake, and a false failure costs more than a
slow one. Transient infrastructure failures are excluded for the same reason.
WorkerResourceTemplate makes the same split by API error kind, since it has a
single reason for every apply failure (see isTerminalWorkerResourceError).
The trade-off is that a typo'd connectionRef still hangs to timeout, exactly
as before this change; a grace period would fix that and is left as follow-up.

Both types also advance status.observedGeneration on blocked paths. Most error
paths return before the point where it was previously set, so editing a
working resource into a broken one left it stale — and kstatus checks it
before any condition, so it would have masked the new conditions entirely in
the most common failure case.

That last change required removing the generation gate on the connection
finalizer release. Once blocked reconciles advance observedGeneration, the
gate stops firing, and a connectionRef repointed at a Connection that does not
exist yet would leave our finalizer on the old Connection forever, making it
undeletable. The inner ObservedConnectionRef comparison was always the real
test; the gate only saved two string compares. Note this specific path has no
direct test — existing tests call releaseConnectionFinalizerIfUnused directly
rather than driving it through Reconcile.

The WorkerResourceTemplate status write also no longer skips unconditionally
when every apply was a no-op. A spec edit that renders byte-identically bumps
metadata.generation without changing any hash — reordering keys in
spec.template does exactly that, since ComputeRenderedObjectHash marshals a
map and Go sorts map keys — which would have left observedGeneration behind
permanently. The refreshing Get is cache-served and the write still only
happens when something changed, so the optimisation is preserved.

Refs #478

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds sigs.k8s.io/cli-utils as a test-only dependency and asserts what kstatus
concludes about each of the six CRD kinds, since that is what Helm --wait and
Flux will conclude. Objects are converted to unstructured and passed to the
real status.Compute, reproducing how those tools read our resources off the
wire rather than trusting a hand-written expectation.

Unit coverage (internal/controller/kstatus_test.go):
  - WorkerDeployment across every rollout state, both blocking classes, the
    recovery path, and deletion
  - WorkerResourceTemplate across applied, terminal failure, transient
    failure, an unobserved spec edit, and a missing WorkerDeployment
  - the classification invariants: Stalled and Reconciling are never both
    True, and a successful reconcile clears both
  - isTerminalWorkerResourceError against real apierrors constructors
  - Connection and ClusterConnection (Current — configuration only, no
    controller), and both deprecated migration stubs (InProgress, plus
    Terminating once marked for deletion, which is what keeps the documented
    migration from stalling a release)

Integration coverage (internal/tests/internal/kstatus_integration_test.go)
runs the same verdicts against envtest and a real Temporal server, reading
each object back from the API server. Only this level can show that Stalled,
Reconciling and observedGeneration survive the CRD's OpenAPI schema and the
status subresource round trip — Stalled and Reconciling are new condition
types, and every unit test would still pass if the schema pruned them.

The deprecated-stub cases drive the real DeprecatedTWDReconciler and
DeprecatedTCReconciler rather than hand-building conditions, so they assert
what those stubs actually write.

Each exempt kind carries a comment explaining why its verdict is what it is,
so the tests double as the record of that decision: if someone later adds a
Ready condition to Connection, or makes a migration stub report ready, a test
fails and points at the reasoning.

Refs #478

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
cd-rollouts.md described two conditions; there are now four. Documents the two
pairs and why both exist: Ready and Progressing describe the rollout in the
controller's own terms and carry the diagnostic detail, while Stalled and
Reconciling say the same thing in the vocabulary kstatus reads.

States which failures are terminal and which are not, and the reasoning, so
the trade-off is discoverable: a typo'd connectionRef reports in-progress
until your tool's timeout rather than failing immediately, because a missing
reference cannot be told apart from a normal ordering gap during a deploy.
Readers are pointed at the reason field and Events to see what is blocking.

The recommended ArgoCD health check now reads Stalled and Reconciling instead
of Progressing. This is a user-visible behaviour change: the previous script
mapped Progressing=False to Degraded, which after this work would show
Degraded for a Connection that is a second away from existing. Anyone still
running the old script keeps the old semantics. Also notes the script can be
registered for WorkerResourceTemplate, which emits the same conditions.

Adds a note that Connection and ClusterConnection are configuration only —
no controller, no conditions, so condition-based tools treat them as healthy
as soon as they exist, as Kubernetes does for ConfigMap and Secret. Broken
connections are still reported, on the referencing WorkerDeployment.

migration-crd-rename.md notes that the deprecated kinds read as not-ready to
condition-based health checks for as long as they exist, and that a resource
already marked for deletion reports Terminating instead — so following the
documented migration steps does not leave a release waiting.

Refs #478

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The kstatus integration tests added in the previous commit import
sigs.k8s.io/cli-utils, which the tests module's go.mod did not declare. It
still built, because CI and `make test-integration` construct a go.work
workspace whose build list supplies the dependency from the root module.

`make tidy` runs `GOWORK=off go mod tidy` per module, so without this the
linters workflow's tidy job leaves the tree dirty and fails.

Refs #478

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@zainawaisn
zainawaisn requested review from a team, eniko-dif and jlegrone as code owners September 10, 2026 17:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Ensure that all CRD statuses are compatible with kstatus (used by Argo and Helm)

1 participant