fix(placement): register a new file with its nearest ancestor kustomization - #319
Conversation
…zation
A declared path into a subdirectory of a kustomize folder produced a document
that no kustomization lists, so `kubectl apply -k` never rendered it: one
byType line was enough to put a file in Git that nothing applies.
governingKustomization looked only in the file's own directory, plus a special
case for the write scope's root, so registration depended on whether
render-root scoping happened to be in force — a difference the user cannot see.
It now walks up from the file's own directory to the nearest kustomization,
bounded by the write jail, because a kustomization above the write scope is a
read-only base whose resources: list is not ours to edit. That also reaches
namespaceIsInheritedFromContext, which was unreachable for these paths: the
document used to carry a metadata.namespace line the folder's own documents
omit.
Drop the {version} requirement from IdentityCompletePlacementTemplate. The
built-in canonical path is deliberately versionless, because two served
versions of one group/resource are the same object, so a version segment
separates no identities. Requiring it judged the canonical shape as not
identity-complete, which is what would make any spec-level default fail our own
validation gate.
Fixes #295
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (6)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughThe change fixes declared placement registration in kustomize subdirectories and updates identity validation for the versionless canonical template. Documentation and regression tests cover ancestor registration, metrics, rendered content, and validation. ChangesPlacement corrections
Estimated code review effort: 3 (Moderate) | ~20 minutes Merge Risk: 🔵 Low · up to When creating a new manifest, a failure to update its governing kustomization can still commit the file without rendering it, and later updates may not repair that registration; the change is otherwise confined to the writable subtree, so it is mergeable with explicit owner awareness or follow-up. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Linked Issues checkExplanation The changes satisfy issue Full details: Docstring CoverageExplanation Docstring coverage is 70.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 10 functions across 5 files. (1 skipped: 1 unsupported.) Full details: Description checkExplanation The description explains the defects, implementation changes, linked issue, acceptance criteria, regression tests, and local test results. It does not reproduce every template section, but it provides the required review context.
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 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 |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
…t express Reverses this document's own thesis. It argued that a path template is the wrong primitive and proposed replacing spec.placement with a spec.layout discriminated union. That argument rested on five points, and three of them were retired by #319: a new file is now registered with the nearest kustomization that governs it, whatever chose its path. Once the template is no longer asked to express "beside this folder's one kustomization", the discriminator has nothing left to discriminate — registration was always the best idea in the model, and it is the part that already shipped. So the template stays and two additive fields join it, each with a default equal to today's behavior. serializeNamespace (Auto, Always, Never) is the one thing a path genuinely cannot express: kustomize takes metadata.namespace from either the document or a governing root, and where the file sits decides neither. It is not called writeNamespace because "write" is this API's most loaded word — mode: Write, the write boundary, the write jail, WriteBoundaryRefused — so writeNamespace: Never invites the reading "never write to this namespace", a permission, which is exactly what the neighbouring sourceNamespace fields are. kustomizeRoot (Adopt, Create, Require) answers "do we want kustomize" on one axis: what to do when no kustomization governs the path. When one does, every value registers, because that is the invariant. Adopt is today's behavior, Create is the empty-repository bootstrap that was the last surviving argument from the old thesis, and Require is the safety value the Never guard needs — if the root disappears, stop writing rather than commit files nothing renders. Four kustomize facts are measured rather than recalled, and three contradict assumptions the earlier model was built on: a root does not require a flat folder, nested roots work one per subfolder and supply their own namespace, there is no ambient pickup (globs and bare directories both fail), and an unlisted file in a listed subdirectory renders nothing. The first is why the path may be anything; the third is why registration must be an invariant. Two values are considered and not taken, with reasons. Ignore is rejected: spec.path already expresses it, since the ancestor walk is bounded by the write jail, and it is the only candidate that changes what happens when a root IS present — the half that should be invariant. CreatePerDirectory is deferred with its trigger recorded, because fact 2 proves it would work and it is what would make Never safe in a multi-namespace tree. Also takes the maintainer review's status findings, since they are decisions this document owes: renderRootReason becomes a condition reason rather than a bespoke field, the accumulating counters go to metrics where placements_total already carries them, and conditions plus observedGeneration are shown. The headline is what this deletes: spec.layout, kind, scope, kustomize.create, the LayoutProfile question, the immutability machinery, and the migration. The layout model was the largest breaking change in the queue; on this shape it is not a breaking change at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The plan still described the #295 correctness fixes as PR 1's first half and called PR 1 "this PR". Both fixes are on main and released in 0.42.1 via #319: the ancestor walk and the versionless identity gate. PR 1 is the corpus alone now, and the shipped fixes become the first thing it asserts, which makes the corpus regression cover from its first commit rather than scaffolding for later PRs. Worth stating plainly that the ancestor walk is also why this plan reversed. The visibility page kept two claims the reversal invalidated. It said the CRD default for placement.default was superseded because layout.kind is the defaultable thing; there is no layout.kind, the template stays, and the question is live again — with a weaker objection than before, because a defaulted path into a subdirectory is now registered with the kustomization that governs it. And status.layout is status.placement since spec.layout stopped existing, so the section says so once at the top rather than being renamed throughout, since this page is still where the field is argued. Also fixes a Go comment pointing at docs/design/manifest/version2/, a path that has not existed for some time and that the folder move only half-corrected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#293 is closed with the evidence that reversed it, and its successor is #322: serializeNamespace and kustomizeRoot as additive fields, which is why the row leaves the wave. The Tier 1 placement-correctness row shipped in 0.42.1 via #319, and B2 carries the status.placement rename. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…d shrink the source-scope surface (#325) * docs(layout): reverse the layout model onto two optional booleans, and shrink the source-scope surface The layout question had grown to eight documents across `spec/`, `design/` and `future/`, so following the argument meant knowing which folder each step lived in. `docs/layout/` now holds it: two current-behavior contracts, one design page, and the worked examples. Everything else stays where it was. **The model reverses its own earlier thesis and leads with the reversal.** #319 shipped the ancestor walk, which made registration an invariant and retired three of the five arguments against path templates. So the template stays, and what it cannot express becomes two optional booleans on the existing `spec.placement`: - `useKustomize` — create and maintain the folder's `kustomization.yaml`. Registering a new file with a root that already governs it is an invariant, not a setting; the flag's one job is what to do when there is no root. Its `true` half is the only genuinely new machinery here, and it is what makes an empty repository bootstrappable. - `serializeNamespace` — whether the document carries `metadata.namespace`. A `*bool`, because no plain default preserves today's behavior: `false` breaks a flat folder, `true` writes a redundant line into every kustomize folder that already supplies one. Unset means infer, which is not a guess — the existing inference omits the namespace only when the governing kustomization sets it to this resource's own namespace, so an explicit `false` is an override of a correctness rule and gets a post-scan guard. The two pair up: with `useKustomize: true` the operator owns the root it writes `namespace:` into, which is what makes both the omission provable and the created kustomization meaningful rather than an empty file. `spec.layout`, `kind`, `scope`, `kustomize.create`, the LayoutProfile question, the migration and four maintainer-review findings are deleted with it. Nothing in the placement work is breaking, so none of it waits for a coordinated consumer bump. **Source scope.** Declines Flux-style impersonation and deletes `GitTarget.spec.allowedSourceNamespaces` with its selector machinery (4,569 lines, and the only cross-cluster read in the authorization path), on an API reading rather than a security one: the chain from a Git folder back to the object that fills it never leaves one namespace. Keeps `allowedNamespaces`, renamed `accessFrom`, reversing an earlier draft. Redefines `sourceNamespace: "*"` as one cluster-wide list and watch. Also raises F9's envtest to Tier 1 — the only queue item whose answer is unknown rather than whose work is unscheduled — and prices staying on `v1alpha3` as a one-consumer countdown rather than a constant. The Go changes are comment-only: doc-path citations following the move. `task lint` and `task test` pass; e2e is unaffected because no executable line changed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(layout): lift serializeNamespace to the spec's top level, and say why a folder-wide claim is ordinary `spec.placement` means "where a NEW document goes"; everything already written is match-first and never moves, which is what makes a template safe to change. `serializeNamespace` does not have that property, and the write path already proves it: plan_flush.go strips metadata.namespace from a new document using the placement result and from an UPDATE using the document's own observed namespace source, and a document whose namespace is inherited is located in the file bytes by a namespace-less identity. A field that rewrites existing documents as they are next touched, and decides how they are found, does not belong inside a struct documented as new-files-only. So it is `spec.serializeNamespace`. `useKustomize` stays in `spec.placement`, because it passes the same test: it decides whether a new file's directory has a root to join, and creates one when there is none. Group by blast radius, not by topic. Also answers whether a folder-wide namespace claim is a strange thing to state. It is not: "no document carries its namespace" is the portable-artifact convention behind every kustomize base, Flux's targetNamespace, Argo's destination.namespace and a chart's .Release.Namespace, while "every document carries it" is the convention for a folder applied directly. Cluster-scoped resources are exempt, and the one genuinely non-uniform shape — a tree of nested roots, each supplying its own namespace — is what the unset default already handles per document. The uniform claim is what an explicit setting is for; the non-uniform folder is what unset is for. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(namespace): pin the two folder shapes where reading the folder is not enough Answers whether the layout situations under discussion can hurt us, by measuring rather than reasoning. Two shapes exist where the manifest store's view of a document's namespace and the namespace kustomize renders it into can disagree, and neither is refused when the folder is read: - a document carrying metadata.namespace: beta inside a folder whose kustomization sets namespace: alpha. An explicit namespace is authoritative as written and the transformer is never consulted, so the store indexes beta with NO diagnostic while the folder renders alpha. - a parent root and its child root that both assign. kustomize is deterministic here (the parent transformer runs last and wins); we decline to guess, so the document is namespace-less, unmatchable by identity, and placement treats the live object as new. Both are caught, and nothing reaches the worktree: the first refuses with "does not render to the live object after the write", and the second gets as far as proposing a second file before kustomize's own ID conflict fails the build — neither the file nor the resources: entry survives. So the render check at the write path is the backstop for this whole class, and it holds. Nothing pinned that until now: a change that relaxed the check would have turned a refusal into a silent write with no test failing. Adds the read-side halves to the contextual-namespace corpus, which docs/layout/contextual-namespace.md says is meant to grow one folder per "can we support X?" question, plus the multi-namespace tree that was missing from it — a parent assigning nothing and each child root carrying its own namespace, which is the shape the model leaves to inference rather than to a flag. The remaining gap is legibility, not safety: both failures surface as an opaque render error rather than as the one fixable thing that is wrong, which is what the post-scan validation pass is for. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(layout): fence namespace-free folders, and merge the two fixture folders Five things, all documentation, on the two-booleans branch. **The one-source-namespace rule.** A GitTarget with an explicit serializeNamespace: false admits exactly one source namespace; the second is refused. Replaces the proposed enforceSingleNamespace boolean: once the template half is dropped (a deployer applies bytes, not filenames) the refusal follows from one field's own meaning rather than from a correlation between two, so it needs no new API surface. Keys on explicit false only, never on inference, which is what keeps a nested-root tree — legitimately multi-namespace and namespace-free — working. Under useKustomize: true the multi-namespace case is the one place the operator would actively construct the silent-mislabel folder, so refusing there is the only defensible behavior. Ships with the field in PR 4. **One definition of record for sourceNamespace: "*".** The cluster-wide redefinition was decided but unbuilt, so the shipped reference docs correctly describe the old semantics and are deliberately left alone. Marked the section in source-scope-simplification.md as the single home, carrying both readings, and cut the three restatements. One of those (TODO.md) was actively harmful: it proposed a competing direction the wave deletes. **docs/design/build-order.md.** The five in-flight changes were spread across five sequencing lists, none of which could see the other four. They resolve to three tracks that do not block each other, with the two real couplings named and three that people keep assuming and that do not exist — notably that PR 4 does not wait for the wave, since the rule reads WatchRules rather than the policy field being deleted. Records that the corpus is read by no Go file today, and that the harness seam already exists at both levels. **Merged the two fixture folders.** Four of six examples/ scenarios were use-case retellings of shapes that specify the same behavior more precisely and with the refusal halves included; deleted, with their two salvageable points folded into shapes 3 and 5. The two ecosystem scenarios and the shared prerequisites moved to specific-examples/. One fixture convention now, not two. **Fixture and reference corrections.** Three GitTargets used sourceNamespace overrides with no allowedSourceNamespaces and no ClusterProvider specimen, which is a terminal SourceNamespaceAuthorized=False; three examples still spoke the deleted spec.layout vocabulary (Auto/Kustomize/Tree/MultiNamespace); the claim that model.md superseded placement-visibility's Question 2 was backwards, since model.md reversed and reopened it; nine api-wave.md link texts named a path that does not exist; and shapes/ was titled "Seven folder shapes" over eight rows. task lint-docs passes: 221 markdown files, every reference resolves. Docs-only, so the full suite does not apply. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(plan): cut the queue to three PRs, and de-duplicate the layout set Squashes track A's four PRs into two and states the whole queue as three, because merging is cheap where the parts share a property and expensive where they do not. **PR 1 is the corpus, spec.suspend, status.placement and the post-scan pass's Ambiguous rule.** They are one review rather than four because none of them changes what the operator writes: suspend with no status shows nothing, status without suspend arrives after the first write, and the corpus is what proves either behaves as written. PR 2 is the two booleans with the supplier rule and the one-source-namespace refusal; PR 3 is the breaking source-scope wave, last only because it is the one that spends a coordinated bump. Two consequences the old cut did not have. The post-scan pass no longer lands whole -- its Ambiguous rule reads only the scan, while its supplier rule reads a field PR 2 introduces -- so the pass exists in PR 1 with one rule in it. And since every PR here is squashed, the internal commits exist for the review and nowhere afterwards: bisect and revert granularity is now the PR, and the changelog entry is the PR title. Ordering work inside a PR still shapes what a reviewer reads, which is why creating a kustomization.yaml stays the last commit of PR 2 -- but it buys nothing after the merge, and saying otherwise would be false comfort. The written-and-skipped scenario rule is the one property the merge cannot erode, because the test suite enforces it rather than history. Track C (patch authoring) is deliberately not one of the three: it blocks nothing and would tie a field rename to a fortnight of patch machinery. The alternative that keeps a literal count of three -- merging PRs 2 and 3 -- is recorded and refused, since it makes additive placement work breaking by association. **De-duplication.** Six passages had been stated in two or three places each, including one verbatim sentence across model.md and shapes/README.md. Resolved by one rule: the cross-cutting argument lives in one place and each shape states its own applied consequence in a line. model.md keeps the one-source-namespace argument, shape 2 keeps the worked failure, shapes/README keeps the per-shape table; "why only a leaf" and the overlay-bootstrap finding move to shapes/README with shapes 6 and 7 keeping what is specific to them; shapes 1 and 3 stop re-explaining what a target namespace does to a folder. build-order.md had begun duplicating itself -- a track table restating the PR table, and the independence claim made three times -- which is what its own header warns against. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…ze, and fence a namespace-free folder to one source namespace (#328) * feat(api): add spec.serializeNamespace, and honour it on every write A path decides where a document sits; it cannot decide what is inside it. spec.serializeNamespace does: a *bool at the top level of GitTargetSpec, where nil keeps inferring per document (today's behavior exactly), true always writes metadata.namespace, and false never does. It is not inside spec.placement because it governs the bytes of every write rather than the destination of a new one. The override reaches all three sites that decide whether the namespace is in the bytes: a new document's placement, an in-place update, and the identity a managed document is located by. The third one needed more than an override. A namespace-free document that no kustomization governs belongs, as the folder reads it, to no namespace at all, so the live object it mirrors matches nothing and the NEXT write of the same object appends a second copy of it beside the first. The store now attributes such a document to the target's single source namespace when the target declared the folder namespace-free (WithDeclaredNamespace, NamespaceDeclared), which is exactly what the one-source-namespace rule guarantees is single. Where the answer is not single, two namespaces or a wildcard, nothing is attributed. Shapes 2 and 4 of the layout corpus run unskipped. Their expected patches lose a pair of quotes the writer never emitted: every executed fixture already said `timeout: 15m`, and only the two skipped ones said `"15m"`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(git): refuse a second source namespace against an explicit serializeNamespace: false The write-plan precondition of docs/layout/model.md's second guard. A target that declared its folder namespace-free admits exactly one source namespace, and the second is refused before a byte moves. What two namespaces produce is not a collision but a MATCH: shop/config and billing/config both resolve to a config.yaml whose bytes carry no namespace, so their manifest identities are equal, the bundling rule never fires, and one document flips between two live objects with nothing in Git recording that it happened. Everywhere else in this model losing a distinction produces a refusal or a bundle; only here does it produce a match, which is why this one refuses. Only an EXPLICIT false is fenced. Inference is never constrained by it: a tree of nested roots is legitimately multi-namespace and namespace-free in its documents, and that is the case unset exists for. A rules[] item naming "*" is refused statically, under either reading of "*", with nothing enumerated. It is the correctness layer and it holds whatever admission did, so it lands first and alone; the WatchRule admission check follows. It is raised after the layout is published rather than with the acceptance gate: the folder is fine and its shape is still worth reporting, and what is wrong is the configuration pointed at it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(webhook): reject a WatchRule that brings a second source namespace The feedback half of the one-source-namespace rule. The precondition that shipped with it refuses the write, which leaves a target refusing every commit until someone reads its status; this rejects the rule at the moment the mistake is made, which is what an admission webhook is for. It is not the enforcement and must not be mistaken for it. The check is one-shot, so it cannot see a serializeNamespace flipped to false after the rules were created; the webhook is fail-open; and the operator runs perfectly well with no admission server at all. failurePolicy therefore stays Ignore even though this endpoint can now reject: the write path refuses the same configuration whether or not the webhook ran. Every way of failing to evaluate allows, deliberately. A rule naming a GitTarget that does not exist yet is ordinary rather than wrong, and a rejection the handler cannot justify would be a rejection of the user's object on the strength of a GitTarget nobody read. An UPDATE is judged on what the rule would become, excluding its own current value, so the edit that narrows a rule back to one namespace is not blocked by the namespace it is removing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(api): add placement.useKustomize, creating the root a folder has none of The one thing in this operator that writes a file nobody asked for by name, so it lands last and on its own. Its only job is the empty case: when no kustomization governs the path a new document lands at and the target declared useKustomize, a kustomization.yaml is created at spec.path and the document is registered in it in the same commit. Registering into a root that is already there happens either way; that is #319's invariant, not this flag. Three things follow from creating a root, and each is a real change rather than a detail: The new document goes BESIDE the root that is about to exist, not at the canonical path. Creating a root and then placing the document in a tree no resources: graph can reach would be worse than not creating one. It reports the same placement source as the rung it stands in for, adding no member to a metric label set that is a public contract. The created root carries namespace: only when exactly one source namespace reaches the target. That is what makes it a meaningful kustomization rather than an empty file, and it is what makes an accompanying serializeNamespace: false provable rather than trusted: the operator owns the file the omission depends on. The render oracle now treats a root that did not EXIST before the flush as having rendered nothing, rather than as a root that failed to build. The two were the same code path and they are not the same fault. This also deletes the corpus's harness-local GitTarget and the filter that hid the two unbuilt fields from the real type. Every scenario now decodes strictly into v1alpha3.GitTarget, which is what makes "the API the worked examples describe" and "the API that got built" one thing. Every skip naming PR 2 is gone; shape 8's images: authoring still names track C. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: describe both layout fields, and stop calling them unshipped configuration.md gains a section per field: what each value writes, why unset is not the same as false, and the one rule that refuses. The shapes and specific-examples sets lose the banner saying neither field exists in the current release, because both now do and every folder in them is executed against the write path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(placement): make a created kustomization adopt the folder it is written into Review finding: enabling useKustomize on a folder that already holds manifests wrote a root listing only the new document. Every other file stayed in Git and fell out of every render the moment a consumer ran kustomize build — mirrored to look at, applied by nothing, with no signal that it had happened. The oracle could not catch it either: a created root has no before-render to compare against. A created root now lists every managed document already in the folder as well as the new one, at the paths those files already have. Nothing is moved, rewritten or re-encoded. Turning a folder into a kustomize folder means the folder. Two boundaries come with it. A folder that already has a render root never gains a SECOND one, even when a declared template puts the new document outside the first: two render roots is the Ambiguous case, and an ambiguous folder stops accepting new documents at all, which is a far larger fault than one unregistered file. And a file some other kustomization governs is never adopted, because listing it twice is a duplicate resource kustomize refuses to build. The oracle stops reading adoption as a blast radius. Objects rendering for the first time under a root that did not exist before are rendering because the root was created, not because the flush touched them — it touched no byte of those files. The root must still build with the write applied, and every write intent must still render to its live object; only the before/after comparison for objects nobody targeted is skipped, and only for a root this flush wrote. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(placement): stop a created kustomization from pinning the namespace A created root now carries an apiVersion, a kind and its resources: list, and nothing else. spec.serializeNamespace: false means the artifact does not encode its deployment namespace; writing it into a root we authored honoured the letter of the field and broke its meaning, pinning the namespace one file up where it is harder to see and impossible for an installer to override. The namespace comes from the documents when the field is unset or true, and from whatever installs the folder when it is false. docs/design/created-root-namespace.md records the five answers considered, how likely each is to serve a real user, and why this one won. It also records the three facts an earlier revision got wrong: a namespace-less kustomization.yaml is ordinary (three already ship in our own corpus), both Flux and Argo supply one downstream, and what refused the namespace-less root was our own fidelity gate rather than kustomize. That gate is the real change. It compared the rendered namespace against the live object's whenever a flush touched a kustomization, so one declaration got two different answers depending only on whether a root file happened to exist: a namespace-free flat folder was never checked, and the same folder with a root was refused. The comparison now ignores metadata.namespace only when the render itself supplies none, and it is scoped by the RENDER rather than by the setting: a root declaring namespace: shop still rejects a live billing object, because that is a relocation and is exactly what the gate is for. Every other field is compared in every case. The second half of the review: under useKustomize, a placement no kustomization would render is now refused (GitPathAccepted=False, UnrenderedPlacement) instead of committed. It arises where the folder already has a render root and a template puts the document outside it — a second root would make the folder ambiguous, so the choice was to write a file nothing applies or to say so. Targets that never declared useKustomize are unaffected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: say how to choose between the two layout fields The reference sections describe each field on its own, which leaves a reader who has not decided anything yet with two independent switches and no way in. Two questions, three combinations people actually reach for, and the one combination to avoid: serializeNamespace: false on a folder nothing installs, where every document lands in default and nothing in the repository can warn you, because the installer is not in the repository. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Fixes #295. Split out of #318 so the fix can ship on its own — #318 stays open for the layout docs and the maintainer-review feedback.
What was wrong
A declared path into a subdirectory of a kustomize folder produced a document that no
kustomization.yamllists, sokubectl apply -knever rendered it. OnebyTypeline was enough to put a file in Git that nothing applies:governingKustomizationlooked only in the file's own directory, plus a special case for the write scope's root, so registration depended on whether render-root scoping happened to be in force — a difference the user cannot see.What changed
resources:list is not ours to edit. That also reachesnamespaceIsInheritedFromContext, which was unreachable for these paths: the document used to carry ametadata.namespaceline the folder's own documents omit.IdentityCompletePlacementTemplateno longer requires{version}. The built-in canonical path is deliberately versionless, because two served versions of one group/resource are the same object, so a version segment separates no identities. Requiring it judged the canonical shape as not identity-complete, which is what would make any spec-level default fail our own validation gate.Acceptance
byTypepath into a subdirectory of a kustomize folder is registered with the root that governs it, with a test that fails before the change.placement_kustomization_entries_totalno longer counts these asfailed.IdentityCompletePlacementTemplateaccepts the versionless canonical shape; the Secret-safety gate still rejects what it should.task lint,task testandtask test-e2eall pass locally (e2e: 80 passed, 0 failed, 23 skipped opt-in corners).🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes