feat(kustomize): tolerate a patch, without authoring one#235
Conversation
…input
The writer mirrors a live object into the file that produced it. Under kustomize
that file is not what the cluster runs, and mirroring the live object straight
back writes the BUILD'S OUTPUT into the build's INPUT.
Measured, on a folder we accept today, with nothing changed in the cluster and
nothing changed in the render: a kustomization declaring `labels:` +
`commonAnnotations:` and nothing else commits the overlay's `env: prod` and
`owner: platform` into the base manifest, on the first reconcile of an unchanged
folder. Every reconcile of an in-sync folder produced a commit, and the file was
left wrong. In a base shared by two overlays, the value baked in is one
environment's.
The fix is one rule, and it models no transformer:
WHERE THE LIVE OBJECT AND THE RENDER AGREE, THE SOURCE KEEPS ITS BYTES.
WHERE THEY DISAGREE, THE USER CHANGED SOMETHING, AND THAT IS WHAT WE WRITE.
Agreement means the build already produces exactly what the cluster runs, so the
source is — by construction — what produced it, and there is nothing to write.
Disagreement is the user's edit: it is routed to an images:/replicas: entry when
the dye says one supplies the field, and written through to the source otherwise
— where, if the build owns the field, the re-render refuses the flush.
Because it needs no model of labels, of namespace, or of a patch, it closes all
of them at once. It is also the gate on tolerating `patches:` at all: a patched
base would otherwise absorb one environment's values, and no re-render can catch
that (the patch re-imposes its value, so the render comes out identical).
Two behavior changes go with it:
- the re-render now runs for ANY document a render root produces, not only one
an override chain governs. A change to a build-supplied field in a folder
with no images:/replicas: entries used to be committed and silently never
converge; it is now a reported refusal.
- a live change the projection cannot place is refused (`unplaceable-edit`):
the build and the user both rewrote one list whose elements carry no unique
`name:` to pair them by. Pairing by position is not a conservative guess, it
is measurably wrong — kustomize PREPENDS a container a patch adds.
The corpus no-op invariant now compares the WHOLE document, not just its images,
which is how a projection that quietly rewrote every field we had not modelled
passed it for as long as it did: 8 documents checked before, 53 now.
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…e, fix the vars contradiction The SourceFormRefusedError message (surfaced to the user via WriteBoundaryRefused) and its IssueUnplaceableEdit doc comment both cut off mid-clause; complete them. And §2's vars bullet claimed the folder is accepted today while a parenthetical said it now refuses -- rewrite the passage in past tense (both leaks it cites are now closed, by #229 and by sourceForm).
9cfa2a8 to
7024d3d
Compare
…lementation Bring the support-boundary design docs onto this PR — they are the design that sits next to this real implementation, so they belong together rather than in a separate PR: render-fidelity.md (our render is not the orchestrator's; the render-vs-live fence, the blocking RenderFaithful condition, and where to implement it), admission-consent.md, orchestrator-reconcile-trigger.md, and the kpt-and-krm-functions orientation note, plus their README index rows. Consolidated from PR #237, now closed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A `patches:` block used to refuse the whole GitTarget. Not the edit — the TARGET.
A folder whose patch pins a replica count also lost images:/replicas: edit-through,
which the patch has nothing to do with. It is the single biggest refusal cause in
the layout corpus.
A strategic-merge patch named by `path:` is now READ-ONLY BUILD CONTEXT:
- the folder is accepted, and what it renders is mirrored;
- the patch FILE is retained, never managed. This is the part nothing else in the
store would have got right: a sparse patch IS a KRM document, so materialised it
would be indexed as a manifest, matched to a live object, mirrored over with a
whole Deployment, or swept as an orphan nothing in the cluster answers to. That
a patch is not a resource is not our claim — it is the RENDER's: a patch file
never appears as a rendered object's origin;
- images:/replicas: edit-through works in a patched folder, exactly as anywhere;
- an edit to a field the PATCH owns is refused per OBJECT, not per folder.
Tolerating a patch is not authoring one. Nothing is ever written into a patch file.
Exactly one shape is tolerated; the rest refuse BY NAME, because "unsupported" is not
something a user can act on and "your patch is inline, and we can only read one from a
file" is: patches-inline (which is also where an inline JSON6902 op list arrives, since
it decodes into the same field), patches-json6902 (a path to an op list — a YAML
sequence, not a sparse KRM document), patches-outside-tree.
MEASURED, NOT ASSUMED: FixKustomization folds `bases` into resources and `imageTags`
into images, but it does NOT fold `patchesStrategicMerge` or `patchesJson6902` into
`Patches`. They keep refusing under their own names, and a test pins that so a kustomize
bump cannot silently widen what we accept.
THE CORPUS SAYS SOMETHING THE PLAN DID NOT EXPECT, and it is the deliverable here:
tolerating patches accepts ZERO new candidates. Every patched overlay in the corpus also
reads a base from OUTSIDE its own folder, so `patches` was masking the real refusal.
flux-monorepo/apps/{staging,production} now report overlay-fan-out-unsupported — the
verdict render-root-scoping.md §5 records as never having been observed, because
refused-structural always fired first and hid it. Render-root scoping, not patches, is
the single blocker on the corpus's most tractable layout.
The mechanism itself is proven by fixtures at both levels: a self-contained patched root
is accepted, its image bump routes to the entry, its in-sync state is a no-op, and its
patch-owned scale is refused.
The prompt's stage 1 shipped, and three of its premises did not survive contact with the measurement: the gate was the projection rather than the deny-list, FixKustomization does not fold the deprecated spellings, and tolerating patches accepted zero new corpus candidates because every patched overlay in the corpus is really blocked on render-root scoping.
7024d3d to
77fcf73
Compare
|
Folded into #239. The two are the connected halves of the same capability — render-root scoping (reading The two patches-tolerance commits ( |
What changes
A
patches:block used to refuse the wholeGitTarget. Not the edit — the target. A folder whose patch pins a replica count also lostimages:/replicas:edit-through, which the patch has nothing to do with. It is the single biggest refusal cause in the layout corpus.A strategic-merge patch named by
path:is now read-only build context:images:/replicas:edit-through works in a patched folder, exactly as anywhere else;Tolerating a patch is not authoring one. Nothing is ever written into a patch file.
The patch file is a build input, not a manifest
This is the part nothing else in the store would have got right, and it is why "just drop
patchesfrom the deny-list" is not the change. A sparse strategic-merge patch is a KRM document —apiVersion,kind,metadata.name. Materialised, the store would index it as a manifest, match a live object to it, mirror a whole Deployment over the sparse patch, or sweep it away as an orphan nothing in the cluster answers to.That a patch is not a resource is not our claim — it is the render's: a patch file never appears as a rendered object's origin.
TestPatches_ThePatchFileIsNeverARenderOriginpins exactly that, so the day it stops being true, retention stops hiding a resource.One shape in, the rest refused by name
"Unsupported" is not something a user can act on. "Your patch is inline, and we can only read one from a file" is.
patches: [{path: patch.yaml}]— a sparse KRM document inside the treepatches: [{patch: "..."}]— inline (including an inline JSON6902 op list)patches-inlinepatches: [{path: json-patch.yaml}]where the file is anop/path/valuelistpatches-json6902path:naming no file in the tree, or escaping itpatches-outside-treepatchesStrategicMerge:/patchesJson6902:Measured, not assumed — the prompt for this work suspected the opposite:
FixKustomizationfoldsbasesintoresourcesandimageTagsintoimages, but it does not fold the deprecated patch spellings intoPatches. They stay in their own fields and refuse under their own names.TestPatches_DeprecatedSpellingsAreNotFoldedIntoPatchespins it, so a kustomize bump that starts folding them cannot silently widen what we accept to a shape we have never looked at.The corpus says something the plan did not expect
This is the deliverable of the stage, and it is unflattering:
Tolerating patches accepts zero new candidates.
Every patched overlay in the corpus also reads a base from outside its own folder — so
patcheswas masking the real refusal.flux-monorepo/apps/{staging,production}now report:That verdict is one
render-root-scoping.md§5 records as never having been observed, becauserefused-structuralalways fired first and hid it. So the finding is: render-root scoping, not patches, is the single blocker on the corpus's most tractable layout.The two rows that move:
flux-monorepo/apps/{staging,production}refused-structural: patcheskustomize-overlay/overlay-fan-out-unsupportedkustomize-overlays/apps/frontend/overlays/*…, patches, …patchesdrops out; still refused for generators +namePrefix/nameSuffixThe mechanism itself is proven by fixtures rather than by the corpus, at both levels: a self-contained patched root is accepted, its image bump routes to the entry, its in-sync state is a no-op, and its patch-owned scale is refused, naming the object.
What is still deferred
Stages 2–4 of the milestone (attribute a patch scalar with the dye, route an edit into the patch, per-field refusal accounting). Nothing here reads a patch's contents to decide anything — reading a patch tells you what it asks for, never what the build does, and the ordering fact pinned in #234 (
[PatchTransformer, ReplicaCountTransformer, ImageTagTransformer]— the transformers win) is exactly what you would get wrong by trying.Validation
task lint·task test(coverage 75.7% → 75.8%) ·task test-e2e(55/55) all pass.🤖 Generated with Claude Code