manifestanalyzer

package
v0.41.2 Latest Latest
Warning

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

Go to latest
Published: Aug 25, 2026 License: Apache-2.0 Imports: 7 Imported by: 0

Documentation

Overview

Package manifestanalyzer is the public answer to two questions a tool built around GitOps Reverser needs to ask about a Git repository, without a cluster and without writing anything:

ScanFolder — may this folder become a GitTarget, and if not, why?
ScanRepo   — which folders in this repository could, and what shape is each one?

The decisions come from the same acceptance gate the operator's writer enforces before it commits a byte, so a tool built on this package cannot drift from the operator that will later refuse the folder. Nothing here re-implements a rule.

Stability: none yet

GitOps Reverser is pre-1.0, and so is this package. It is the surface a tool is meant to build on rather than reaching into internal/, but it carries no compatibility guarantee: fields may be renamed, removed, or given a new meaning in any release. Pin a version: each release is tagged `vX.Y.Z`, so `go get github.com/ConfigButler/gitops-reverser@vX.Y.Z` (and `go install github.com/ConfigButler/gitops-reverser/cmd/manifest-analyzer@vX.Y.Z`) resolve to that release rather than an opaque pseudo-version — the whole repository is one Go module, versioned as a unit.

Two habits will nonetheless save you work. Ignore fields you do not recognise, because new ones do get added. Do not switch on the human-readable strings — Issue.Message and RefusalReason.Detail are prose, while IssueKind and the refusal reason codes are the values worth matching on.

The report is a KRM document, and it says what produced it

Both reports carry APIVersion and a kind, the scan request in spec and the findings in status. A reader that does not know the apiVersion it is handed should refuse it rather than best-effort parse, by the same rule every Kubernetes client already follows; adding a field is not a version bump. The document is never served and never applyable — see TypeMeta.

FolderReportStatus.Generator and RepoReportStatus.Generator name the build that produced the report, so a document that outlives the process that made it still says which release decided its contents. `manifest-analyzer --version` prints the same pair.

A refusal says whether it can be solved

Issue and RefusalReason carry a `solvable` boolean and, when someone can act, an Actor. A code alone cannot tell "one broken document away from working" from "this folder cannot be adopted", and guessing from the code is how a consumer ends up telling a user to go fix something only their platform team can, or nothing at all. The answer describes this release and makes no promise about the future, so read it on every scan rather than caching a mapping from it.

The field is always present. A report produced before it shipped carries no `solvable` key at all, which is the one case where "nobody said" is distinguishable from "no".

Everything under internal/ carries no guarantee either, and is not importable from another module. One format from there is nonetheless a contract you may build on: a resource's identity key is "{group}/{version}/{resource}/{namespace}/{name}", with the namespace segment dropped (not emitted empty) for a cluster-scoped resource and an empty group segment for core resources, so the four shapes are "apps/v1/deployments/prod/api", "rbac.authorization.k8s.io/v1/clusterroles/admin", "/v1/secrets/prod/db" and "/v1/nodes/node-1". It is specified and golden-tested at ResourceIdentifier.Key in internal/types/identifier.go, which also records why a join that must survive a storage-version bump keys on the versionless Git path instead.

The command-line equivalents are `manifest-analyzer --mode scan-folder --format json` and `--mode scan-repo --format json`, which emit exactly the documents FolderReport and RepoReport marshal to. `--format yaml` prints the same document in the serialization a KRM document reads best in. Exec the binary if Go is not your language; import this package if it is.

To see a whole document before you write a line of parsing, read pkg/manifestanalyzer/testdata/repo-report.golden.yaml and folder-report.golden.yaml. They are checked-in output, regenerated from the real scan, so they cannot describe a shape you will not receive.

What it does not do

Neither entry point resolves types against a live cluster, so neither reports whether a document's kind is actually served, nor produces a write plan. Both are structure-only: they read bytes, never follow symlinks, and never write. The operator applies the same gate plus the cluster-aware checks when a GitTarget adopts the folder — a folder this package accepts can still be refused for a reason only a cluster can see (an unresolved kind, an out-of-scope resource).

Index

Constants

View Source
const (
	// APIVersion is the group/version of both report kinds.
	APIVersion = "manifestanalyzer.configbutler.ai/v1alpha1"
	// KindFolderReport is the kind of a [FolderReport].
	KindFolderReport = "FolderReport"
	// KindRepoReport is the kind of a [RepoReport].
	KindRepoReport = "RepoReport"
)

The report envelope. A report is a KRM document — apiVersion, kind, spec, status — and not a bespoke JSON shape, because the three questions a bespoke shape leaves open ("what does a version bump assert", "must a reader refuse a version it does not know", "what bumps it at all") are already answered by the Kubernetes API conventions every consumer of a GitOps tool has read. Citing that document is cheaper than writing one.

APIVersion replaces the former SchemaVersion marker outright. It follows the alpha/beta/GA contract: an alpha version may change incompatibly in any release, and a reader that does not know the version it is handed must refuse it rather than best-effort parse — the same rule every Kubernetes client already follows. Adding a field is still not a version bump, so ignore fields you do not recognise.

View Source
const (
	// ModeScanFolder is the mode of a [FolderReport]: may THIS folder become a GitTarget?
	ModeScanFolder = "scan-folder"
	// ModeScanRepo is the mode of a [RepoReport]: which folders in this repository could?
	ModeScanRepo = "scan-repo"
)

Scan modes, as reported in a report's spec.

View Source
const GeneratorName = "manifest-analyzer"

GeneratorName is the tool name every report carries. It names the analyzer, not the binary that hosts it: a report produced by the library linked into another program still says manifest-analyzer, because that is what decided the contents.

Variables

This section is empty.

Functions

func Version added in v0.40.0

func Version() string

