Agreed scope from #412 with @mesutoezdil. Argo CD gets the same read side Flux already has: curated Application columns, an :argocd view, a count in :fleet, and an ownership tree behind a keypress. Everything comes from the Application CRD, no Argo API or token.
Three PRs, in this order, each mergeable on its own with tests and docs. The closed drafts #518, #519, #520 already follow this split, so rebasing them onto this issue is the expected path.
Ground rules
- CRD only. Read
argoproj.io/v1alpha1 Applications. Never the Argo CD API, a token, or Argo's cluster Secrets.
- Same shape as Flux. Pure module
src/argocd.rs (unit tested without a cluster) plus a driver src/app/argocd.rs, mirroring gitops.rs / app/gitops.rs. The view reuses explain::Finding, Level, Target and ui::draw_findings. No new rendering code.
- Reads only. Sync and suspend stay on
t.
- Tests through
handle_key for every user-visible behaviour. Cluster::fake() already registers argoproj.io/applications.
PR 1: Application columns and :argocd view
Columns
columns_for("argoproj.io", "applications") returns a curated set, six columns like Flux:
| Column |
Source |
NAME |
|
SYNC |
status.sync.status |
HEALTH |
status.health.status |
REVISION |
status.sync.revision, falling back to status.operationState.syncResult.revision |
AUTO-SYNC |
three states, see below |
AGE |
|
REVISION shortens a full 40-char SHA to 7. Chart versions and branch names stay as they are.
AUTO-SYNC is three states, not a boolean:
on: spec.syncPolicy.automated is present
manual: no automated block and no stash annotation. Normal config, not a fault.
suspended: no automated block and sofka.io/argocd-automated is set (what t → Suspend writes, see app/helpers.rs)
SYNC / HEALTH sort as text, the way FLUX_OBJECT_COLUMNS READY does.
- ApplicationSets keep default columns.
:argocd view
Palette names argocd and argo. Keymap mode argocd with the same bindings as gitops: enter accept, esc back, q close, j/k, g/G, r refresh.
Opens from two places:
- An Application. Re-read it like
spawn_gitops does, then render.
- Anything Argo manages. Resolve the owning Application from tracking metadata and render it, with the headline stating the relationship (
Deployment/web is managed by Application/guestbook).
Owner resolution:
argocd.argoproj.io/tracking-id annotation first (<app>:<group>/<kind>:<ns>/<name>). Only Argo writes it. If it names an Application that doesn't exist, that's a warning (dangling reference).
- Instance label second (
app.kubernetes.io/instance, then argocd.argoproj.io/instance). Helm stamps the same label on everything it installs, so a label with no matching Application renders as "not managed by Argo CD", not as an error.
- Argo writes
<namespace>_<name> for Applications outside its own namespace, bare <name> otherwise. _ is illegal in both, so the split is exact.
- Look up by
metadata.name field selector across namespaces, not a full list. Clusters hold thousands of multi-KiB Applications. If several match, pick the one whose status.resources[] lists the object.
Findings, top to bottom:
- Headline:
Application/<name>: <sync> / <health>. Level from the pair: Synced+Healthy Good, Progressing Info, OutOfSync Warn, Degraded/Missing/Unknown Critical.
- Application: project, destination (see below) and namespace, auto-sync state, deployed revision.
- Source: repo URL, path or chart,
targetRevision. Multi-source Applications show the first source.
- Managed resources: one line per
status.resources[] entry with its own sync and health. Capped at 50 with a remainder line. Each line carries a Target resolved through the kind registry by kind and group, a Knative Service must not open the core one. Application entries (app-of-apps) jump to that Application, where :argocd opens its view again.
- Blocking: what stops this Application from being Synced and Healthy, most serious first, or one line saying nothing does:
- suspended by sofka (stash annotation present)
status.operationState.phase Failed / Error with its message
- conditions by priority:
ComparisonError, InvalidSpecError, SyncError, UnknownError, then SharedResourceWarning, OrphanedResourceWarning, RepeatedResourceWarning
Degraded / Missing resources by name
OutOfSync resources by name
- Degraded with nothing above naming a cause: say so. Argo rolls health up from live state it doesn't publish per resource.
Destination
spec.destination.server (or .name) is resolved against the kubeconfig:
https://kubernetes.default.svc (with or without :443), or a URL equal to the connected Cluster.cluster_url, is the current cluster.
- Otherwise match against every kubeconfig cluster's
server and show the context name. Needs a small helper next to k8s::context_info, which today resolves one context at a time.
- Unresolved: show the raw server URL.
Only a current destination yields jump targets. On a remote destination enter flashes where the object lives instead of opening a same-named object here. This reads the user's kubeconfig, never Argo's cluster Secrets.
Shared with :gitops
Request-generation guard so only the latest report updates findings. Closing the view, refreshing, changing source or context cancels pending results. A missing Application is a warning, a failed read stays an error. Never render a denied read as "not found".
Tests
src/argocd.rs: accessors, three auto-sync states, blocking precedence, annotation vs label, dangling vs unmanaged, group-aware targets, remote destination yields no targets, SHA shortening, the cap.
src/app/tests.rs through handle_key: open on an Application, open on a managed Deployment, enter jumps to a managed resource, enter on a remote line doesn't navigate, r re-gathers, esc cancels.
columns.rs cell tests.
Docs
docs/keys.md row, docs/features.md (GitOps and Helm section plus the palette command list), ? help in src/ui.rs, docs/keybindings.md (new argocd mode table).
PR 2: Argo CD count in :fleet
- New
FleetRow.argocd_degraded: Option<usize>. None when the cluster has no argoproj.io Application kind, rendered argo — like flux —.
- Counts Applications that are
OutOfSync, or whose health is Degraded, Missing or Unknown. Progressing is transient and not counted. Empty status is a freshly created Application, not a fault.
- Pages with
limit / continue (500 per page) and keeps only the count. list_or_warn materialises the whole list, which at thousands of multi-KiB Applications across concurrent contexts is tens of MiB. Leave the Flux path alone in this PR.
- A failed or timed-out list is
None plus a warn entry so the row goes to FleetStatus::Error, not a confident zero.
is_healthy() treats a non-zero count as unhealthy. draw_fleet gets an argo cell next to flux.
- Tests: fleet row healthy / unhealthy with and without the CRD, paging count over a fake page sequence.
- Docs:
docs/providers.md#fleet-dashboard, docs/features.md.
PR 3: c expands what a managed resource owns
- In
:argocd, c (discover_children) on a managed-resource line with a current-cluster target walks ownerReferences down: Deployment → ReplicaSet → Pod, CronJob → Job → Pod, StatefulSet / DaemonSet / Job → Pod. c again collapses.
- Reuse
adjacent. Child kinds come from adjacent::children_for (BUILTIN_CHILDREN plus [views."…"].children), matching with adjacent::owned_by. No second hard-coded child list. Depth capped at 2. Cost is one GET for the parent UID plus one LIST per child kind in the namespace, reused at every depth.
- Drop ReplicaSets scaled to zero that own no pods. With the default
revisionHistoryLimit the live one is otherwise buried under ten dead ones.
- Child rows are indented findings with their own
Target and a one-word state: pod phase or waiting reason (CrashLoopBackOff), ReplicaSet ready/desired, Job Complete / Failed. Level follows the state.
- Behind a keypress, not on open. A failed LIST is a warning line, not an empty tree.
r collapses everything. Expansion results are request-guarded like the main report.
- Tests through
handle_key: expand shows the pod under the ReplicaSet, a dead ReplicaSet is hidden, c twice collapses, c on a remote or heading line flashes and does nothing.
- Docs:
docs/keys.md, docs/features.md, docs/keybindings.md (c in argocd mode), ? help in src/ui.rs.
Out of scope
ApplicationSet columns or views, diff, sync options, rollback, Argo CD API access, reading Argo cluster Secrets, a new config section. Separate discussions if wanted.
Open questions
- Fleet count: include
Progressing or not. Spec says no.
PROJECT column in the table or view only. Spec says view only, keeps the table at six columns like Flux.
Discussion: #412
Agreed scope from #412 with @mesutoezdil. Argo CD gets the same read side Flux already has: curated Application columns, an
:argocdview, a count in:fleet, and an ownership tree behind a keypress. Everything comes from the Application CRD, no Argo API or token.Three PRs, in this order, each mergeable on its own with tests and docs. The closed drafts #518, #519, #520 already follow this split, so rebasing them onto this issue is the expected path.
Ground rules
argoproj.io/v1alpha1Applications. Never the Argo CD API, a token, or Argo's cluster Secrets.src/argocd.rs(unit tested without a cluster) plus a driversrc/app/argocd.rs, mirroringgitops.rs/app/gitops.rs. The view reusesexplain::Finding,Level,Targetandui::draw_findings. No new rendering code.t.handle_keyfor every user-visible behaviour.Cluster::fake()already registersargoproj.io/applications.PR 1: Application columns and
:argocdviewColumns
columns_for("argoproj.io", "applications")returns a curated set, six columns like Flux:NAMESYNCstatus.sync.statusHEALTHstatus.health.statusREVISIONstatus.sync.revision, falling back tostatus.operationState.syncResult.revisionAUTO-SYNCAGEREVISIONshortens a full 40-char SHA to 7. Chart versions and branch names stay as they are.AUTO-SYNCis three states, not a boolean:on:spec.syncPolicy.automatedis presentmanual: noautomatedblock and no stash annotation. Normal config, not a fault.suspended: noautomatedblock andsofka.io/argocd-automatedis set (whatt→ Suspend writes, seeapp/helpers.rs)SYNC/HEALTHsort as text, the wayFLUX_OBJECT_COLUMNSREADYdoes.:argocdviewPalette names
argocdandargo. Keymap modeargocdwith the same bindings asgitops:enteraccept,escback,qclose,j/k,g/G,rrefresh.Opens from two places:
spawn_gitopsdoes, then render.Deployment/web is managed by Application/guestbook).Owner resolution:
argocd.argoproj.io/tracking-idannotation first (<app>:<group>/<kind>:<ns>/<name>). Only Argo writes it. If it names an Application that doesn't exist, that's a warning (dangling reference).app.kubernetes.io/instance, thenargocd.argoproj.io/instance). Helm stamps the same label on everything it installs, so a label with no matching Application renders as "not managed by Argo CD", not as an error.<namespace>_<name>for Applications outside its own namespace, bare<name>otherwise._is illegal in both, so the split is exact.metadata.namefield selector across namespaces, not a full list. Clusters hold thousands of multi-KiB Applications. If several match, pick the one whosestatus.resources[]lists the object.Findings, top to bottom:
Application/<name>: <sync> / <health>. Level from the pair:Synced+HealthyGood,ProgressingInfo,OutOfSyncWarn,Degraded/Missing/UnknownCritical.targetRevision. Multi-source Applications show the first source.status.resources[]entry with its own sync and health. Capped at 50 with a remainder line. Each line carries aTargetresolved through the kind registry by kind and group, a KnativeServicemust not open the core one. Application entries (app-of-apps) jump to that Application, where:argocdopens its view again.status.operationState.phaseFailed/Errorwith its messageComparisonError,InvalidSpecError,SyncError,UnknownError, thenSharedResourceWarning,OrphanedResourceWarning,RepeatedResourceWarningDegraded/Missingresources by nameOutOfSyncresources by nameDestination
spec.destination.server(or.name) is resolved against the kubeconfig:https://kubernetes.default.svc(with or without:443), or a URL equal to the connectedCluster.cluster_url, is the current cluster.serverand show the context name. Needs a small helper next tok8s::context_info, which today resolves one context at a time.Only a current destination yields jump targets. On a remote destination
enterflashes where the object lives instead of opening a same-named object here. This reads the user's kubeconfig, never Argo's cluster Secrets.Shared with
:gitopsRequest-generation guard so only the latest report updates findings. Closing the view, refreshing, changing source or context cancels pending results. A missing Application is a warning, a failed read stays an error. Never render a denied read as "not found".
Tests
src/argocd.rs: accessors, three auto-sync states, blocking precedence, annotation vs label, dangling vs unmanaged, group-aware targets, remote destination yields no targets, SHA shortening, the cap.src/app/tests.rsthroughhandle_key: open on an Application, open on a managed Deployment,enterjumps to a managed resource,enteron a remote line doesn't navigate,rre-gathers,esccancels.columns.rscell tests.Docs
docs/keys.mdrow,docs/features.md(GitOps and Helm section plus the palette command list),?help insrc/ui.rs,docs/keybindings.md(newargocdmode table).PR 2: Argo CD count in
:fleetFleetRow.argocd_degraded: Option<usize>.Nonewhen the cluster has noargoproj.ioApplication kind, renderedargo —likeflux —.OutOfSync, or whose health isDegraded,MissingorUnknown.Progressingis transient and not counted. Empty status is a freshly created Application, not a fault.limit/continue(500 per page) and keeps only the count.list_or_warnmaterialises the whole list, which at thousands of multi-KiB Applications across concurrent contexts is tens of MiB. Leave the Flux path alone in this PR.Noneplus awarnentry so the row goes toFleetStatus::Error, not a confident zero.is_healthy()treats a non-zero count as unhealthy.draw_fleetgets anargocell next toflux.docs/providers.md#fleet-dashboard,docs/features.md.PR 3:
cexpands what a managed resource owns:argocd,c(discover_children) on a managed-resource line with a current-cluster target walksownerReferencesdown: Deployment → ReplicaSet → Pod, CronJob → Job → Pod, StatefulSet / DaemonSet / Job → Pod.cagain collapses.adjacent. Child kinds come fromadjacent::children_for(BUILTIN_CHILDRENplus[views."…"].children), matching withadjacent::owned_by. No second hard-coded child list. Depth capped at 2. Cost is one GET for the parent UID plus one LIST per child kind in the namespace, reused at every depth.revisionHistoryLimitthe live one is otherwise buried under ten dead ones.Targetand a one-word state: pod phase or waiting reason (CrashLoopBackOff), ReplicaSetready/desired, JobComplete/Failed. Level follows the state.rcollapses everything. Expansion results are request-guarded like the main report.handle_key: expand shows the pod under the ReplicaSet, a dead ReplicaSet is hidden,ctwice collapses,con a remote or heading line flashes and does nothing.docs/keys.md,docs/features.md,docs/keybindings.md(cinargocdmode),?help insrc/ui.rs.Out of scope
ApplicationSet columns or views, diff, sync options, rollback, Argo CD API access, reading Argo cluster Secrets, a new config section. Separate discussions if wanted.
Open questions
Progressingor not. Spec says no.PROJECTcolumn in the table or view only. Spec says view only, keeps the table at six columns like Flux.Discussion: #412