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 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 Allowlist
- type CauseKind
- type Class
- 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 PlacementRequest
- type PlacementResult
- type PlacementSource
- type Plan
- type PlanAction
- type PlanActionKind
- type Policy
- type RecordRef
- type RefusalReason
- type RenderRefusedError
- type RenderedImage
- type RenderedOverrides
- type RenderedReplicas
- type ReplicaOverride
- type RepoCandidate
- type RepoReport
- type RepoSummary
- type Report
- type ResourceCounts
- type RetainedDocument
- type ScanPolicy
- type ScanResult
- type Summary
- type WriteIntent
Constants ¶
const ( ReasonOverlayFanOutUnsupported = "overlay-fan-out-unsupported" ReasonRefusedStructural = "refused-structural" )
Refusal reason codes. The distinction between the two is load-bearing: ReasonOverlayFanOutUnsupported is a forward-looking "not yet" that flips to accepted if render-root scoping ships; ReasonRefusedStructural is the permanent boundary. Discovery must never collapse them into one "refused".
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).
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 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"`
}
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 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 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
// 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 // 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 / 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), 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 / patches / 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. 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" )
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 that diverge while overlays stay unsupported: a kustomize-overlay has a well-understood layout yet is not accepted until render-root scoping lands.
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). // The operator hard-scopes to one subtree and cannot see the base, so it is refused // today with the forward-looking overlay-fan-out-unsupported reason — it flips to // accepted if render-root scoping ships. 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 permanent support boundary, never a "not yet". 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
}
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.
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) declaring the entry.
KustomizationPath string
// Edit is the bounded scalar update the manifestedit editor applies.
Edit manifestedit.KustomizationEdit
}
OverrideEdit routes one live-value change to a field of an existing 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, ) (*unstructured.Unstructured, []OverrideEdit)
SplitDesiredForOverrides maps the live desired object back through what kustomize actually renders. It returns the object the source document should be compared against — a copy of desired with every override-produced value restored to its SOURCE form, so the file keeps its bytes — plus the entry edits for the values an override entry supplies.
It is driven by RenderedOverrides, which carries both halves of the answer straight from the renderer: what each field renders to, and which entry supplied it (read off a dyed counterfactual build). Nothing in here re-implements a kustomize transformer, which is the entire point of this workstream — every shipped bug in this area came from the re-implementation, and all of them are deleted with it.
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.
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.
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), in the writer (encrypt by classification), and in cohortMembers (inference never crosses the encrypted boundary) — not a second map to configure.
A nil *PlacementPolicy, or one with no matching ByType entry and no Default, falls through to sibling inference (Option C) and then the canonical fallback.
type PlacementRequest ¶
type PlacementRequest struct {
Identifier types.ResourceIdentifier
Kind string
Sensitive bool
}
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
// Cohort describes the sibling cohort and ladder step that produced Path;
// empty unless Source is PlacementSourceInferred.
Cohort string
// 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) 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 (see design doc: "the new file inherits its
// sibling's NamespaceSource").
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 an existing sibling cohort decides (Option C, steps 1/2 — same type+namespace, then same type+any namespace); otherwise the canonical path.
store MUST be the pre-plan snapshot for the whole batch and must never be mutated mid-batch, so a batch of several new creates resolves order-independently regardless of event order — a new resource never becomes another new resource's sibling within the same commit (P2 of the design doc).
Step 3 (same namespace, any type) is deliberately not implemented: the design doc's own P5 discussion flags it as the highest-risk rung (an unbounded namespace-wide bundle that swallows every new type sharing a namespace), and steps 1/2/4 already cover the launch use cases (per-type bundles, per-type files, canonical). A namespace-bundle layout remains reachable via Option B.
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, for logging and the scan/dry-run "why here" trace (P8 in the design doc).
const ( // PlacementSourceDeclared is Option B: an explicit placement.byType/default // template matched. PlacementSourceDeclared PlacementSource = "declared" // PlacementSourceInferred is Option C: no declared template matched, but an // existing sibling cohort determined the destination. PlacementSourceInferred PlacementSource = "inferred" // PlacementSourceCanonical is the built-in, versionless // {namespaceOrCluster}/{group}/{resource}/{name}.yaml fallback: no declared // template and no sibling to follow (e.g. an empty repository, or the // type/namespace is new). 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
}
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.
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
}
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.
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 ¶
RefusalReason is one machine-readable reason a candidate is not accepted, with a human detail. A candidate carries none when accepted.
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 {
// 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 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 base directories outside this candidate's own subtree that its
// kustomization reads. Empty for plain and self-contained candidates.
ReadScope []string `json:"readScope,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"`
// 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"`
// FleetRoot is true when the repo root is a cluster/fleet root (top-level clusters/ +
// apps/ + infra/): a GitTarget points at an app subtree, never such a root. The root
// is never itself a candidate; leaf folders still surface normally.
FleetRoot bool `json:"fleetRoot,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) 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
}
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).
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 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 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.