Version reports the release that produced a report, resolved in three steps: the ldflags-injected version if the build set one; otherwise the module version runtime/debug records — which is what `go install ...@vX.Y.Z` (the install path this package's own documentation recommends) produces for free, with no build change and no release-workflow change; otherwise the literal "dev".

It is never empty, so a consumer may read an ABSENT generator as "produced before this field shipped" without having to distinguish that from "produced by a build that did not know itself".

Types

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 matters because some refusals are solvable ONLY by the person who owns the GitTarget, who is often 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

ScanFolder and ScanRepo are STRUCTURE-ONLY: they read files and never a cluster. They report ActorUnknown or ActorRepositoryAuthor, and never ActorPlatformOperator — not as an accident of the current code, but because the two refusals that name the platform operator both need something only a cluster can supply. IssueOutOfScope needs a GitTarget's declared scope, and an unfollowable-type refusal needs a type registry that has seen the API server. A structure-only scan has neither, and a scan that cannot see the cluster has no business claiming a CRD is missing.

So a consumer's platform-operator branch is unreachable for these two entry points. That is a guarantee rather than a gap: it is what makes it safe to render a refusal from a repo scan as "the repository author can fix this, or nobody can". Only the operator's own live write path names the platform operator.

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 Candidate

type Candidate struct {
	// Path is slash-separated and relative to the repository root.
	Path   string `json:"path"`
	Layout Layout `json:"layout"`
	// AcceptedByOperator reports whether the operator would adopt this folder today.
	AcceptedByOperator bool            `json:"acceptedByOperator"`
	RefusalReasons     []RefusalReason `json:"refusalReasons,omitempty"`
	// RenderRoot reports whether the candidate is a kustomize render root.
	RenderRoot bool `json:"renderRoot"`
	// ReadScope lists the directories outside this candidate's 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.
	//
	// It is the field that explains the most confusing accept the scan produces: a folder
	// reported acceptedByOperator with resources.editable 0 is an overlay whose documents
	// live in a base it does not own. Read with [Candidate.ReadBy] and
	// [RepoSummary.ReadEdges], it is one direction of the repository's folder graph.
	ReadScope []string `json:"readScope,omitempty"`
	// ReadBy names the candidates whose build renders THIS directory's content — the edge
	// that decides whether adopting a folder is merely wrong or actively disruptive.
	//
	// It is usually empty, and structurally so: a directory another kustomization
	// references is never a render root, so it is never offered as a candidate at all, and
	// those edges end at a directory no candidate list mentions instead. The exception is a
	// plain folder holding a file some distant kustomization lists under resources: — a
	// candidate that another folder genuinely depends on.
	ReadBy []string `json:"readBy,omitempty"`
	// InferredNamespace is the namespace the candidate resolves to, when unambiguous.
	InferredNamespace string         `json:"inferredNamespace,omitempty"`
	Resources         ResourceCounts `json:"resources"`
	// RenderedTypes says which type this folder renders into which namespace. For a render
	// root it is read off a real kustomize build, so it covers what the folder pulls from a
	// base outside its own subtree and has the namespace transformer already applied; for a
	// plain folder the documents are the render.
	//
	// It answers the two questions that gate provisioning from a folder scan, and both gate
	// a step that is not cheap to undo. The schemas have to be served wherever this is
	// applied — a type the destination does not serve does not degrade, the apply fails
	// with "no matches for kind" and waits for the next resync. And naming a GitTarget's
	// allowed source namespaces, or one watch rule per (type, namespace), needs the exact
	// pairs: [Candidate.InferredNamespace] is one name, and a folder can render into
	// several.
	//
	// Everything is empty for a render root kustomize could not build. What it renders is
	// not knowable, and saying nothing is the honest answer.
	RenderedTypes RenderedTypes `json:"renderedTypes"`
	// OverlapsWith lists candidate paths this one nests with. Two overlapping candidates
	// can never both become GitTargets — a folder has exactly one owner.
	OverlapsWith []string `json:"overlapsWith,omitempty"`
}

Candidate is one folder that could become a GitTarget.

type FolderReport

type FolderReport struct {
	TypeMeta `json:",inline"`

	Spec   FolderReportSpec   `json:"spec"`
	Status FolderReportStatus `json:"status"`
}

FolderReport answers "may this folder become a GitTarget?". It is a KRM document: the scan REQUEST is the spec, what was FOUND is the status. See TypeMeta for why the envelope, and why the document is never served or applyable.

func ScanFolder

func ScanFolder(ctx context.Context, root string) (FolderReport, error)

ScanFolder runs the adoption gate over a folder on disk. It is read-only, needs no cluster, and never follows symlinks.

The returned error covers only I/O: a folder that cannot be adopted is a successful scan with Accepted=false, not an error.

func ScanFolderFS

func ScanFolderFS(ctx context.Context, fsys fs.FS) FolderReport

ScanFolderFS is ScanFolder over an arbitrary io/fs.FS — an in-memory tree, a tarball, or a Git tree exposed as a filesystem. It cannot fail: an unreadable file becomes an issue, not an error.

func (FolderReport) WriteJSON

func (r FolderReport) WriteJSON(w io.Writer) error

WriteJSON writes the report as indented JSON — byte-for-byte what `manifest-analyzer --mode scan-folder --format json` prints.

func (FolderReport) WriteYAML added in v0.40.0

func (r FolderReport) WriteYAML(w io.Writer) error

WriteYAML writes the report as YAML — byte-for-byte what `manifest-analyzer --mode scan-folder --format yaml` prints. The report is a KRM document, so this is the serialization it reads best in, and the one a human can commit beside the manifests it describes.

type FolderReportSpec added in v0.40.0

type FolderReportSpec struct {
	// Root is the scanned folder as passed to ScanFolder. Empty for ScanFolderFS.
	Root string `json:"root,omitempty"`
	// Mode is always [ModeScanFolder].
	Mode string `json:"mode"`
}

FolderReportSpec is the scan that was asked for.

type FolderReportStatus added in v0.40.0

type FolderReportStatus struct {
	// Generator names the build that produced this report. Never empty.
	Generator Generator `json:"generator"`
	// Accepted is the gate decision. When false, Issues says why.
	Accepted bool `json:"accepted"`
	// Issues is empty when Accepted, and never nil in the marshaled JSON.
	Issues []Issue `json:"issues"`
	// Retained lists the build directives read as context.
	Retained []RetainedDocument `json:"retained,omitempty"`
}

FolderReportStatus is what the scan found.

type Generator added in v0.40.0

type Generator struct {
	// Name is the producing tool, e.g. "manifest-analyzer".
	Name string `json:"name"`
	// Version is the release, e.g. "v0.39.1", or "dev" for a build carrying no version.
	Version string `json:"version"`
}

Generator names the build that produced a report. Never empty: a report that cannot say what produced it is the failure this field exists to prevent. A tool that execs the binary holds the JSON document and nothing else — without this it cannot tell a report from one release consumed against a writer from another, which is a wrong answer that looks entirely normal.

A bare version string would not do: kind names the document, Name names what produced it, and a report piped into another tool is exactly where conflating the two bites.

type Identity

type Identity struct {
	APIVersion string `json:"apiVersion"`
	Kind       string `json:"kind"`
	Namespace  string `json:"namespace"`
	Name       string `json:"name"`
}

Identity names one Kubernetes document as it appears in the file.

type Issue

type Issue struct {
	Kind IssueKind `json:"kind"`
	// Path is the offending file, slash-separated and relative to the scan root. Empty
	// when the issue is about the folder as a whole.
	Path string `json:"path"`
	// DocumentIndex is the zero-based index of the offending document within Path.
	DocumentIndex int `json:"documentIndex"`
	// Message is a human-readable explanation. It is not a stable string.
	Message string `json:"message"`
	// Solvable says whether anyone can make this folder acceptable with this release. It
	// is set by the check that raised the issue, because only that check knows: one code
	// answers differently depending on which branch emitted it, which is why this is a
	// field on the emitted value rather than a table beside the constants.
	//
	// It describes THIS RELEASE and promises nothing about the future, so read it on every
	// scan rather than caching a mapping from it.
	Solvable bool `json:"solvable"`
	// Actor names who can solve it. Empty unless Solvable — a code alone cannot tell "fix
	// your YAML" from "this folder cannot be adopted", and rendering an out-of-scope
	// refusal as "fix your repository" to someone who cannot is what this half prevents.
	Actor Actor `json:"actor,omitempty"`
}

Issue is one reason a folder is not accepted, or one fact a stricter policy may treat as blocking.

type IssueKind

type IssueKind string

IssueKind classifies why a folder was not accepted. The values are the operator's own refusal codes. They are the part of an Issue worth matching on, and they change less often than the surrounding shape — but pre-1.0 they can still change.

The set below is complete: every kind the operator can raise is declared here, including the three that only a live write path emits (they reach a consumer through GitTarget status rather than through ScanFolder or ScanRepo, which are structure-only). A consumer matching on these constants can recognise every code it will ever be handed.

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"
	// IssueImpureManagedFile marks a file holding managed resources that also holds a
	// non-managed document. 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 (a kustomization). Allowlisted KRM must be retained in its own file.
	IssueMixedFile IssueKind = "mixed-managed-allowlisted"
	// IssueUnresolvedKRM marks recognized KRM that cannot be tied to a single served,
	// followable resource. Only a cluster-aware scan reports this.
	IssueUnresolvedKRM IssueKind = "unresolved-krm"
	// IssueOutOfScope marks a watched kind whose resource falls outside the GitTarget's
	// scope (right kind, wrong namespace). Only a cluster-aware scan reports this.
	IssueOutOfScope IssueKind = "out-of-scope"
	// IssueUnsupportedKustomize marks a kustomization.yaml using a feature the writer
	// cannot map back to editable source documents (generators, patches, components,
	// Helm inflation, replacements, transformers, name prefixes, remote bases).
	IssueUnsupportedKustomize IssueKind = "unsupported-kustomize"
	// IssueForeignFile marks a non-YAML regular file the operator cannot manage.
	IssueForeignFile IssueKind = "foreign-file"
	// IssueForeignSymlink marks a symlink, which a writer could follow out of the subtree.
	IssueForeignSymlink IssueKind = "foreign-symlink"
	// IssueForeignSubmodule marks a nested Git submodule.
	IssueForeignSubmodule IssueKind = "foreign-submodule"
	// IssueIgnoreShadowsManaged marks a .gittargetignore pattern that matches a path the
	// operator writes, which would blind it to its own file.
	IssueIgnoreShadowsManaged IssueKind = "ignore-shadows-managed"
	// IssueWriteEscapesScope marks a planned write that would leave the GitTarget's path.
	IssueWriteEscapesScope IssueKind = "write-escapes-scope"
	// IssueWriteFanIn marks an in-place edit of a source file that more than one kustomize
	// render root reaches.
	IssueWriteFanIn IssueKind = "write-fan-in"
	// IssueRenderRefused marks a planned write kustomize itself will not vouch for: the
	// flush was re-rendered with the write applied, and the result was not the live
	// object. Only a live write raises it; a structure-only scan never can.
	IssueRenderRefused IssueKind = "kustomize-render-refused"
	// IssueRenderDoesNotMatchLive marks a rendered ${...} value whose corresponding live
	// field is absent or different. Only a live write raises it.
	IssueRenderDoesNotMatchLive IssueKind = "render-does-not-match-live"
	// IssueUnplaceableEdit marks a live change the writer could not place in any source
	// document. Only a live write raises it.
	IssueUnplaceableEdit IssueKind = "unplaceable-edit"
)
const (
	// ReasonOverlayFanOutUnsupported was the forward-looking refusal for an external-base
	// overlay. Render-root scoping shipped, so the scanner now ADOPTS such an overlay and no
	// longer emits this code; it is retained only so a consumer pinning the string still
	// compiles. A kustomize-overlay candidate is accepted (its editable count shows how much
	// it owns); an overlay refused for a real fault carries that fault's own code.
	//
	// Deprecated: no longer emitted; kept for source compatibility.
	ReasonOverlayFanOutUnsupported IssueKind = "overlay-fan-out-unsupported"
	// ReasonRefusedStructural is the support boundary at a render root: a kustomization
	// using a construct the writer cannot map back to editable source.
	//
	// READ [RefusalReason.Solvable], NOT THIS CODE, to decide whether the refusal can be
	// cleared. This code answers both ways and is right to: a root refused for a
	// configMapGenerator is nobody's to solve, while one refused because its
	// kustomization.yaml does not parse is one commit from adoptable, and both arrive here.
	// A consumer that reads the code as permanent tells the second author "this can never
	// be synced" when the truth is "not yet", which is the worse of the two lies.
	ReasonRefusedStructural IssueKind = "refused-structural"
)

