Skip to content

[FEATURE] serializeNamespace and kustomizeRoot: the two things a path template cannot say #322

Closed

Description

@sunib

Successor to #293, which proposed replacing the placement path template with a spec.layout
discriminated union and was reversed. The template stays; the two things it genuinely cannot express
become additive fields. Design:
docs/layout/model.md, sequenced as PR 4 of
docs/layout/implementation-plan.md (both land
with #318).

The fields

serializeNamespace: Auto | Always | Never — whether a written document carries
metadata.namespace.

  • Auto (default): omit it when the governing kustomization already sets this resource's namespace.
    Today's inferred behavior, now named.
  • Always: always write it. The only safe choice when nothing downstream supplies it.
  • Never: never write it, and prove something else does.

The name deliberately avoids writeNamespace: "write" is the most loaded word in this API (the
write boundary, the write jail, WriteBoundaryRefused), and writeNamespace: Never invites the
reading "never write to this namespace", which is a permission — precisely what the neighbouring
sourceNamespace fields are.

kustomizeRoot: Adopt | Create | Require — what the target does about the folder's root.

  • Adopt (default): use the one that is there; do nothing if there is none.
  • Create: write a kustomization.yaml the user did not author, which is what bootstraps an empty
    repository and what makes serializeNamespace: Never provable there — the operator owns the file
    the omission depends on.
  • Require: refuse to write when no root governs the path, instead of committing documents that
    land in whatever namespace the applier defaults to.

CreatePerDirectory is deliberately deferred, with its trigger written down in model.md.

Why this is not breaking

Both defaults equal today's behavior, so this ships in any release without a coordinated consumer
bump. That is the largest consequence of the reversal: the whole placement story (PRs 1–4) is
additive, and #294's wave lost its largest member.

What has to ship with it

A post-scan validation pass, in PR 3, because these two fields make promises no CEL rule can
check — the precondition is a property of the observed folder, not of the spec:

Rule Precondition
serializeNamespace: Never requires a namespace supplier a kustomization with namespace: governs the path
kustomizeRoot: Require needs a root one governs the path
a declared single-root assertion the folder has exactly one root

Done when

The six scenarios in docs/layout/examples/ run
unskipped. PR 1 builds them as an executable corpus with every case that needs these fields skipped
and naming this work in its skip message, so this is finished when the last skip is gone — which
is the definition of done the plan is built to produce.

Open questions carried from the design

  • Does serializeNamespace: Never need to name its supplier (KustomizeRoot,
    FluxTargetNamespace, Asserted) so the post-scan pass checks the guarantee rather than infers
    which one was meant?
  • Is Require the right default for a folder that already contains a kustomization when the target
    is created?
  • Should placement.default gain a CRD default now that the shipped ancestor walk (fix(placement): register a new file with its nearest ancestor kustomization #319) registers
    a defaulted path with the kustomization that governs it? The remaining objection is legibility,
    not correctness.

Activity

  1. added a commit that references this issue on Aug 28, 2026
  2. sunib commented on Sep 3, 2026

    @sunib
    ContributorAuthor

    Shipped in #326 and #328, with a deliberately different API contract from the one proposed here.

    What shipped

    • spec.serializeNamespace as a nullable boolean, not Auto | Always | Never. Unset already is
      the third state and means infer, so the enum bought a spelling rather than a capability.
    • spec.placement.useKustomize as a boolean, not Adopt | Create | Require. Adopt and Create are
      the two states; Require was dropped.

    Why Require was dropped

    It and the "Never requires a namespace supplier" precondition both rest on proving a supplier
    exists in the repository. That check cannot be written honestly: the supplier of a namespace-free
    folder is a Flux targetNamespace or an Argo destination.namespace in a different cluster, and
    there may be more than one, so a rule keyed on the folder's own contents fires on the intended
    configuration. See
    model.md § Why false needs no guard.

    What replaced it is checkable from the config cluster alone: an explicit serializeNamespace: false
    admits exactly one source namespace, and a second is refused. The post-scan pass shipped with its
    one checkable rule, Ambiguous.

    Done-when is satisfied. The corpus runs, and no skip names this work; the single remaining skip
    names patch authoring.

    If the supplier proof is still wanted it needs a new issue arguing against that section, rather than
    reopening this one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions