feat(gitops): show ArgoCD ApplicationSets in the fleet view - #1609
Conversation
The GitOps fleet view only ever fetched Applications, Kustomizations, and HelmReleases as table rows — ApplicationSet was a documented "Supported CRD" with working detail-page routing, but never appeared as a row of its own, only implicitly via the Applications it generates. Union ApplicationSet rows into the same Applications-mode table (same pattern already used for Kustomization/HelmRelease coexisting there), behind a new Kind filter (Applications / ApplicationSets) that only appears once a cluster actually has ApplicationSets. ApplicationSet has no sync/health of its own in the sense the table shows for Applications — its own status.conditions use a different vocabulary (ErrorOccurred, ParametersGenerated, ResourcesUpToDate, never Ready) — so it correctly falls through the existing generic status mapper to Unknown/Unknown rather than us inventing a fleet-health rollup that doesn't exist.
PR Summary by QodoShow Argo CD ApplicationSets in the GitOps fleet view
AI Description
Diagram
High-Level Assessment
Files changed (3)
|
Code Review by Qodo
1. Application-only actions exposed
|
| tool: 'argo', | ||
| kindName: 'applicationsets', | ||
| kind: 'ApplicationSet', |
There was a problem hiding this comment.
1. Application-only actions exposed 🐞 Bug ≡ Correctness
ApplicationSet rows are marked only as tool: 'argo', so the shared row menu exposes Sync, Refresh, Hard refresh, Suspend/Resume, and Terminate actions intended for Applications. The OSS handler sends those operations to Application mutation endpoints using the ApplicationSet's namespace and name, causing failed requests or potentially targeting a same-named Application.
Agent Prompt
## Issue description
ApplicationSet rows inherit the complete Argo Application action menu because action selection checks only `row.tool`. These operations use Application-specific mutations and are invalid for ApplicationSets.
## Issue Context
The detail page already distinguishes Applications using `kind === 'applications'`. Apply the same distinction to table row actions, returning no Application mutation actions for ApplicationSets unless dedicated ApplicationSet operations are implemented.
## Fix Focus Areas
- packages/k8s-ui/src/components/gitops/GitOpsTableView.tsx[1408-1479]
- packages/k8s-ui/src/components/gitops/GitOpsTableView.tsx[1986-1990]
- web/src/components/gitops/GitOpsView.tsx[262-296]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| {kindCounts.ApplicationSet > 0 && ( | ||
| <GitOpsFilterSection icon={Layers} title="Kind"> | ||
| <GitOpsFacetButton label="Applications" count={kindCounts.Application} active={kindFilters.has('Application')} onClick={() => onToggleKind('Application')} /> | ||
| <GitOpsFacetButton label="ApplicationSets" count={kindCounts.ApplicationSet} active={kindFilters.has('ApplicationSet')} onClick={() => onToggleKind('ApplicationSet')} /> | ||
| </GitOpsFilterSection> |
There was a problem hiding this comment.
2. Active kind filter disappears 🐞 Bug ≡ Correctness
The Kind section is hidden whenever the current rows contain no ApplicationSets, even though a persisted kind URL filter remains active and continues filtering Applications. After switching cluster or namespace with kind=ApplicationSet, users can therefore see Applications disappear without any visible facet indicating or directly disabling the responsible filter.
Agent Prompt
## Issue description
A persisted kind filter remains effective when the Kind facet is hidden because the current data has no ApplicationSets. This silently removes Argo Application rows after cluster, namespace, or URL changes.
## Issue Context
Render the Kind section whenever ApplicationSets exist or `kindFilters` is non-empty. Alternatively, clear or validate the kind filter when ApplicationSets are unavailable, while avoiding URL-state changes during transient loading.
## Fix Focus Areas
- packages/k8s-ui/src/components/gitops/GitOpsTableView.tsx[345-347]
- packages/k8s-ui/src/components/gitops/GitOpsTableView.tsx[484-487]
- packages/k8s-ui/src/components/gitops/GitOpsTableView.tsx[971-976]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| lastSync: '', | ||
| autoSync: false, | ||
| terminating: isTerminating(resource), |
There was a problem hiding this comment.
3. Applicationsets counted as manual 🐞 Bug ≡ Correctness
normalizeArgoApplicationSet assigns autoSync: false, causing every ApplicationSet to inflate the Manual automation count and pass the Manual sync-policy filter. ApplicationSets do not themselves have the Application sync policy represented by this facet, so the count and filtered results are incorrect.
Agent Prompt
## Issue description
ApplicationSets are represented as manually synced because the required boolean `autoSync` field is set to false. This makes an inapplicable policy appear as a real Manual policy in counts and filtering.
## Issue Context
Exclude ApplicationSet rows from automation counts and predicates, or make automation applicability explicit in the row model rather than encoding “not applicable” as false. Preserve existing behavior for Applications and Flux workloads.
## Fix Focus Areas
- packages/k8s-ui/src/components/gitops/GitOpsTableView.tsx[427-431]
- packages/k8s-ui/src/components/gitops/GitOpsTableView.tsx[477-481]
- packages/k8s-ui/src/components/gitops/GitOpsTableView.tsx[2012-2014]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit d5e23fd. Configure here.
| <GitOpsFacetButton label="ApplicationSets" count={kindCounts.ApplicationSet} active={kindFilters.has('ApplicationSet')} onClick={() => onToggleKind('ApplicationSet')} /> | ||
| </GitOpsFilterSection> | ||
| )} | ||
|
|
There was a problem hiding this comment.
ApplicationSet rows expose Application actions
Medium Severity
ApplicationSet rows set tool to argo, so the table action menu offers Sync, Refresh, Hard refresh, and Suspend. Those mutations call the Application APIs with the ApplicationSet name. The detail page already gates the same actions with kind === 'applications'. A same-named Application in that namespace can be mutated by mistake; otherwise the request fails.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit d5e23fd. Configure here.
…1628) ## Summary Two places in the GitOps fleet code treat "this row is an Argo object" as "this row is an Argo Application". That holds today only because Applications are the only Argo kind that becomes a row. It stops holding the moment a second one does, and #1609 adds ApplicationSets. ### Row actions were gated on the tool, not the kind `buildRowActionItems` offered Sync / Refresh / Hard refresh / Suspend to any row with `tool === 'argo'`. All four post to `/argo/applications/{namespace}/{name}`, and the handler resolves that name against the Application GVR. So on a non-Application Argo row those buttons either 404, or — when an Application happens to share the name, which Kubernetes permits since they are different resource types — quietly mutate that unrelated Application and report success. Gated on `kindName === 'applications'` instead, which is how the detail page already gates the same actions (`isArgoApp`). `RowActionMenu` now renders nothing for an empty item list, so a kind with no actions gets no dead three-dot trigger. ### ApplicationSet status went through the Flux condition reader `getGitOpsResourceStatus` special-cased `applications` and sent everything else to `fluxConditionsToGitOpsStatus`, which looks for `Ready` / `Reconciling` / `Stalled`. An ApplicationSet reports through `ErrorOccurred` / `ParametersGenerated` / `ResourcesUpToDate` and never sets `Ready`, so every ApplicationSet bottomed out at Unknown — a generator failing outright read exactly like a healthy one. This is live today on the ApplicationSet detail page, independent of #1609. Added a reader for those conditions. Sync stays Unknown in all cases: Sync answers "is the declared state applied", and an ApplicationSet applies nothing — it generates Applications, each carrying its own sync state. ## Test plan - 6 unit tests on the new mapper covering failed / healthy / no-status, that Sync stays Unknown either way, and that `ErrorOccurred=False` is not treated as a fault - `packages/k8s-ui`: 3387 passed - `web`: 569 passed, `tsc` clean, lint 0 errors - Verified against a live `kind` cluster (Argo CD 2.13.2, the `scripts/gitops-demo.sh` fixtures plus an ApplicationSet with an unreachable git generator), built with #1609 merged on top: - before: both ApplicationSets Unknown/Unknown; Suspend on one of them suspended a same-named Application and returned 200 - after: the failing generator reads Degraded and sorts to the top, the healthy one reads Healthy, Sync stays Unknown, and neither row offers an action menu The action gate is a no-op on `main` as it stands, since Applications are the only Argo rows today. The status reader is not — it fixes the ApplicationSet detail page now. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes how GitOps row mutations are offered (prevents mistaken Application API calls on non-Application Argo rows) and how ApplicationSet status is shown in detail/list surfaces; UI-only but affects operator triage and action safety. > > **Overview** > **GitOps fleet table** now limits Argo **Sync / Refresh / Suspend** (and related) actions to rows with `kindName === 'applications'`, matching the detail page. Other Argo kinds (e.g. ApplicationSets) no longer get menus that POST to `/argo/applications/...` and could 404 or hit a same-named Application. Flux reconcile actions are explicitly limited to `tool === 'flux'`. **`RowActionMenu`** renders nothing when there are no items, so rows without actions do not show a useless ⋮ control. > > **ApplicationSet health/sync** is no longer derived via the Flux `Ready` condition path. **`getGitOpsResourceStatus`** routes `applicationsets` through a new **`argoApplicationSetConditionsToGitOpsStatus`** mapper (`ErrorOccurred`, `ResourcesUpToDate`, etc.): failed generators show **Degraded** with the controller message, healthy generators **Healthy**, and **sync stays Unknown** because ApplicationSets do not apply cluster state themselves. Unit tests cover these cases. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 37d9fa5. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
Follow-up to #1609. Several fields still described an ApplicationSet using values that only mean something for an Application. Source, project and destination came from a git generator or were left blank. A git generator's repo is scanned to produce parameters and is often not a deployment source; what the generated Applications deploy is on spec.template.spec. Read the template first and fall back to the generator. Template values can carry per-app placeholders ({{path}}, {{server}}), so a value is surfaced only when it is literal - rendering the raw placeholder is worse than rendering nothing. Last Sync fell through to the creation timestamp, dating the object rather than reporting activity on it. It now carries the transition time of the condition the status was read from. The Automation (sync policy) facet counted every ApplicationSet as Manual, answering 'what is not auto-syncing' with objects that never sync. Skip the kind in the counts and the predicate. describeArgoAutoSync had the same problem server-side: an ApplicationSet's spec.syncPolicy governs how generated Applications are created and deleted and never carries 'automated', so it reported Manual to the detail header, the insights summary and the rollback gating alike. Fixed there rather than at one call site. The Kind section rendered only while ApplicationSets were present, but the filter applied regardless, so scoping to a namespace without any hid every Application with no facet on screen to explain it. Keep the section while a kind filter is set. noOtherFiltersActive did not account for it either, leaving the Total tile reading as unfiltered. An empty destination means in-cluster for an Application, Argo's default when unset. For an ApplicationSet it means the template was templated away, so the detail header no longer makes that claim. Source fields resolve as a unit rather than independently. A literal template repo paired with a git generator's path named a tree that existed in neither, which reads as a real location and is not one. The destination namespace no longer falls back to the row's own namespace for an ApplicationSet. That fallback suits a resource sitting beside what it deploys; an ApplicationSet sits in the controller namespace and generates Applications that land elsewhere.
Follow-up to #1609. Several fields still described an ApplicationSet using values that only mean something for an Application. Source, project and destination came from a git generator or were left blank. A git generator's repo is scanned to produce parameters and is often not a deployment source; what the generated Applications deploy is on spec.template.spec. Read the template first and fall back to the generator. Template values can carry per-app placeholders ({{path}}, {{server}}), so a value is surfaced only when it is literal - rendering the raw placeholder is worse than rendering nothing. Last Sync fell through to the creation timestamp, dating the object rather than reporting activity on it. It now carries the transition time of the condition the status was read from. The Automation (sync policy) facet counted every ApplicationSet as Manual, answering 'what is not auto-syncing' with objects that never sync. Skip the kind in the counts and the predicate. describeArgoAutoSync had the same problem server-side: an ApplicationSet's spec.syncPolicy governs how generated Applications are created and deleted and never carries 'automated', so it reported Manual to the detail header, the insights summary and the rollback gating alike. Fixed there rather than at one call site. The Kind section rendered only while ApplicationSets were present, but the filter applied regardless, so scoping to a namespace without any hid every Application with no facet on screen to explain it. Keep the section while a kind filter is set. noOtherFiltersActive did not account for it either, leaving the Total tile reading as unfiltered. An empty destination means in-cluster for an Application, Argo's default when unset. For an ApplicationSet it means the template was templated away, so the detail header no longer makes that claim. Source fields resolve as a unit rather than independently. A literal template repo paired with a git generator's path named a tree that existed in neither, which reads as a real location and is not one. The destination namespace no longer falls back to the row's own namespace for an ApplicationSet. That fallback suits a resource sitting beside what it deploys; an ApplicationSet sits in the controller namespace and generates Applications that land elsewhere.
Follow-up to #1609. Several fields still described an ApplicationSet using values that only mean something for an Application. Source, project and destination came from a git generator or were left blank. A git generator's repo is scanned to produce parameters and is often not a deployment source; what the generated Applications deploy is on spec.template.spec. Read the template first and fall back to the generator. Template values can carry per-app placeholders ({{path}}, {{server}}), so a value is surfaced only when it is literal - rendering the raw placeholder is worse than rendering nothing. Last Sync fell through to the creation timestamp, dating the object rather than reporting activity on it. It now carries the transition time of the condition the status was read from. The Automation (sync policy) facet counted every ApplicationSet as Manual, answering 'what is not auto-syncing' with objects that never sync. Skip the kind in the counts and the predicate. describeArgoAutoSync had the same problem server-side: an ApplicationSet's spec.syncPolicy governs how generated Applications are created and deleted and never carries 'automated', so it reported Manual to the detail header, the insights summary and the rollback gating alike. Fixed there rather than at one call site. The Kind section rendered only while ApplicationSets were present, but the filter applied regardless, so scoping to a namespace without any hid every Application with no facet on screen to explain it. Keep the section while a kind filter is set. noOtherFiltersActive did not account for it either, leaving the Total tile reading as unfiltered. An empty destination means in-cluster for an Application, Argo's default when unset. For an ApplicationSet it means the template was templated away, so the detail header no longer makes that claim. Source fields resolve as a unit rather than independently. A literal template repo paired with a git generator's path named a tree that existed in neither, which reads as a real location and is not one. The destination namespace no longer falls back to the row's own namespace for an ApplicationSet. That fallback suits a resource sitting beside what it deploys; an ApplicationSet sits in the controller namespace and generates Applications that land elsewhere. Documents the Kind filter in docs/gitops.md, which lists the fleet view's filters and had not been updated when the facet shipped, and rewrites the filter's own comment to describe what it does rather than why its scope was drawn where it was.
…1634) ## Summary Follow-up to #1609. Several fields still described an ApplicationSet using values that only mean something for an Application. Verified against a live `kind` cluster running Argo CD 2.13.2. ### Source, project and destination These were read from a git generator, or left blank. A git generator's repo is scanned to produce parameters and is often not a deployment source at all; what the generated Applications deploy is on `spec.template.spec`. The template is now read first, with the generator kept as a fallback only for templates that declare no source of their own. Repo, revision and path resolve as a unit rather than independently. Mixing a literal template repo with a generator's path names a tree that exists in neither, which reads as a real location and is not one. The branch turns on whether the template *declares* a source, not on whether that source *resolved*, so a template whose `repoURL` is itself a placeholder still claims the row and renders blank. Template values can carry per-app placeholders (`{{path}}`, `{{server}}`), so a value is surfaced only when it is literal. Rendering the raw placeholder is worse than rendering nothing, and the table already shows a dash for an empty destination. The destination namespace no longer falls back to the row's own namespace for an ApplicationSet. That fallback suits a resource sitting beside what it deploys; an ApplicationSet sits in the controller namespace and generates Applications that land elsewhere, so it named a namespace the object never deploys to. ### Last Sync Empty, so the column fell through to the creation timestamp and dated the object rather than reporting activity on it. It now carries the transition time of the condition the status was read from. ### Sync policy The Automation facet counted every ApplicationSet as Manual, answering "what is not auto-syncing" with objects that never sync. The kind is now skipped in the counts and the predicate. `describeArgoAutoSync` had the same problem server-side. An ApplicationSet's `spec.syncPolicy` governs how generated Applications are created and deleted and never carries `automated`, so it reported `Manual` to the detail header, the insights summary and the rollback gating alike. Fixed there rather than at one call site, since all three read the same field. ### Kind facet The section rendered only while ApplicationSets were present, but the filter applied regardless. Scoping to a namespace without any hid every Application and left an empty table with no facet on screen to explain it or switch it off. The section now stays while a `kind` filter is set. `noOtherFiltersActive` did not account for the filter either, so the Total tile still read as unfiltered. ### Destination on the detail page An empty destination means in-cluster for an Application, which is Argo's default when the field is unset. For an ApplicationSet it means the template's destination was a generator placeholder, so the header no longer makes that claim. ## Test plan - 21 unit tests on the normalizer: template vs generator precedence, source resolved as a unit, declared-but-templated sources, placeholder blanking, literal globs, condition timestamps, the sync-policy predicate and the destination-namespace label - 2 Go tests on `describeArgoAutoSync` covering ApplicationSet shapes and pinning that Applications are unaffected - `packages/k8s-ui` 3408 passed, `web` 569 passed, `tsc` clean, lint 0 errors, `pkg` and backend suites pass - Live cluster with eight ApplicationSets covering list, cluster, matrix, mixed-source, templated-fields, templated-repo, templated-namespace and failing-git generators: - source, project and path resolve from the template; no `{{...}}` renders anywhere - a template that declares a source never borrows the generator's repo or path - a cluster generator's `{{server}}` destination shows as a namespace rather than a false "in-cluster" - a templated destination namespace shows nothing rather than the controller namespace - Last Sync reports condition times, not creation age - Automation counts cover only the Applications - the Kind section stays visible and unsettable when scoped to a namespace with no ApplicationSets - Applications and Flux resources are unchanged: counts, filters, row actions and detail header all as before ## Not included Multi-source (`spec.sources`) is unrendered for Applications too, so a sources-only ApplicationSet template reads blank rather than being handled ahead of the rest of the view. "Total Applications" counts every row, which has always included Flux Kustomizations and HelmReleases. The label predates this and is a naming decision rather than a bug here. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Display and normalization fixes for ApplicationSet rows with broad test coverage; Argo Applications and Flux behavior are intentionally unchanged. > > **Overview** > **ApplicationSet fleet rows** now derive source, project, destination, and “last activity” from `spec.template.spec` (with git-generator fallback only when the template declares no source), resolve repo/revision/path as a single unit, and hide `{{…}}` placeholders instead of showing misleading literals. > > **Automation and detail metadata** stop treating ApplicationSets like syncable Applications: `hasSyncPolicy` excludes them from Automation counts/filters, `destinationNamespaceLabel` avoids wrongly using the controller namespace, the **Kind** facet stays visible while a kind filter is active (and **Total** respects that filter), and `describeArgoAutoSync` returns empty for ApplicationSets so the detail header/insights no longer show **Manual**. > > **ApplicationSet detail** no longer implies in-cluster when the template destination server is empty; namespace-only display is used when appropriate. Docs note the Argo-only **Kind** filter. Covered by new Vitest and Go tests. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 47e8fa1. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->


