feat: let projects read their own ArgoCD sync status - #16
Conversation
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>
There was a problem hiding this comment.
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/RoleBindingtemplate scopingget/watchto a project's own Applications, plus onboarding §D and decision 14 documenting it and itslist-is-Forbidden limitation. - Wiring: whitelist
Roleinproject-infraAppProject and addargocd-status-rbac.yamlto theprojectsetinclude-glob; correct the onboarding file count (6 → 7). install.mdfixes: carrysyncOptionsthrough 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.
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>
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>
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
noforget,listandwatchonapplications.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-cmis empty today and the break-glassadminis unrestricted), while the RBAC route reuses the identity and tooling projects already have.What it adds
An opt-in per-project
Roleinargocd, scoped withresourceNamesto that project's own Application(s), bound to its GitHub team. Verbs aregetandwatchonly — a project can watch a sync, but cannot start or abort one.Verified live on the test cluster (then cleaned up)
monitoring)patch/deleteon its own appServer-side dry-run on the rendered template passed; no test resources remain in the cluster.
Known limitation — deliberate, and documented
kubectl get app -n argocdwith no name is Forbidden, and the Application does not appear in Headlamp's list view. Kubernetes RBAC cannot filter a collection, soresourceNamespermitsgeton named objects but does not restrictlist. A client-side label selector does not help — the request is refused before filtering. Granting unscopedlistwould 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-projectresourceNameswork is reusable if it is built.Two load-bearing wiring changes
project-infrapermittedRoleBindingbut notRole— the Role would have been rejected with "not permitted in project". The Role is per-project rather than a shared ClusterRole because it must carryresourceNames.projectsetinclude-glob syncs an explicit filename list; a file matching nothing is ignored silently while ArgoCD reportsSynced/Healthy.Two unrelated defects fixed while editing
install.mdstill carried the"sync":{}patch that fix(docs): close four runbook gaps found onboarding a project database live #4 fixed inargocd.md.syncOp.SyncOptionshas no fallback tospec.syncPolicy.syncOptions, so that recipe silently dropsCreateNamespace=true.install.mdsuggested 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.mdandinstall.md.