Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/design/support-boundary/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ overlay owns (a pure passthrough overlay is adopted yet `editable: 0`).
| Render attribution and proof | [Render attribution](render-attribution.md), [render fidelity](render-fidelity.md) |
| Out-of-folder Flux/Argo render context | [Render fidelity](render-fidelity.md) |
| Controller-expanded resources and provenance | [Expansion boundary](expansion-boundary-and-corpus-organisation.md), [orchestrator knowledge](orchestrator-knowledge-boundary.md) |
| Git-authored Helm values and ConfigMap-backed content | [Values-content architecture](values-content-architecture.md), [values-file projection](values-file-projection.md) |
| Kpt packages and KRM functions | [Kpt and KRM functions](kpt-and-krm-functions.md) |
| Secrets and other document capabilities | [Resource capability model](resource-capability-model.md), [write-only encrypted secrets](write-only-encrypted-secrets.md) |

Expand All @@ -78,6 +79,9 @@ distinguishes those planned gates from the permanent construct boundary.
- [Renderer abstraction](renderer-abstraction-idea.md) and
[Kustomize token writeback](kustomize-token-writeback-explained.md) — exploratory and
teaching records.
- [Values-content architecture](values-content-architecture.md) — why a values file is a
Git content surface rather than a field to copy into an `Application` or `HelmRelease`, and
the staged design for safely editing it.
- [Admission consent](admission-consent.md), [unreflectable edits](unreflectable-edits-and-write-gating.md),
and [reconcile trigger](orchestrator-reconcile-trigger.md) — write-gating decisions.
- Historical implementation records: [image/replica edit-through](finished/images-and-replicas-edit-through.md)
Expand Down
8 changes: 6 additions & 2 deletions docs/design/support-boundary/acceptance-precision.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,11 @@ read. It fits the closed claim vocabulary in
path, read from a document we already parse, needing no orchestrator code. Reading it turns
`ci-metadata.yaml` from a fatal stray into a declared non-target.

**1c. A referenced values file is context.** Handled in
[values-file-projection.md](values-file-projection.md) §2.
**1c. A referenced values file is context.** **Shipped (2026-07-16, Argo CD `Application`s and
Flux `HelmRelease`s).** A `values.yaml` a release names through `helm.valueFiles` or
`spec.chart.spec.valuesFiles` is now read-only context, so `platform/cert-manager` and its
`ClusterIssuer` are accepted instead of refused as `non-krm-yaml`. Designed and recorded in
[values-file-projection.md](values-file-projection.md) §2 (Move 1).
Comment thread
coderabbitai[bot] marked this conversation as resolved.

**1d. Consider partial acceptance.** The three fixes above shrink the blast radius but do
not remove it — the next unknown file still stops the target. Whether an unmanaged file
Expand Down Expand Up @@ -187,6 +190,7 @@ Ranked by value per hour, and all five are independent:
one fixture, and every real repo has a `.gitignore`.
2. **Per-feature messages** (§4) — the data is already computed; pass it through.
3. **Referenced values files as context** (§1c) — rescues a folder that holds real config.
**Shipped** for Argo CD `Application`s and Flux `HelmRelease`s.
4. **`Generated{path}`** (§3) — stops us writing into files a script will overwrite.
5. **The transformer leak** (§2) — the live correctness bug, but last, because it needs the
subtract-or-refuse decision made first and neither answer is one line.
Expand Down
391 changes: 391 additions & 0 deletions docs/design/support-boundary/values-content-architecture.md

Large diffs are not rendered by default.

72 changes: 46 additions & 26 deletions docs/design/support-boundary/values-file-projection.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
# The values file: refused for where it sits, not for what it is

> **design** — direction-setting; ships no code. Nothing it describes is supported today.
> Captured: 2026-07-14
> **design + implementation record** — Move 1 (a referenced values file is read-only context)
> has shipped; Move 2 (projecting the file as an editable object) is still design.
> Captured: 2026-07-14. Move 1 shipped: 2026-07-16.
> Related:
> [README.md](README.md),
> [support-contract.md](support-contract.md),
> [expansion-boundary-and-corpus-organisation.md](expansion-boundary-and-corpus-organisation.md),
> [write-only-encrypted-secrets.md](write-only-encrypted-secrets.md),
> [orchestrator-knowledge-boundary.md](orchestrator-knowledge-boundary.md),
> [values-content-architecture.md](values-content-architecture.md),
> [acceptance-precision.md](acceptance-precision.md),
> [finished/higher-level-krm-documents.md](finished/higher-level-krm-documents.md)

Expand All @@ -16,6 +18,10 @@ names the free-standing Helm values file as *"the single highest-leverage thing
for the Helm story"* and says it *"deserves a design of its own rather than a decision
here."* This is that design.

The wider architecture — including the distinction between an editable Git content surface and a
release object's path/reference field, ConfigMap-backed YAML, and the delivery sequence — lives in
[values-content-architecture.md](values-content-architecture.md).

The boundary it must not move: **we edit the intent layer, never the expansion layer.** We
never render `templates/`. We never learn what a value *means*. Everything below treats a
values file as **bytes with a home in Git**, and nothing more.
Expand Down Expand Up @@ -50,26 +56,40 @@ The worst case is worth spelling out. `platform/cert-manager/values.yaml` is:
- refused as `non-krm-yaml: "YAML is not a Kubernetes manifest"`, which **also takes down a
perfectly valid `ClusterIssuer`** sitting beside it, because acceptance is all-or-nothing.

We refuse the folder on the grounds that we do not know what the file is. The repository is
telling us what it is, in a field we already parse.
We used to refuse the folder on the grounds that we did not know what the file was. The release
field now gives Move 1 a deliberately limited reason to retain it as named context. That is not yet
proof that this checkout is the source the deployer consumes; the formal Argo/Flux distinction is
in [values-content-architecture.md](values-content-architecture.md).

---

## 2. Two moves, in order

### Move 1 (cheap, immediate): a referenced values file is context, not junk
### Move 1 (cheap, immediate): named values context, not junk

> **Shipped (2026-07-16).** A values file named by an `Application`'s `helm.valueFiles` is
> read-only context in the acceptance gate: understood, never written, and never a refusal for
> the folder it sits in. The claim is read in
> [`internal/manifestanalyzer/valuefiles.go`](../../../internal/manifestanalyzer/valuefiles.go)
> and suppresses the `non-krm-yaml` refusal in `acceptance.go`; `platform/cert-manager` flips
> refused → accepted in [support-today.md](../../../test/fixtures/gitops-layouts/support-today.md).
> Both path-valued spellings ship: an Argo CD `Application`'s `helm.valueFiles` and a Flux
> `HelmRelease`'s `spec.chart.spec.valuesFiles`, recognized by one scan-local candidate matcher
> (repo-root `$values/…`, whole-repo, and co-located subtree). Move 1 does **not** prove that the
> matching local file is the deployer's source; see [values-content-architecture.md](values-content-architecture.md).

A values file named by an `Application`'s **`helm.valueFiles`**, or by a `HelmRelease`'s
**`spec.chart.spec.valuesFiles`**, is **Read-only context** — a file we understand, never
write, and never refuse the folder over.
A values-shaped YAML file that this matcher associates with an `Application`'s **`helm.valueFiles`**,
or with a `HelmRelease`'s **`spec.chart.spec.valuesFiles`**, is **Read-only context** — a file we
retain, never write, and never refuse the folder over. Move 1 deliberately calls this named context,
not proof that the deployer consumes this particular local file.

**Only the path-valued fields, and this distinction is load-bearing**, because the three
spellings are three different surfaces and only one of them is a file at all (both verified
against upstream, not inferred from the names):

| Field | What it holds | Surface |
|---|---|---|
| Argo `helm.valueFiles`, Flux `spec.chart.spec.valuesFiles` | a **path** — `$values/platform/cert-manager/values.yaml` | **a file in the repo.** This document's subject. |
| Argo `helm.valueFiles`, Flux `spec.chart.spec.valuesFiles` | a **path** — `$values/platform/cert-manager/values.yaml` | **a file in a selected source.** Move 1 recognizes a local candidate; source identity is needed before any read-provenance or editability claim. |
| Argo `helm.valuesObject` (a `runtime.RawExtension`), Flux `spec.values` | **inline YAML**, embedded in the Application/HelmRelease itself | not a file. It is a *field of a KRM document we already parse*, so it is editable exactly as any other field of that document — no projection, no new claim. |
| Flux `spec.valuesFrom` | a **KRM object reference** — `Secret/my-secret-values`, with a `valuesKey` | not a file either. It names a ConfigMap or Secret, which is *already* a document the store handles (and a Secret drags in [write-only-encrypted-secrets.md](write-only-encrypted-secrets.md), a different problem with a different answer). |

Expand Down Expand Up @@ -106,11 +126,10 @@ Why this is not chart inflation, and not a widening of the boundary:
already performs on every manifest — the pipeline is kind-agnostic, which is precisely
what [finished/higher-level-krm-documents.md](finished/higher-level-krm-documents.md)
proved when a `HelmRelease` needed no new code.
- **Fan-in = 1 gates it for free.** The analyzer can already see which
`Application`/`HelmRelease` references which file. A values file referenced by exactly one
is editable. `values/ingress-nginx/common.yaml` — referenced by several — has fan-in > 1
and **is refused by the existing rule with no new machinery**. The corpus contains both
cases, plus two orphan values files referenced by nobody.
- **Fan-in must be modelled separately.** The current analyzer records only a path set; it cannot
report consumers or calculate values-file fan-in. A future `InputBinding` graph can refuse a
shared `values/ingress-nginx/common.yaml` and accept only a proven, single-consumer input. The
existing Kustomize resource fan-in rule does not provide this automatically.
- **We still never learn what a value means.** `replicaCount: 4` is a scalar at a path. The
operator edits the field a human pointed it at. It does not know that the chart turns it
into a `Deployment`.
Expand Down Expand Up @@ -142,8 +161,8 @@ and that is probably where it belongs.
**Layered values.** `helm-environment-values` layers `chart/values.yaml` →
`values/common.yaml` → `values/<env>.yaml` → the Application's `parameters` → and then a
hidden `chart/.argocd-source.yaml` that **overrides all of them**. Editing "the value" is
ambiguous when five files can supply it. Fan-in = 1 refuses the shared layers; the hidden
dotfile is an `OverriddenBy` claim and must refuse the folder or the edit, loudly. **This
ambiguous when five files can supply it. A future source-aware fan-in rule must refuse shared
layers; the hidden dotfile is an `OverriddenBy` claim and must refuse the edit, loudly. **This
document should not try to resolve the layering. It should refuse it and say so.**

**The `helm.values` string blob.** `Application.spec.source.helm.values` is a YAML document
Expand All @@ -154,11 +173,11 @@ to **name `valuesObject` as the supported surface** — structured, ordinary YAM
with no special case — and to document the string form as a blob we do not reach into.
`valuesObject` is un-adjudicated in the docs today and this is the moment to adjudicate it.

**What makes a chart a chart?** Move 1 needs to tell a values file apart from a chart's own
`values.yaml`. `Chart.yaml` + `templates/` is the signal, and the whole chart folder is
skipped as a unit. `mixed-and-hostile` plants a `templates/` directory with no `Chart.yaml`,
so the detector must not be fooled by either half alone. This residual is already logged in
[expansion-boundary-and-corpus-organisation.md](expansion-boundary-and-corpus-organisation.md).
**What makes a source local?** Move 1 does not need this answer because it only retains context.
Move 2 does: an Argo `$ref` must resolve to the writable Git target or a Flux `GitRepository`
source must prove that identity. A bare path on an external chart or a Flux `HelmRepository` is
inside the fetched chart, not the co-located checkout. Chart-folder recognition (`Chart.yaml` plus
`templates/`) may be an additional refusal rule, but it is not a substitute for source resolution.

---

Expand All @@ -170,12 +189,13 @@ so the detector must not be fooled by either half alone. This residual is alread
| Change a value on a `HelmRelease` (`spec.values`) | **Editable** today |
| Change a value via `Application` `parameters` / `valuesObject` | **Editable** today |
| Change a value held in a hand-written `ConfigMap` (`valuesFrom`) | **Editable** today |
| Change a value in a values file **referenced by exactly one release** | **Planned: Editable** — §2, Move 2 |
| Keep a values file in a folder without killing the folder | **Planned: Read-only context** — §2, Move 1 |
| Change a value in a **shared** values file (`common.yaml`) | **Refused** fan-in > 1, and correctly so |
| Change a value in a values file **proven to be referenced by exactly one release** | **Planned: Editable** — §2, Move 2 |
| Keep a values file in a folder without killing the folder | **Read-only context** — §2, Move 1 (shipped) |
| Change a value in a **shared** values file (`common.yaml`) | **Refused** once a source-aware input graph proves fan-in > 1 |
| Change a value that a hidden `.argocd-source.yaml` overrides | **Refused** — say why, loudly |
| Edit the chart's `templates/` | **Refused**, permanently |
| Edit a chart-rendered object | **Not mirrored** — expansion layer |

