kustomize

package
v0.3.2 Latest Latest
Warning

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

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

Documentation

Overview

Package kustomize wraps sigs.k8s.io/kustomize/api so the rest of flate never invokes the `kustomize` CLI. It provides:

  • Build / RenderFlux: render a kustomization directory to YAML documents. Build is the plain krusty surface; RenderFlux adds the Flux generator that handles spec.components and embedded inline Contents.
  • Prepare: the standard pre-render dance (Clone + expand postBuild.substituteFrom) for embedders rendering a single Kustomization. Symmetric to helm.Prepare for HelmReleases, minus a values cache — the substituteFrom path has no YAML parse to memoize; see Prepare's docstring.
  • Substitute: envsubst-style "${VAR}" / "${VAR:=default}" used for Flux post-build substitutions.

Concurrency. Two locks guard krusty's non-thread-safety:

  • A per-path lock (stageLocks) serializes builds against the SAME staged directory, which krusty mutates in place.
  • The process-wide BuildMutex (flux.go) serializes EVERY krusty invocation flate runs — including the helm post-renderer's krusty.Run in pkg/helm — because kustomize's package-global state (the openapi schema registry, builtin plugin/transformer factories) is not goroutine-safe. fluxcd/pkg/kustomize guards its own SecureBuild for the same reason; every flate-owned krusty entrypoint MUST hold BuildMutex.

Caching boundary. kustomize output caching is deferred to the controller layer (the Kustomization controller's spec+source fingerprint dedup), NOT memoized inside this package the way helm.Client caches template output. RenderFlux mutates the staged workspace and krusty plugins/generators are not guaranteed deterministic, so the stable point to memoize is the coarse spec+source hash the controller already computes — not the render engine's fluid internals.

Index

Constants

This section is empty.

Variables

View Source
var BuildMutex sync.Mutex

BuildMutex serializes every krusty/kustomize build flate runs in a process. kustomize's krusty pipeline mutates package-global state (the openapi schema registry + builtin-plugin/transformer factories) that is NOT goroutine-safe — fluxcd/pkg/kustomize guards its own SecureBuild with an internal mutex for exactly this reason, but that mutex does not extend to OTHER krusty entrypoints in the same process (flate's helm postRenderer runs krusty.Run directly). Two concurrent builds — one KS SecureBuild, one HR postRender — race on the shared globals and produce nondeterministic corruption: empty / torn rendered output surfacing as "missing metadata.name" decode errors, dropped resources, or cascade failures that flip run-to-run. Every flate-owned krusty invocation MUST hold this lock.

Functions

func Prepare

Prepare runs the standard pre-render dance for a Kustomization so it is ready to feed into RenderFlux:

  1. Clone ks so subsequent mutations don't touch the store-canonical copy (the immutability contract every flate controller honors — see pkg/manifest/doc.go).
  2. Expand spec.postBuild.substituteFrom references against the supplied values provider so ks.PostBuildSubstitute reflects the merged result a render would consume.

Embedders rendering a single Kustomization without standing up the orchestrator's KS controller call Prepare then RenderFlux.

Unlike helm.Prepare, Prepare takes no *values.Cache — and that asymmetry is intentional, not an oversight. The helm valuesFrom path yaml.Unmarshals each ref into a nested map[string]any, so a platform-wide values CM referenced by N HRs is worth parsing once and memoizing. The substituteFrom path resolves only FLAT string vars (CM/Secret data -> map[string]string via decodeBag), does no yaml.Unmarshal, and the per-KS work that dominates its cost (the newline strip + varname-regex validation + UpdatePostBuildSubstitutions) runs regardless of any lookup cache. There is no parsed tree to memoize. BenchmarkExpandPostBuildSubstituteReference_SharedCM (pkg/values) measures the whole path at single-digit microseconds per KS even for a large shared CM — sub-millisecond aggregate across a big repo — so threading a cache here would add wiring for no measurable gain.

func RenderFlux

func RenderFlux(ctx context.Context, cache *StagingCache, sourceRoot, sourceFingerprint, subPath string, rawSpec map[string]any) ([]byte, error)

RenderFlux renders a Flux kustomize.toolkit.fluxcd.io Kustomization using the same library that Flux's kustomize-controller uses (`github.com/fluxcd/pkg/kustomize`).

The Generator merges spec.patches / spec.images / spec.components / spec.targetNamespace / spec.namePrefix / spec.nameSuffix into the kustomization.yaml before krusty runs. spec.commonMetadata is applied post-build (see applyCommonMetadata) because the Generator does not handle it — kustomize-controller does it after build via ssautil.SetCommonMetadata.

ctx is honored at coarse boundaries — between path validation, after acquiring the per-path lock, and before/after SecureBuild — because fluxcd/pkg/kustomize.NewGenerator/SecureBuild do not themselves accept a ctx. A cancelled ctx returns ctx.Err() rather than completing the (potentially expensive) build.

The source tree at sourceRoot is never modified — staging is handled by `cache` which produces a writable copy. rawSpec must be the original Flux Kustomization document (the Contents field on manifest.Kustomization). subPath is the spec.path value relative to sourceRoot. sourceFingerprint, when non-empty, keys the persistent content-addressed stage cache so subsequent runs against the same resolved artifact skip the copyTree pass entirely. Empty falls back to per-process scratch staging (the right behavior for local-path sources whose mtimes shift faster than a fingerprint can keep up).

