0051. UI and CLI writes refuse a CR that Argo CD or Flux tracks

Status: Superseded by ADR 0101 · Date: 2026-10-26 · Design section: GitOps and Argo CD Decided: Every write to an existing CR in internal/uiapi (so the UI and the CLI, backend or fallback) goes through one Server.requireUntracked, which runs gitopstracking.IsTracked and prmode.Decide(trackedBy, false) and refuses anything but ModeDirectWrite with FailedPrecondition; a failed check refuses too; Create is not gated; the RBAC is two cluster-scoped rules, one narrowed by resourceNames

Context

pkg/gitopstracking and pkg/prmode (ADR 0037, 0038, 0044) were complete but nothing imported them, so the backend wrote CRs that Argo CD or Flux owns. Architecture: a tracked CR without a repository connection is read-only with an export button. No repository connection exists in the CRDs yet, so for now every tracked CR is read-only; opening the PR for ModeOpenPR is a later unit.

Decisions

1. One gate. Server.requireUntracked(ctx, obj) in internal/uiapi/server.go, called right after the object is read in ApproveMachine, RejectMachine, UpdatePatch and DeletePatch (the only Update/Delete sites). It passes hasRepoConnection=false; any mode other than ModeDirectWrite (including a ModeOpenPR that cannot occur yet) is refused. The CLI fallback builds the same Server, so it is covered without extra code.

2. Refusal. connect.CodeFailedPrecondition: "<ns/name>" is tracked by Argo CD|Flux, so it is read-only here: run ichor gitops export to put it in Git and change it with a pull request.

3. A failed check refuses. An IsTracked error returns CodeUnavailable and writes nothing: "cannot tell" is never "may write" (ADR 0044).

4. Create is not gated. A CR that does not exist has no Argo CD annotation or label, no Flux field manager and no inventory entry, so IsTracked can only answer "not tracked". Whether the new CR lands in a path or namespace a tool will later own is not knowable from the cluster; that belongs to the PR-mode unit, which decides where a new file goes. Creating the CR directly is the same as kubectl apply today.

5. RBAC. Two rules added to the UI ClusterRole (rbac_markers.go, config/rbac/ichor-ui/, the chart): get on configmaps with resourceNames: [argocd-cm], and list on kustomizations.kustomize.toolkit.fluxcd.io. They are cluster-scoped because Argo CD's namespace is not fixed and a Role/RoleBinding into a namespace that does not exist fails the chart install (ADR 0050 made the same call for Longhorn). resourceNames keeps the ConfigMap read to one name, so there is no cluster-wide ConfigMap or Secret read. The namespace comes from Server.ArgoCDNamespace (ICHOR_ARGOCD_NAMESPACE in ichor-ui), default argocd.

Consequences

  • The namespace is configurable end to end: the chart sets ichorUI.argocdNamespace (default argocd) as ICHOR_ARGOCD_NAMESPACE on the ichor-ui Deployment, and the CLI's --argocd-namespace (default argocd, the same flag ichor gitops export has) sets the namespace the --local fallback reads. Before this, a non-default Argo CD namespace silently let a tracked CR be written directly.
  • Flux has no PR path (ADR 0037), so a Flux-tracked CR stays read-only even once a repository connection exists.

Search the docs

Search pages and headings