Refusal reason codes that are NOT IssueKind values. This block is not the enumeration of what RefusalReason.Code can hold — it is the two codes that come from somewhere other than the acceptance gate. Every other code a candidate carries is an IssueKind, because that is where it came from: the gate raised an issue, and RefusalReason is that issue projected one level up.

type Layout

type Layout string

Layout is the structural shape of a candidate folder.

const (
	// LayoutPlain is a directory of raw KRM documents with explicit namespaces and no
	// kustomization. Accepted.
	LayoutPlain Layout = "plain"
	// LayoutKustomizeSingle is a self-contained render root: one kustomization whose
	// resources graph stays within its own subtree. Accepted.
	LayoutKustomizeSingle Layout = "kustomize-single"
	// LayoutKustomizeOverlay is a render root reaching a base outside its own subtree
	// (the classic base/ + overlays/{env} shape). Accepted: render-root scoping shipped,
	// so the base is read as read-only context and writes stay in the overlay. The
	// candidate's editable count shows how much of what it renders it can own. An overlay
	// refused for a real fault carries that fault's own code, never a forward-looking one.
	LayoutKustomizeOverlay Layout = "kustomize-overlay"
	// LayoutRefusedStructural is a render root whose kustomization uses a construct the
	// writer cannot map back to editable source. This is the support boundary; each
	// refusal it carries says whether anyone can solve it.
	LayoutRefusedStructural Layout = "refused-structural"
)

type OverlapConflict

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

OverlapConflict records that Ancestor strictly contains Descendant.

type ReadEdge added in v0.40.0

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

ReadEdge is one folder-to-folder read: From's build renders documents that live in To, a directory outside From's own subtree.

The edges come from the same render rules the writer uses — which bases a kustomization actually reaches through resources: and relative paths — so a consumer draws the graph the operator would act on rather than one reconstructed from directory names. From is always a candidate. To is a directory offered to nobody unless it is the file-reference case on Candidate.ReadBy, in which case it is a candidate too.

An edge is REACH, not a direct reference: a root that reaches a base through another base gets one edge to each, not a chain, and a directory nested under another directory the same root reads is folded into its parent (reading the parent already reaches it). So the edges answer "which folders does this build depend on" exactly, and "who references whom, in what order" not at all — that is kustomize's business, and asking it is what produced these edges.

type RefusalReason

type RefusalReason struct {
	// Code is an [IssueKind] value, or [ReasonRefusedStructural]. The type makes that
	// relationship compile-checked rather than merely stated: a candidate's refusal is
	// the acceptance gate's own issue, projected up.
	Code IssueKind `json:"code"`
	// Detail is human-readable and not a stable string.
	Detail string `json:"detail"`
	// Solvable says whether anyone can make this candidate acceptable with this release.
	// A report produced before this field shipped carries no `solvable` key at all; read
	// that as "nobody said", not as false.
	Solvable bool `json:"solvable"`
	// Actor names who can solve it. Empty unless Solvable.
	Actor Actor `json:"actor,omitempty"`
}