The first four rows already work. What the product is missing is rows five and six, and row
six is a week's work in the acceptance gate.
The first four rows already work, and row six (Move 1) now ships — a referenced values file is
read-only context, so the folder is no longer refused for holding it. What the product is still
missing is row five: projecting the file as an editable object (§2, Move 2).
9 changes: 9 additions & 0 deletions internal/manifestanalyzer/acceptance.go
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,15 @@ func recordlessRefusal(store *ManifestStore, d manifestedit.Diagnostic) (Accepta
if managed {
return impureIssue(d, "a non-KRM document"), true
}
if store.isReferencedValuesFile(d.Path) {
// A values file a release names by a locally matching path — an Argo CD Application's
// helm.valueFiles or a Flux HelmRelease's spec.chart.spec.valuesFiles — is NAMED
// read-only context, not a stray: the repository points at it in a field we already
// parse, so it is retained (never written) and must not refuse the folder it sits in.
// This is a name match, not proof the deployer consumes the file. See
// docs/design/support-boundary/values-file-projection.md §2 (Move 1).
return AcceptanceIssue{}, false
}
return AcceptanceIssue{
Kind: IssueNonKRM, Path: d.Path, DocumentIndex: d.DocumentIndex,
Message: "YAML is not a Kubernetes manifest",
Expand Down
50 changes: 50 additions & 0 deletions internal/manifestanalyzer/acceptance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,56 @@ func TestAccept_StandaloneNonKRMRefuses(t *testing.T) {
onlyIssue(t, acc, IssueNonKRM, "values.yaml", 0)
}

func TestAccept_ReferencedValuesFileIsContext(t *testing.T) {
// The hybrid folder the design rescues: an Argo Application, the values.yaml it names
// through helm.valueFiles, and a co-located ClusterIssuer. The values file is retained as
// named read-only context, so the folder is accepted and the ClusterIssuer rides along —
// instead of one stray non-krm-yaml refusing all three.
fsys := fstest.MapFS{
"application.yaml": {Data: []byte(argoAppMultiSourceValues)},
"values.yaml": {Data: []byte(helmValuesFile)},
"clusterissuer.yaml": {Data: []byte(clusterIssuerYAML)},
}
store, acc := acceptanceOf(t, fsys, nil, AcceptancePolicy{})
if !acc.Accepted || len(acc.Issues) != 0 {
t.Fatalf("a referenced values file must not refuse its folder, got %+v", acc.Issues)
}
if !store.isReferencedValuesFile("values.yaml") {
t.Errorf("values.yaml should be recognised as read-only context")
}
if store.FilesByPath["values.yaml"] != nil {
t.Errorf("a read-only values file must never become managed (it is never written)")
}
}

func TestAccept_FluxHelmReleaseValuesFileIsContext(t *testing.T) {
// The Flux counterpart to the Argo case: a HelmRelease names the values.yaml through
// spec.chart.spec.valuesFiles, so the values file is read-only context and the folder is
// accepted rather than refused as non-krm-yaml.
fsys := fstest.MapFS{
"helmrelease.yaml": {Data: []byte(fluxHelmReleaseValues)},
"values.yaml": {Data: []byte(helmValuesFile)},
}
store, acc := acceptanceOf(t, fsys, nil, AcceptancePolicy{})
if !acc.Accepted || len(acc.Issues) != 0 {
t.Fatalf("a HelmRelease-referenced values file must not refuse its folder, got %+v", acc.Issues)
}
if !store.isReferencedValuesFile("values.yaml") {
t.Errorf("values.yaml should be recognised as read-only context")
}
if store.FilesByPath["values.yaml"] != nil {
t.Errorf("a read-only values file must never become managed (it is never written)")
}
}

func TestAccept_UnreferencedValuesFileStillRefuses(t *testing.T) {
// Nothing in the folder names this values file, so it stays the bucket-2 dangerous
// unknown — the suppression is reference-driven, never a blanket "tolerate any values.yaml".
fsys := fstest.MapFS{"values.yaml": {Data: []byte(helmValuesFile)}}
_, acc := acceptanceOf(t, fsys, nil, AcceptancePolicy{})
onlyIssue(t, acc, IssueNonKRM, "values.yaml", 0)
}

func TestAccept_StandaloneInvalidRefuses(t *testing.T) {
fsys := fstest.MapFS{"broken.yaml": {Data: []byte(brokenYAML)}}
_, acc := acceptanceOf(t, fsys, nil, AcceptancePolicy{})
Expand Down
7 changes: 4 additions & 3 deletions internal/manifestanalyzer/scan_repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -630,12 +630,13 @@ func reachedResourceFilesFrom(rootDir string, kusts map[string]*kustomizationDoc
}

// nonKRMUnder counts non-KRM YAML documents and foreign entries under dir. Retained
// build directives, operator artifacts, and accepted benign passengers are excluded
// (they are neither KRM nor noise).
// build directives, operator artifacts, accepted benign passengers, and values files a
// release names as read-only context are excluded (they are neither KRM nor noise).
func nonKRMUnder(store *ManifestStore, dir string) int {
n := 0
for _, d := range store.Diagnostics {
if d.Reason == manifestedit.ReasonNotKRM && pathWithin(d.Path, dir) {
if d.Reason == manifestedit.ReasonNotKRM && pathWithin(d.Path, dir) &&
!store.isReferencedValuesFile(d.Path) {
n++
}
}
Expand Down
Loading