Skip to content

feat: let projects read their own ArgoCD sync status - #16

Merged
hakan-persson merged 1 commit into
mainfrom
feat/project-argocd-status
Aug 23, 2026
Merged

feat: let projects read their own ArgoCD sync status#16
hakan-persson merged 1 commit into
mainfrom
feat/project-argocd-status

Conversation

@hakan-persson

Copy link
Copy Markdown
Member

Answers a question from the design presentation: can a project see the sync status of its own project? Today, no — verified by impersonation before this branch: a project developer gets no for get, list and watch on applications.argoproj.io.

Chosen approach is Kubernetes RBAC, not an exposed ArgoCD UI. The reasoning is in decisions.md entry 14; the short version is that the UI costs a new public endpoint plus a second authorisation model (argocd-rbac-cm is empty today and the break-glass admin is unrestricted), while the RBAC route reuses the identity and tooling projects already have.

What it adds

An opt-in per-project Role in argocd, scoped with resourceNames to that project's own Application(s), bound to its GitHub team. Verbs are get and watch only — a project can watch a sync, but cannot start or abort one.

Verified live on the test cluster (then cleaned up)

Check Result
Reads its own app's sync/health/revision ✅ yes
Another project's app ✅ Forbidden
An infra app (monitoring) ✅ Forbidden
patch / delete on its own app ✅ no
A different team reading this project's app ✅ no

Server-side dry-run on the rendered template passed; no test resources remain in the cluster.

Known limitation — deliberate, and documented

kubectl get app -n argocd with no name is Forbidden, and the Application does not appear in Headlamp's list view. Kubernetes RBAC cannot filter a collection, so resourceNames permits get on named objects but does not restrict list. A client-side label selector does not help — the request is refused before filtering. Granting unscoped list would expose every project's Application to every project, so developers name theirs instead.

This answers "is my thing synced and healthy?" and gives the failure reason via operationState. It does not give a diff view or a sync button; if projects ask for those, that is the concrete argument for the UI, and this per-project resourceNames work is reusable if it is built.

Two load-bearing wiring changes

  • project-infra permitted RoleBinding but not Role — the Role would have been rejected with "not permitted in project". The Role is per-project rather than a shared ClusterRole because it must carry resourceNames.
  • The projectset include-glob syncs an explicit filename list; a file matching nothing is ignored silently while ArgoCD reports Synced/Healthy.

Two unrelated defects fixed while editing

  • install.md still carried the "sync":{} patch that fix(docs): close four runbook gaps found onboarding a project database live #4 fixed in argocd.md. syncOp.SyncOptions has no fallback to spec.syncPolicy.syncOptions, so that recipe silently drops CreateNamespace=true.
  • install.md suggested ArgoCD's bundled Dex if a dashboard is ever added. This cluster runs a standalone Dex, so that would mean a second identity provider for the same GitHub org.

Also corrects the §A file count (6 → 7), which this branch's new template file made stale.

Docs

decisions entry 14, onboarding §D, cross-links from argocd.md and install.md.

Projects could see the effects of a sync in their own namespaces, but not
whether ArgoCD was failing to apply something — a stalled sync looks exactly
like "nothing has happened yet". Verified by impersonation beforehand: a
project developer got `no` for get, list and watch on applications.argoproj.io.

Adds an opt-in per-project Role in `argocd`, scoped with resourceNames to that
project's own Application(s) and bound to its GitHub team. Verbs are get and
watch only, so a project can watch a sync but cannot start or abort one.

Verified live on the test cluster, then cleaned up: reads its own app; another
project's app, an infra app, and patch/delete on its own app are all Forbidden;
a different team is refused.

Known limitation, recorded in decisions.md: `kubectl get app -n argocd` with no
name is Forbidden, and the Application does not appear in Headlamp's list view.
Kubernetes RBAC cannot filter a collection, so resourceNames permits get on
named objects but does not restrict list. Granting unscoped list would expose
every project's Application to every project.

Two wiring changes this depends on, both load-bearing:
- project-infra permitted RoleBinding but not Role, so the Role would have been
  rejected with "not permitted in project".
- the projectset include-glob syncs an explicit filename list; a file matching
  nothing is ignored silently while ArgoCD reports Synced/Healthy.

Also fixes two unrelated defects found while editing:
- install.md still carried the `"sync":{}` patch that PR #4 fixed in argocd.md.
  syncOp.SyncOptions has no fallback to spec.syncPolicy.syncOptions, so that
  recipe silently drops CreateNamespace=true.
- install.md suggested ArgoCD's bundled Dex if a dashboard is ever added; this
  cluster runs a standalone Dex, so that would mean a second identity provider
  for the same GitHub org.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR grants each project opt-in, read-only visibility into its own ArgoCD Application sync/health status via Kubernetes RBAC (a per-project Role+RoleBinding in the argocd namespace scoped by resourceNames), rather than by exposing an ArgoCD UI. It fits the repo's "everything in Git / identity-reuse" model by binding the project's existing GitHub team to get/watch on its own named Applications. It also does the required wiring (permitting Role in the project-infra AppProject and adding the new filename to the projectset include-glob), records the rationale as decision 14, and fixes two unrelated install.md defects (the sync:{} patch dropping syncOptions, and the bundled-Dex suggestion).

