Skip to content

[FEATURE] Placement visibility: status.layout, the ambiguous render root, and the metric/prose split #296

Description

@sunib

The legibility residue of #291. The sibling-inference deletion shipped, and so did the counters that
say where every new file landed (placements_total{source, disposition, gittarget_namespace, gittarget_name, group, version, resource}, placement_refusals_total{reason},
placement_kustomization_entries_total{outcome}). What is still missing is the durable half:
a metric says a fall-back happened, and nothing says what the operator understood about the folder.

Shapes, with five worked examples (greenfield, kustomize overlay, brownfield missing one rule, two
ambiguous roots, a refusal from an operator-configured sensitive type):
docs/layout/placement-visibility-and-declared-defaults.md
(moved there by #318; it was docs/design/...).

The field is status.placement, not status.layout. #293's spec.layout was reversed, and the
status field was renamed with it. The shape below is unchanged; read every status.layout here as
status.placement. It is PR 3 of
docs/layout/implementation-plan.md, and it ships
additively — none of this issue is breaking any more.

Contents

  • GitTarget.status.placement (config surface B2). Highest value per line of code in the
    config-surface doc, and more so after the deletion. An epoch-scoped per-target roll-up marked
    from the write path over the existing MarkTargetRetention seam, which already enqueues on
    change — that retires the "the data plane cannot notify the GitTarget" objection. It should carry
    the per-target record of which types resolved by declaration and which fell back.
    ambiguousDocuments in particular is a correctness-relevant fallback that is currently a
    debug-level store diagnostic.
  • renderRootReason: Ambiguous. Two supported kustomizations under one GitTarget produce a
    file that is unreachable and uncounted today. Publish the signal as its own series rather than
    as a third outcome on a counter that counts entry attempts. The refuse-or-write policy belongs
    to the post-scan validation pass, which ships in the same PR.
  • Split source="declared" into byType and default, keep canonical. Today a byType hit
    and a default hit are one series, so a catch-all quietly swallowing a type you meant to name
    explicitly looks identical to a rule working as intended. For a metric whose job is "is a rule
    missing?", that is the wrong place to lose resolution.
  • Unify the prose on "canonical". architecture.md still calls the built-in path "the built-in
    default" while the CRD comment calls it "the built-in canonical path", and placement.default is
    a user declaration. One name, one meaning.
  • {kindLower}, plus the single-namespace recipe and its identity caveat in
    configuration.md. A toLower function syntax would be a language; one variable answers the
    actual need.
  • Express the canonical path as a template constant rendered through RenderPlacementTemplate,
    pinned byte-for-byte against ResourceIdentifier.ToGitPath() across cluster-scoped, core,
    grouped and sensitive identities. Removes the hand-written canonicalPath duplication.

The Event question, still undecided

Whether a fall-back to canonical also raises a corev1.Event on the GitTarget. The split argued in
the design docs is: an Event for timeliness, status.placement for durability, because an Event is
deduplicated and expires (--event-ttl defaults to 1h) while a status field is what someone reads a day
later and the only one of the two a kubectl get -o yaml in a bug report contains.

Two facts constrain it: LocateNew runs on the branch worker with no reconcile context, so the write
path has to hand the fact back rather than emit it (the pathRefusalGitPathAccepted=False
projection is that seam); and that seam does not enqueue the GitTarget, so it surfaces on the next
requeue, up to ten minutes later — good enough for a durable condition, weak for a notification.

The metric now carries the actionable part in the meantime: one placements_total{source="canonical"}
series names the target and the exact byType key that is missing.

Relationship to the layout model

Both of this section's original premises are gone, and the conclusion survives them.

status.placement was to wait for #293's vocabulary; there is no discriminated union to borrow a
vocabulary from, and the field's own shape (renderRoot, serializeNamespace, observedRevision,
conditions) is settled in
implementation-plan.md. It was also to do double
duty as the Observe-mode dry run; spec.mode is dropped, and the dry run is now a suspended
target that keeps scanning — which needs this field just as much, so the reason to build it early is
unchanged.

It still ships with the placement work rather than alone, because the post-scan validation pass reads
the same scan. The metric-value split, {kindLower} and the template constant depend on nothing here
and can land any time.

Two items on this list have shipped in 0.42.1 via #319, and were tracked on the sibling issue
#295: registering a declared path with the kustomization that governs it, and dropping the {version}
requirement from IdentityCompletePlacementTemplate.

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