feat: render-root scoping + tolerate patches and patched overlays reading ../../base#239
Conversation
Render-root scoping (docs/design/support-boundary/render-root-scoping.md §4): a GitTarget whose spec.path is a kustomize overlay may now read a base OUTSIDE its subtree via ../../base, while writes stay inside spec.path. The writer re-roots its scan at renderBase — the lowest common ancestor of spec.path and every base it reaches — so the store, attribution, and the render oracle run in one coordinate system with no ..-escaping paths. The write jail (writeSubdir) keeps a planned write inside spec.path. When a subtree reads no out-of-scope base, renderBase == spec.path and writeSubdir == "", so every existing layout is byte-identical to before. Effects: - an overlay pointed at ../../base now renders and is accepted (was refused for a kustomize build failure); - a base-derived object materialises, so an images:/replicas: bump routes to the overlay's own entry and the read-only base keeps its bytes; - a base-owned field edit an overlay cannot express is refused and reported, never silently written through; - fanInPrecondition is generalised to any file reachable from more than one render root (ReachedByMultipleRenderRoots), not only the override-ambiguous case. Adds the minimal-overlay corpus fixture (2-rendered/kustomize-overlay-minimal), the first to surface the kustomize-overlay / overlay-fan-out-unsupported verdict; support-today.md regenerated (existing rows unchanged). Unit tests exercise the write path with real kustomize builds. Deferred to a follow-up: the discovery-side flip (scan_repo still reports the overlay unsupported) and a dedicated cluster e2e. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Warning Review limit reached
Next review available in: 4 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (33)
✨ 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 |
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.
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
The tolerate-patches work changed parseKustomization's signature (adding the file tree it resolves patch paths against). Adapt render-scope's KustomizationResourceEntries caller — it reads only the resources graph, so a nil tree is correct. Regenerate the layout baseline over both changes: with patches tolerated, flux-monorepo and kustomize-overlays drop `patches` from their refusals and now report the render-root-scoping barrier (overlay-fan-out-unsupported) that patches was masking. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Codecov flagged the render-scope error/edge paths as the PR's coverage gap. Add tests for the branches the happy-path suite missed: a transitively-read base (overlay -> ../../base -> ../shared) with a .yml base kustomization, foreign content in the overlay re-keyed to render coordinates, a base's non-manifest file skipped, a remote base skipped rather than scanned, and the common-ancestor / relative-under / minimal-dirs helpers at their boundaries. Raises render_scope.go coverage (rekeyScan 71%->93%; several helpers to 100%); the residual is OS-error paths that need fault injection. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t, dual-role patches Five review findings on render-root scoping + tolerate-patches: - F1/F3: resolve the EXACT reachable file set of the resources/patches graph instead of walking whole base directories. An external resource FILE (resources: [../../shared/x.yaml]) is now pulled into the render scope (was refused); unrelated content a base does not reference is no longer imported (it could wrongly refuse a buildable folder); and an ancestor base no longer re-scans overlay-local files into a duplicate refusal. Adds KustomizationBuildRefs (resources + patch path files). - F2: placement now rebases a declared/canonical path under the write jail (WriteScope) instead of resolving against renderBase and being skipped, so an overlay GitTarget's new objects land inside spec.path as documented. - F4: a file listed as both a resource and a patch is materialised and mirrored, not silently retained as a patch-only build input. - F5: configuration.md / architecture.md / support-contract.md updated — path-based patches are tolerated and overlays reading ../../base are shipped; the scan-repo discovery flip is called out as the remaining follow-up. Tests: external resource file, transitive base, unrelated-content exclusion, write-jail placement (canonical + declared), dual-role path mirrored. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ase kustomizations - P1 (blocker): a new object in an overlay could be committed to a file no kustomization includes — never rendered, and the oracle skipped (silent divergence), introduced when F2 replaced the earlier safe skip with a rebased write. Placement now (a) restricts sibling inference to WRITABLE siblings so it never infers beside a read-only base doc, (b) restricts the single-kustomization- root fallback to WRITABLE kustomizations so an overlay resolves to its own root, and (c) governs a new overlay object by the write scope's own render root when its subdirectory has none. The object lands beside the overlay kustomization, gains a resources: entry, renders, and is oracle-verified. All gated on WriteScope != "", so self-contained placement is unchanged. - P2 (dual kustomization files): a base holding both kustomization.yaml and .yml is now imported whole, so the render reaches kustomize's own "multiple kustomization files" refusal instead of masking it by reading only the first. - P2 (symlink): readKustomization goes through the guarded reader (Lstat + regular-file), so external-base discovery never follows a symlinked kustomization outside the worktree. Tests: new object in an overlay renders (end-to-end), dual-file base imported both, symlinked base kustomization not followed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rrides and $patch:delete (#244) * test(overlay): prove new-object placement into an external-base overlay; fix stale docs render-root-scoping.md §4 labelled "new object -> overlay-local file plus resources: entry" as Planned, but #239's render-root scoping already wired it: WriteScope threads the overlay's write jail into placement, the base kustomization is skipped as out-of-jail (writableCohort / resolveKustomizeRoot), appendKustomizationResource adds the resources: entry in the same commit, and the render oracle verifies the result. It simply had no test proving it and the docs still read "Planned". Add TestPlacement_ExternalBaseOverlay_NewObject: a GitTarget rooted at an overlay that reads ../../base gains a new ConfigMap. It asserts the file lands inside the overlay, the overlay's OWN kustomization gains the resources: entry (never the base's), the read-only base is untouched byte-for-byte, and the render oracle accepts the flush. Correct the stale docs (render-root-scoping §1/§4/§5/top, README, support-contract, kustomize-support-boundary) to mark new-object creation shipped. What remains for an overlay: adding a MISSING images:/replicas: declaration (no source value to attribute yet) and base-owned-field patch authoring. No production code changed — this proves and documents existing behaviour. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(overlay): author a missing images:/replicas: entry when a base value is changed per-environment An external-base overlay could edit an EXISTING images:/replicas: entry, but when the base itself supplied an image or replica count, changing it in one environment tried to write the read-only base and was refused ("planned write path base/deployment.yaml escapes the GitTarget write scope"). So "edit the image in dev" did not work unless the overlay already declared an entry. Author the entry instead. When the projection finds a diverging image component or replica count whose supplier is the SOURCE document and that source is out of the write jail (a base an overlay reads), it emits a Create OverrideEdit into the overlay's own kustomization rather than writing the base. The writer applies it with a new manifestedit.AppendKustomizationOverride primitive (creating the images:/replicas: section if the overlay has none), and the base source form stays unchanged so nothing is written to the base. Every authored entry is put to the existing re-render oracle before it can commit, so a proposal that over-reaches (an images: name shared by another object) is refused there, not written. Idempotent on resync: once the entry exists the store sees it and the change routes to it, no duplicate. Tests: manifestedit primitive unit tests; overrides projection call-site updates; and writer integration TestOverlayAuthors_{ImageEntry,ReplicaEntry,Idempotent}. Docs updated (render-root-scoping, support-contract, README, kustomize-support-boundary). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(overlay): author $patch: delete for an object inherited from a base Deleting an object an external-base overlay inherits from its base tried to delete the base document (out of the write jail, shared by other environments) and was refused. Author a `$patch: delete` in the overlay instead: when the delete target is out of the write jail and the overlay has a supported render root, applyDelete writes a small `$patch: delete` document inside spec.path, names it in the overlay's own patches: (new manifestedit.AppendKustomizationPatch primitive, creating the patches: section if absent), and keeps the existing Removed intent so the re-render oracle proves the object leaves the render. The base is never touched. The patch pins the object by apiVersion/kind/namespace/name; a patch that does not match makes kustomize fail to build and the oracle refuses the flush (proven: the namespace-less first cut was correctly refused, the namespaced one commits). It needs no field attribution, so it does not wait on strategic-merge field-patch authoring — which stays refused. This is a deliberate, narrow widening of the patch-authoring boundary for the delete slice only; noted in patch-authoring.md and the render-root-scoping/support-contract records. Tests: AppendKustomizationPatch unit tests; integration TestOverlayAuthors_DeletePatch_ForInheritedObject. Coverage baseline bumped 76.4% -> 76.5%. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
What
Delivers the two connected halves that make a patched kustomize overlay reading
../../basework end-to-end — folding in the tolerate-patches work from #235, which is incomplete on its own (see below). Implements render-root-scoping.md §7 steps 1, 4, and 6 (step 3, the oracle, already shipped).Two capabilities, deliberately combined because neither is useful for a real overlay without the other:
1. Read
../../baseas render context (render-root scoping)A
GitTargetwhosespec.pathis an overlay may now read a base outside its subtree via../../base, while writes stay insidespec.path. The writer re-roots its scan atrenderBase— the lowest common ancestor ofspec.pathand every base it reaches (internal/git/render_scope.go) — so the store, attribution, and the render oracle run in one coordinate system with no..-escaping paths. The write jail (writeSubdir) keeps a planned write insidespec.path. When a subtree reads no out-of-scope base, this is the identity — every existing layout is byte-identical to before.2. Tolerate a patch, without authoring one (folded from #235)
A path-based strategic-merge
patches:is now read-only build context: the folder is accepted, the patch file is retained (never managed/swept),images:/replicas:edit-through works in a patched folder, and an edit to a field the patch owns is refused per object. Inline / JSON6902 / out-of-tree patches refuse by name (patches-inline,patches-json6902,patches-outside-tree).Why combine them
#235's own finding: "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. Render-root scoping, not patches, is the single blocker on the corpus's most tractable layout." Conversely, render-root scoping alone leaves a patched overlay refused on thepatches:key. Together,flux-monorepo/apps/{production,staging}(patch +../../base) is handled by the writer: rendered, accepted, image/replica edit-through, patch-owned edits refused per-object.Effects on the corpus baseline
flux-monorepo/apps/{staging,production}refused-structural: patcheskustomize-overlay/overlay-fan-out-unsupportedkustomize-overlays/apps/frontend/overlays/*…, patches, …patchesdrops out (still refused for generators + name(pre/suf)fix)2-rendered/kustomize-overlay-minimal(new)kustomize-overlay/overlay-fan-out-unsupportedAlso
fanInPreconditiongeneralised to any file reachable from more than one render root (ReachedByMultipleRenderRoots), not only the override-ambiguous case.kustomize-overlayverdict (every other overlay trippedrefused-structuralfirst).Tests
Writer + analyzer unit tests exercise the write path against real kustomize (krusty) builds: overlay-base resolution, image edit-through to the overlay entry with the base untouched, in-sync no-op, base-field refusal, shared-base fan-in refusal, base-escaping-repo-root refusal; plus the patches tests (tolerated
path:merge, refused inline/json6902/out-of-tree, "patch file is never a render origin", deprecated spellings not folded).Validation
task fmt / generate / manifests / vet / lint✓ ·task test✓ (unit coverage → 76.2%, baseline bumped) ·task test-e2erunning locally + on CI.Deferred to a follow-up
scan_repostill classifies these overlays asoverlay-fan-out-unsupported; this PR changes the live writer, not the discovery/onboarding report.Folds in and supersedes #235.
🤖 Generated with Claude Code