Skip to content

feat(gitops): observe prints the refusals a cell records against a delivered workload - #1294

Merged
antoinetoussaint-byte merged 1 commit into
mainfrom
feat/observe-rollout-admission-refusals
Nov 4, 2026
Merged

antoinetoussaint-byte merged 1 commit into
mainfrom
feat/observe-rollout-admission-refusals

Conversation

@antoinetoussaint-byte

@antoinetoussaint-byte antoinetoussaint-byte commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Problem

After the promotion PR merges and Argo CD syncs the Blue cell, a pod refused by the cell's ValidatingAdmissionPolicy is visible only in kubectl describe rs events (no approved execution matches this pod…, a container of this pod declares an exec lifecycle hook or an exec probe…). Argo CD reports the application Degraded/Progressing with no reason, and codefly deploy gitops observe waited its whole budget to say health observation on Argo CD timed out: … — or signal: killed when the budget ran out during an argocd app get.

Seam

codefly deploy gitops observe, not a new publish --wait or doctor cell --live:

  • observe is already the verb run after a publish, already polls the publication's Argo CD applications until Synced + Healthy (which is each delivered workload rolled out), already has the bounded wait (--timeout, default 10 min) and already reads the application's status.resources — the list of workloads the publication delivered. The gap is only that, while an application is not Healthy, nothing reads why.
  • deploy gitops publish ends when the promotion pull request is open; the rollout happens after a review and a merge it does not wait for, so a --wait there would be a second observe. doctor judges declarations, not a live cluster (feat(doctor): judge what a cell would enforce, not just a solution's shape #1273 judges what a cell would enforce).
  • The CLI has no client-go; every cluster call is kubectl through exec. pkg/control.ObserveGitOps already selects the environment, so it resolves the cluster there with deployments.GetK8sConfig and env.Cluster.Context — the same resolution every other cluster call of the environment uses — and hands them to the observation.

Change

  • pkg/gitops/observe_workloads.go: a workloadObserver that, for the applications a sweep found pending, reads each namespace's events (and, for a Deployment, its ReplicaSets, to attribute an event on one to the Deployment that owns it) through kubectl, keeps a FailedCreate — or a Warning that is an admission denial — recorded against a delivered workload since its application's sync began (operationState.startedAt; an older event is a previous rollout's, a ReplicaSet kept in the Deployment's history or a Job re-synced under the same name; an event whose time cannot be read is kept rather than hidden), and prints each distinct refusal once, verbatim, naming the workload (payments/Deployment/api FailedCreate: Error creating: pods "api-…" is forbidden: ValidatingAdmissionPolicy 'cell-admission' … denied request: no approved execution matches this pod).
  • pkg/gitops/observe.go: the polling loop moves to awaitApplications (unchanged in what it awaits; it also brought Observe under the gocyclo ceiling). After a sweep that is not all Healthy it inspects the pending applications; an admission denial — the denial's own wording, denied request, not the is forbidden every 403 carries — ends the observation non-zero at once, because no retry creates that pod. Any other FailedCreate (a quota exceeded, a service account the admission cache has not seen yet: forbidden too, but retried past) is printed and the wait continues. The timeout message now adds what it was still waiting on (Argo CD's per-resource health and message), every refusal seen, and whether the cluster could not be read; the budget running out during an argocd call is reported as that timeout rather than signal: killed.
  • ObserveRequest gains Kubeconfig, KubeContext and Output; the command passes stdout; pkg/control.ObserveGitOps fills the cluster from the environment when the caller did not.
  • docs/commands.md: what observe prints and when it stops.

Classification: fix, at the place that owns post-publish observation. Reused: argocd app get's status.resources for the delivered workloads, the existing --timeout budget and poll loop, the command exec helper and its PATH-fake test seam, deployments.GetK8sConfig and the environment's declared cluster.context. Deliberately not reused: pkg/deployments.completionObserver — it observes a deploy the CLI applied itself against a verified k3d target and reads workload status, not controller events; what is missing here is the events, and importing it into pkg/gitops would bring the verified-target machinery along for nothing. No --wait was added to publish.

Review

