diff

package
v0.4.1 Latest Latest
Warning

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

Go to latest
Published: Jun 11, 2026 License: AGPL-3.0 Imports: 20 Imported by: 0

Documentation

Overview

Package diff compares two sets of rendered Kubernetes manifests and reports the resources whose rendered form differs.

RenderDocs is the entry point, and it takes one of two paths by Format. The dyff text styles (human default, github, brief, gitlab, gitea) render the whole set through dyff at once: dyff pairs documents by their Kubernetes identity and labels each diff natively with apiVersion/kind/namespace/name. The plain unified diff takes a per-resource path — each resource is paired against its counterpart, keyed by parent KS/HR and resource identity (not just name, so a Deployment from HelmRelease A never diffs against the same-named Deployment from HelmRelease B), and the per-resource bodies are concatenated.

Most styles delegate to dyff, whose K8s-aware comparison pairs named list entries (containers, env vars) by their identifier, so reordering a list yields an `⇆ order changed` marker instead of a wall of phantom value-line churn:

  • human (default) — dyff's colored, human-readable report.
  • github — dyff path-keyed diff syntax (`@@ <path> @@`, `+`/`-`, `!`); GitHub's diff lexer renders it natively.
  • brief — dyff's one-line-per-change summary.
  • gitlab / gitea — dyff diff syntax with forge-specific prefixes.
  • diff — a plain unified diff (`diff -u`) of each resource's YAML; not K8s-aware, but consumable by any unified-diff tooling.

Options.StripAttrs is applied to a deep-copied tree before the comparison runs — used to drop chart-bump noise (`helm.sh/chart`, `checksum/config`, …) that rotates on every Helm upgrade but carries no review-relevant signal. ConfigMap binaryData is summarized to a content hash for the same reason. DefaultStripAttrs and DefaultStripFields are the lists `flate diff` uses out of the box.

SDK usage

RenderDocs returns formatted bytes. A consumer that needs the diff as *data* — to build its own API payload, web UI, or image report — instead calls Changes, which returns the same paired, normalized, noise-filtered set as a []Change (added / changed / removed, with the per-side manifests and the captured helm.sh/chart label). Render two cluster directories with orchestrator.RenderTrees (it owns the two-orchestrator, shared-cache, changed-only dance), then feed the Results to Changes:

base, head, _ := orchestrator.RenderTrees(ctx,
	orchestrator.Tree{RepoRoot: baseDir}, orchestrator.Tree{RepoRoot: headDir}, cfg)
changes := diff.Changes(
	diff.DocsFromManifests(base.Result.Manifests, nil),
	diff.DocsFromManifests(head.Result.Manifests, nil),
	diff.Options{
		StripAttrs:  diff.DefaultStripAttrs,
		StripFields: diff.DefaultStripFields,
		Normalize:   redact, // optional extra per-manifest scrub
	},
)

DocsFromManifests is the store-free adapter from a render Result's per-parent manifests to the flat []Doc both Changes and RenderDocs take. Options.Normalize is an optional per-manifest hook (applied after the built-in strips) for noise the defaults don't cover — e.g. redacting Secret values or chart-minted TLS certs. See the package Example.

Example