func SkipStageDir

func SkipStageDir(base string) bool

SkipStageDir reports whether a directory base name is excluded from the staged copy: `node_modules` and every dot-prefixed dir (which captures `.git`, `.flate-cache`, IDE state, etc.). It is the single source of truth for that set — the working-tree fingerprint that keys the persistent stage cache (kustomization controller) MUST apply the identical rule, or a cache hit can land a stage missing a directory the fingerprint counted (or vice-versa), yielding a structurally broken tree. Apply to non-root directories only; callers own root detection.

func Substitute

func Substitute(data []byte, vars map[string]string) ([]byte, error)

Substitute replaces ${var} placeholders in data using the supplied vars map. Delegates to fluxcd/pkg/envsubst — the exact engine Flux source-controller uses — so behavior matches Flux bit-for-bit:

  • $${VAR} passes through as literal ${VAR} (escape).
  • ${VAR:-default}, ${VAR:=default}, ${VAR:+alt}, ${VAR:?msg} handle the unset case per POSIX parameter expansion.
  • Bash-only constructs like ${VAR[@]} or ${VAR%%:*} that aren't recognized by envsubst are emitted literally, not erroneously matched as bare variable references (a divergence the previous regex-based implementation had).
  • Undefined ${VAR} without a default expands to the empty string, matching kustomize-controller's default (strict mode is the opt-in `StrictPostBuildSubstitutions` feature gate, off by default). Returning the empty string with exists=true keeps envsubst out of its strict-mode error path and lines flate up with what real Flux renders against an incomplete substitute map.

Types

type StagingCache

type StagingCache struct {
	// contains filtered or unexported fields
}

StagingCache materializes one-or-more source roots into a stage directory so Flux's kustomize Generator can safely write into the staged copy without touching the user's working tree.

Two staging modes:

  • Content-addressed (the fast path). When the caller supplies a source fingerprint (git commit SHA, OCI digest, etc.), the stage lands at <layout.Stage()>/<fp[:2]>/<fp>/ guarded by a sentinel file. Subsequent runs — including across processes — observe the sentinel and skip the copyTree pass entirely. Eviction is LRU by mtime, capped by maxBytes.
  • Per-process scratch (the fallback). Used when the source has no stable fingerprint (local-path sources whose mtimes shift on every editor save, for instance). Each Stage call materializes a `flate-stage-*` tempdir under layout.Stage(); the entire set is removed on Close.

Lifecycle for both modes is tied to the surrounding orchestrator run — call Close to clean up scratch stages and release in-memory state. Persistent CAS stages survive Close so the next process reuses them.

func NewStagingCache

func NewStagingCache(parent string, maxBytes int64) (*StagingCache, error)

NewStagingCache constructs a cache that places per-process scratch stages and persistent content-addressed stages under parent. If parent is empty, the OS tempdir is used (and persistent staging is effectively disabled — every Stage call materializes a scratch dir). maxBytes caps the persistent cache; 0 disables eviction.

Sweeps any `flate-stage-*` directory under parent that's older than staleStageAge — those are crashed-process leftovers from runs where Close didn't fire (SIGKILL, panic, ctx not honored). Best-effort: a sweep error doesn't fail construction; the dirs just stay until the next successful sweep.

func NewStagingCacheFromLayout

func NewStagingCacheFromLayout(layout cacheroot.Layout, maxBytes int64) (*StagingCache, error)

NewStagingCacheFromLayout is the orchestrator's preferred constructor — it pulls the persistent stage root from the supplied Layout so all path policy lives in one place.

func (*StagingCache) Close

func (c *StagingCache) Close() error

Close removes every per-process scratch stage. Persistent content-addressed stages survive — they're the whole point of the cache and are owned by the LRU sweep + GC subcommand.

func (*StagingCache) FetchRemote

func (c *StagingCache) FetchRemote(ctx context.Context, urlStr string) ([]byte, error)

FetchRemote returns the body of urlStr, fetched at most once per (url, success) cache entry. Successful bodies are cached for the StagingCache lifetime; transient errors (DNS, connection reset, timeout, 5xx) are NOT cached — the next caller retries. Only definitive HTTP 4xx responses are cached as negative entries (they won't change between retries within a run).

Without the success-only cache, a single transient hiccup at orchestrator startup poisoned every subsequent reconcile of every KS referencing that URL for the rest of the run.

The fetch runs in a background goroutine seeded with a detached context (httpGetURL applies remoteFetchTimeout internally) so a cancellation on the first caller doesn't propagate into the cached error. Each caller still honors its own ctx via the select below.

func (*StagingCache) Stage

func (c *StagingCache) Stage(ctx context.Context, source, fingerprint string) (string, error)

Stage returns the on-disk staged copy of source.

When fingerprint is non-empty, the persistent content-addressed path is used: <root>/<fp[:2]>/<fp>/ guarded by a sentinel. A previous (or concurrent peer) run that finished the same fingerprint lets us skip copyTree entirely — the single largest CPU saving across cold and warm reruns.

When fingerprint is empty (local-path sources whose mtimes shift faster than a hash could keep up, or any source for which no canonical digest is available), the cache falls back to the legacy per-process behavior: a `flate-stage-*` tempdir under root, memoized by source path for the life of the cache, removed on Close.

Jump to

Keyboard shortcuts

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