Independent fresh-context review at 4cbac8b found two blockers, both fixed in this head with a test each: forbidden made retried refusals (quota, service-account cache lag) terminal, and events of a previous rollout (an old ReplicaSet in the Deployment's history) were attributed to the current one.

Tests

pkg/gitops/observe_workloads_test.go, with a fake argocd answering a sequence of application states and a fake kubectl on PATH (the repo's "fake clientset") answering get events / get replicasets and logging its arguments:

  • an admission denial on the Deployment's ReplicaSet ends the observation, the error and stdout carry the message verbatim exactly once, and kubectl was called with the environment's --kubeconfig and --context;
  • a quota FailedCreate (Kubernetes' real is forbidden: exceeded quota: wording) is printed exactly once across three pending sweeps and the observation still succeeds once Argo CD turns Healthy;
  • a denial recorded against the previous publication's ReplicaSet, before this sync began, neither ends nor is printed by this observation; a denial whose event carries no readable time is kept;
  • the timeout names the workload it waited on with Argo CD's message and the refusal seen;
  • StatefulSet and Job events are attributed to their workloads and an event on a workload the publication did not deliver is ignored;
  • a Healthy application never consults kubectl (the existing healthy-evidence test is unchanged);
  • an unreadable cluster is reported once, kubectl is not retried, and the timeout says the events were unreadable.

These do not compile on main (Output, Kubeconfig, KubeContext did not exist) and the timeout/denial assertions fail against the previous loop by construction. go test ./pkg/gitops/... ./pkg/control/... ./cmd/..., golangci-lint run on the touched packages (0 issues) and TestCLIReferenceIsCurrent (no flag changed) pass.

Not verified

No live cluster: the exact shape of the Blue cell's FailedCreate messages is taken from the observation in the task (kubectl describe rs) and from Kubernetes' pods "x" is forbidden: ValidatingAdmissionPolicy … denied request: wrapping; a denial that is worded without denied would be printed but not end the wait early. A real codefly deploy gitops observe … --env blue against the cell was not run.

Would file (not opened)

  • Argo CD's per-resource health.message is also where a Progressing Deployment says 0 of 1 updated replicas are available; a follow-up could read the Deployment's conditions (ProgressDeadlineExceeded) to end the wait earlier on a rollout that will not finish for reasons other than admission.

🤖 Generated with Claude Code

@antoinetoussaint-byte
antoinetoussaint-byte force-pushed the feat/observe-rollout-admission-refusals branch from 4cbac8b to 4c585ae Compare November 4, 2026 04:50
…livered workload

After a promotion merges and Argo CD syncs, a pod the cell's
ValidatingAdmissionPolicy refuses is never created. Argo CD reports the
application Progressing or Degraded and says nothing of why; the why is a
FailedCreate event on the workload's controller (the Deployment's
ReplicaSet, the StatefulSet, the DaemonSet, the Job), visible only in
`kubectl describe rs`. `codefly deploy gitops observe` waited its whole
budget and ended with "health observation on Argo CD timed out" — or,
when the budget ran out during an `argocd app get`, with "signal: killed".

`observe` now reads, while an application is still Progressing or
Degraded, the events of the delivered workloads' controllers through the
cluster the environment declares (its kubeconfig and context, resolved
the way every other cluster call of the environment resolves them), and
prints every FailedCreate verbatim, once, as it is first seen, naming the
workload. An admission denial ends the observation at once, non-zero: no
retry of the controller creates that pod. Any other FailedCreate is
printed and the wait goes on; the timeout then names the workloads it
was still waiting on, with Argo CD's health message, and every refusal
seen. A cluster that cannot be read is reported once and the Argo CD
health wait continues as before. The budget running out mid-call is now
reported as the timeout and what it waited on, not as the killed call.

The polling loop moves to awaitApplications, unchanged in what it awaits.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@antoinetoussaint-byte
antoinetoussaint-byte force-pushed the feat/observe-rollout-admission-refusals branch from 4c585ae to 45f935b Compare November 4, 2026 04:57
@antoinetoussaint-byte
antoinetoussaint-byte added this pull request to the merge queue Nov 4, 2026
Merged via the queue into main with commit ac2888a Nov 4, 2026
19 checks passed
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.

1 participant