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
- func IdentityCompletePlacementTemplate(tmpl string, narrowedToOneType bool) bool
- func IsRemoteBaseEntry(entry string) bool
- func KustomizationBuildRefs(content []byte) ([]string, []string, bool)
- func LoadGitTargetIgnore(content []byte) (*IgnoreMatcher, []AcceptanceIssue)
- func PlacementTypeKey(group, version, resource string) string
- func RefusalError(acc Acceptance) error
- func RenderJSON(w io.Writer, rep Report) error
- func RenderPlacementTemplate(tmpl string, vars map[string]string) (string, error)
- func RenderRepoText(w io.Writer, rep RepoReport)
- func RenderScanText(w io.Writer, result ScanResult)
- func RenderText(w io.Writer, rep Report)
- func ValidPlacementTemplatePath(tmpl string) error
- func ValidPlacementTemplateSyntax(tmpl string) error
- func ValidateResolvedPlacementPath(p string) error
- func VerifyBatchRenders(before, after []manifestedit.FileContent, intents []WriteIntent) error
- type Acceptance
- type AcceptanceIssue
- type AcceptancePolicy
- type AcceptanceRefusedError
- type Actor
- type Allowlist
- type CauseKind
- type Class
- type Classification
- type DesiredResource
- type DocumentCause
- type DocumentModel
- type DocumentReport
- type EntryRole
- type FileModel
- type FileReport
- type FolderScan
- type ForeignEntry
- type ForeignKind
- type GVK
- type IgnoreMatcher
- type ImageOverride
- type IssueKind
- type KustomizationInfo
- type KustomizeOverrides
- type Layout
- type ManifestStore
- func BuildStore(ctx context.Context, fsys fs.FS, lookup typeset.Lookup) *ManifestStore
- func BuildStoreFromFiles(ctx context.Context, files []manifestedit.FileContent, lookup typeset.Lookup, ...) *ManifestStore
- func BuildStoreFromScan(ctx context.Context, scan FolderScan, lookup typeset.Lookup, ...) *ManifestStore
- type MappingOutcome
- type NamespaceSource
- type NamespaceSourceKind
- type OverlapConflict
- type OverrideEdit
- type PlacementPolicy
- type PlacementRefusalReason
- type PlacementRefusedError
- type PlacementRequest
- type PlacementResult
- type PlacementSource
- type Plan
- type PlanAction
- type PlanActionKind
- type Policy
- type ReadEdge
- type RecordRef
- type RefusalReason
- type RenderDivergence
- type RenderRefusedError
- type RenderedImage
- type RenderedOverrides
- type RenderedReplicas
- type RenderedTypes
- type ReplicaOverride
- type RepoCandidate
- type RepoReport
- type RepoSummary
- type Report
- type ResourceCounts
- type RetainedDocument
- type ScanPolicy
- type ScanResult
- type SourceFormRefusedError
- type Summary
- type SweepMode
- type WriteIntent
Constants ¶
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).
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.
Variables ¶
This section is empty.
Functions ¶
func IdentityCompletePlacementTemplate ¶
IdentityCompletePlacementTemplate reports whether tmpl is guaranteed to render a distinct path for every distinct resource identity — the structural guarantee "Sensitive placement and uniqueness" in the design doc requires of every accepted sensitive template. narrowedToOneType is true for a ByType entry (the map key itself already names one exact type); a Default template must additionally carry the type variables since it applies across every type the class does not name explicitly.
func IsRemoteBaseEntry ¶ added in v0.37.0
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
KustomizationBuildRefs returns the local files a kustomization loads from disk: its resources+bases graph entries and its patch `path:` files, each raw and relative to the kustomization's own directory (deprecated bases: folded into resources exactly as the builder does). ok is false when the bytes are not a parseable kustomization. Remote and inline entries are omitted — they name no local file to scan.
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 — a referenced file or base kustomization — never a whole sibling directory. See internal/git/render_scope.go and 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 ¶
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 ¶
RenderJSON writes the report as indented JSON. It is the machine-readable form shared by the controller status path and the CLI.
func RenderPlacementTemplate ¶
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 ¶
RenderText writes a human-readable summary of the report.
func ValidPlacementTemplatePath ¶
ValidPlacementTemplatePath statically rejects a declared template whose own literal text (never mind any variable substitution, which sanitizePlacementSegment already defends per-value) could render outside the GitTarget's spec.path or with the wrong kind of file name: an explicit ".." path segment, a leading "/" (absolute), a "\" separator, or a suffix that isn't ".yaml"/".yml" (a template ending in the literal "{sensitiveSuffix}" placeholder is accepted without rendering it, since that variable only ever expands to ".yaml" or ".sops.yaml"). This runs at the GitTarget's Validated gate — before any repository scan, and before any resource can ever trigger a write — so a bad template fails fast and visibly instead of silently skipping (or, without ValidateResolvedPlacementPath's runtime backstop, escaping) resource by resource.
func ValidPlacementTemplateSyntax ¶
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 ¶
ValidateResolvedPlacementPath enforces the design doc's "Path validation" contract against a fully-resolved (variable-substituted) placement path, regardless of which mechanism produced it: non-empty, a clean relative path staying under the GitTarget's spec.path (no "..", not absolute, no redundant segments), no Windows-style backslash separators, a non-empty final file name, and a recognized YAML suffix (".sops.yaml"/".sops.yml" satisfy this too, since they end in ".yaml"/".yml"). finishPlacement runs this on every path before a single byte is written, so a bad declared template (Option B) can never escape the folder the writer owns — sanitizePlacementSegment already defends each individual variable's value, but the template's own literal text is author-supplied and unconstrained without this gate.
func VerifyBatchRenders ¶
func VerifyBatchRenders(before, after []manifestedit.FileContent, intents []WriteIntent) 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:
- every document the flush writes renders to exactly the live object, and
- 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 M4 adoption gate: the distinct step between "build the store" and "use it as the planning model", described in docs/spec/current-manifest-support-review.md ("Acceptance Checks On First Materialization"). A GitTarget folder is adopted only when it passes; any blocking refusal stops it and reconciles nothing until a human cleans the folder.
The gate implements the five-bucket classification and the refuse rules:
- duplicate manifest identity (we will not guess which copy the author meant);
- a managed file that is not entirely valid KRM — a multi-document file may hold only managed KRM documents, never an empty/comment/non-KRM/invalid passenger (Non-Negotiable Design Decision #2). This is what lets the store drop the per-document index: an accepted managed file's documents are contiguous;
- a standalone non-KRM or invalid YAML file (bucket 2: the dangerous unknown);
- unwatched API-backed KRM (bucket 4: served, but this GitTarget does not watch it) — refused, never pruned;
- recognised 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 that mixes managed resources with an allowlisted non-API KRM document (allowlisted KRM must live in its own retained file).
Allowlisted non-API KRM such as kustomization.yaml is retained outside the model (store.Retained) and never materialised — see the Allowlist type. Non-YAML files and standalone empty documents are ignored and never cause a refusal.
The mapping-aware refusals (unwatched/unresolved/out-of-scope) require an API source: a structure-only store cannot judge them, so they are skipped, leaving the structure-only starter checks (duplicate, impure managed file, non-KRM, invalid). This matches the design's "starter requirement".
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 — duplicate identity, impure managed file, standalone non-KRM/invalid YAML, a managed resource hiding in an allowlisted build-directive, and an unsupported kustomization. It NEVER runs the mapping-aware refusals (unwatched / out-of-scope), which depend on live followability discovery and can blink on a discovery wobble.
This is the live writer's entry point. The writer's store is built with a ready followability registry, so hasAPISource would be true and plain Accept would also run the mapping refusals — but the writer must refuse only on the cases we already know are 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.
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 either of that value's two acceptance sites: IssueOutOfScope needs a declared AcceptancePolicy.InScope, and IssueUnresolvedKRM needs MappingNotFollowable, which a not-ready registry never produces because it resolves every document to MappingNoSource instead.
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 ¶
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.
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
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.
This is a full-snapshot input — the "Resync" path of the design's "Two Paths, One Plan Type" (docs/spec/reconcile-via-watchlist-mark-and-sweep.md). It is NOT a per-event PendingChange: BuildPlan mark-and-sweeps every watched document absent from this set as a managed drop, so the set must be the whole desired state (scan mode / resync), never a partial batch. Steady-state, per-event planning that targets a single identity and emits an explicit delete-document — without sweeping — is the separate pending-change path (M7, on M6's delete-identity resolution).
Object must be non-nil: every entry in a desired snapshot is a resource that exists. A nil Object is a malformed entry — deliberately NOT a delete tombstone, because in a sweeping planner a lone tombstone is indistinguishable from "every other document is now an orphan". It cannot simply be skipped either: because the planner mark-and-sweeps, skipping a nil entry would leave the matching managed document unmatched and let the Git-only sweep DROP it. So BuildPlan instead protects the matching document from the sweep (by resolved resource identity) and emits a diagnostic, so a malformed entry never causes a destructive drop. A genuine per-event delete (a DELETED watch event) is resolved separately by 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 override-produced value — the values read off the real render, the
// suppliers read off a dyed counterfactual one. It is what the write-side projection
// inverts against, and it replaced ~400 lines that re-implemented kustomize's
// transformers in order to guess the same thing.
//
// Nil when no render root supplies a chain, when distinct roots disagree, or when the
// dyed build could not be trusted (see attributeRoot). Nil means NO ATTRIBUTION: the
// writer routes nothing to an entry, and the verification re-render adjudicates whatever
// the source document alone can carry. 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. It is byte-free: the full manifestedit node tree is built only when a plan action touches the document (Snapshot is the lazy handle), and it deliberately stores neither its file path nor its position. The file path is the containing FileModel's; the document's TRUE file index is reconstructed when needed (by reconstructManagedIndices) from the record-less diagnostic gaps — every empty/non-KRM/invalid document leaves a diagnostic at its position, so the managed documents fill the remaining positions in document order. That recovers the right index for any file, contiguous or not, so the report, the planner (documentLocations), and the acceptance gate all agree without storing a fragile mutable field. The M4 acceptance gate additionally refuses any managed file that is not entirely valid KRM (Decision #2), so an accepted file is contiguous anyway. manifestedit is given the position only at apply time. See docs/spec/current-manifest-support-review.md ("Concrete Data Structures") and the M4 acceptance gate (acceptance.go).
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.
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 ¶
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.
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 retained kustomization.yaml that uses a feature // the contextual-namespace writer cannot map back to editable source documents // (generators / components / helm / replacements / transformers / name(pre|suf)fix / // remote bases). The folder is refused rather than written, because the operator cannot // take responsibility for content produced this way. // // `patches:` is NOT on that list any more: a strategic-merge patch named by path is // tolerated as read-only build context (the render is mirrored, the patch file is never // managed, and nothing is routed into it). The shapes we cannot read still refuse by name — // an inline patch, a JSON6902 op list, a path outside the tree. 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. // // It is the write-plan half of "attribution may be heuristic, verification may not" // (docs/design/support-boundary/render-attribution.md §5). The projection is ALLOWED to // guess which file an edit belongs in, precisely because this refuses the guess when the // renderer disagrees. And it must refuse LOUDLY: a write that does not survive the // re-render is one that would not converge — the entry overrides it straight back on the // next render — so absorbing it would leave a resource silently un-mirrored forever, // which is the exact failure this whole path exists to prevent. 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). // // The alternative to refusing is aligning the two lists by position, and that is not a // conservative guess — it is measurably wrong: kustomize's strategic merge PREPENDS a // container a patch adds, so the source's first element is not the render's first element in // exactly the case where it matters. Writing one element's fields into another is the kind of // corruption no re-render can catch, because the patch re-imposes its own values and the // render comes out identical either way. So this refusal is not the oracle being cautious; it // is the one place the oracle cannot see, and it must fail loudly instead. 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" )
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.
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 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: it is collected first-occurrence-wins
// over the documents that CLAIM their identity (the collapse), so a later
// document that duplicates an earlier identity is not the winner and is
// detectable as such. Claiming mirrors manifestedit's duplicate rule exactly —
// cleanly-editable and encrypted documents claim, documents with disallowed
// constructs do not — so the collapse and manifestedit's duplicate diagnostic
// agree. The diagnostic is emitted by the manifestedit index pass that feeds the
// collapse.
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 (slash paths) a release document — an Argo
// CD Application's helm.valueFiles or a Flux HelmRelease's spec.chart.spec.valuesFiles — names
// by a path that matches locally: NAMED read-only context, not junk, and not proof the deployer
// consumes this file. The acceptance gate consults it so a values file the repository points at
// no longer refuses its folder as non-krm-yaml, and the discovery scan does not count it as
// noise. It is never materialised, so the operator retains the file yet never writes it. See
// docs/design/support-boundary/values-file-projection.md §2 (Move 1) and
// values-content-architecture.md.
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, in-memory structure model of a GitTarget folder described in docs/spec/current-manifest-support-review.md ("Concrete Data Structures"). It is the backbone the live writer, scan mode, the CLI, and status all consume; the analyzer Report is rendered as a projection over it.
Only MANAGED files live in FilesByPath: YAML files carrying at least one KRM document. Non-YAML auxiliary files and YAML files with no KRM document are known to the analyzer but never become FileModels, so they have no document set to empty and can never be swept or deleted.
func BuildStore ¶
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 byte-free structure model from already-collected file bytes, rather than walking an fs.FS (BuildStore). It is the live writer's entry point: the writer reads the worktree subtree once at a commit boundary — it needs the bytes anyway, to hydrate and apply — and hands the same FileContent slice here, so the store and the bytes the plan is applied to are one snapshot.
lookup resolves each document's GVK to a served resource identity; a nil lookup keeps it structure-only (no resource index), exactly as BuildStore. allowlist names the build-directive files retained outside the model; pass the zero value to materialise every KRM document.
func BuildStoreFromScan ¶
func BuildStoreFromScan( ctx context.Context, scan FolderScan, lookup typeset.Lookup, allowlist Allowlist, ) *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 Kind is Kustomize); 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/spec/contextual-namespace-and-kustomize-folder-editing.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" )
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.
It is two rules, and neither models a transformer:
- WHERE THE LIVE OBJECT AND THE RENDER AGREE, THE SOURCE KEEPS ITS BYTES (sourceForm). The build already produces what the cluster runs, so the source is by construction what produced it. This is what stops the writer mirroring the build's own output back into the build's input — an injected label, a patched CPU request — and it needs to know nothing about labels or patches to do it.
- WHERE THEY DISAGREE, THE USER CHANGED SOMETHING. If an images:/replicas: entry supplies that field — which the dye says, read off a counterfactual render — the change is routed to the ENTRY and the source keeps its bytes there too. Otherwise it is written through to the source document.
Anything it cannot route safely — a component removal an entry supplies, a component a sibling entry clears, or two containers demanding different values for one entry field — routes NOTHING and leaves the live value in place. That is not a guess and not a fallback to another heuristic: the proposal then has to 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 quietly never converges.
The one thing it refuses outright is a list the build and the user BOTH changed whose elements cannot be paired by name (*SourceFormRefusedError): there is no honest way to say which of the source's bytes the user meant to keep, and aligning by position is measurably wrong.
gitRaw is the source document parsed as JSON-typed maps (sigs.k8s.io/yaml); desired is the sanitized projection the writer would otherwise compare. The returned object is always a copy; desired is never mutated. authorInto is the kustomization the writer may AUTHOR a new images:/replicas: entry into when a value the SOURCE document supplies diverges in live and the source is out of the write jail (a base an overlay reads read-only). It is "" for a self-contained subtree and for an in-jail document, where a source-supplied change is written into the file directly. When set, a diverging source-supplied image component or replica count becomes a proposed new entry rather than a refused base write — the "edit a specific environment, get the override authored" capability of docs/design/support-boundary/render-root-scoping.md §4.
type PlacementPolicy ¶
PlacementPolicy is a resolved GitTarget placement declaration (Option B2 of docs/spec/gittarget-new-file-placement-rules.md): a single exact-type map plus a fallback default template, consulted for every resource regardless of sensitivity. It mirrors api/v1alpha3.GitTargetPlacementSpec field-for-field but is defined locally so this analyzer package stays free of any Kubernetes API type dependency; the git package converts the CRD spec into this shape.
There is no sensitive/normal split here: sensitivity is a write-safety property (encrypt the content, keep the path identity-complete, never append or co-mingle) enforced after resolution — in finishPlacement (sensitive never appends) and in the writer (encrypt by classification) — not a second map to configure.
A nil *PlacementPolicy, or one with no matching ByType entry and no Default, falls through to the kustomize-root fallback and then the canonical path.
type PlacementRefusalReason ¶ added in v0.41.0
type PlacementRefusalReason string
PlacementRefusalReason names WHY a placement was refused, from a closed set. It is a metric label value (placement_refusals_total{reason}) as well as a log field, so the strings are lower_snake_case and are part of the observability contract.
A refusal is a resource the operator did NOT mirror. It has to be countable per (GitTarget, type): before this it left a log line and, on the resync path, a single integer in a summary — neither of which a dashboard or an alert can reach, so a misconfigured template that silently skipped one Secret on every reconcile was invisible unless somebody read 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/design/manifest/version2/ gittarget-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, per docs/spec/gittarget-new-file-placement-rules.md: a declared template (Option B) wins when present; otherwise the folder's one supported kustomize root, if it has exactly one; otherwise the canonical path.
There is no step that reads the layout of the *other* documents of this type. Sibling-cohort inference (Option C) was removed: it let a human's edit to the repository change where the operator writes, with no Kubernetes object changing and nothing in status recording the move, and its central namespace-agnosticism guard had already failed once by cascading. The argument, and what replaced it (a declared byType line, plus the placements_total metric that says which (GitTarget, type) needs one), is in docs/design/open-asks-priority.md.
store is still the pre-plan snapshot for the whole batch and must never be mutated mid-batch: the remaining store reads — does the resolved path already hold an append-safe file, does its directory carry a kustomization — must answer the same way for every resource in one batch, so a batch of several new creates resolves order-independently regardless of event order (P2 of the design doc).
An error is returned only when the resolved placement cannot be honoured safely — currently, a sensitive resource whose resolved path already exists (sensitive documents are never appended; see "Sensitive placement and uniqueness" in the design doc). The caller must skip creating that resource and surface the error as a diagnostic rather than writing into a shared or multi-document sensitive file.
type PlacementSource ¶
type PlacementSource string
PlacementSource names which mechanism produced a PlacementResult's Path. It is the "why here" answer for one new document, and it is reported three ways: the write path's log line, the placements_total metric's `source` label, and the scan/dry-run trace. The values are a public observability contract — they are metric label values — so they are lower_snake_case and are not renamed lightly.
There are exactly three, and the list is closed by construction: a declaration, one structural fact about the folder, and the built-in path. Nothing here reads the repository's *layout* to guess an intent — that was Option C's sibling-cohort ladder, and it is gone (see the deletion argument in docs/design/open-asks-priority.md).
const ( // PlacementSourceDeclared is Option B: an explicit placement.byType/default // template matched. PlacementSourceDeclared PlacementSource = "declared" // 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
}
Plan is the first-class, cross-layer contract described in docs/spec/current-manifest-support-review.md ("Writer Model: Plan, Apply, Dirty Flush"). It is a pure function of (ManifestStore, desired set, policy): the same value the live writer applies, scan mode renders, the CLI prints, and GitTarget status summarizes. M3 builds the model and its computation; applying it to a worktree is M7.
It carries enough detail to render text/JSON/status without recomputing any decision: each action names its kind, the document it concerns, and a reason.
func BuildPlan ¶
func BuildPlan( store *ManifestStore, files []manifestedit.FileContent, desired []DesiredResource, policy Policy, ) Plan
BuildPlan computes the Plan from the byte-free ManifestStore, the file bytes that back it (hydration source for the patch/no-op decision), the COMPLETE desired snapshot, and the policy. It graduates manifestreport.BuildReport's read-only create/update/delete/skip comparison into the materialized model's plan.
This is the full-snapshot "Resync" planner (scan mode, CLI, initial reconcile / resync): it mark-and-sweeps — every watched document with no entry in desired is a managed drop — so desired MUST be the whole desired state, never a partial batch. The steady-state path (one plan action per live event, where a DELETED event is an explicit delete-document and nothing re-sweeps) is PlanDelete for removals (M6); the per-event create/patch twin and the writer that folds both arrive with M7.
The store is expected to have been built with the same mapper whose watched set produced desired; under a structure-only store (no resolved mappings) no managed drop is ever emitted, preserving the no-cluster promise even if a desired set is passed by mistake. policy.Sweep is the second, independent gate on the same deletions — the caller's declared prune policy — 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 desired set is upserted as usual, but the Git-only mark-and-sweep only drops/skips a managed document whose RESOLVED resource identity is in scope — every out-of-scope document is left untouched, never swept. It is the per-type (M12) primitive: a reconcile passes that type's desired objects with a predicate matching that type's (group, resource); a sweep passes an EMPTY desired set with the same predicate, so a removed type's documents drop and no sibling type is ever collaterally deleted. The caller MUST keep desired in scope, since the desired set is the scope on the upsert side.
With allInScope this is exactly BuildPlan — the full-snapshot mark-and-sweep — so the two share one implementation and one set of safety guarantees. See docs/spec/type-lifecycle-events-and-wobble-settling.md (Proposal 3 / M12).
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
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 ¶
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
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.
//
// It is NOT a list of cluster-scoped types. It holds two facts this scan cannot tell
// apart: a genuinely cluster-scoped type, and a namespaced type relying on whatever
// namespace the applier defaults to. Separating them needs API discovery, which a
// structure-only scan does not have.
//
// A type can appear here AND under ByNamespace. Two ConfigMaps, one carrying a
// namespace and one not, is an ordinary folder, not a contradiction.
NamespaceUndeclared []string `json:"namespaceUndeclared,omitempty"`
}
RenderedTypes is what a folder renders, expressed so that the pairing between a type and the namespace it lands in survives. A set of types beside a set of namespaces loses it: a folder rendering a Deployment into frontend and a Service into backend would read as four combinations, and a tool generating one watch rule per pair would authorize two that match nothing in the repository.
Every type is a canonical GVK string — "group/version/kind", or "version/kind" for the core group, which is GVK.String and the same spelling Summary.ByGVK already uses.
For a render root the sets come off a real kustomize build, so a base outside the subtree is included and a `namespace:` transformer is already applied. For a plain folder the documents are the render. A root that failed to build reports nothing at all: 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: one edge per (candidate, directory
// it renders from outside its own subtree), sorted. It is the same relation each
// candidate's readScope/readBy report, collected in one place so a consumer can draw
// the graph without walking the candidates.
//
// Most edges end at a directory that is NOT a candidate — a folder a kustomization
// references is never a render root, so it is offered to nobody. Those nodes are the
// edge targets absent from Candidates, and nothing else identifies them: 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 ¶
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 ¶
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 is true for a whole-file kustomization retention that the operator
// cannot map back to editable source documents, for either of two reasons:
//
// - it uses a feature outside the supported contextual-namespace subset
// (generators / patches / components / helm / replacements / transformers /
// name(pre|suf)fix / remote bases), or declares malformed images/replicas; or
// - it is a render root KUSTOMIZE CANNOT BUILD (reasonRenderFailed). If the build
// fails, Flux cannot deploy the folder either, and we cannot know what it renders
// to — and a silent pass would be worse than useless, because a root that yields
// no chain also yields no ambiguity, which disarms the write-fan-in guard.
//
// The acceptance gate refuses either (IssueUnsupportedKustomize) rather than writing
// into content it cannot safely manage. Only ever 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 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 source DELETE event never reaches this planner — it is resolved by PlanDelete and gated at the writer — so PruneNever and PruneOnEvent both map to SweepRetainOrphans. The two 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
DropsOrphans reports whether this mode may emit a managed drop. Only SweepDropOrphans does; every other value, including an unrecognized one, retains.
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.
Source Files
¶
- acceptance.go
- acceptance_refusal.go
- analyzer.go
- delete_plan.go
- dye.go
- gittargetignore.go
- kustomization_parse.go
- kustomize_render.go
- override_chain.go
- overrides.go
- overrides_attribution.go
- overrides_projection.go
- placement.go
- plan.go
- render.go
- render_fidelity.go
- render_scope.go
- render_verify.go
- scan.go
- scan_repo.go
- solvable.go
- source_form.go
- store.go
- valuefiles.go