manifestanalyzer

package
v0.45.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 9, 2026 License: Apache-2.0 Imports: 31 Imported by: 0

Documentation

Overview

Package manifestanalyzer is a runtime-independent analyzer for a folder of Kubernetes manifests. It is the proof-of-concept core described in docs/spec/current-manifest-support-review.md: build the manifest model once, classify every file, and report what we know about it — without any controller runtime, and without writing anything.

The package is deliberately decoupled from the controller so the same logic can back both the live writer and a standalone CLI:

  • Filesystem access goes through fs.FS, so it runs against a git worktree, an arbitrary directory (os.DirFS), or an in-memory tree (fstest.MapFS).
  • The analysis path is strictly read-only; it produces a Report and never mutates the tree.

This first slice is structure-only and needs no cluster: it classifies files, detects duplicates, and reports the inventory of every GVK found. Comparing those GVKs against a live API (the "what is in the API" source of truth, which decides what is watched, unwatched, or orphaned) is a deliberate later step.

It builds on internal/git/manifestedit for the YAML mechanism (splitting, manifest identity, duplicate detection, SOPS handling) and adds classification, a bounded summary, and acceptance issues on top.

Index

Constants

View Source
const GitTargetIgnoreFileName = ".gittargetignore"

GitTargetIgnoreFileName is the basename of the in-repo escape hatch described in docs/spec/gitpath-foreign-content-stringency.md (§4). Exactly ONE copy is honoured — the file at the GitTarget path root — and the patterns it carries name content the operator must NEVER read, even when it is YAML. A copy deeper in the subtree is NOT honoured and is refused as foreign content (D-foreign-2).

View Source
const ReasonRefusedStructural = "refused-structural"

ReasonRefusedStructural is the support boundary: a render root whose kustomization uses a construct the writer cannot map back to editable source. It is the only render-root refusal reason now that external-base overlays are adopted through render-root scoping (the former forward-looking overlay-fan-out-unsupported reason is retired; the public pkg/manifestanalyzer constant is kept for consumers pinning the string, but the classifier no longer emits it). An overlay refused for a real structural fault carries that fault's own gate-issue code instead.

View Source
const SourceNamespaceWildcard = "*"

SourceNamespaceWildcard is the "every namespace" spelling of a rule's sourceNamespace. It is duplicated from api/v1alpha3 rather than imported, because this package is deliberately free of any Kubernetes API type dependency; the value is part of the CRD's user-facing contract and changing it would be a breaking API change either way.

The SPELLING is what is duplicated, and only the spelling. Its meaning has changed once already (from "every namespace the GitTarget admits" to "every namespace the credential can read") with no edit here, because everything this package does with it is refuse to guess how many namespaces it stands for.

Variables

This section is empty.

Functions

func GitPathRefusalReason added in v0.43.0

func GitPathRefusalReason(refused *AcceptanceRefusedError) string

GitPathRefusalReason maps a refusal onto the GitPathAccepted condition reason it is published under. A refusal made up PURELY of one recognised shape gets that shape's own reason; any mix falls back to the umbrella UnsupportedContent, because a mixed refusal has no single answer and naming one of its halves would send the reader to the wrong fix.

It lives here, beside the IssueKind constants it reads, rather than in the watch package that publishes it. Both the projection and the corpus that pins the fixtures need the same answer, and the corpus cannot import watch (watch imports git). Before this the mapping was in watch with three comments elsewhere asking callers to keep their strings in sync with it by hand.

The strings mirror the controller's GitTargetReason* constants — neither package can import controller without a cycle — and every one of them is a member of the controller's stalled-reason set, so a refusal surfaces as Stalled=True / kstatus Failed whichever it is.

func IdentityCompletePlacementTemplate

func IdentityCompletePlacementTemplate(tmpl string, narrowedToOneType bool) bool

IdentityCompletePlacementTemplate reports whether tmpl renders a distinct path for every distinct resource identity, which every accepted sensitive template must. narrowedToOneType is true for a ByType entry; a Default template must carry the type variables itself.

Those are {groupPath} and {resource}, deliberately NOT {version}: two served versions of one group/resource are the SAME object, so a version segment splits one identity rather than separating two.

func IsRemoteBaseEntry added in v0.37.0

func IsRemoteBaseEntry(entry string) bool

IsRemoteBaseEntry reports whether a resources/bases entry points outside the repository (a URL or a git/host-qualified path). The writer skips such an entry when resolving read scope: a remote base is refused before any build (the operator never fetches one), so it never becomes a directory to scan. It wraps the same predicate the acceptance gate and the pre-build refusal use, so the two cannot drift.

func KustomizationBuildRefs added in v0.37.0

func KustomizationBuildRefs(content []byte) ([]string, []string, bool)

KustomizationBuildRefs returns the local files a kustomization loads: its resources+bases graph entries and its patch `path:` files, relative to its own directory. Remote and inline entries name no local file, so they are omitted.

The live writer follows these to resolve the exact files an overlay reads from OUTSIDE its spec.path, so render-root scoping pulls in only what kustomize would load, never a whole sibling directory. See docs/design/support-boundary/render-root-scoping.md §4.

func LoadGitTargetIgnore

func LoadGitTargetIgnore(content []byte) (*IgnoreMatcher, []AcceptanceIssue)