Summary
The GitOps fleet view only ever fetched Applications, Kustomizations, and HelmReleases as table rows — ApplicationSet was already a documented "Supported CRD" with working detail-page routing, but never appeared as a row of its own, only implicitly through the Applications it generates.
This unions ApplicationSet rows into the same Applications-mode table (the same pattern already used for Kustomization/HelmRelease coexisting there), behind a new Kind filter (Applications / ApplicationSets) that only appears once a cluster actually has ApplicationSets.
ApplicationSet has no sync/health of its own in the sense the table shows for Applications — its
status.conditionsuse a different vocabulary (ErrorOccurred,ParametersGenerated,ResourcesUpToDate, neverReady) — so it correctly falls through the existing generic status mapper to Unknown/Unknown rather than inventing a fleet-health rollup that doesn't exist for this kind.Test plan
Note
Low Risk
Read-only list fetch and UI filtering/normalization; no auth or mutation paths changed beyond including another CRD in the existing GitOps query.
Overview
Argo CD ApplicationSets now appear as first-class rows in the GitOps fleet table alongside Applications, Kustomizations, and HelmReleases. The web layer fetches
applicationsetswhen the CRD is present, maps them through a newnormalizeArgoApplicationSethelper (template destination, best-effort git-generator source hints, empty project/lastSync/autoSync; sync/health stay Unknown via existing status mapping), and wires detail normalization the same way.The shared
GitOpsTableViewadds a URL-backed Kind facet (Applications / ApplicationSets) that only renders when at least one ApplicationSet exists. The filter narrows Argo Application/ApplicationSet rows only and leaves Flux kinds untouched.normalizeArgoApplicationSetis re-exported from@skyhook-io/k8s-ui.Reviewed by Cursor Bugbot for commit d5e23fd. Bugbot is set up for automated code reviews on this repo. Configure here.