Example shows the SDK wiring an external consumer uses to turn two renders into a structured diff: orchestrator.Result.Manifests → DocsFromManifests → Changes. (Here the two manifest maps are hand-built for a self-contained example; in practice they're baseRes.Manifests and headRes.Manifests from two orchestrator.Render calls.)

package main

import (
	"fmt"

	"github.com/home-operations/flate/pkg/diff"
	"github.com/home-operations/flate/pkg/manifest"
)

func main() {
	parent := manifest.NamedResource{Kind: manifest.KindHelmRelease, Namespace: "media", Name: "sonarr"}
	deployment := func(chartVer, image string) map[string]any {
		return map[string]any{
			"apiVersion": "apps/v1", "kind": "Deployment",
			"metadata": map[string]any{
				"name": "sonarr", "namespace": "media",
				"labels": map[string]any{"helm.sh/chart": "app-template-" + chartVer},
			},
			"spec": map[string]any{
				"template": map[string]any{"spec": map[string]any{
					"containers": []any{map[string]any{"name": "main", "image": image}},
				}},
			},
		}
	}
	base := map[manifest.NamedResource][]map[string]any{parent: {deployment("4.0.0", "ghcr.io/home-operations/sonarr:4.0.0")}}
	head := map[manifest.NamedResource][]map[string]any{parent: {deployment("4.1.0", "ghcr.io/home-operations/sonarr:4.1.0")}}

	changes := diff.Changes(
		diff.DocsFromManifests(base, nil),
		diff.DocsFromManifests(head, nil),
		diff.Options{StripAttrs: diff.DefaultStripAttrs, StripFields: diff.DefaultStripFields},
	)

	for _, c := range changes {
		fmt.Printf("%s %s %s/%s (chart %s -> %s)\n", c.Status, c.Kind, c.Namespace, c.Name, c.OldChart, c.NewChart)
	}
}
Output:
changed Deployment media/sonarr (chart app-template-4.0.0 -> app-template-4.1.0)

Index

Examples

Constants

This section is empty.

Variables

View Source
var DefaultStripAttrs = []string{
	"helm.sh/chart",
	"checksum/",
	"app.kubernetes.io/version",
	"chart",
}

DefaultStripAttrs is the default set of metadata annotation/label keys dropped before diffing — the noise `flate diff` strips out of the box, exported so SDK consumers normalize identically (and don't drift from a hand-copied list). A trailing "/" is a prefix match (see manifest.StripResourceAttributes): "checksum/" covers checksum/config, checksum/secret, checksum/secrets, and every other checksum/<x> a chart emits, in one entry.

These rotate on every Helm chart bump and carry no review-relevant signal, so leaving them in surfaces a spurious change on every resource a touched chart renders. A `checksum/<x>` annotation also rotates whenever the hashed ConfigMap/Secret content changes — but the diff already shows that underlying change directly, so the annotation is duplicate noise worth stripping regardless. (The per-render-random component some checksums fold in — e.g. matrix-synapse's `registration_shared_secret | default (randAlphaNum 24)` — is now made reproducible by seeded rendering in pkg/helm/deterministic; this strip stays for the legitimate content- and version-driven rotations.)

Treat as read-only; copy before mutating.

View Source
var DefaultStripFields = []string{}

DefaultStripFields is the default set of dotted spec field-paths deleted before diffing — for volatile values a chart templates into the spec rather than metadata. It is empty: the one field it used to carry, the TrueCharts common library's

spec.restic.unlock: {{ now | date "20060102150405" }}

on every volsync ReplicationSource, was a per-render timestamp. Seeded rendering (pkg/helm/deterministic) now pins `now` to a fixed clock, so the field renders identically on both diff sides; stripping it would only hide a genuine change. The slice stays exported (non-nil) so `--strip-field` and SDK consumers have a base set to extend.

Treat as read-only; copy before mutating.

Functions

func RenderDocs

func RenderDocs(left, right []Doc, opts Options) ([]byte, error)

RenderDocs is the top-level entry point: it compares the two doc sets and returns the formatted diff for opts.Format. The dyff text styles (github/human/brief/gitlab/gitea, and the zero value) render the whole set through dyff for native per-resource labels; FormatDiff takes the per-resource unified-diff path.

Types

type Change added in v0.3.2

type Change struct {
	// Parent is the Flux Kustomization / HelmRelease that rendered this
	// resource — the pairing discriminator (so a Deployment from HR A is
	// never matched against the same-named Deployment from HR B).
	Parent                Parent
	Kind, Namespace, Name string
	Status                ChangeStatus
	// Old / New are the NORMALIZED manifests (Options strip + binaryData
	// redaction + the optional Normalize hook applied). Old is nil for an
	// add; New is nil for a remove.
	Old, New map[string]any
	// OldChart / NewChart are the resource's "helm.sh/chart" label
	// ("<name>-<version>") captured BEFORE normalization stripped it —
	// "" when absent. Surfaces a chart-version bump the stripped Old/New
	// no longer carry.
	OldChart, NewChart string
}

Change is one resource that differs between two rendered doc sets — the structured form of what RenderDocs formats. SDK consumers build their own output (an API payload, a web UI, an image report) from a []Change instead of re-implementing the pairing + normalization that RenderDocs does internally.

func Changes added in v0.3.2

func Changes(left, right []Doc, opts Options) []Change

Changes pairs left against right by (parent, apiVersion, kind, namespace, name), drops byte-identical resources, and returns the remaining differences as structured data. Each side is normalized (opts.StripAttrs / opts.StripFields, ConfigMap binaryData redaction, then the optional opts.Normalize hook) before the equality check, so render-time noise — chart-bump annotations, volatile spec fields, a consumer's secret/cert redaction — never reads as a spurious change. opts.Format is ignored (Changes returns data, not formatted bytes).

This is the data RenderDocs renders: exposed so SDK consumers stop re-implementing the pairing. The result is sorted by parent then resource identity for deterministic output.

type ChangeStatus added in v0.3.2

type ChangeStatus string

ChangeStatus classifies how a resource differs between two doc sets.

const (
	// StatusAdded — present only on the right (New) side.
	StatusAdded ChangeStatus = "added"
	// StatusChanged — present on both sides with differing content.
	StatusChanged ChangeStatus = "changed"
	// StatusRemoved — present only on the left (Old) side.
	StatusRemoved ChangeStatus = "removed"
)

type Doc

type Doc struct {
	Manifest map[string]any
	Parent   Parent
}

Doc pairs a rendered manifest with its parent.

func DocsFromManifests added in v0.3.2

func DocsFromManifests(manifests map[manifest.NamedResource][]map[string]any, pathOf func(manifest.NamedResource) string) []Doc

DocsFromManifests flattens an orchestrator render Result's per-parent rendered manifests into the flat []Doc that RenderDocs and Changes consume, tagging each document with the Flux Kustomization / HelmRelease that produced it. The input is exactly orchestrator.Result.Manifests (map[NamedResource][]map[string]any), so an SDK consumer wires two renders straight into a diff without re-implementing the walk.

pathOf, when non-nil, supplies a parent's Flux Kustomization spec.path — it disambiguates two same-named Kustomizations rendered from different overlays (a real-world pairing collision); pass nil when the path isn't available (HelmRelease parents and most consumers don't need it). Documents are grouped and ordered by producing parent (each parent's documents keep their render emission order) so the result is deterministic across the input map's random iteration order.

type Format

type Format string

Format selects the diff output flavor. The github/human/brief/gitlab/ gitea styles map to dyff's own output styles; diff is a plain unified diff.

const (
	// FormatGitHub is dyff's `--output github` mode: path-based diff
	// syntax (`@@`, `+`, `-`, `!`) that GitHub's diff lexer renders
	// natively as a colored diff block when wrapped in a “`diff
	// fence. K8s-aware: list entries are matched by identifier
	// (container name, env-var name, etc.), so reordering a list
	// produces no diff churn.
	FormatGitHub Format = "github"
	// FormatDiff is a standard unified diff (`diff -u` / `git diff`
	// style) of each resource's YAML. Not Kubernetes-aware — it diffs
	// lines, so a reordered list shows churn — but familiar and
	// consumable by any unified-diff tooling.
	FormatDiff Format = "diff"
	// FormatHuman is dyff's colored, human-readable report — the default
	// style, and the zero value.
	FormatHuman Format = "human"
	// FormatBrief is dyff's one-line-per-change summary.
	FormatBrief Format = "brief"
	// FormatGitLab is dyff's GitLab diff syntax (`=` path/root prefixes).
	FormatGitLab Format = "gitlab"
	// FormatGitea is dyff's Gitea/Forgejo diff syntax.
	FormatGitea Format = "gitea"
	// FormatHTML renders a self-contained HTML document: a per-resource,
	// GitHub-style diff with YAML syntax highlighting (via chroma) and a
	// side-by-side ⇄ unified toggle. Built on the same line diff as
	// FormatDiff (not Kubernetes-aware). Meant for browser review or a CI
	// artifact, not the terminal.
	FormatHTML Format = "html"
)

Recognized Format values.

type Options

type Options struct {
	// StripAttrs lists annotation/label keys removed from each
	// manifest's metadata (and pod-template metadata) before the diff
	// is computed. Cuts chart-bump noise — annotations like
	// `helm.sh/chart` or `checksum/config` whose values rotate on
	// every chart bump would otherwise produce a diff entry per
	// resource. dyff matches K8s lists by identifier but still
	// reports string-value changes verbatim, so this pre-filter still
	// earns its keep.
	StripAttrs []string
	// StripFields lists dotted spec field-paths (e.g.
	// "spec.restic.unlock") deleted from each manifest before the diff
	// is computed. Same rationale as StripAttrs but for volatile values
	// charts template into the spec rather than metadata — notably
	// volsync's `unlock: {{ now }}`, which rotates every render and
	// cannot be stabilized at render time (Helm exposes no funcMap
	// hook). See manifest.StripResourceFields.
	StripFields []string
	// Normalize, when non-nil, is an extra per-manifest scrub applied to
	// each (cloned) document AFTER StripAttrs/StripFields and binaryData
	// redaction, before resources are paired and compared. Use it to
	// suppress render-time noise the built-in strips don't cover —
	// e.g. an SDK consumer redacting Secret values or chart-minted TLS
	// certs so a per-render-random value doesn't read as a change.
	// nil = no extra scrub (the CLI default).
	Normalize func(map[string]any)
	// Format selects the output style (see the Format constants). The
	// zero value renders the human default.
	Format Format
}

Options tunes RenderDocs behavior.

type Parent

type Parent struct {
	Kind      string
	Namespace string
	Name      string
	// Path is the Flux Kustomization spec.path (only set for KS
	// parents). Slash-normalized, with the conventional `./` prefix
	// stripped. Disambiguates two KS parents that share a (kind, ns,
	// name) but render from different overlays.
	Path string
}

Parent identifies the Flux Kustomization or HelmRelease that rendered a manifest. It never appears in the output — it's a pairing discriminator (see pairKey) so a Deployment rendered by HelmRelease A never diffs against the same-named Deployment from HelmRelease B.

Jump to

Keyboard shortcuts

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