LoadGitTargetIgnore parses the bytes of a root .gittargetignore into a matcher and the parse-time refusals its catastrophic patterns earn. Comments (#) and blank lines are skipped; every other line is a gitignore pattern. A file with no effective patterns yields a nil matcher (nothing ignored) and no issues. The matcher is always returned even when a catastrophic pattern is present — acceptance refuses on the issues, so the matcher is never consulted for a write in that case.

func PlacementTypeKey

func PlacementTypeKey(group, version, resource string) string

PlacementTypeKey renders the exact-type key used by GitTargetPlacementSpec.ByType: "{group}/{version}/{resource}", with the group segment omitted for core resources ("v1/secrets", "apps/v1/deployments", "cert-manager.io/v1/certificates").

func RefusalError

func RefusalError(acc Acceptance) error

RefusalError returns an *AcceptanceRefusedError when the acceptance was not accepted, or nil when the folder is clean. The writer calls this immediately after running the gate, so a refusal aborts the commit before any file is touched.

func RenderJSON

func RenderJSON(w io.Writer, rep Report) error

RenderJSON writes the report as indented JSON. It is the machine-readable form shared by the controller status path and the CLI.

func RenderPlacementTemplate

func RenderPlacementTemplate(tmpl string, vars map[string]string) (string, error)

RenderPlacementTemplate expands a brace-variable path template ("{namespace}/ secret-{name}.sops.yaml") against vars, then collapses empty path segments left behind by an omitted variable (e.g. "{groupPath}" for a core resource) so "{groupPath}/{version}/..." renders "v1/..." rather than "/v1/...". It returns an error naming any "{...}"-shaped placeholder that is not a known variable, so a typo in a declared template is caught rather than silently left as literal text.

func RenderRepoText

func RenderRepoText(w io.Writer, rep RepoReport)

RenderRepoText writes a compact human summary of the repo report: one line per candidate, then the roll-up. It is a convenience view; JSON is the contract.

func RenderScanText

func RenderScanText(w io.Writer, result ScanResult)

RenderScanText writes a human-readable view of a scan: the acceptance decision and its refusals, the retained allowlisted documents, and the full plan. It is the M5 dry-run output for the CLI and doubles as a GitTarget status summary.

func RenderText

func RenderText(w io.Writer, rep Report)

RenderText writes a human-readable summary of the report.

func ValidPlacementTemplatePath

func ValidPlacementTemplatePath(tmpl string) error

ValidPlacementTemplatePath statically rejects a template whose own literal text could render outside spec.path or with the wrong file name: "..", a leading "/", a "\" separator, or a non-YAML suffix. Runs at the Validated gate, before any scan, so a bad template fails fast and visibly instead of skipping resource by resource.

func ValidPlacementTemplateSyntax

func ValidPlacementTemplateSyntax(tmpl string) error

ValidPlacementTemplateSyntax reports whether tmpl references only known placement variables, independent of any resource identity — the check a GitTarget's Validated gate runs statically at reconcile time, before any repository scan.

func ValidateResolvedPlacementPath

func ValidateResolvedPlacementPath(p string) error

ValidateResolvedPlacementPath checks a fully-substituted placement path: non-empty, clean and relative, under spec.path, no backslashes, a real file name, a YAML suffix. Run on every path before a byte is written, because sanitizePlacementSegment defends each variable's VALUE but the template's own literal text is author-supplied and otherwise unconstrained.

func VerifyBatchRenders

func VerifyBatchRenders(
	before, after []manifestedit.FileContent,
	intents []WriteIntent,
	opts VerifyOptions,
) error

VerifyBatchRenders re-renders every root of the subtree twice — as the flush found it, and as the flush would leave it — and proves both halves of the oracle:

  1. every document the flush writes renders to exactly the live object, and
  2. every object it does NOT write is byte-for-byte unchanged.

(2) is not a nicety. A kustomization is shared context: an images: entry edited to converge one Deployment governs every other object it matches, and a base is rendered by every overlay above it. A proposal that fixes its own target and moves a second object has written a live value into a file another render root also reads — the one edit write-fan-in exists to forbid.

before and after are complete file trees. The cost is two builds per render root, once per flush, and it is only paid when the flush routed something through a kustomization.

Types

type Acceptance

type Acceptance struct {
	// Accepted is true only when no blocking refusal was found.
	Accepted bool
	// Issues names every refusal, each carrying the offending file and document so a
	// human (and GitTarget status) can resolve it. Empty when Accepted.
	Issues []AcceptanceIssue
	// Retained lists the allowlisted documents kept outside the managed model. It is
	// informational: retention never blocks acceptance on its own (only a managed
	// file that shares bytes with one does, via IssueMixedFile).
	Retained []RetainedDocument
}

Acceptance is the adoption gate between building the store and using it as the planning model. A folder is adopted only when it passes; any blocking refusal reconciles nothing until a human cleans the folder. It refuses:

  • a duplicate manifest identity (we will not guess which copy the author meant);
  • a managed file that is not entirely valid KRM, never an empty/non-KRM passenger. This is what lets the store drop the per-document index: an accepted file's documents are contiguous;
  • a standalone non-KRM or invalid YAML file;
  • unwatched API-backed KRM: refused, never pruned;
  • KRM the mapper cannot tie to a single served, watched resource and that is not allowlisted;
  • a watched resource outside this GitTarget's scope (right kind, wrong namespace);
  • a managed file mixing managed resources with an allowlisted non-API KRM document.

Non-YAML files and standalone empty documents are ignored and never refuse.

The mapping-aware refusals need an API source, so a structure-only store skips them and runs only the structural checks. See docs/spec/current-manifest-support-review.md.

func Accept

func Accept(store *ManifestStore, policy AcceptancePolicy) Acceptance

Accept runs the adoption acceptance gate over a built store. The store must have been built with policy.Allowlist (via buildStoreFS or Scan) so that allowlisted documents are already in store.Retained rather than FilesByPath; Accept does not re-derive retention. It is a pure function of (store, policy) and writes nothing.

func AcceptStructureOnly

func AcceptStructureOnly(store *ManifestStore) Acceptance

AcceptStructureOnly runs only the refusals that are pure structural facts about the folder. It NEVER runs the mapping-aware ones, which depend on live discovery and can blink on a wobble.

This is the live writer's entry point. Its store has a ready followability registry, so plain Accept would run the mapping refusals too — but the writer must refuse only on what is a problem from structure alone, never on a transient discovery fact.

type AcceptanceIssue

type AcceptanceIssue struct {
	Kind          IssueKind `json:"kind"`
	Path          string    `json:"path"`
	DocumentIndex int       `json:"documentIndex"`
	Message       string    `json:"message"`
	// Solvable says whether anyone can make this folder acceptable with the current
	// release, and Actor names who. Both are decided by the check that RAISED the issue,
	// because only that check knows: several kinds answer differently depending on which
	// branch emitted them. Actor is empty unless Solvable.
	Solvable bool  `json:"solvable"`
	Actor    Actor `json:"actor,omitempty"`
	// Field and Token add structured diagnostics for a render-fidelity refusal. They are empty
	// for structural acceptance issues, which predate the render-vs-live gate.
	Field string `json:"field,omitempty"`
	Token string `json:"token,omitempty"`
}

AcceptanceIssue is a fact about the tree that a stricter adoption policy may treat as blocking. The analyzer always reports issues; deciding whether they block (the refuse/scan/prune policy) is left to the caller.

func AmbiguousLayoutRefusal added in v0.43.0

func AmbiguousLayoutRefusal(resolution LayoutResolution, specPath string) []AcceptanceIssue

AmbiguousLayoutRefusal is the write-path form of the Ambiguous verdict: a folder covering several render roots is refused rather than silently placed into whichever one an arbitrary rule picks.

It refuses at the WRITE rather than at the GitTarget's Validated gate, and the difference is recoverability. Validated is evaluated before the data plane exists, so a target failing it never registers a worker, never scans, and could therefore never observe that the folder had been fixed — the refusal would be permanent, and for a target that had never scanned it could never fire in the first place. Refusing here keeps the target declared and scanning: the periodic resync re-reads the folder, so splitting the target down to a leaf overlay clears the refusal the same way fixing any other unsupported content does.

It returns no issue for a folder that is not ambiguous, so the caller can raise it unconditionally.

func MultipleSourceNamespacesRefusal added in v0.43.0

func MultipleSourceNamespacesRefusal(
	declaredNamespaceFree bool,
	namespaces []string,
	wildcard bool,
	specPath string,
) []AcceptanceIssue

MultipleSourceNamespacesRefusal is the write-plan precondition behind docs/layout/model.md § "The second guard": an explicit serializeNamespace: false admits exactly one source namespace, and the second is refused.

What follows from two 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 each write flips one document between two live objects. 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 guard refuses where the others report.

It is derived from one setting rather than inferred from two, so it needs no field of its own. The path plays no part either: a deployer applies bytes rather than filenames, so the rule keys on what serializeNamespace means and not on whether the template happens to omit {namespace}.

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 it is exactly the case unset exists for, so the refusal costs that user nothing beyond the setting that was already correct for them.

wildcard says a rule names "*". It is refused without enumerating anything: "*" is one cluster-wide watch, which cannot be shown to be one namespace from the spec alone. It was equally unprovable when "*" enumerated a GitTarget policy's admitted set, which is the property that let this survive that redefinition unchanged.

It returns no issue for a target that is not fenced, so a caller can raise it unconditionally.

func UnrenderedPlacementRefusal added in v0.43.0

func UnrenderedPlacementRefusal(useKustomize, governed bool, resolvedPath, renderRoot string) []AcceptanceIssue

UnrenderedPlacementRefusal refuses a placement that would commit a document nothing renders.

It arises in one shape. The folder already has a render root, a byType or default template puts the new document outside it, and creating a second root is not available: two render roots is Ambiguous, and an ambiguous folder stops placing new documents at all. What is left is to write the file unrendered or to refuse, and under useKustomize the target has declared this folder is a kustomize folder — a file no resources: list names is one that looks mirrored in Git and is applied by nothing, which is the failure #295 was and #319 made an invariant.

It is raised ONLY under useKustomize. A target that made no claim about kustomize keeps today's behavior, so this is not a new refusal for folders that never asked for one.

type AcceptancePolicy

type AcceptancePolicy struct {
	// Allowlist names the non-API KRM kinds retained outside the managed model. It
	// is applied at store-build time (buildStoreFS), so allowlisted documents never
	// enter FilesByPath; the gate only refuses a managed file that illegally shares
	// bytes with one.
	Allowlist Allowlist
	// InScope reports whether a resolved resource belongs to this GitTarget's scope.
	// A nil predicate means "no scope restriction": every resolved resource is in
	// scope. The controller injects a namespace-aware predicate (M7); the CLI passes
	// nil.
	InScope func(types.ResourceIdentifier) bool
}

AcceptancePolicy configures the gate. The zero value allows no non-API KRM and restricts no scope, which is the structure-only analyzer / CLI default.

type AcceptanceRefusedError

type AcceptanceRefusedError struct {
	Issues []AcceptanceIssue
}

AcceptanceRefusedError is the writer-facing error for a GitTarget folder the acceptance gate refused. It carries every issue so the surface (GitTarget status / a blocked stream) can name the offending file and reason. errors.As recovers it from a wrapped flush or resync error, so the watch layer can translate a refusal into a Blocked stream while a plain write fault keeps its existing handling.

func (*AcceptanceRefusedError) AllIssuesOfKinds

func (e *AcceptanceRefusedError) AllIssuesOfKinds(kinds ...IssueKind) bool

AllIssuesOfKinds reports whether every issue in the refusal is one of the given kinds. The surface uses it to pick a precise status reason: a refusal made up purely of IssueIgnoreShadowsManaged is the unrecoverable .gittargetignore-shadows-a-write case (§4.3), and one made up purely of the write-boundary kinds (IssueWriteEscapesScope, IssueWriteFanIn) is a refused write-boundary violation — each deserves its own reason, whereas any mix falls back to the umbrella UnsupportedContent. An empty issue set returns false.

func (*AcceptanceRefusedError) BlockMessage

func (e *AcceptanceRefusedError) BlockMessage() string

BlockMessage returns a bounded, human-readable one-liner suitable for a GitTarget status condition / stream-block message. It is the same text as Error today, named separately so the surface intent is explicit at the call site.

func (*AcceptanceRefusedError) Error

func (e *AcceptanceRefusedError) Error() string

Error returns a bounded one-liner: the first offending file and reason, plus a count of any others. Stable ordering comes from Accept's sortIssues, so the "first" issue is deterministic.

type Actor added in v0.40.0

type Actor string

Actor names who can solve a refusal. It is empty unless the refusal is Solvable — naming someone for a refusal they cannot act on is worse than naming nobody.

It exists because some refusals are solvable ONLY by the person who owns the GitTarget, who is usually not the person reading the message. Rendering an out-of-scope refusal as "fix your repository" to a repository author who cannot is the failure this half prevents.

Which scan can report which value

CLUSTER-AWARENESS is the gate, and it is structural rather than incidental. A STRUCTURE-ONLY scan — one whose typeset.Lookup is not ready, which is every ScanFolder and ScanRepo — reports only ActorUnknown or ActorRepositoryAuthor. It cannot reach ActorPlatformOperator through any of that value's three acceptance sites: IssueOutOfScope needs a declared AcceptancePolicy.InScope, IssueUnresolvedKRM needs MappingNotFollowable, which a not-ready registry never produces because it resolves every document to MappingNoSource instead, and IssueAmbiguousLayout is raised only by the live write path (AmbiguousLayoutRefusal has one caller, in the flush planner) — a scan resolves the layout but never refuses on it.

So a consumer of a structure-only report has a platform-operator branch that never fires, and that is by design: a scan that cannot see the cluster cannot know a CRD is missing or a GitTarget's scope is narrow. Only a cluster-aware scan and the live write path name the platform operator. TestStructureOnlyScanNeverNamesThePlatformOperator pins it.

const (
	// ActorUnknown is the zero value: nobody can solve this refusal, or the check did not
	// say who.
	ActorUnknown Actor = ""
	// ActorRepositoryAuthor is the person who owns the files in the repository.
	ActorRepositoryAuthor Actor = "repository-author"
	// ActorPlatformOperator is the person who owns the GitTarget — its scope, its path,
	// and the CRDs installed in the cluster it mirrors.
	ActorPlatformOperator Actor = "platform-operator"
)

type Allowlist

type Allowlist struct {
	// contains filtered or unexported fields
}

Allowlist is the set of build-directive files that are retained on disk but never materialised — kustomization.yaml and friends. Membership is keyed by file basename, not GVK: a real kustomization.yaml carries no metadata.name, so it never becomes a KRM record and a GVK match would never see it. Matching the basename (as kustomize itself does) recognises the file regardless of its contents. The zero value allows nothing, the structure-only analyzer / legacy report default.

func DefaultAllowlist

func DefaultAllowlist() Allowlist

DefaultAllowlist returns the built-in build-directive allowlist: the kustomize entrypoint filenames, which are KRM but never served by the Kubernetes API. It returns a fresh value on every call, so no shared global state can be mutated.

func NewAllowlist

func NewAllowlist(basenames ...string) Allowlist

NewAllowlist builds an allowlist from the given file basenames. No names yields the empty allowlist (allows nothing).

func WriterAllowlist

func WriterAllowlist() Allowlist

WriterAllowlist returns the allowlist the live writer and resync apply build their store with: the kustomize build directives (DefaultAllowlist) plus the operator's own non-KRM bootstrap artifact, the ".sops.yaml" creation-rules config. That file legitimately lives in a managed subtree (it is staged by the bootstrap template when encryption is configured) but is never KRM the operator materialises, so the acceptance gate must retain it rather than refuse it as a standalone non-KRM file. Encrypted Secret payloads keep their own "<name>.sops.yaml" basenames and are still materialised as the KRM Secrets they are.

func (Allowlist) Allows

func (a Allowlist) Allows(path string) bool

Allows reports whether the file at path is an allowlisted build directive, matching on basename.

type CauseKind

type CauseKind string

CauseKind is the structured kind of a DocumentCause.

const (
	// CauseNone is a cleanly editable document — no impediment.
	CauseNone CauseKind = ""
	// CauseEncrypted is a SOPS-encrypted document: authoritative but never patched
	// in place.
	CauseEncrypted CauseKind = "encrypted"
	// CauseNonEditable is a document using a construct the editor refuses (anchor,
	// alias, merge key, unusual tag, duplicate key).
	CauseNonEditable CauseKind = "non-editable"
)

type Class

type Class string

Class is the bucket a file or document falls into, mirroring the design doc: non-YAML files are ignored, non-KRM YAML is the dangerous unknown, and KRM is every valid Kubernetes manifest. Which GVKs those manifests are is reported via the GVK inventory (Summary.ByGVK) rather than by sub-classing the bucket; comparing them against a live API is a deliberate later step.

const (
	// ClassNonYAML is a file that is not YAML by extension. Always ignored.
	ClassNonYAML Class = "non-yaml"
	// ClassEmpty is a YAML document that is empty or comment-only.
	ClassEmpty Class = "empty"
	// ClassInvalidYAML is a document that does not parse as YAML.
	ClassInvalidYAML Class = "invalid-yaml"
	// ClassNonKRM is valid YAML that is not a Kubernetes manifest.
	ClassNonKRM Class = "non-krm"
	// ClassKRM is a valid Kubernetes manifest.
	ClassKRM Class = "krm"
)

type Classification added in v0.40.0

type Classification struct {
	Solvable bool
	Actor    Actor
}

Classification is the pair a raise site attaches to a refusal: can it be solved, and by whom. Passing them together keeps a solvable refusal from losing the actor half on its way out of the check.

type DesiredResource

type DesiredResource struct {
	Resource types.ResourceIdentifier
	Object   *unstructured.Unstructured
}

DesiredResource is one resource in the COMPLETE desired snapshot the planner compares Git against: a resource the cluster currently has, paired with the API-side identity the controller already resolved from the GVR it watched (or the CLI from the mapper). Carrying the ResourceIdentifier is what lets a create place its new file (ResourceIdentifier.ToGitPath) at apply time (M7) without re-resolving the mapping.

A FULL-SNAPSHOT input, never a partial batch: BuildPlan mark-and-sweeps every watched document absent from this set as a managed drop.

Object must be non-nil. A nil entry is malformed, deliberately NOT a delete tombstone: in a sweeping planner a lone tombstone is indistinguishable from "every other document is an orphan". It cannot be skipped either, since skipping would leave the matching document unmatched and let the sweep DROP it. BuildPlan instead protects that document and emits a diagnostic. A genuine per-event delete is PlanDelete, which targets one identity and never sweeps.

type DocumentCause

type DocumentCause struct {
	Kind   CauseKind `json:"kind,omitempty"`
	Detail string    `json:"detail,omitempty"`
}

DocumentCause is the structured reason a document is not cleanly editable. Kind drives classification; Detail is a short, display-only token (e.g. the offending construct) and is never read to make a decision.

type DocumentModel

type DocumentModel struct {
	// ManifestIdentity is the EFFECTIVE content identity (apiVersion + kind +
	// namespace + name). For a namespace-less namespaced resource it may carry a
	// namespace inherited from a kustomization.yaml that references the document
	// through its resources graph; NamespaceSource records that provenance.
	ManifestIdentity manifestedit.Identity

	// NamespaceSource records where ManifestIdentity.Namespace came from: the file
	// itself, a kustomization context, or nowhere (absent and unsupplied/ambiguous).
	// See NamespaceSourceKind.
	NamespaceSource NamespaceSource

	// Overrides is the unambiguous kustomize images/replicas override chain
	// governing this document's file, in build order — nil when no supported
	// render root supplies one, or when distinct roots disagree (the
	// ambiguous-kustomize-overrides diagnostic). The writer routes a live change
	// produced by one of these entries back to the entry instead of writing it
	// through into the source document. See
	// docs/design/support-boundary/finished/images-and-replicas-edit-through.md.
	Overrides *KustomizeOverrides

	// Rendered is what kustomize ACTUALLY renders this document to, plus which override entry
	// supplied each value: values off the real render, suppliers off a dyed counterfactual one.
	//
	// Nil when no root supplies a chain, roots disagree, or the dyed build could not be trusted.
	// Nil means NO ATTRIBUTION: the writer routes nothing to an entry.
	// See docs/design/support-boundary/render-attribution.md.
	Rendered *RenderedOverrides

	// ResourceIdentity is the API-side identity (GVR + namespace + name). It is set
	// only when the injected GVK->GVR mapper resolves the document's GVK to a single
	// served, allowed resource; structure-only analysis (and any unresolved lookup)
	// leaves it nil.
	ResourceIdentity *types.ResourceIdentifier

	// Mapping records why ResourceIdentity is or is not set, derived from the
	// followability registry. Structure-only analysis is always MappingNoSource
	// because no API source is wired in.
	Mapping MappingOutcome

	// MappingRefusal carries the solvability of a MappingNotFollowable outcome, decided
	// where the registry answer is still in hand. A GVK the registry has never heard of
	// is a CRD the platform operator can install; a GVK it knows but will not follow —
	// ambiguous, unserved, missing a verb — is not something either side can solve. The
	// acceptance gate sees only the outcome, so the split has to be recorded here.
	MappingRefusal Classification

	// Editable is false for SOPS-encrypted or otherwise non-patchable documents;
	// Cause carries the structured reason.
	Editable bool

	// Cause is the structured reason behind Editable — never free-text
	// classification. CauseNone for a cleanly editable document.
	Cause DocumentCause

	// Snapshot is the lazy body handle. It is unbuilt (zero) until a plan action
	// touches the document; identity indexing needs only a cheap header parse.
	Snapshot manifestedit.SnapshotRef
}

DocumentModel is one managed KRM document, byte-free: the node tree is built only when a plan action touches it. It stores neither file path nor position. The path is the containing FileModel's; the index is reconstructed from record-less diagnostic gaps, since every non-managed document leaves a diagnostic at its position. That keeps the report, the planner and the acceptance gate agreeing without a fragile mutable field.

func (*DocumentModel) NamespaceAbsentFromFile added in v0.43.0

func (dm *DocumentModel) NamespaceAbsentFromFile() bool

NamespaceAbsentFromFile reports whether the document's namespace is NOT in its own bytes — it came from a kustomization's namespace: transformer, or from the GitTarget's declaration that this folder's documents carry none. The writer uses it for the two decisions that follow from the bytes rather than from any setting: keep metadata.namespace out of the file on an update, and locate the document by its raw (namespace-less) identity.

func (*DocumentModel) NamespaceInheritedFromContext

func (dm *DocumentModel) NamespaceInheritedFromContext() bool

NamespaceInheritedFromContext reports whether the document's effective namespace comes from build context (a kustomization.yaml) rather than metadata.namespace in the file. The writer uses it to keep metadata.namespace out of the file and to locate the document by its raw (namespace-less) identity in the file bytes.

type DocumentReport

type DocumentReport struct {
	Index    int                   `json:"index"`
	Class    Class                 `json:"class"`
	GVK      GVK                   `json:"gvk"`
	Identity manifestedit.Identity `json:"identity"`
	Editable bool                  `json:"editable"`
	// Cause is the structured reason a KRM document is not cleanly editable
	// (encrypted, non-editable construct). It is nil for an editable document and
	// for non-KRM/empty/invalid rows. Duplicate identity is no longer a per-document
	// attribute — it surfaces as an acceptance issue and a diagnostic instead.
	Cause *DocumentCause `json:"cause,omitempty"`
}

DocumentReport describes one YAML document inside a file.

type EntryRole

type EntryRole int

EntryRole is the role a single walked filesystem entry falls into under the foreign-content policy. It is the pure verdict shared by every folder walker (the fs.FS analyzer scan and the live writer's worktree scan) so the .gittargetignore filter, the operator-artifact recognition, and the foreign-content refusal can never drift between the two paths.

const (
	// RoleIgnored is an entry that is recognized and deliberately NOT modeled: an
	// ignored file/symlink, or the one root .gittargetignore itself. The walker does
	// nothing with it — never reads it, never refuses it.
	RoleIgnored EntryRole = iota
	// RoleSkipDir is a directory the walker must not descend: the .git metadata directory,
	// or a whole subtree the root .gittargetignore matches (the "never read" semantic).
	RoleSkipDir
	// RoleManagedYAML is a YAML file the walker must read into the model (managed KRM, or a
	// retained build directive / operator .sops.yaml the store's allowlist handles).
	RoleManagedYAML
	// RoleOperatorArtifact is an accepted non-YAML operator artifact (README.md). It is
	// listed in the report's non-YAML inventory but is never foreign.
	RoleOperatorArtifact
	// RoleBenignPassenger is an accepted non-YAML file the operator never manages but that a
	// careful human keeps in a GitOps folder: documentation (*.md / *.markdown), a license
	// (LICENSE / COPYING / NOTICE), and Git-hygiene metadata (.gitignore / .gitattributes /
	// .gitkeep / .keep). Unlike an operator artifact it is USER content, so it is recognized
	// AFTER the root .gittargetignore filter (a user may still ignore it) rather than being
	// protected from it. It is listed in the non-YAML inventory and is never foreign, so its
	// mere presence no longer refuses the whole folder — the reverser can adopt an existing
	// repo without a human first hand-writing a .gittargetignore for its README and license.
	RoleBenignPassenger
	// RoleForeignFile is a foreign non-YAML regular file: refused.
	RoleForeignFile
	// RoleForeignSymlink is a foreign symlink: refused.
	RoleForeignSymlink
	// RoleDescend is a normal directory the walker descends into.
	RoleDescend
)

func ClassifyEntry

func ClassifyEntry(rel string, d fs.DirEntry, ignore *IgnoreMatcher) EntryRole

ClassifyEntry decides the role of one walked entry. rel is the entry's slash-separated path relative to the scanned root; d is its directory entry; ignore is the active root matcher (nil when the path carries no .gittargetignore). It is a pure function — the single source of truth for the precedence in §4.1 of the design:

operator artifacts + build directives  →  root .gittargetignore filter  →
  managed KRM  →  benign passenger  →  foreign

so a user cannot use .gittargetignore to hide the operator's own files (README.md, .sops.yaml) or to silence a hard-kustomize refusal (kustomization.yaml). Benign-passenger hygiene files (a license, docs, .gitignore) are accepted after the ignore filter — so they no longer refuse a folder yet remain user-suppressible — while every other unknown non-YAML entry is refused unless an ignore pattern names it.

type FileModel

type FileModel struct {
	// Path is the file location relative to the scanned root.
	Path string

	// Documents are every managed document in the file, in document order.
	Documents []*DocumentModel

	// Original and Current are hydrated lazily at the commit boundary, and only for
	// the files a batch touches — they are nil for every untouched file, so the
	// resident store is byte-free. Structure-only analysis never hydrates, so both
	// stay nil.
	Original []byte // worktree bytes once hydrated; nil for a new or unhydrated file
	Current  []byte // bytes after applying plan actions; nil means "delete this file"
}

FileModel is one managed file under the scanned root. Its document set and classification are resident and cheap (header parse only); its bytes are hydrated lazily and only at a commit boundary.

func (*FileModel) Deleted

func (f *FileModel) Deleted() bool

Deleted reports whether the file should be removed (its last managed document was dropped). It is derived, never stored.

func (*FileModel) Dirty

func (f *FileModel) Dirty() bool

Dirty reports whether the file's bytes changed and should be re-written. It is derived, never stored: two byte slices are the whole state machine.

type FileReport

type FileReport struct {
	Path      string           `json:"path"`
	IsYAML    bool             `json:"isYaml"`
	Documents []DocumentReport `json:"documents,omitempty"`
}

FileReport describes one file under the scanned root. Non-YAML files carry no documents; YAML files carry one DocumentReport per document.

type FolderScan

type FolderScan struct {
	YAMLFiles    []manifestedit.FileContent
	NonYAML      []string
	Foreign      []ForeignEntry
	Ignore       *IgnoreMatcher
	IgnoreIssues []AcceptanceIssue
	Diagnostics  []manifestedit.Diagnostic
}

FolderScan is the structural view of a scanned GitTarget subtree: the YAML files to model, the non-YAML inventory for the report, the foreign entries to refuse, and the active root .gittargetignore matcher with any parse-time refusals. It is the one shape every folder walker produces, so the analyzer scan and the live writer feed the store and the acceptance gate identically.

type ForeignEntry

type ForeignEntry struct {
	Path string      `json:"path"`
	Kind ForeignKind `json:"kind"`
}

ForeignEntry is one filesystem entry under spec.path that matches no recognized role (managed KRM, active build directive, operator artifact, or .gittargetignore-ignored) and is therefore refused. Path is slash-separated and relative to the scanned root.

type ForeignKind

type ForeignKind string

ForeignKind classifies a non-managed filesystem entry found under a GitTarget path — the foreign role of the five-role model in docs/spec/gitpath-foreign-content-stringency.md (§3). A foreign entry is refused, not ignored: the path is an operator-exclusive subtree.

const (
	// ForeignFile is a non-YAML regular file in no recognized role (notes.txt, deploy.sh,
	// blob.bin, a nested .gittargetignore). YAML that is not managed KRM is already refused
	// as non-KRM, so this names only the non-YAML case.
	ForeignFile ForeignKind = "file"
	// ForeignSymlink is any symlink under the subtree. A writer materialising into a folder
	// with a symlink could follow it out of the subtree, so it is refused rather than skipped.
	ForeignSymlink ForeignKind = "symlink"
	// ForeignSubmodule is a gitlink / git submodule under the subtree — content the operator
	// cannot own or reason about. It is part of the model so a future gitlink-aware scan can
	// surface it; the structural fs.FS walk does not currently detect submodules (a nested
	// .git directory is skipped like any VCS metadata), so this is reserved for that hardening.
	ForeignSubmodule ForeignKind = "submodule"
)

type GVK

type GVK struct {
	Group   string `json:"group"`
	Version string `json:"version"`
	Kind    string `json:"kind"`
}

GVK is a parsed group/version/kind. Group is empty for core resources.

func ParseGVK

func ParseGVK(apiVersion, kind string) GVK

ParseGVK derives a GVK from a manifest's apiVersion and kind. An apiVersion of "apps/v1" yields group "apps"; a bare "v1" yields an empty group.

func (GVK) Empty

func (g GVK) Empty() bool

Empty reports whether the GVK carries no information (a non-KRM document).

func (GVK) String

func (g GVK) String() string

String renders a GVK as "group/version/kind", or "version/kind" for core.

type IgnoreMatcher

type IgnoreMatcher struct {
	// contains filtered or unexported fields
}

IgnoreMatcher is the parsed, active root .gittargetignore: a go-git gitignore matcher plus the raw patterns it was built from. It is reused git's own matching semantics rather than reinventing glob handling. A nil *IgnoreMatcher matches nothing, so callers need no nil guard around Match.

func (*IgnoreMatcher) Match

func (m *IgnoreMatcher) Match(path string, isDir bool) bool

Match reports whether the slash-separated path is ignored (isDir distinguishes a directory match such as "docs/"). A nil matcher never matches.

func (*IgnoreMatcher) MatchingPattern

func (m *IgnoreMatcher) MatchingPattern(path string, isDir bool) string

MatchingPattern returns the raw pattern that causes path to be ignored, or "" when the path is not ignored. It mirrors Match's last-wins priority so a shadowing diagnostic can name the exact pattern at fault (§4.3). A nil matcher returns "".

type ImageOverride

type ImageOverride struct {
	// Source is the kustomization file path (slash) that declares the entry.
	Source string
	// Index is the entry's position within its file's images: sequence, so the
	// writer can pin the exact entry even when two entries share a name.
	Index int
	// Name matches an image whose name equals it at that point in the build chain.
	Name string
	// NewName / NewTag / Digest replace the matched image's components; each is
	// meaningful only when its Has* flag is set.
	NewName string
	NewTag  string
	Digest  string
	// HasNewName / HasNewTag / HasDigest record which keys the entry declares.
	HasNewName bool
	HasNewTag  bool
	HasDigest  bool
}

ImageOverride is one parsed images: entry, carrying the kustomization file it came from so the writer knows which file to edit. The Has* booleans record key presence: the writer only ever updates a field the entry already declares.

type IssueKind

type IssueKind string

IssueKind classifies an acceptance issue.

const (
	// IssueImpureManagedFile marks a file holding managed resources that also holds a
	// non-managed document (empty/comment-only, non-KRM, or invalid YAML). A managed
	// file may contain only valid KRM documents.
	IssueImpureManagedFile IssueKind = "impure-managed-file"
	// IssueMixedFile marks a managed file that also holds an allowlisted non-API KRM
	// document. Allowlisted KRM must be retained in its own file.
	IssueMixedFile IssueKind = "mixed-managed-allowlisted"
	// IssueUnresolvedKRM marks recognised KRM the followability registry could not tie
	// to a single served, followable resource and that is not allowlisted (not served,
	// denied by policy, ambiguous, or missing a verb). It is refused, never pruned.
	IssueUnresolvedKRM IssueKind = "unresolved-krm"
	// IssueOutOfScope marks a watched kind whose resource falls outside this
	// GitTarget's scope (right kind, wrong namespace).
	IssueOutOfScope IssueKind = "out-of-scope"
	// IssueUnsupportedKustomize marks a kustomization using a feature the writer cannot map back
	// to editable source. The folder is refused rather than written, because the operator cannot
	// take responsibility for content produced that way.
	//
	// `patches:` is NOT on that list: a strategic-merge patch named by path is tolerated as
	// read-only build context. The shapes we cannot read still refuse by name.
	IssueUnsupportedKustomize IssueKind = "unsupported-kustomize"
	// IssueForeignFile marks a non-YAML regular file under spec.path that matches no
	// recognized role — the operator-exclusive subtree refuses content it cannot manage
	// (docs/spec/gitpath-foreign-content-stringency.md §3). Foreign YAML is already
	// refused as IssueNonKRM; this is the non-YAML case the gate was previously blind to.
	IssueForeignFile IssueKind = "foreign-file"
	// IssueForeignSymlink marks any symlink under spec.path. A writer could follow it out
	// of the subtree, so it is refused rather than silently skipped.
	IssueForeignSymlink IssueKind = "foreign-symlink"
	// IssueForeignSubmodule marks a gitlink / git submodule under spec.path — content the
	// operator cannot own or reason about.
	IssueForeignSubmodule IssueKind = "foreign-submodule"
	// IssueIgnoreShadowsManaged marks a .gittargetignore that would blind the operator to a
	// path it writes (§4.3): a catastrophic parse-time pattern, or — via the writer's
	// write-plan precondition — an ignore pattern matching a planned write/edit/delete path.
	// It surfaces as the GitTarget reason IgnoreShadowsManagedPath.
	IssueIgnoreShadowsManaged IssueKind = "ignore-shadows-managed"
	// IssueWriteEscapesScope marks a planned write whose path escapes the GitTarget write
	// scope (spec.path) — an absolute or ".."-escaping destination. It is the write-plan half
	// of the L1 write-boundary invariant: the operator reads shared context outside the scope
	// but never writes outside it. Enforced by the writer's pathScopePrecondition; today it is
	// defense-in-depth (planned write paths are base-relative by construction), made explicit
	// and tested per
	// docs/design/support-boundary/gittarget-granularity-and-cross-environment-edits.md §1.
	IssueWriteEscapesScope IssueKind = "write-escapes-scope"
	// IssueWriteFanIn marks a planned in-place edit of a source file that more than one
	// kustomize render path reaches with override entries at stake (write-fan-in > 1). Writing
	// the change through would corrupt what another render root renders, so the flush is
	// refused instead of falling back to write-through. It is the L2 write-boundary invariant
	// made explicit; the broader "any file shared by multiple render roots" generalization is
	// Per-render-root scoping would generalize this.
	IssueWriteFanIn IssueKind = "write-fan-in"
	// IssueRenderRefused marks a planned write that kustomize itself will not vouch for: the
	// flush was re-rendered with the write applied, and either the edited document did not
	// come out as the live object, or the write moved an object it never set out to touch.
	//
	// The write-plan half of "attribution may be heuristic, verification may not": the projection
	// is ALLOWED to guess which file an edit belongs in precisely because this refuses the guess
	// when the renderer disagrees. It must refuse LOUDLY, because a write that does not survive
	// the re-render would never converge and absorbing it leaves a resource silently un-mirrored.
	// See docs/design/support-boundary/render-attribution.md §5.
	IssueRenderRefused IssueKind = "kustomize-render-refused"
	// IssueRenderDoesNotMatchLive marks a rendered ${...} value whose corresponding live field is
	// absent or different. It is a runtime fidelity refusal, distinct from structural Git-path
	// acceptance and from a proposed write that fails the post-write render oracle.
	IssueRenderDoesNotMatchLive IssueKind = "render-does-not-match-live"
	// IssueUnplaceableEdit marks a live change the projection could not place in the source
	// document: the BUILD and the USER both rewrote one list whose elements carry no unique
	// name to pair the source's with the render's by (see SourceFormRefusedError).
	//
	// Aligning the two lists by position is not a conservative guess but measurably wrong:
	// kustomize's strategic merge PREPENDS a container a patch adds, so the source's first element
	// is not the render's first in exactly the case where it matters. Writing one element's fields
	// into another is corruption no re-render can catch, since the patch re-imposes its values.
	// This is the one place the oracle cannot see, so it must fail loudly.
	IssueUnplaceableEdit IssueKind = "unplaceable-edit"
)

IssueKind values added by the acceptance gate, beyond the structure-only IssueDuplicate / IssueNonKRM / IssueInvalidYAML the analyzer already reports.

const (
	// IssueDuplicate marks a document that duplicates an earlier manifest identity.
	IssueDuplicate IssueKind = "duplicate-identity"
	// IssueNonKRM marks YAML that does not parse as a Kubernetes manifest.
	IssueNonKRM IssueKind = "non-krm-yaml"
	// IssueInvalidYAML marks a document that does not parse as YAML.
	IssueInvalidYAML IssueKind = "invalid-yaml"
)
const IssueAmbiguousLayout IssueKind = "ambiguous-layout"

IssueAmbiguousLayout marks a GitTarget folder covering more than one render root. It is a property of the OBSERVED folder rather than of the spec, so no CEL rule and no admission check can reach it — the folder is only ambiguous once the operator has read it.

const IssueMultipleSourceNamespaces IssueKind = "multiple-source-namespaces"

IssueMultipleSourceNamespaces marks a GitTarget that declared its folder namespace-free (spec.serializeNamespace: false) and that more than one source namespace reaches.

Unlike every other kind in this package it is a property of the CONFIGURATION rather than of the observed folder: it is decided from the GitTarget and the WatchRules naming it, needs no scan and no repository state, and is raised here only because this is where the writer's refusals and their status reasons live together.

const IssueUnrenderedPlacement IssueKind = "unrendered-placement"

IssueUnrenderedPlacement marks a new document a kustomize folder would hold but never render: spec.placement.useKustomize declares the operator maintains this folder's root, and the path the document resolved to is governed by no resources: list.

Like IssueMultipleSourceNamespaces it is not a property of the folder's CONTENT — the folder is perfectly good kustomize — but of the configuration aimed at it, and it is raised at the write because only the write knows where the document was about to land.

type KustomizationInfo

type KustomizationInfo struct {
	// Path is the kustomization.yaml's own file path (slash), relative to the
	// scanned root.
	Path string

	// Resources holds the resources + bases entries exactly as written (local file
	// names, child-directory bases, or remote URLs), in file order. It is the raw
	// text, not resolved paths — cleanJoin resolves an entry against Path's directory.
	Resources []string

	// Unsupported is true when the kustomization uses a feature outside the
	// supported subset (hasUnsupportedKustomizeFeature) or a remote base, or is
	// unparseable. The writer must never edit an unsupported kustomization.
	Unsupported bool

	// Namespace is the kustomization's namespace: transformer value, empty when
	// it sets none. A new document placed directly in this kustomization's
	// directory (resolveKustomizeRoot) omits metadata.namespace when this is
	// set, exactly as an existing document in this context already does.
	Namespace string
}

KustomizationInfo is the write-relevant view of one kustomization.yaml exposed for new-file placement: whether the directory it lives in carries a supported kustomization and, if so, its local resources/bases entries (raw, relative to its own directory) — the list a new sibling file must be added to so kustomize includes it.

func GoverningKustomization added in v0.43.0

func GoverningKustomization(store *ManifestStore, writeScope, resolvedPath string) *KustomizationInfo

GoverningKustomization is governingKustomization for the writer, which has to ask a question LocateNew's result cannot answer: PlacementResult.Kustomization is set only when a governing root exists AND does not already list the path, so a nil there means either "no root" or "already listed". Creating a root is only correct for the first of those.

type KustomizeOverrides

type KustomizeOverrides struct {
	Images   []ImageOverride
	Replicas []ReplicaOverride
}

KustomizeOverrides is the flattened, unambiguous override chain governing a document: every images:/replicas: entry from the kustomizations along the single reference path root→file, in build order (innermost kustomization's entries first — kustomize renders bases before applying a parent's transformers). Nil on a DocumentModel means no chain, or an ambiguous one.

type Layout

type Layout string

Layout is the structural shape of a candidate subtree. Layout and acceptedByOperator are two distinct truths: a kustomize-overlay has a well-understood layout and is now adopted when its render scope passes the gate, but its editable count can still be 0 when every rendered field is base-owned.

const (
	// LayoutPlain is a directory of raw KRM documents with explicit namespaces and no
	// kustomization — the "one plain folder per environment" launch layout. Accepted.
	LayoutPlain Layout = "plain"
	// LayoutKustomizeSingle is a self-contained render root: one kustomization whose
	// resources graph stays within its own subtree (local files, or a base directory
	// nested underneath it). Accepted — the operator can render the whole subtree.
	LayoutKustomizeSingle Layout = "kustomize-single"
	// LayoutKustomizeOverlay is a render root that reaches a base kustomization OUTSIDE
	// its own subtree (the classic base/ + overlays/{env} shape reached via ../../base).
	// Render-root scoping shipped, so the operator adopts it when the adoption gate accepts
	// its render scope; the editable count shows how much of the render the overlay owns (a
	// pure passthrough overlay is adopted yet editable: 0). It is a distinct layout from
	// kustomize-single because it still renders — and cannot edit — an out-of-subtree base.
	LayoutKustomizeOverlay Layout = "kustomize-overlay"
	// LayoutRefusedStructural is a render root whose kustomization uses a feature the
	// contextual-namespace writer cannot map back to editable source (helm inflation,
	// generators, patches, components, name(pre|suf)fix, remote bases, malformed
	// images/replicas). This is the support boundary: the folder cannot be adopted as it
	// stands, and each refusal says whether anyone can solve that.
	LayoutRefusedStructural Layout = "refused-structural"
)

type LayoutMode added in v0.43.0

type LayoutMode string

LayoutMode is how a folder is written. It mirrors v1alpha3.PlacementMode, which is the published form; the duplication is the usual one-way dependency rule — the analyzer does not import the API types.

const (
	// LayoutModePlain is a folder no kustomization governs.
	LayoutModePlain LayoutMode = "Plain"
	// LayoutModeKustomizeRoot is a self-contained folder governed by exactly one kustomization.
	LayoutModeKustomizeRoot LayoutMode = "KustomizeRoot"
	// LayoutModeKustomizeOverlay is a folder governed by one kustomization that renders a base
	// outside the write scope.
	LayoutModeKustomizeOverlay LayoutMode = "KustomizeOverlay"
)

type LayoutReason added in v0.43.0

type LayoutReason string

LayoutReason is the resolved shape of a GitTarget folder, and it is a condition reason on LayoutResolved rather than a status field: every consumer in this ecosystem already reads reasons from conditions, and a bespoke field would be a second place to look.

const (
	// LayoutSingleKustomization is a folder governed by exactly one supported, writable
	// kustomization. A new document lands beside it and joins its resources:.
	LayoutSingleKustomization LayoutReason = "SingleKustomization"
	// LayoutAmbiguous is a folder covering more than one render root. Placement declines to
	// pick one rather than guessing, so the folder is not a write partition: point the target
	// at a leaf instead. See test/fixtures/layout-corpus/shapes/README.md § "Why only a leaf can be a
	// kustomize target".
	LayoutAmbiguous LayoutReason = "Ambiguous"
	// LayoutNone is a folder with no supported kustomization at all. New documents land at a
	// declared template's path, or at the built-in canonical path.
	LayoutNone LayoutReason = "None"
)

type LayoutResolution added in v0.43.0

type LayoutResolution struct {
	// Reason is the verdict, projected onto the LayoutResolved condition.
	Reason LayoutReason
	// Mode is how the folder is written — plain files, a self-contained kustomize root, or an
	// overlay over a base outside the write scope. Empty under Ambiguous, which has no single
	// answer.
	Mode LayoutMode
	// RenderRoot is the governing kustomization's directory relative to the write scope, "."
	// for the scope itself. Empty for every reason but LayoutSingleKustomization.
	RenderRoot string
	// RenderRoots is every writable render root the scan found, sorted. It is what makes an
	// Ambiguous message able to name the folders it actually covers instead of only counting
	// them.
	RenderRoots []string
	// ReadOnlyBases is every kustomization the scan holds that lies OUTSIDE the write scope,
	// relative to it, sorted. Non-empty exactly when Mode is LayoutModeKustomizeOverlay, and it
	// is what a WriteBoundaryRefused message is predictable from.
	ReadOnlyBases []string
}

LayoutResolution is what one scan resolved about a folder's layout.

func ResolveLayout added in v0.43.0

func ResolveLayout(store *ManifestStore, writeScope string) LayoutResolution

ResolveLayout resolves a folder's layout from a scan.

writeScope is the write jail relative to the scanned root — empty for a self-contained subtree, non-empty only when render-root scoping anchored the scan past spec.path into a base the folder renders. Read scope is wider than write scope, always, so a base above the jail is read to render the folder and is never a candidate root for it.

type ManifestStore

type ManifestStore struct {
	// Root is the scanned root, mirroring Report.Root. It is informational and
	// empty for an in-memory fs.FS.
	Root string

	// FilesByPath holds only managed files — those with at least one tracked KRM
	// document. A FileModel therefore always has at least one document until its
	// last is dropped, at which point Current goes nil and Deleted() fires.
	FilesByPath map[string]*FileModel

	// Indexes hold pointers into FilesByPath, not (path, index) pairs, so a
	// document delete that shifts a file's slice never invalidates them.
	//
	// ByManifestIdentity is single-valued, first-occurrence-wins over documents that CLAIM their
	// identity, so a later duplicate is detectable as such. Claiming mirrors manifestedit's
	// duplicate rule exactly, so the collapse and its diagnostic agree.
	ByManifestIdentity map[manifestedit.Identity]*DocumentModel
	// ByResourceIdentity is populated once the GVK->GVR mapper resolves resource
	// identities (Track B / B3). It is empty under structure-only analysis.
	ByResourceIdentity map[types.ResourceIdentifier]*DocumentModel
	// ByGVK groups every managed document by its derived GroupVersionKind. It is
	// multi-valued: many resources of one kind are normal.
	ByGVK map[schema.GroupVersionKind][]*DocumentModel

	// Diagnostics are the scan- and index-level diagnostics gathered while building
	// the store, in scan order (scan diagnostics first, then per-document index
	// diagnostics).
	Diagnostics []manifestedit.Diagnostic

	// Retained holds the allowlisted non-API KRM documents (build directives such as
	// kustomization.yaml) recognised during the scan but deliberately kept OUT of
	// FilesByPath and the indexes — exactly like non-YAML auxiliary files. They have
	// no document set to empty, so they can never be swept, edited, or planned. They
	// are recorded only so the acceptance gate can name them and refuse a managed
	// file that illegally shares its bytes with one (a mixed file). It is empty
	// unless the store was built with a non-empty allowlist.
	Retained []RetainedDocument

	// Kustomizations indexes every kustomization.yaml found under the scanned root by
	// its directory (slash, relative to the root; "." for the root itself). New-file
	// placement consults it to decide whether a candidate directory is
	// kustomize-governed and, if so, which file's resources: list a new sibling must
	// be added to. Populated independent of the allowlist — build-directive discovery
	// does not depend on which files the writer materialises.
	Kustomizations map[string]*KustomizationInfo

	// Foreign lists the filesystem entries under spec.path that matched no recognized role
	// (non-YAML files, symlinks, submodules) and survived the .gittargetignore filter. The
	// acceptance gate refuses each one (foreignContentRefusals); the path is an
	// operator-exclusive subtree. See docs/spec/gitpath-foreign-content-stringency.md.
	Foreign []ForeignEntry

	// Ignore is the active root .gittargetignore matcher (nil when the path carries none).
	// It is consulted by the writer's write-plan precondition (§4.3) to assert that no path
	// the operator writes is shadowed by an ignore pattern — the one unrecoverable case.
	Ignore *IgnoreMatcher

	// IgnoreIssues carries parse-time .gittargetignore refusals (the catastrophic-pattern
	// denylist). The acceptance gate appends them so a footgun fails the GitTarget at the
	// same surface as any other refusal.
	IgnoreIssues []AcceptanceIssue

	// ValueFileRefs is the set of scanned non-KRM files a release document names by a locally
	// matching path: NAMED read-only context, not junk, and not proof the deployer consumes it.
	// The acceptance gate consults it so such a file no longer refuses its folder as non-krm-yaml.
	// Never materialised: retained, never written.
	// See docs/design/support-boundary/values-file-projection.md §2 (Move 1).
	ValueFileRefs map[string]struct{}

	// RenderedInventory records what each render root RENDERS TO, keyed by the root's
	// directory: which type lands in which namespace, from the objects kustomize actually
	// produced. It is what a tool must settle before it provisions from a folder, and it is
	// read off a real build, so a `namespace:` transformer or a base outside the subtree is
	// already accounted for. Empty for a root that failed to build, and absent for a
	// directory that is not a render root.
	RenderedInventory map[string]RenderedTypes
	// contains filtered or unexported fields
}

ManifestStore is the byte-free structure model of a GitTarget folder, consumed by the live writer, scan mode, the CLI and status alike. See docs/spec/current-manifest-support-review.md.

Only MANAGED files live in FilesByPath: YAML carrying at least one KRM document. Everything else is known to the analyzer but never becomes a FileModel, so it can never be swept or deleted.

func BuildStore

func BuildStore(ctx context.Context, fsys fs.FS, lookup typeset.Lookup) *ManifestStore

BuildStore walks fsys and returns the byte-free ManifestStore: the managed FileModels and the scan/index diagnostics. It is the structure spine the Report is projected from, and the entry point downstream layers (planner, live writer) will consume directly. It is read-only and never fails.

lookup resolves each managed document's GVK to a served resource identity; pass nil (or an un-ready registry) to keep the no-cluster, structure-only mode.

BuildStore materialises every KRM document (the empty-allowlist case). Scan mode passes the acceptance policy's allowlist through buildStoreFS so non-API KRM such as kustomization.yaml is retained outside the model rather than materialised.

func BuildStoreFromFiles

func BuildStoreFromFiles(
	ctx context.Context,
	files []manifestedit.FileContent,
	lookup typeset.Lookup,
	allowlist Allowlist,
) *ManifestStore

BuildStoreFromFiles builds the store from already-collected bytes rather than walking an fs.FS. It is the live writer's entry point: the writer reads the subtree once at a commit boundary and hands the same slice here, so the store and the bytes the plan is applied to are one snapshot. A nil lookup keeps it structure-only, exactly as BuildStore.

func BuildStoreFromScan

func BuildStoreFromScan(
	ctx context.Context,
	scan FolderScan,
	lookup typeset.Lookup,
	allowlist Allowlist,
	opts ...StoreOption,
) *ManifestStore

BuildStoreFromScan builds the store from a FolderScan that already carries the YAML files, the foreign-content view, and the active .gittargetignore matcher. It is the live writer's and resync apply's entry point: they walk the worktree subtree once (the same scan the planner reads) and hand the whole structural view here, so the store, the bytes the plan is applied to, and the foreign/ignore facts the acceptance gate enforces are one snapshot. lookup and allowlist behave exactly as BuildStoreFromFiles.

func (*ManifestStore) DocumentLocations

func (s *ManifestStore) DocumentLocations() map[*DocumentModel]RecordRef

DocumentLocations returns the (file path, document index) of every managed document in the store. It is the public form of the planner's per-document position reconstruction (record-less diagnostic gaps), computed once so a caller folding many events over one commit-boundary store does not pay the O(store) reconstruction per lookup. Pair it with ByManifestIdentity to resolve an identity to its RecordRef.

func (*ManifestStore) IsDuplicate

func (s *ManifestStore) IsDuplicate(dm *DocumentModel) bool

IsDuplicate reports whether dm is an identity-claiming document that lost the first-occurrence-wins contest for its manifest identity — i.e. a duplicate the GitTarget would refuse. It reads only the collapsed index, never a diagnostic message, and agrees with manifestedit's duplicate detection (encrypted documents included).

func (*ManifestStore) OverridesAmbiguousAt

func (s *ManifestStore) OverridesAmbiguousAt(rel string) bool

OverridesAmbiguousAt reports whether the store refused to route a kustomize override chain for a document in the file at the given base-relative (slash) path, because more than one render path reaches it with override entries at stake (reasonAmbiguousOverrides). It is the store-side signal for the writer's write-fan-in precondition: editing such a file in place would write a live change through into source context shared by multiple render roots — the one edit the write-fan-in = 1 invariant forbids — so the flush is refused rather than corrupting what another root renders. Derived from the build-time diagnostics the store already carries, so it needs no extra per-file state.

func (*ManifestStore) ReachedByMultipleRenderRoots added in v0.37.0

func (s *ManifestStore) ReachedByMultipleRenderRoots(rel string) bool

ReachedByMultipleRenderRoots reports whether the managed file at the given render-scope-relative (slash) path is reached by more than one render root through the resources graph — the generalised write-fan-in signal. Writing a live change into such a file in place would change what every root that reads it renders, which is the one edit the write-fan-in = 1 invariant forbids. It generalises OverridesAmbiguousAt (which only fired when override entries were at stake) to any shared source file, so the guard no longer leans on the emergent side effect that a namespace-ambiguous base never becomes dirty. See docs/design/support-boundary/render-root-scoping.md §4.

type MappingOutcome

type MappingOutcome int

MappingOutcome records why a document's ResourceIdentity is or is not set, derived from the followability registry. It is the analyzer's view of the single followability question — there is no status vocabulary to interpret, only three outcomes: followable (resolved), not followable (a source said so), or no API source at all (structure-only / the registry is not ready, so nothing is judged).

const (
	// MappingNoSource means no API source was consulted (structure-only analysis, or a
	// registry that is not ready). It is the honest "this looks like KRM but nothing was
	// asked what serves it"; it never drives a watched/unwatched or destructive decision.
	MappingNoSource MappingOutcome = iota
	// MappingFollowable means the GVK resolved to a single served, followable resource;
	// ResourceIdentity is set.
	MappingFollowable
	// MappingNotFollowable means a ready source was consulted but the kind is not
	// followable (not served, denied, ambiguous, or missing a verb); ResourceIdentity
	// is nil. Why it is not followable is recorded centrally by the registry, not here.
	MappingNotFollowable
)

func (MappingOutcome) String

func (o MappingOutcome) String() string

String renders a MappingOutcome for diagnostics and tests.

type NamespaceSource

type NamespaceSource struct {
	Kind NamespaceSourceKind
	Path string
}

NamespaceSource records where a document's effective namespace came from. Kind drives the one write-time decision the live writer makes (keep metadata.namespace out of the file and locate by raw identity only when the namespace came from somewhere other than the file — see DocumentModel.NamespaceAbsentFromFile); Path is the kustomization file that supplied the namespace, set only for NamespaceKustomize.

type NamespaceSourceKind

type NamespaceSourceKind string

NamespaceSourceKind classifies where a document's effective namespace comes from. It replaces an earlier "namespace came from kustomize" boolean so the store can also explain the no-context and ambiguous cases to status, duplicate diagnostics, and future placement — see docs/layout/contextual-namespace.md.

const (
	// NamespaceExplicit means the namespace is authoritative as written in the file
	// (metadata.namespace present), or the document is cluster-scoped / not yet
	// resolved so no context is consulted. The file bytes own the namespace.
	NamespaceExplicit NamespaceSourceKind = "Explicit"
	// NamespaceKustomize means the namespace was inherited from a kustomization.yaml
	// that references the document through its resources graph. metadata.namespace is
	// absent from the file and must stay absent on write; Path names the kustomization.
	NamespaceKustomize NamespaceSourceKind = "Kustomize"
	// NamespaceNone means a namespaced, followable document omits metadata.namespace
	// and no single supported context supplies one — either nothing references it, or
	// the references disagree (ambiguous). The document is left namespace-less rather
	// than guessed; an ambiguous case also emits a reasonAmbiguousNamespace diagnostic
	// for the repository-validity layer.
	NamespaceNone NamespaceSourceKind = "None"
	// NamespaceDeclared means metadata.namespace is absent from the file and no kustomization
	// supplies one, but the GitTarget DECLARED the folder namespace-free
	// (spec.serializeNamespace: false) and exactly one source namespace reaches it, so that
	// namespace is the document's. Like Kustomize, the namespace must stay out of the file and
	// the document is located in the bytes by a namespace-less identity.
	//
	// Without it the operator writes a namespace-less document, reads it back as belonging to no
	// namespace, and the NEXT write matches nothing and appends a second copy. The
	// one-source-namespace refusal is what makes it safe: two namespaces have no single answer.
	NamespaceDeclared NamespaceSourceKind = "Declared"
)

type OverlapConflict

type OverlapConflict struct {
	Ancestor   string `json:"ancestor"`
	Descendant string `json:"descendant"`
}

OverlapConflict records a nesting conflict between two candidates: ancestor strictly contains descendant in the folder tree.

type OverrideEdit

type OverrideEdit struct {
	// KustomizationPath is the kustomization file (slash, relative to the
	// GitTarget subtree) that carries the entry.
	KustomizationPath string
	// Create AUTHORS a new entry (via AppendKustomizationOverride) rather than editing a
	// scalar on an existing one: an overlay overriding a value its base supplies, where no
	// entry exists yet. Edit.EntryName is the new entry's name: (the image name for an
	// images: entry, the resource name for a replicas: entry); Edit.EntryIndex is unused.
	// Every authored entry is put to the re-render oracle before it can commit, so a proposal
	// that over-reaches is refused there, not written.
	Create bool
	// Edit is the bounded scalar update the manifestedit editor applies.
	Edit manifestedit.KustomizationEdit
}

OverrideEdit routes one live-value change to a field of a kustomization override entry.

func ReplicaCountEdit

func ReplicaCountEdit(dm *DocumentModel, count int64) (OverrideEdit, bool)

ReplicaCountEdit returns the entry edit that absorbs a live replica count for the document, when a replicas: entry supplies spec.replicas. The writer's field-patch path (the /scale subresource) uses it to route a scale onto the entry instead of writing the count into the source manifest, where the transformer would override it back.

func SplitDesiredForOverrides

func SplitDesiredForOverrides(
	gitRaw map[string]interface{},
	desired *unstructured.Unstructured,
	rendered *RenderedOverrides,
	authorInto string,
) (*unstructured.Unstructured, []OverrideEdit, error)

SplitDesiredForOverrides maps the live desired object back through what kustomize actually renders. It returns the object the source document should be compared against — the SOURCE FORM of the live state, so the file keeps every byte the build supplied — plus the entry edits for the values an override entry supplies.

Two rules, and neither models a transformer:

  1. WHERE LIVE AND THE RENDER AGREE, THE SOURCE KEEPS ITS BYTES. The build produces what the cluster runs, so the source is by construction what produced it. This stops the writer mirroring the build's own output back into its input, knowing nothing about labels or patches to do it.
  2. WHERE THEY DISAGREE, THE USER CHANGED SOMETHING. If an entry supplies that field (the dye says which) the change is routed to the ENTRY; otherwise it is written to the source.

Anything it cannot route safely routes NOTHING and leaves the live value in place. Not a guess: the proposal must survive the verification re-render, which for a field an entry governs it will not, so it becomes a reported refusal rather than a commit that never converges.

It refuses outright only a list the build and the user BOTH changed whose elements cannot be paired by name: there is no honest way to say which bytes the user meant to keep, and aligning by position is measurably wrong.

authorInto is the kustomization the writer may AUTHOR a new entry into when a SOURCE-supplied value diverges and the source is out of the write jail. "" for a self-contained subtree or an in-jail document, where the file is written directly. See docs/design/support-boundary/render-root-scoping.md §4.

type PlacementPolicy

type PlacementPolicy struct {
	ByType  map[string]string
	Default string
	// UseKustomize is spec.placement.useKustomize: create a kustomization.yaml at the write jail's
	// root when nothing governs a new document's path, and register the document in it. It is read
	// by the writer rather than by LocateNew, because creating a file is not a path decision.
	UseKustomize bool
}

PlacementPolicy is a resolved GitTarget placement declaration: an exact-type map plus a fallback default template. It mirrors api/v1alpha3.GitTargetPlacementSpec but is defined locally so this package stays free of Kubernetes API types.

No sensitive/normal split: sensitivity is a write-safety property enforced after resolution, not a second map to configure. A nil policy falls through to the kustomize-root fallback, then canonical. See docs/layout/new-file-placement-rules.md.

type PlacementRefusalReason added in v0.41.0

type PlacementRefusalReason string

PlacementRefusalReason names WHY a placement was refused, from a closed set. A metric label value as well as a log field, so lower_snake_case and part of the observability contract.

A refusal is a resource the operator did NOT mirror, so it must be countable per (GitTarget, type): a misconfigured template silently skipping one Secret every reconcile is otherwise invisible unless somebody reads the logs.

const (
	// PlacementRefusedInvalidPath is a resolved path that failed the write-jail gate:
	// empty, absolute, unclean, escaping via "..", or not a YAML file name. In practice
	// a declared template whose literal text is wrong.
	PlacementRefusedInvalidPath PlacementRefusalReason = "invalid_path"
	// PlacementRefusedSensitiveAppend is a sensitive resource whose resolved path already
	// holds a document. Sensitive documents are never appended, so the write is skipped
	// rather than co-mingled — usually a declared template that is not identity-complete.
	PlacementRefusedSensitiveAppend PlacementRefusalReason = "sensitive_append"
	// PlacementRefusedPlaintextOntoEncrypted is a plaintext resource routed onto a file
	// that already holds an encrypted document: appending would produce a
	// partially-encrypted file and overwriting would destroy the encrypted document.
	PlacementRefusedPlaintextOntoEncrypted PlacementRefusalReason = "plaintext_onto_encrypted"
	// PlacementRefusedMixedSensitivityNewFile is two resources of different sensitivity
	// resolving to the SAME brand-new path within one batch. LocateNew cannot see this
	// (it resolves every resource against the pre-batch snapshot), so the writer refuses
	// it; the value is defined here to keep one closed label domain for every refusal.
	PlacementRefusedMixedSensitivityNewFile PlacementRefusalReason = "mixed_sensitivity_new_file"
	// PlacementRefusedMultiDocumentTarget is a resolved path that holds a multi-document
	// file the writer cannot append to (one of its documents is not cleanly editable), so
	// the write would have to overwrite it and drop the siblings. Raised by the writer.
	PlacementRefusedMultiDocumentTarget PlacementRefusalReason = "multi_document_target"
)

type PlacementRefusedError added in v0.41.0

type PlacementRefusedError struct {
	Reason   PlacementRefusalReason
	Resource string
	Path     string
	// contains filtered or unexported fields
}

PlacementRefusedError is a placement that resolved but cannot be honoured safely. The caller must skip creating that resource and surface the refusal rather than writing. It carries a bounded Reason so the write path can count refusals by cause without matching on message text.

func (*PlacementRefusedError) Error added in v0.41.0

func (e *PlacementRefusedError) Error() string

func (*PlacementRefusedError) Unwrap added in v0.41.0

func (e *PlacementRefusedError) Unwrap() error

Unwrap exposes the underlying validation error for an invalid-path refusal, so errors.Is/As still reach it.

type PlacementRequest

type PlacementRequest struct {
	Identifier types.ResourceIdentifier
	Kind       string
	Sensitive  bool
	// WriteScope is the write jail relative to the scanned (render) root, set only when
	// render-root scoping re-rooted the scan past spec.path into a base an overlay reads.
	// Placement is documented as relative to spec.path, so a resolved path that would land
	// outside the jail (a declared or canonical path resolved against the render anchor) is
	// rebased under WriteScope rather than escaping it. Empty for a self-contained subtree,
	// where the scan root IS spec.path and every resolved path is already in scope.
	WriteScope string
}

PlacementRequest describes a resource with no existing document in Git — the only case placement runs for (an existing document is always updated in place at its current location; see docs/layout/new-file-placement-rules.md, "Existing manifests are still match-first").

type PlacementResult

type PlacementResult struct {
	// Path is the resolved file path (slash-separated), relative to the scanned
	// root (the GitTarget's spec.path).
	Path string
	// Append is true when Path already exists as a managed file the new document
	// should be appended to as an additional document; false for a brand-new file.
	Append bool
	// Source names which mechanism produced Path.
	Source PlacementSource
	// Kustomization is set when Path's directory carries a supported
	// kustomization.yaml whose resources: list does not already name Path — the
	// writer must add it as part of the same commit so kustomize picks the file
	// up ("add to the right kustomize file").
	Kustomization *KustomizationInfo
	// NamespaceInherited is true when Path's destination infers its namespace
	// from build context (a kustomization.yaml's namespace: transformer set to this
	// resource's own namespace) rather than from metadata.namespace in the file —
	// mirroring DocumentModel.NamespaceInheritedFromContext for a document that does
	// not exist yet. The writer must keep metadata.namespace out of the written
	// bytes, exactly as it already does for an in-place edit of an existing
	// document in the same context.
	NamespaceInherited bool
}

PlacementResult is where a new resource should be written.

func LocateNew

func LocateNew(store *ManifestStore, policy *PlacementPolicy, req PlacementRequest) (PlacementResult, error)

LocateNew resolves the placement of a resource with no existing document: a declared template wins; otherwise the folder's one supported kustomize root; otherwise the canonical path. Nothing reads the layout of the OTHER documents of this type.

store is the pre-plan snapshot and must never be mutated mid-batch: its reads must answer the same way for every resource in one batch, so several new creates resolve order-independently.

An error means the placement cannot be honoured safely (a sensitive resource whose path already exists; sensitive documents are never appended). The caller must skip that resource rather than write into a shared file. See docs/layout/new-file-placement-rules.md.

type PlacementSource

type PlacementSource string

PlacementSource names which mechanism produced a Path: the "why here" answer for one document. The values are a public observability contract (metric label values), so lower_snake_case and not renamed lightly.

Exactly four, closed by construction: two kinds of declaration, one structural fact about the folder, and the built-in path. Nothing reads the repository's layout to guess intent.

The two declared values are separate because a catch-all quietly swallowing a type you meant to name explicitly otherwise looks identical to a rule working as intended, and telling those apart is the whole job of a metric that answers "is a placement rule missing?".

const (
	// PlacementSourceByType is an explicit placement.byType entry for this exact type.
	PlacementSourceByType PlacementSource = "by_type"
	// PlacementSourceDefault is the declared catch-all, placement.default: no byType entry named
	// this type, so the target's own fallback template answered instead. Distinct from
	// PlacementSourceCanonical, which is the absence of any declaration at all.
	PlacementSourceDefault PlacementSource = "default"
	// PlacementSourceKustomizeRoot is the structural fallback: no declared template
	// matched, and the whole writable subtree is governed by exactly one supported
	// kustomization, so the new document goes beside it and into its resources: list
	// (see resolveKustomizeRoot). It is a fact about reachability, not a guess about
	// convention: a file that root cannot reach would never render at all.
	PlacementSourceKustomizeRoot PlacementSource = "kustomize_root"
	// PlacementSourceCanonical is the built-in, versionless
	// {namespaceOrCluster}/{group}/{resource}/{name}.yaml fallback: no declared
	// template, and no single kustomize root to hang the file off. For a repository
	// with a hand-authored layout this is the signal that a placement.byType or
	// placement.default line is missing — which is why it is counted per
	// (GitTarget, type) rather than only logged. See recordPlacement in internal/git.
	PlacementSourceCanonical PlacementSource = "canonical"
)

type Plan

type Plan struct {
	// Actions are the decided changes, in a deterministic order (by file path, then
	// document index, then identity), so output is stable regardless of map
	// iteration order. A resource already in sync produces NO action — the plan
	// lists only what would change.
	Actions []PlanAction
	// Diagnostics are planning-level problems (e.g. a touched file whose bytes were
	// not provided for hydration). Store-level diagnostics stay on the ManifestStore.
	Diagnostics []manifestedit.Diagnostic
	// RetainedOrphans counts the managed drops the sweep policy SUPPRESSED: documents
	// that would have been PlanDropOrphan under SweepDropOrphans and produced no action
	// at all under SweepRetainOrphans. It exists because a suppressed drop leaves no other
	// trace — the whole point is that it is absent from Actions, so nothing downstream
	// could otherwise tell "the mirror is converged" from "the mirror has stale documents
	// the policy is deliberately keeping". Purely informational: it is a count, not an
	// action, and it never reaches the commit path.
	RetainedOrphans int
	// RetainedOrphansByType breaks the same population down by resource TYPE. A bare total names
	// nothing an operator can go and look at. Keyed by the type alone, never by object identity:
	// the consumers publish group/version/resource, so per-object keys would be one map entry per
	// document for a series they all share.
	RetainedOrphansByType map[schema.GroupVersionResource]int
}

Plan is a pure function of (ManifestStore, desired set, policy): the same value the live writer applies, scan mode renders, the CLI prints and status summarizes. It carries enough detail to render text/JSON/status without recomputing any decision. See docs/spec/current-manifest-support-review.md.

func BuildPlan

func BuildPlan(
	store *ManifestStore,
	files []manifestedit.FileContent,
	desired []DesiredResource,
	policy Policy,
) Plan

BuildPlan computes the Plan from the store, the bytes backing it, the COMPLETE desired snapshot and the policy.

The full-snapshot planner: it mark-and-sweeps, so desired MUST be the whole desired state, never a partial batch. The per-event removal path is PlanDelete.

Under a structure-only store (no resolved mappings) no managed drop is emitted, preserving the no-cluster promise even if a desired set is passed by mistake. policy.Sweep is a second, independent gate on the same deletions, and its zero value retains.

func BuildScopedPlan

func BuildScopedPlan(
	store *ManifestStore,
	files []manifestedit.FileContent,
	desired []DesiredResource,
	policy Policy,
	inScope func(types.ResourceIdentifier) bool,
) Plan

BuildScopedPlan is BuildPlan restricted to the documents inScope reports: the sweep only touches a managed document whose RESOLVED identity is in scope, so no sibling type is collaterally deleted. The per-type primitive: a reconcile passes that type's objects with a matching predicate, a sweep passes an EMPTY desired set with the same one. The caller MUST keep desired in scope, since it is the scope on the upsert side.

With allInScope this is exactly BuildPlan, so the two share one implementation and one set of safety guarantees. See docs/spec/type-lifecycle-events-and-wobble-settling.md.

func (Plan) Counts

func (p Plan) Counts() map[PlanActionKind]int

Counts returns the number of actions per kind, for a bounded status summary.

type PlanAction

type PlanAction struct {
	// Kind is what the action does.
	Kind PlanActionKind
	// Ref is the Git document the action concerns. It is the zero value for
	// PlanCreate, which has no existing location.
	Ref RecordRef
	// Identity is the manifest identity (apiVersion + kind + namespace + name)
	// involved, always set.
	Identity manifestedit.Identity
	// Resource is the resolved API-side identity (GVR + namespace + name). For a
	// desired-side action (create / patch / replace / skip) it is the cluster-truth
	// identity carried on the DesiredResource, so a create carries everything
	// ResourceIdentifier.ToGitPath needs to place a new file at apply time (M7)
	// without re-resolving the mapping. For a Git-only managed drop it is the
	// store-resolved identity. It is the zero value only when neither is known (a
	// skip over a document a structure-only store never resolved).
	Resource types.ResourceIdentifier
	// Desired is the clean object Git should contain, set for create/patch/replace
	// and nil for removals and skips.
	Desired *unstructured.Unstructured
	// Reason is a human-readable explanation, carried so renderers and status need
	// no recomputation.
	Reason string
}

PlanAction is one decided change over a single document or desired resource.

func PlanDelete

func PlanDelete(
	store *ManifestStore,
	resource types.ResourceIdentifier,
) (PlanAction, bool)

PlanDelete resolves a steady-state DELETE watch event to a single delete-document plan action over the store, or no action when Git holds no managed document for the resource. It is the M6 milestone: closing the delete-identity gap the review names (docs/spec/current-manifest-support-review.md, "Cons And Gaps") so a moved manifest is still deleted, and the writer (M7) deletes by RecordRef instead of regenerating a canonical path.

This is the per-event delete path of the design's "Two Paths, One Plan Type" (docs/spec/reconcile-via-watchlist-mark-and-sweep.md). Unlike BuildPlan's full-snapshot mark-and-sweep, it targets exactly ONE identity and NEVER sweeps, so a lone delete intent can never be mistaken for "every other document is now an orphan". M7's steady-state loop folds this over its coalesced PendingChanges (a delete is a PendingChange whose Object is nil).

A DELETE event carries only a GVR-based resource identity and NO object body, so the manifest identity cannot be derived from the event. The document is therefore located only by its RESOLVED RESOURCE identity — the ByResourceIdentity index B3 built while scanning the GitTarget folder. If that inventory has no entry, there is no managed document to delete.

Deletion is content-agnostic (manifestedit.DeleteDocument never decrypts or merges), so an encrypted or non-editable document is still removed when its resource leaves the cluster — editability gates patches, not removals.

PlanDelete is a commit-boundary operation, not a per-event one: like the rest of the planner it reuses documentLocations / collidedIdentities (each O(store)). M7 hoists those per-commit maps so folding many deletes stays bounded by the batch; M9 caches across batches.

type PlanActionKind

type PlanActionKind string

PlanActionKind enumerates what a single action does. The seven kinds are the full vocabulary the materialized model speaks; which milestone *emits* each is noted below, because M3 (this milestone) computes the plan from a whole desired set, while a few kinds only arise once the apply/event layers land.

const (
	// PlanCreate places a desired resource that has no managed document in Git yet.
	// The target path is a placement decision made at apply time (M7), so a create
	// action carries the Desired object but a zero Ref.
	PlanCreate PlanActionKind = "create"
	// PlanPatch edits an existing document field-by-field (Decide said the Git
	// document differs from desired and a mapping-root patch is possible).
	PlanPatch PlanActionKind = "patch"
	// PlanReplace re-renders an existing document canonically (Decide could not
	// patch field-by-field, e.g. a non-mapping root).
	PlanReplace PlanActionKind = "replace"
	// PlanDeleteDocument removes one document from a multi-document file. The
	// full-snapshot BuildPlan does not emit it — a resync managed drop is a
	// PlanDropOrphan — but the steady-state per-event path does: PlanDelete (M6) emits
	// it for a single live DELETE event, the design's "Two Paths, One Plan Type". Whether
	// removing the document also empties the file (a follow-on file delete) is realized
	// mechanically at apply time (M7), so a delete is one PlanDeleteDocument either way.
	PlanDeleteDocument PlanActionKind = "delete-document"
	// PlanDeleteFile removes a whole file (its last managed document was dropped).
	// Like PlanDeleteDocument it is realized at apply time (M7), not emitted by the
	// M3 planner. Reserved here for completeness.
	PlanDeleteFile PlanActionKind = "delete-file"
	// PlanDropOrphan deletes a watched resource the API no longer has — the managed
	// drop. It is emitted only for a document whose GVK the registry resolved to a
	// followable resource (MappingFollowable) that has no desired counterpart.
	// Duplicate identities and not-followable KRM produce NO plan action: they are
	// acceptance facts (M4), not planning outcomes. Allowlisted non-API KRM produces
	// none either (it never resolves to a followable resource).
	PlanDropOrphan PlanActionKind = "drop-orphan"
	// PlanSkip marks a document that exists but cannot be edited in place
	// (encrypted, a disallowed construct, or a soft Decide skip). It is reported,
	// never silently acted on.
	PlanSkip PlanActionKind = "skip"
)

type Policy

type Policy struct {
	// Project maps a live API object to the clean desired state Git should contain.
	// A nil Project is treated as an identity passthrough (the object compared as-is).
	Project func(*unstructured.Unstructured) *unstructured.Unstructured
	// EditOptions are the manifestedit options (canonical renderer, list-match) used
	// when Decide must compare and choose patch vs. whole-replace.
	EditOptions manifestedit.EditOptions
	// Sweep decides whether the Git-only mark-and-sweep may emit managed drops. Its zero
	// value retains, so every caller that wants convergence must say so — see SweepMode.
	Sweep SweepMode
}

Policy is the injected planning policy. The planner stays a pure function and pulls every cluster-shaped or rendering-shaped decision out into this struct, so the production wiring (manifestreport.Project / EditOptions) lives at the call sites and tests can substitute their own.

func FolderScanPlanPolicy added in v0.39.0

func FolderScanPlanPolicy() Policy

FolderScanPlanPolicy is the planning policy for an offline folder scan: it DROPS orphans.

A scan is a report, not a write, and it is run against a folder rather than against a GitTarget — there is no spec.prune to consult. Reporting the full convergence view is the analysis the tool exists to produce; suppressing it would make a folder full of stale documents render identically to a converged one, with nothing on screen to distinguish them. Nothing here can delete anything: Scan writes nothing, and the live writer builds its own policy from the target.

In practice a CLI scan also passes a nil mapper, so no document resolves to MappingFollowable and no drop is emitted regardless. This is the deliberate choice for the day that changes.

type ReadEdge added in v0.40.0

type ReadEdge struct {
	From string `json:"from"`
	To   string `json:"to"`
}

ReadEdge is one folder-to-folder read: From's build renders documents that live in To, which is outside From's own subtree. From is always a candidate; To is a candidate only in the file-reference case described on RepoCandidate.ReadBy, and is otherwise a directory the scan offers to nobody.

type RecordRef

type RecordRef struct {
	FilePath      string
	DocumentIndex int
}

RecordRef is a stable (file path, document index) reference to one document. It is a plan-level value — the live, mutable store navigates by *DocumentModel pointers — pinned for the lifetime of a single plan.

type RefusalReason

type RefusalReason struct {
	Code   string `json:"code"`
	Detail string `json:"detail"`
	// Solvable and Actor carry the refusal's own answer to "can this be solved, and by
	// whom" — projected verbatim from the acceptance issue that raised it, or decided by
	// the raise site for the reasons that are not issue kinds. Empty means unclassified; say
	// nothing about whether it can be solved.
	Solvable bool  `json:"solvable"`
	Actor    Actor `json:"actor,omitempty"`
}

RefusalReason is one machine-readable reason a candidate is not accepted, with a human detail. A candidate carries none when accepted.

type RenderDivergence added in v0.37.0

type RenderDivergence struct {
	Field string
	Token string
}

RenderDivergence identifies a rendered ${...} value that is not present unchanged in the sanitized live object. Field is a human-readable structured path; Token is the first token in the rendered scalar, retained so the refusal tells an operator why the field is protected.

func RenderTokenDivergences added in v0.37.0

func RenderTokenDivergences(rendered, live map[string]interface{}) []RenderDivergence

RenderTokenDivergences returns every rendered ${...} scalar whose matching live field is absent or differs. It walks parsed object values, never YAML source bytes: comments therefore do not participate, and a token kustomize overwrote in the render is not considered. Only ${...} is a token; native Kubernetes $(...) syntax is deliberately outside this predicate.

type RenderRefusedError

type RenderRefusedError struct {
	// Reasons are the individual findings, sorted, so the message is stable across runs.
	Reasons []string
}

RenderRefusedError is the oracle's verdict: the bytes the flush was about to commit do not render to the live cluster state, or they move something the flush never intended to touch. It aborts the flush — nothing is written — and it names the file and the object, because the correct outcome here is a REPORTED refusal, never a resource that is quietly not mirrored (render-attribution.md §7).

func (*RenderRefusedError) Error

func (e *RenderRefusedError) Error() string

type RenderedImage

type RenderedImage struct {
	// Rendered is the image kustomize produces for this slot.
	Rendered string
	// Name, Tag and Digest are the entries supplying each component, or nil when the
	// source document does. They are the dye's answer, and the reason renderImage is gone.
	Name, Tag, Digest *ImageOverride
}

RenderedImage is one image slot: what it renders to, and who supplied each component.

type RenderedOverrides

type RenderedOverrides struct {
	// Object is the whole object kustomize renders this document to. It is what the live
	// object is compared against, and it is the reason the writer can tell a value the BUILD
	// supplied from a value the USER set without modelling a single transformer: where the
	// live object and the render agree, the source keeps its bytes (see sourceForm).
	//
	// It is JSON-normalised, because a rendered object is not valid unstructured: kustomize
	// hands numbers back as Go `int`, which makes DeepCopyJSON panic outright.
	Object map[string]interface{}
	// Images is keyed by image slot (the container list path plus the container name), so
	// the live object, the Git document and the render all address the same field.
	Images map[string]RenderedImage
	// Replicas is set only when the document actually renders a spec.replicas — which is
	// kustomize's decision, not ours.
	Replicas *RenderedReplicas
}

RenderedOverrides is what kustomize renders one document to, plus the override entry behind each override-produced value. A nil supplier means THE SOURCE DOCUMENT supplies that value — so an edit to it belongs in the file, not in an entry.

type RenderedReplicas

type RenderedReplicas struct {
	Rendered int64
	Entry    *ReplicaOverride
}

RenderedReplicas is the rendered spec.replicas and the entry that pinned it (nil when the source document supplies the count).

type RenderedTypes added in v0.40.0

type RenderedTypes struct {
	// ByNamespace lists the types that land in each namespace, sorted, keyed by namespace.
	ByNamespace map[string][]string `json:"byNamespace,omitempty"`

	// NamespaceUndeclared lists the types that render WITHOUT a namespace, sorted.
	//
	// NOT a list of cluster-scoped types: it cannot tell a genuinely cluster-scoped type from a
	// namespaced one relying on the applier's default, which needs API discovery. A type can
	// appear here AND under ByNamespace, which is an ordinary folder, not a contradiction.
	NamespaceUndeclared []string `json:"namespaceUndeclared,omitempty"`
}

RenderedTypes is what a folder renders, keeping the PAIRING between a type and the namespace it lands in. A set of types beside a set of namespaces loses it: a Deployment into frontend and a Service into backend would read as four combinations, and a tool generating one rule per pair would authorize two that match nothing.

For a render root the sets come off a real kustomize build, so an outside base is included and a `namespace:` transformer is applied. A root that failed to build reports nothing: what it renders is not knowable.

type ReplicaOverride

type ReplicaOverride struct {
	// Source is the kustomization file path (slash) that declares the entry.
	Source string
	// Index is the entry's position within its file's replicas: sequence.
	Index int
	// Name matches the target document's metadata.name.
	Name string
	// Count is the replica count the entry pins.
	Count int64
}

ReplicaOverride is one parsed replicas: entry, carrying its source kustomization file. It applies to spec.replicas of a Deployment, ReplicaSet, or StatefulSet whose metadata.name equals Name.

type RepoCandidate

type RepoCandidate struct {
	// Path is the candidate directory, slash-separated and relative to the repo root.
	Path string `json:"path"`
	// Layout is the candidate's structural shape.
	Layout Layout `json:"layout"`
	// AcceptedByOperator reports whether the operator would adopt this subtree today.
	AcceptedByOperator bool `json:"acceptedByOperator"`
	// RefusalReasons explains a non-acceptance; empty when accepted.
	RefusalReasons []RefusalReason `json:"refusalReasons,omitempty"`
	// RenderRoot reports whether the candidate is a kustomize render root (versus a
	// plain KRM folder).
	RenderRoot bool `json:"renderRoot"`
	// ReadScope lists the directories outside this candidate's own subtree whose content
	// its build renders: base kustomization directories its resources graph reaches, and
	// the directories holding individual resource files it renders from elsewhere. Empty
	// for plain and self-contained candidates.
	ReadScope []string `json:"readScope,omitempty"`
	// ReadBy is the reverse edge: the candidates whose build reads THIS directory. It is
	// usually empty, and structurally so — a directory another kustomization references is
	// never a render root, hence never a candidate — but not always: a plain folder holding
	// one file some distant kustomization lists in resources: is a candidate that another
	// folder depends on, and adopting it is the disruptive case a consumer must be able to
	// see. The rest of these edges end at directories no candidate list mentions; see
	// [RepoSummary.ReadEdges].
	ReadBy []string `json:"readBy,omitempty"`
	// InferredNamespace is the namespace the candidate resolves to: the kustomization's
	// namespace transformer for a render root, or the single explicit metadata.namespace
	// for a plain folder. Empty when none is set or the folder is ambiguous.
	InferredNamespace string `json:"inferredNamespace,omitempty"`
	// Resources counts the KRM this candidate covers (rendered vs editable) plus non-KRM.
	Resources ResourceCounts `json:"resources"`
	// RenderedTypes says which type lands in which namespace when this candidate renders.
	// It is what a tool must know before it provisions from the folder: the schemas that
	// have to be served where this is applied, and the exact (type, namespace) pairs a
	// watch rule can be written for. Empty for a render root kustomize could not build.
	RenderedTypes RenderedTypes `json:"renderedTypes"`
	// OverlapsWith lists other candidate paths this one nests with. Two overlapping
	// candidates can never both be proposed (one-owner-per-folder); the conflict is
	// reported, not resolved, in this cut.
	OverlapsWith []string `json:"overlapsWith,omitempty"`
}

RepoCandidate is one subtree the product could turn into a GitTarget, with its layout, current operator acceptance, and the facts a tool built on top needs to decide. This cut reports these; it proposes no GitTarget/WatchRule.

type RepoReport

type RepoReport struct {
	// Root is the scanned repository root as passed to ScanRepo. It is informational.
	Root string `json:"root,omitempty"`
	// Candidates are the enumerated subtrees, sorted by path.
	Candidates []RepoCandidate `json:"candidates"`
	// Summary is the repo-level roll-up.
	Summary RepoSummary `json:"summary"`
}

RepoReport is the whole-repo discovery report: the machine-readable contract the a tool built on top of the operator consumes.

func ScanRepo

func ScanRepo(ctx context.Context, root string) (RepoReport, error)

ScanRepo is the whole-repo discovery pass (the library entry point; the CLI --mode scan-repo is a thin wrapper). It is read-only, writes nothing, needs no cluster, and never follows symlinks — the same posture as ScanDir, just over the whole tree rather than one subtree. It verifies root is a directory, then walks os.DirFS(root).

type RepoSummary

type RepoSummary struct {
	// CandidatesByLayout counts candidates per layout class.
	CandidatesByLayout map[Layout]int `json:"candidatesByLayout"`
	// Accepted and Refused count candidates by current operator acceptance.
	Accepted int `json:"accepted"`
	Refused  int `json:"refused"`
	// OverlapConflicts lists every nesting conflict between candidates.
	OverlapConflicts []OverlapConflict `json:"overlapConflicts,omitempty"`
	// ReadEdges is the repo's folder dependency graph, collected in one place so a consumer can
	// draw it without walking the candidates.
	//
	// Most edges end at a directory that is NOT a candidate, and nothing else identifies those
	// nodes: they are not all kustomize bases, since a referenced resource file or an
	// out-of-subtree patch makes its folder one too.
	ReadEdges []ReadEdge `json:"readEdges,omitempty"`
	// UnsupportedConstructs is the sorted, de-duplicated set of unsupported kustomize
	// features seen across refused-structural candidates, so a product can say "this repo
	// uses Helm inflation, which we don't manage".
	UnsupportedConstructs []string `json:"unsupportedConstructs,omitempty"`
}

RepoSummary is the repo-level roll-up a product uses to describe onboardability.

type Report

type Report struct {
	Root        string                    `json:"root"`
	Files       []FileReport              `json:"files"`
	Summary     Summary                   `json:"summary"`
	Issues      []AcceptanceIssue         `json:"issues"`
	Diagnostics []manifestedit.Diagnostic `json:"diagnostics"`
}

Report is the full result of analyzing a tree.

func Analyze

func Analyze(fsys fs.FS) Report

Analyze scans fsys and returns a Report. It is read-only and never fails: any per-entry problem (unreadable file, walk error, invalid YAML) becomes a diagnostic rather than an error. The Report is a projection rendered from the ManifestStore built by buildStore.

func AnalyzeDir

func AnalyzeDir(root string) (Report, error)

AnalyzeDir analyzes the directory at root. It verifies root is a directory, then runs Analyze over os.DirFS(root). Symlinks are never followed.

type ResourceCounts

type ResourceCounts struct {
	// Rendered is the number of managed KRM documents the candidate renders: its own
	// subtree plus every base it reads (readScope).
	Rendered int `json:"rendered"`
	// Editable is the number of managed KRM documents physically in the candidate's own
	// subtree — the source the operator would own and write in place.
	Editable int `json:"editable"`
	// NonKRM is the number of non-KRM YAML documents and foreign (non-YAML/symlink)
	// entries in the candidate's own subtree. Retained build directives (kustomization
	// files), operator artifacts (README.md), and accepted benign passengers (a license,
	// docs, .gitignore) are neither KRM nor NonKRM and are not counted.
	NonKRM int `json:"nonKrm"`
}

ResourceCounts splits the KRM a candidate covers into what it renders versus what it can actually edit. For a plain or self-contained kustomize candidate the two are equal; for an overlay they diverge — rendered counts the documents pulled from the out-of-subtree base, editable counts only the source physically in the candidate's own subtree (zero for a pure overlay), making the gap legible at a glance.

type RetainedDocument

type RetainedDocument struct {
	Location manifestedit.Location
	Identity manifestedit.Identity
	GVK      schema.GroupVersionKind
	// Unsupported marks a kustomization retention the operator cannot map back to editable source:
	// it uses a feature outside the supported subset, or it is a root KUSTOMIZE CANNOT BUILD. The
	// second matters because a root yielding no chain also yields no ambiguity, which would disarm
	// the write-fan-in guard. The acceptance gate refuses either. Only set on a whole-file
	// retention.
	Unsupported bool
	// UnsupportedFeatures names the constructs that made this retention unsupported, as
	// the user wrote them ("configMapGenerator", "remote-base", "render-failed"). It
	// carries the parse-time answer to the refusal, which is what lets the acceptance
	// gate decide solvability per construct instead of per code — one code with two very
	// different answers is the whole reason the solvability field exists. Sorted; empty
	// when Unsupported is false.
	UnsupportedFeatures []string
}

RetainedDocument records an allowlisted build-directive that is excluded from the managed model. There are two shapes:

  • a whole-file retention (the common case): Location.Path names an allowlisted file (e.g. kustomization.yaml), Identity is the zero value. The file is retained as auxiliary input and never materialised, planned, or swept.
  • a named record hiding in an allowlisted file: Location and Identity both set. A managed-looking resource must not live in a build-directive file, so the acceptance gate refuses it (IssueMixedFile) rather than silently un-managing it.

type ScanPolicy

type ScanPolicy struct {
	// Acceptance configures the adoption gate (allowlist + scope). Its allowlist also
	// drives store construction, so allowlisted documents are retained, not planned.
	Acceptance AcceptancePolicy
	// Plan configures the planner (projection + edit options). A dry-run has no GitTarget
	// and therefore no prune policy to read, so a caller that wants the folder's orphans
	// listed must set Plan.Sweep to SweepDropOrphans deliberately — see FolderScanPlanPolicy.
	Plan Policy
}

ScanPolicy bundles the acceptance and planning policy for a dry-run scan, so a caller configures the whole pipeline in one value.

type ScanResult

type ScanResult struct {
	Store      *ManifestStore
	Acceptance Acceptance
	Plan       Plan
}

ScanResult is the dry-run outcome: the built store, the acceptance decision, and the full plan. It carries everything needed to render the human, JSON, and status views without recomputation.

func Scan

func Scan(
	ctx context.Context,
	fsys fs.FS,
	lookup typeset.Lookup,
	desired []DesiredResource,
	policy ScanPolicy,
) ScanResult

Scan is the M5 dry-run: the one planner shared by the manifest-analyzer CLI and the controller's scan path, described in docs/spec/current-manifest-support-review.md ("Scan Mode (Dry-Run)"). It builds the store (applying the policy's allowlist), runs the acceptance gate, and computes the full plan against the desired set — then stops. It writes nothing.

The plan is ALWAYS computed, even when acceptance refuses, so an operator can see exactly what reconcile would do (creates, patches, managed drops) alongside the reasons a folder would be rejected. Whether to act on the plan is gated on Acceptance.Accepted by the caller: the live writer (M7) applies a plan only for an accepted folder. desired must be the COMPLETE desired snapshot (the planner mark-and-sweeps); pass nil for a structure-only scan with no cluster.

func ScanDir

func ScanDir(
	ctx context.Context,
	root string,
	lookup typeset.Lookup,
	desired []DesiredResource,
	policy ScanPolicy,
) (ScanResult, error)

ScanDir is Scan over the directory at root (the CLI entry point). It verifies root is a directory, then scans os.DirFS(root). Symlinks are never followed.

type SourceFormRefusedError added in v0.37.0

type SourceFormRefusedError struct {
	// Field is the path of the list that could not be aligned, as a user would read it.
	Field string
}

SourceFormRefusedError is the projection giving up rather than guessing. The build and the live object BOTH changed one list, and the source's elements cannot be paired with the render's, so there is no way to say which of the source's bytes the user meant to keep.

Refusing is the only safe answer. Guessing an alignment writes one element's fields into another, and index alignment is not a safe guess: kustomize's strategic merge PREPENDS a container a patch adds (measured — the render is [sidecar, app] over a source of [app]), so source[0] is not render[0] in the one case where it matters most.

func (*SourceFormRefusedError) Error added in v0.37.0

func (e *SourceFormRefusedError) Error() string

type StoreOption added in v0.43.0

type StoreOption func(*storeSettings)

StoreOption is a scan-wide fact the store cannot read out of the repository, supplied by the caller that knows it. There is one today, and it exists because the GitTarget's declaration is not in the folder: see WithDeclaredNamespace.

func WithDeclaredNamespace added in v0.43.0

func WithDeclaredNamespace(ns string) StoreOption

WithDeclaredNamespace attributes every namespace-less document that no kustomization governs to ns. Pass it only when the GitTarget declares spec.serializeNamespace: false AND exactly one source namespace reaches it — the two conditions together are what make the attribution a fact rather than a guess, and the write path refuses the second namespace precisely so this stays true. An empty ns is the default: attribute nothing.

type Summary

type Summary struct {
	FilesTotal   int                                  `json:"filesTotal"`
	YAMLFiles    int                                  `json:"yamlFiles"`
	NonYAMLFiles int                                  `json:"nonYamlFiles"`
	Documents    int                                  `json:"documents"`
	Duplicates   int                                  `json:"duplicates"`
	Encrypted    int                                  `json:"encrypted"`
	ByClass      map[Class]int                        `json:"byClass"`
	ByGVK        map[string]int                       `json:"byGvk"`
	Diagnostics  map[manifestedit.DiagnosticLevel]int `json:"diagnostics"`
}

Summary is a bounded, status-friendly overview of a Report. It never grows with the number of resources beyond the small set of class and GVK keys.

type SweepMode added in v0.39.0

type SweepMode string

SweepMode decides whether the Git-only mark-and-sweep may turn an unmatched managed document into a managed drop. It is the planner-local shape of a GitTarget's spec.prune.mode, kept as its own type (like PlacementPolicy) so manifestanalyzer stays free of any Kubernetes API type dependency.

Only the INFERRED deletion path is modelled here: an explicit DELETE never reaches this planner, so PruneNever and PruneOnEvent both map to SweepRetainOrphans. They differ only on the path this type knows nothing about.

const (
	// SweepUnspecified is the ZERO VALUE, and it RETAINS.
	//
	// The direction is not arbitrary. A caller that forgets this field gets the outcome
	// whose failure mode is a stale document; the other default's failure mode is deleting
	// a tenant's manifests from a snapshot that was never authoritative. Every production
	// caller sets the field explicitly regardless — this is the backstop for the one that
	// is added later, not a default anyone should rely on.
	SweepUnspecified SweepMode = ""
	// SweepRetainOrphans emits no managed drop. An unmatched document produces no action
	// at all, so it is absent from the plan, the plan's action ordering, and the commit —
	// it is not a filtered-out action but an action that was never planned.
	SweepRetainOrphans SweepMode = "retain"
	// SweepDropOrphans emits PlanDropOrphan for every unmatched, followable managed
	// document in scope: full desired-state convergence.
	SweepDropOrphans SweepMode = "drop"
)

func (SweepMode) DropsOrphans added in v0.39.0

func (m SweepMode) DropsOrphans() bool

DropsOrphans reports whether this mode may emit a managed drop. Only SweepDropOrphans does; every other value, including an unrecognized one, retains.

type VerifyOptions added in v0.43.0

type VerifyOptions struct {
	// NamespaceSuppliedDownstream is spec.serializeNamespace: false — the target declared that its
	// documents do not encode a deployment namespace, so something outside this repository supplies
	// one. It relaxes the comparison for a rendered object that HAS no namespace, and only for
	// that object: see compareRoot.
	NamespaceSuppliedDownstream bool
}

VerifyOptions is what the write path knows about the GitTarget that the render comparison cannot see in the files.

type WriteIntent

type WriteIntent struct {
	// SourcePath is the file holding the document, slash-relative to the scan root.
	SourcePath string
	// Kind and Name identify the document within that file.
	Kind, Name string
	// Desired is the live object the document must render to. It is the whole point of
	// the check, and it is nil only when Removed or Unchecked says there is nothing to
	// check against.
	Desired *unstructured.Unstructured
	// Removed marks a document the flush deletes: it must disappear from the render.
	Removed bool
	// Unchecked marks a write whose rendered form we cannot predict, so the object is
	// permitted to move without being compared. There are exactly two: a SENSITIVE
	// document (the file is SOPS-encrypted, so kustomize renders the ciphertext, which
	// no plaintext live object can equal), and a bounded FIELD PATCH (the event carries
	// a few assignments, never a whole object to compare against).
	//
	// Unchecked weakens the oracle for those documents, and it is stated rather than
	// hidden: we can still prove that such a write disturbs nothing ELSE, which is the
	// half that protects other people's environments.
	Unchecked bool
	// Governed marks a document whose write was routed through a kustomization override
	// chain. These are the writes the oracle exists for, so one that turns out not to be
	// rendered by any root at all is a contradiction, and refused rather than skipped.
	Governed bool
}

WriteIntent is one document a flush writes, and what the render must show for it afterwards. Everything the batch does NOT declare an intent for must come out of the render byte-for-byte unchanged — that is the blast-radius half of the oracle, and it is what makes it safe for the projection to guess.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL