manifestanalyzer

package
v0.36.0 Latest Latest
Warning

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

Go to latest
Published: Jul 14, 2026 License: Apache-2.0 Imports: 5 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, and SchemaVersion may bump for reasons that would be breaking after 1.0. Pin a version.

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.

Everything under internal/ carries no guarantee either, and is not importable from another module.

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. Exec the binary if Go is not your language; import this package if it is.

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 (
	// ReasonOverlayFanOutUnsupported is the forward-looking refusal that flips to accepted
	// when render-root scoping ships.
	ReasonOverlayFanOutUnsupported = "overlay-fan-out-unsupported"
	// ReasonRefusedStructural is the permanent support boundary.
	ReasonRefusedStructural = "refused-structural"
)

Refusal reason codes. The distinction is load-bearing and a consumer must not collapse it: one is a "not yet", the other is permanent.

View Source
const SchemaVersion = "v1"

SchemaVersion identifies the JSON contract FolderReport and RepoReport marshal to. It is a marker, not a promise: while the project is pre-1.0 the contract may change under a consumer, with or without a bump. Adding a field never bumps it, so consumers must ignore fields they do not know.

Variables

This section is empty.

Functions

This section is empty.

Types

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 base directories outside this candidate's 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, when unambiguous.
	InferredNamespace string         `json:"inferredNamespace,omitempty"`
	Resources         ResourceCounts `json:"resources"`
	// 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 {
	SchemaVersion string `json:"schemaVersion"`
	// Root is the scanned folder as passed to ScanFolder. Informational; empty for
	// ScanFolderFS.
	Root string `json:"root,omitempty"`
	// 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"`
}

FolderReport answers "may this folder become a GitTarget?".

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.

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"`
}

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.

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"
)

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). Refused today, with a forward-looking
	// reason: it becomes accepted when render-root scoping ships.
	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 permanent support boundary,
	// never a "not yet".
	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 RefusalReason

type RefusalReason struct {
	Code string `json:"code"`
	// Detail is human-readable and not a stable string.
	Detail string `json:"detail"`
}

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

type RepoReport

type RepoReport struct {
	SchemaVersion string `json:"schemaVersion"`
	// Root is the scanned repository root as passed to ScanRepo. Informational.
	Root       string      `json:"root,omitempty"`
	Candidates []Candidate `json:"candidates"`
	Summary    RepoSummary `json:"summary"`
}

RepoReport answers "which folders in this repository could become GitTargets?".

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.

type RepoSummary

type RepoSummary struct {
	CandidatesByLayout map[Layout]int    `json:"candidatesByLayout"`
	Accepted           int               `json:"accepted"`
	Refused            int               `json:"refused"`
	OverlapConflicts   []OverlapConflict `json:"overlapConflicts,omitempty"`
	// FleetRoot reports that the repository root is a cluster/fleet root. A GitTarget
	// points at an app subtree, never at such a root.
	FleetRoot bool `json:"fleetRoot,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.

Jump to

Keyboard shortcuts

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