RefusalReason is one machine-readable reason a candidate is not accepted.

type RenderedTypes added in v0.40.0

type RenderedTypes struct {
	// ByNamespace lists the types that land in each namespace, sorted, keyed by namespace.
	// These are the exact (type, namespace) pairs, and the only ones.
	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, and must not be read as one. 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, and a structure-only scan has none — so the honest reading is "we do not
	// know where these land". A scan that does have discovery can split them, in a field
	// added then rather than a name reserved now.
	//
	// A type can appear here AND under ByNamespace. Two ConfigMaps, one carrying a
	// namespace and one not, is an ordinary folder rather than a contradiction.
	NamespaceUndeclared []string `json:"namespaceUndeclared,omitempty"`
}

RenderedTypes says which type a folder renders into which namespace. It is expressed as a map rather than two lists because the PAIRING is the answer: a set of types beside a set of namespaces reads as every combination of the two, and a folder rendering a Deployment into frontend and a Service into backend would then describe two pairs that exist in no repository — enough to authorize a watch that matches nothing.

Every type is a canonical GVK string: "group/version/kind", or "version/kind" for the core group. Split on "/" and count the segments; a group never contains one.

apps/v1/Deployment
v1/ConfigMap
rbac.authorization.k8s.io/v1/ClusterRole

type RepoReport

type RepoReport struct {
	TypeMeta `json:",inline"`

	Spec   RepoReportSpec   `json:"spec"`
	Status RepoReportStatus `json:"status"`
}

RepoReport answers "which folders in this repository could become GitTargets?". It is a KRM document: the scan REQUEST is the spec, what was FOUND is the status. See TypeMeta for why the envelope, and why the document is never served or applyable.

func ScanRepo

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

ScanRepo walks a whole repository and enumerates the folders that could become GitTargets, classifying each one's layout and reporting why a folder is refused. It is read-only, needs no cluster, and never follows symlinks.

func (RepoReport) WriteJSON

func (r RepoReport) WriteJSON(w io.Writer) error

WriteJSON writes the report as indented JSON — byte-for-byte what `manifest-analyzer --mode scan-repo --format json` prints.

func (RepoReport) WriteYAML added in v0.40.0

func (r RepoReport) WriteYAML(w io.Writer) error