Changes:

  • New opt-in per-project Role/RoleBinding template scoping get/watch to a project's own Applications, plus onboarding §D and decision 14 documenting it and its list-is-Forbidden limitation.
  • Wiring: whitelist Role in project-infra AppProject and add argocd-status-rbac.yaml to the projectset include-glob; correct the onboarding file count (6 → 7).
  • install.md fixes: carry syncOptions through the hand-sync patch, and point a hypothetical dashboard at the standalone Dex instead of ArgoCD's bundled Dex.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated no comments.

Show a summary per file
File Description
k8s/projects/_template/infra/argocd-status-rbac.yaml.example New opt-in Role+RoleBinding template granting a project read-only access to its own Applications.
k8s/argocd/projects/project-infra.yaml Whitelists Role so the per-project status Role can sync under the project-infra AppProject.
k8s/argocd/projects-root/projectset.yaml Adds argocd-status-rbac.yaml to the include-glob so the new file is actually synced.
docs/onboarding.md Adds §D usage guide and updates the template file count (6 → 7).
docs/install.md Fixes the hand-sync patch to carry syncOptions; cross-links the new RBAC route; corrects Dex guidance.
docs/decisions.md Records decision 14 (RBAC over UI) with its rationale and limitations.
docs/argocd.md Adds an infra-access caveat and a routing-table row for the new capability.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@hakan-persson
hakan-persson merged commit d786537 into main Aug 23, 2026
6 checks passed
@hakan-persson
hakan-persson deleted the feat/project-argocd-status branch August 23, 2026 06:36
hakan-persson added a commit that referenced this pull request Aug 24, 2026
The README already states that MinIO is not general-purpose storage (#13),
so the decision entry restated a merged conclusion. It now records only the
reasoning a decision log is for: why the derived-data backup assumption is
what actually blocks it, the two unsolved blockers, and the revisit trigger.

Also resolves two rebase conflicts. The projectset include-glob kept BOTH
filenames — argocd-status-rbac.yaml from #16 and app-state-rbac.yaml here;
dropping either would make that feature inert while ArgoCD still reports
Synced. And the section A file count is now 8, not 7.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
hakan-persson added a commit that referenced this pull request Aug 26, 2026
Answers two questions from projects — *"where do I put a few KB of
state?"* and *"what about a few MB of files?"* — and records why MinIO
is not the answer to either.

**Not urgent.** Opened to marinate while test round 4 is prepared; happy
for it to sit.

## Why this is worth merging even though it is mostly documentation

`main` currently makes a promise it does not keep. The README tells
developers:

> A project needing persistent state uses the shared PostgreSQL or a
PersistentVolumeClaim — see onboarding.md.

That section **does not exist**. PR #13 merged the pointer without the
destination, because the destination was on this branch. `azurefile`
appeared nowhere in the repo before this.

## What it adds

| | |
|---|---|
| New capability | ConfigMap-as-state (template + the include-glob line)
|
| Newly *available* | `files-shared` StorageClass — the
shared-filesystem tier |
| Newly documented | the four tiers, their limits, and their **cost
model** |
| Newly recorded | why not MinIO, and what a project object store would
cost |

## Two things that came out of review and changed the design

**1. `azurefile-csi` was an Azure-specific name leaking into every
project's repo.** Pointing projects at the AKS built-in class would have
put the provider's name in every PVC manifest, so a migration would mean
editing every project's repository — exactly what *"portable by intent"*
exists to prevent, and contrary to the Portability table's own rule
(*"change the provisioner; keep the class names"*).

This branch now ships **`files-shared`**, a repo-owned class following
the same pattern as `disk-*`. Verified live: bound RWX, and a non-root
PSA-compliant pod wrote and read through it. The Portability table gains
a row, and the docs **offer** the tier rather than pushing it — the
class name survives a move, the SMB semantics may not.

**2. Azure Files bills per operation, and the cluster is paid
centrally.** Prices read from the Azure retail API for `swedencentral`,
not estimated:

| Write pattern | `files-shared` | `disk-*` (E4, 32Gi) |
|---|---|---|
| Checkpoint every 30s | $0.56/mo | $2.40/mo |
| Session state, 5 req/s | **$84.24/mo** | $2.40/mo |

A fixed 32Gi disk costs the same as **~370,000 operations a month** —
about 0.14 writes/second. So it is the cheapest tier for *state* and
among the most expensive for *traffic*.

That matters more here than the numbers suggest: **a project never sees
the cost of its own choice**, so the guidance has to do what the invoice
does not. The onboarding note now asks *how often* state is written
rather than how big it is, and names the two patterns explicitly.

It is also the strongest argument for a project object store later — its
cost is the PVC underneath it, so S3 calls are in-cluster and free at
the margin, and it puts one reviewable number on the shared bill instead
of a variable per-project one nobody watches.

## The load-bearing non-doc change

The `projectset.yaml` include-glob gains `app-state-rbac.yaml`. The
`project-infra` ApplicationSet syncs an **explicit list of filenames**,
and a file matching none of them is **ignored silently** while ArgoCD
still reports `Synced`/`Healthy`. Without it, infra commits the RBAC,
sees green, and the app has no permissions.

It also keeps `argocd-status-rbac.yaml` from #16 — dropping either
filename makes that feature inert.

## Verified

- Rebased onto current `main` (`d786537`), two conflicts resolved
deliberately.
- Decision entries **15** and **16**, in numerical order; `main`'s own
`entry 11`/`entry 12` references left pointing where they were.
- The RBAC template and the StorageClass both **tested live** on the
test cluster in both namespaces, then removed.
- All five CI checks pass locally — 78 manifests, 48 decision pointers.

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.

2 participants