WriteYAML writes the report as YAML — byte-for-byte what `manifest-analyzer --mode scan-repo --format yaml` prints. The report is a KRM document, so this is the serialization it reads best in, and the one a human can commit beside the manifests it describes.

type RepoReportSpec added in v0.40.0

type RepoReportSpec struct {
	// Root is the scanned repository root as passed to ScanRepo.
	Root string `json:"root,omitempty"`
	// Mode is always [ModeScanRepo].
	Mode string `json:"mode"`
}

RepoReportSpec is the scan that was asked for.

type RepoReportStatus added in v0.40.0

type RepoReportStatus struct {
	// Generator names the build that produced this report. Never empty.
	Generator  Generator   `json:"generator"`
	Candidates []Candidate `json:"candidates"`
	Summary    RepoSummary `json:"summary"`
}

RepoReportStatus is what the scan found.

type RepoSummary

type RepoSummary struct {
	CandidatesByLayout map[Layout]int    `json:"candidatesByLayout"`
	Accepted           int               `json:"accepted"`
	Refused            int               `json:"refused"`
	OverlapConflicts   []OverlapConflict `json:"overlapConflicts,omitempty"`
	// ReadEdges is the repository's folder dependency graph, sorted: one edge per
	// (candidate, directory it renders from outside its own subtree). It is the same
	// relation [Candidate.ReadScope] and [Candidate.ReadBy] report per folder, collected so
	// a consumer can draw the graph without walking the candidate list.
	//
	// Most edges end at a directory that is not a candidate: a folder some kustomization
	// references is never a render root, so it is offered to nobody, and a consumer that
	// draws only candidates draws no edges at all. Those nodes are exactly the edge targets
	// absent from [RepoReportStatus.Candidates] — the report does not also publish them as a
	// list, because a second copy of a set difference is a second thing to keep true.
	ReadEdges []ReadEdge `json:"readEdges,omitempty"`
	// UnsupportedConstructs is the sorted, de-duplicated set of unsupported kustomize
	// features seen across refused candidates, so a tool can say "this repository uses
	// Helm inflation, which the operator does not manage".
	UnsupportedConstructs []string `json:"unsupportedConstructs,omitempty"`
}

RepoSummary is the repository-level roll-up.

type ResourceCounts

type ResourceCounts struct {
	Rendered int `json:"rendered"`
	Editable int `json:"editable"`
	NonKRM   int `json:"nonKrm"`
}

ResourceCounts splits the KRM a candidate covers into what it renders versus what it could actually edit. The two are equal for a plain or self-contained candidate and diverge for an overlay, which renders documents it cannot own.

type RetainedDocument

type RetainedDocument struct {
	Path          string `json:"path"`
	DocumentIndex int    `json:"documentIndex"`
	// Identity is set only when a named Kubernetes resource is hiding inside an
	// allowlisted build-directive file — the mixed-file case, which is refused. It is nil
	// for the ordinary whole-file retention of a kustomization.yaml, which names no
	// resource.
	Identity *Identity `json:"identity,omitempty"`
	// Unsupported reports that this retained file uses a construct the writer cannot map
	// back to editable source. It is the cause of an IssueUnsupportedKustomize.
	Unsupported bool `json:"unsupported,omitempty"`
}

RetainedDocument is content the operator keeps but never writes: build directives such as a kustomization.yaml. They are read as context, never edited as resources.

type TypeMeta added in v0.40.0

type TypeMeta struct {
	APIVersion string `json:"apiVersion"`
	Kind       string `json:"kind"`
}

TypeMeta is the KRM envelope every report carries. It is deliberately shaped like apimachinery's TypeMeta so a consumer that does link us can mirror or reuse it.

A report is NEVER served and NEVER registered: there is no CRD, no group registration, and it cannot be applied. It observes a path at an instant. That is also why it carries no metadata — a report has no identity, a synthesized name would be noise, and worse, it would suggest the document can be applied. Kpt's own kind: ResourceList carries apiVersion, kind and its payload with no metadata, for the same reason.

Jump to

Keyboard shortcuts

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