fluxcd

package
v0.2.0-beta.15 Latest Latest
Warning

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

Go to latest
Published: Oct 2, 2026 License: Apache-2.0 Imports: 38 Imported by: 0

README

Flux Engine - FluxCD Workflow Implementation

Go Reference

The fluxcd package implements the stack.Workflow interface for FluxCD, providing complete Flux resource generation from domain model definitions.

Overview

The Flux engine transforms Kure's hierarchical domain model (Cluster, Node, Bundle, Application) into FluxCD resources (Kustomizations, source references) organized in a GitOps-ready directory structure.

The engine is composed of three specialized components:

Component Responsibility
ResourceGenerator Generates Flux resources from domain objects
LayoutIntegrator Integrates resources into directory structures
BootstrapGenerator Creates Flux bootstrap manifests

Quick Start

import "github.com/go-kure/kure/pkg/stack/fluxcd"

Every other Go block on this page, except the one marked as an excerpt of the generator's own source, is the body of an Example function in example_test.go, which go test runs: it imports this package as fluxcd, github.com/go-kure/kure/pkg/stack as stack, github.com/go-kure/kure/pkg/stack/layout as layout, kustv1 (github.com/fluxcd/kustomize-controller/api/v1), os, and fmt for the lines that print what the example built. Two helpers are declared in the same file: exampleCluster() builds cluster prod, whose root node apps holds one bundle web with one application, which emits a ConfigMap through github.com/go-kure/kure/pkg/kubernetes and sigs.k8s.io/controller-runtime/pkg/client; printFiles(dir) prints every file below dir.

cluster := exampleCluster()

// Create engine with defaults. Placement is set on the LayoutRules passed
// to the layout call, not on the engine — see Layout Integration below.
engine := fluxcd.Engine()

// Generate all Flux resources for a cluster (paths of a default-rules walk)
objects, err := engine.GenerateFromCluster(cluster)
if err != nil {
    panic(err)
}
for _, obj := range objects {
    fmt.Println(obj.GetObjectKind().GroupVersionKind().Kind, obj.GetName())
}

Engine Construction

// Default engine
engine := fluxcd.Engine()

// The same, built from its components
built := fluxcd.NewWorkflowEngine()
fmt.Println(engine.GetName() == built.GetName(), built.SupportedBootstrapModes())

The engine has no path mode: every Kustomization spec.path is a layout directory (see Kustomization paths).

Placement (FluxIntegratedPerLayout vs FluxSeparate) is configured per call on layout.LayoutRules.FluxPlacement. FluxUnset is normalized to FluxSeparate by LayoutIntegrator.CreateLayoutWithResources — matching layout.DefaultLayoutRules() and the walker. The call's placement is the tree's: IntegrateWithLayout sets it on every layout it integrates, whatever placement the tree was walked with. A tree that already holds Flux Kustomizations an earlier integration or the caller placed keeps the placement they were made for (another is refused; Kustomizations an application emits do not count), and a refused call leaves the tree exactly as it was. See Layout Integration.

Resource Generation

Generate Flux resources from a cluster, from a layout walked from it, or for one bundle:

cluster := exampleCluster()
engine := fluxcd.Engine()
rules := layout.DefaultLayoutRules()
bundle := cluster.Node.Bundle

// From an entire cluster: walks it with layout.DefaultLayoutRules()
objects, err := engine.GenerateFromCluster(cluster)
if err != nil {
    panic(err)
}
fmt.Println(objects[0].GetName(), objects[0].(*kustv1.Kustomization).Spec.Path)

// From a layout you walked (and will write) yourself
ml, err := layout.WalkCluster(cluster, rules)
if err != nil {
    panic(err)
}
objects, err = engine.ResourceGen.GenerateFromLayout(ml, cluster)
if err != nil {
    panic(err)
}
fmt.Println(objects[0].GetName(), objects[0].(*kustv1.Kustomization).Spec.Path)

// For one bundle, at a path you supply
objects, err = engine.ResourceGen.GenerateForBundle(bundle, "clusters/prod/apps")
if err != nil {
    panic(err)
}
fmt.Println(objects[0].GetName(), objects[0].(*kustv1.Kustomization).Spec.Path)

Each directory that renders bundles produces one Flux Kustomization resource (see One Kustomization per directory) with:

  • spec.path = that directory
  • Source reference from Bundle.SourceRef (and a Source when it has a URL)
  • Dependency ordering from Bundle.DependsOn and Bundle.NamedDependsOn
  • Interval and pruning configuration

GenerateFromCluster walks the cluster itself, so it renders every application and runs every LayoutAugmenter: their errors surface there. Its paths are the directories WalkCluster writes under the default rules — the root node at <root>, its children at <root>/<child> — which is also what the bootstrap sync path ./<root> expects. A caller that writes the layout with other rules generates from that layout instead: CreateLayoutWithResources, or GenerateFromLayout on its own WalkCluster result.

Kustomization paths

The layout tree is the only authority on directories. Every walked layout records which stack nodes, bundles and application it renders (ManifestLayout.OriginNodes, OriginBundles, OriginApplication), and layout.IndexOrigins(root, cluster) resolves each bundle to the one layout whose directory holds its resources. A bundle's Kustomization spec.path is that layout's FullRepoPath() — OriginIndex.KustomizationPath(b) — and nothing else:

Tree Rules Bundle spec.path
node platform, bundle web GroupByName web platform/web
node platform, bundle platform GroupByName platform platform/platform
node platform (bundle platform) with child node apps (bundle apps-bundle) bundles and applications GroupFlat, ClusterName: "prod" platform, apps-bundle prod/platform, prod/platform/apps
unnamed root node, bundle web bundles and applications GroupFlat, ClusterName: "." web .

The path is emitted as FullRepoPath() returns it (no ./ prefix; Flux treats x and ./x alike) and is relative to the root of what the writer wrote: the WriteToDisk / WriteToTar base, or <basePath>/<ManifestsDir> for layout.WriteManifest. Root the Flux source there.

One Kustomization per directory

A directory is what a Flux Kustomization applies, so kure emits exactly one per directory that renders bundles. When a GroupFlat axis (or FlattenSingleTier) merges several bundles into one directory, they share that Kustomization, named after the first of them (the absorbing node's own bundle when it has one).

Each such directory has one owner: it is applied by its own Kustomization and by nothing else. No parent kustomization.yaml lists a child directory that renders bundles, in any placement, so two Kustomizations never apply (and prune, and patch) the same objects. The child's CR is what applies it — in flux-system under FluxSeparate, in the parent directory under the integrated placements. Building a parent directory therefore does not include its child units.

The bundles merged into one directory combine as follows:

Bundle setting In the shared Kustomization
SourceRef, Interval, Timeout, RetryInterval, Prune, Wait, Force, Suspend, PostBuild must be the same for every merged bundle (unset compares as the default; an omitted SourceRef namespace is the generator's DefaultNamespace; without a URL only its kind, name and namespace are compared, the fields a Kustomization carries), else an error naming the setting and the bundles
HealthChecks, umbrella health checks combined, each listed once; a check on a Flux Kustomization names the unit that applies that bundle, and one on the unit itself is dropped
Labels, Annotations combined; one key with two values is an error
Patches combined, but only when every patch has a Target that selects none of the other merged bundles' objects (see Patches in a shared directory)
DependsOn mapped to the Kustomization that applies each dependency; dependencies between the merged bundles are dropped
NamedDependsOn combined and mapped like DependsOn: a name that is a rendered bundle's becomes the name of the Kustomization that applies it (dropped when that is this one); any other name is kept as given

GenerateFromLayout and the integrator refuse a set of Kustomizations kure generates that can never all become Ready. Applying waits for every dependsOn to be Ready; becoming Ready waits for every Kustomization it health-checks, unless wait is set (Flux then ignores health checks and waits for everything it applied, the CRs it created included); and, under integrated placement, a CR exists only once the Kustomization whose directory references reach its file has applied — a CR the root directory reaches is created by the Flux bootstrap. Identities are namespace and name. A cycle among these waits is refused, naming the chain. Kustomizations an application emits itself, and objects in other namespaces, are outside this check. A merge can close one (a health check or dependency on a bundle merged into a unit that waits for it), and so can a parent node's bundle depending on a child node's bundle whose CR only the parent's directory holds (both integrated placements). ArgoCD Applications get the unit rule but not this check: only a DependsOn cycle between units is refused there. Give bundles directories of their own (NodeGrouping or BundleGrouping GroupByName) when they need different settings.

Patches in a shared directory

Flux applies a Kustomization's patches to everything that Kustomization builds, not to the bundle that declared them. While a bundle has a directory of its own that is the same thing. Once a GroupFlat axis or FlattenSingleTier puts several bundles in one directory, the shared Kustomization builds all their objects, so a patch meant for one bundle would also change the others' — and a layout setting would silently change what a bundle's patch does. kure refuses that instead of generating it:

  • an untargeted patch in a shared directory is refused;
  • a targeted patch is refused when its Target selects any object another bundle in that directory renders, a ConfigMap an augmenter's configMapGenerator makes included. Matching is kustomize's own: Group, Version, Kind, Name and Namespace are anchored regular expressions (an empty one matches anything; Namespace is matched against the object's effective namespace, default for a namespaced object that names none), LabelSelector and AnnotationSelector are Kubernetes selector expressions. Only objects the shared Kustomization builds count: under FluxIntegratedPerLayout a per-app directory is applied by its own CR, so its objects are outside the patch's reach. A bundle b1 with Target: {Kind: ConfigMap} merged with a bundle that renders a ConfigMap is refused; Target: {Kind: ConfigMap, Name: one-cm}, naming b1's own ConfigMap, is accepted.

The error names both bundles, the target and the object it reaches. Narrow the target to the bundle's own objects, or give the bundles directories of their own (NodeGrouping or BundleGrouping GroupByName). Not covered: objects inside an ExtraFiles file (they are not kustomize resources) and objects without a kind.

The generator computes no path, the integrator matches nothing by name, and FlattenSingleTier rewrites nothing afterwards: when it collapses a tier, the surviving layout takes over the collapsed layout's origins, so when both carried a bundle they share the surviving directory's one Kustomization. An umbrella child's path is its own directory, in every mode (an earlier KustomizationRecursive rule pointed it at the parent bundle's directory, whose kustomization excludes the child).

IndexOrigins refuses a tree it cannot resolve unambiguously: a hand-built, partial or other-cluster tree (the rendered set is not what the cluster reaches), a bundle or node rendered twice, two bundles with one name (the name is the Kustomization's identity), and a node or bundle layout in AppFileSingle mode (written into its Namespace, not its own directory). Build the tree with layout.WalkCluster. Not covered: a bundle whose SourceRef.URL names another artifact — its path is relative to that artifact.

Defaults are declared, named and exported

This package is a workflow layer above pkg/kubernetes, so unlike the builders it may hold opinions — but only as declared inputs with names a consumer can read, compare against and override. Every fallback this package applies is one of the exported identifiers in defaults.go, or comes from layout.DefaultLayoutRules() where the value belongs to pkg/stack/layout; grep for the identifier to find every place its value can reach emitted YAML.

Identifier Value Applies when Override by
DefaultInterval 60m the caller names no interval (a non-empty Bundle.Interval that does not parse is a validation error, not a fallback) assigning .DefaultInterval on either generator
DefaultNamespace flux-system generated resources need a namespace assigning .DefaultNamespace on either generator
DefaultMode layout.KustomizationExplicit informational: the listing mode the layout writers use for a layout with no Mode of its own setting ManifestLayout.Mode (or Config.KustomizationMode for WriteManifest)
DefaultBootstrapName flux-system naming the bootstrap Kustomization assigning BootstrapGenerator.BootstrapName
FluxInstanceName flux naming the FluxInstance in flux-operator mode not overrideable; the CRD admits no other name (see below)
DefaultSourceName flux-system the root node has no name naming the root stack.Node
DefaultFluxMode flux-operator BootstrapConfig.FluxMode is empty setting BootstrapConfig.FluxMode
DefaultSourceKind OCIRepository BootstrapConfig.SourceKind does not name GitRepository, the empty string included setting BootstrapConfig.SourceKind
DefaultBootstrapPathRoot manifests building the bootstrap Kustomization's spec.path not overrideable; the root node's name is joined onto it
DefaultFluxDirName flux-system a separate Flux layout needs a directory not overrideable
DefaultSourceRef latest an OCI source, or an OCI FluxInstance sync, has no SourceRef setting BootstrapConfig.SourceRef
DefaultSyncPath ./ the root node has no name not overrideable; it is the prefix a sync path is built from

Three of these — DefaultInterval, DefaultNamespace and DefaultBootstrapName — are copied into exported generator fields by NewResourceGenerator / NewBootstrapGenerator, and a field assigned afterwards is never overridden. The rest are applied where they are used and are overridden by naming the corresponding input, as the last column says. Three defaults have no override at all and say so, rather than being listed as though they had one; FluxInstanceName is the fourth row without one, and is not a default at all (next but one paragraph).

An empty BootstrapGenerator.BootstrapName resolves back to DefaultBootstrapName at emission. A generator built as a struct literal rather than through NewBootstrapGenerator leaves the field zero, and a Kustomization with no metadata.name is invalid — the field is an override, not a way to remove the name.

FluxInstanceName is in the table but is not a default: it is the one value the flux-operator CRD accepts. The CRD requires metadata.name: flux and rejects any other name at admission (x-kubernetes-validations: self.metadata.name == 'flux', in the vendored install bundle), so BootstrapName does not reach the FluxInstance and nothing else does either. It used to — both objects took bootstrapName() — so a flux-operator bundle with the default BootstrapName, or any override other than flux, was refused with the only accepted name for a FluxInstance is 'flux'. A test compares the constant against the vendored CRD's rule, so an operator bump that changes the rule fails in this package rather than at apply.

DefaultNamespace governs the whole gotk bundle, not just the objects this package constructs itself. The bootstrap bundle has three producers — the root Kustomization, the root source, and the toolkit components generated by install.Generate — and the third takes its namespace from an upstream option whose own default is also flux-system. That option is set from BootstrapGenerator.DefaultNamespace, so overriding the field moves the controllers' Deployments, ServiceAccounts, Services, NetworkPolicies and ResourceQuota along with everything else. The cluster-scoped objects follow without further work — install.Generate derives the emitted Namespace's name and the ClusterRoleBinding names and subjects from the same option, while CRDs and ClusterRoles carry no namespace to correct. Before this was wired up the field half-applied: a non-default value left the components in flux-system while the objects depending on them moved. That split still reconciles, because the components default to WatchAllNamespaces: true — so it surfaces only when someone narrows the controllers to their own namespace, at which point reconciliation stops with no error. WatchAllNamespaces is deliberately left at the upstream default and is not currently derived from any field here.

In flux-operator mode — the default — the field reaches only the FluxInstance. That bundle's other objects come from the vendored upstream install manifest, appended unmodified, so its Namespace, ServiceAccount, Service and Deployment stay at the upstream flux-system whatever this field says. Relocating the operator itself is not something this package offers.

The wiring is guarded on the field being non-empty, which matters only for a struct-literal generator. install.Generate uses the option as the emitted Namespace's name, so assigning it unconditionally would make a generator with a zero DefaultNamespace fail the whole bundle — missing metadata.name in object {{v1 Namespace}} — rather than fall back. Guarded, that generator keeps the upstream flux-system for the components.

That generator is nonetheless inconsistent, and the guard does not fix it: the root Kustomization and the root source read the same empty value and emit no namespace at all, so the components sit in flux-system while the objects referencing them are unnamespaced. This predates the namespace wiring and is a property of struct-literal construction across this package rather than of one option. Construct through NewBootstrapGenerator, which populates the field; a struct literal is not a supported way to get a coherent bundle.

defaults.go also declares ModeGotk = "gotk", which is not a default: nothing falls back to it. It is named so that the bootstrap mode set has one authority. DefaultFluxMode is both the fallback for an empty BootstrapConfig.FluxMode and the mode GenerateBootstrap dispatches on, and SupportedBootstrapModes() reports exactly those two — the validation error for an unrecognised mode reads its list from that method rather than restating the names.

DefaultFluxDirName shares DefaultNamespace's value and is deliberately a separate identifier: one is a path segment and the other a Kubernetes namespace, and renaming the namespace must not silently rename the directory.

Three defaults are not declared here because this package does not own them. The separate Flux layout's file granularity and the fallback for an unset FluxPlacement both come from layout.DefaultLayoutRules(), which is where pkg/stack/layout declares them and where its own walker reads them from. A constant here would be a second copy of a value that can change independently.

The third is that layout's own Mode, left unset. The layout writer resolves an unset mode to KustomizationExplicit, and the separate Flux layout never has children, so the mode selects nothing there.

One resolved source kind, not three

resolvedSourceKind is the only place the source kind is decided. Three sites need it — the source object itself, the bootstrap Kustomization's spec.sourceRef.Kind, and the FluxInstance sync block — and they used to decide it separately, with the sourceRef testing for OCIRepository while the other two tested for GitRepository. The three agreed only when SourceKind named a kind exactly; an empty or unrecognised SourceKind emitted an OCIRepository under a sourceRef naming a GitRepository that was never created.

One resolved source ref, on both bootstrap paths

For an OCI tag, an empty ref or a bare Git branch name, BootstrapConfig.SourceRef selects the same revision in both modes. The gotk source reads it as an OCI tag, falling back to DefaultSourceRef, or as a Git branch. flux-operator renders the FluxInstance's spec.sync.ref as the source's ref.tag for OCI and as ref.name for Git, and ref.name takes a full Git reference. resolvedSyncRef bridges the two: an empty OCI ref becomes DefaultSourceRef, a Git branch name becomes refs/heads/<name>, and an empty Git ref stays empty. A Git ref that already starts with refs/ — a tag, say — passes through unchanged in flux-operator mode only: the gotk source still puts any Git SourceRef into ref.branch, so there a full reference is not equivalent. Before this, spec.sync.ref was SourceRef verbatim: an empty OCI tag where the gotk source used latest, and a bare branch name where Flux needs a full reference.

prune and wait are inputs, not policy

Bundle.Prune and Bundle.Wait are *bool and are passed through untouched. BootstrapConfig.Prune does the same for the bootstrap Kustomization, and ResourceGenerator.Prune for Kustomizations generated from a layout.ManifestLayout, which carries no prune setting of its own.

The two fields resolve differently because their upstream tags differ:

  • KustomizationSpec.Prune is +required with no omitempty, so an unset input cannot leave the key out. Unset emits prune: false — garbage collection off. An unset tri-state previously collapsed onto prune: true, enabling destructive garbage collection for a caller who never asked for it.
  • KustomizationSpec.Wait is +optional with omitempty, so unset and false produce the same YAML: the key is absent and Flux's own default, false, applies.

Sources (GitRepository, OCIRepository) and the FluxInstance are built the way the builder contract prescribes: the pkg/kubernetes/fluxcd Create<Kind> constructor returns a typed object, and the generator assigns the plain fields directly.

gr := pubfluxcd.CreateGitRepository(ref.Name, namespace)
gr.Spec.URL = ref.URL
gr.Spec.Interval = metav1.Duration{Duration: g.DefaultInterval}

Only writes that the contract admits as sugar keep a helper — appending to a slice field, or assigning a pointer field through a constructed value, as with SetGitRepositoryReference. See Kubernetes Builders for the admission rules.

Layout Integration

Combine resource generation with directory structure:

cluster := exampleCluster()
engine := fluxcd.Engine()
rules := layout.DefaultLayoutRules()
out, err := os.MkdirTemp("", "kure-fluxcd-example")
if err != nil {
    panic(err)
}
defer func() { _ = os.RemoveAll(out) }()

// Create layout with Flux resources integrated
ml, err := engine.CreateLayoutWithResources(cluster, rules)
if err != nil {
    panic(err)
}

// Write to disk: every spec.path is relative to <out>/clusters
err = layout.WriteManifest(out, layout.DefaultLayoutConfig(), ml.(*layout.ManifestLayout))
if err != nil {
    panic(err)
}
printFiles(out)

IntegrateWithLayout(ml, cluster, rules) does the same for a layout you walked yourself. It refuses a tree layout.WalkCluster did not build from that cluster (see Kustomization paths). Integrating the same layout twice adds nothing: a CR already present with the same name and spec.path, in the layout that would host it, is kept; the same name in another layout or with another path is an error, and under FluxSeparate an identical flux-system child — same directory, same resources, nothing beneath it — is kept rather than a second one appended; any other is refused. Every Flux Kustomization already in the tree counts — typed or unstructured, placed by an earlier integration, by the caller or emitted by an application, top-level or inside a List (kustomize builds a List's items): an identity (namespace/name) present twice, or taken by a generated CR elsewhere, is refused in every placement, since the kustomize build would register the id twice. A generated Source has one definition across the whole pass: every Source of its identity — kind, namespace and name, whatever the API version (a v1beta2 GitRepository is the same Source as the generated v1 one) — anywhere in the tree, top-level or inside a List, and every Source the pass places in another layout, must have the same API version and content, or the integration is refused. Content is compared as the objects' unstructured form, so a typed Source and an unstructured copy of it are the same.

Under the integrated placements every Source the integration generates is hosted once, in the root node's layout, whichever layouts hold the Kustomizations that use it (go-kure/kure#876), and not in a ClusterName wrapper above it. For a named root node under the default rules that is the directory the bootstrap sync path ./<root> names (see Kustomization paths for other rules), so the Source is in one build, the root's, and exists before any Kustomization that uses it. When the root node renders a bundle, its own Kustomization builds that same directory beside the bootstrap: both hold the same objects, so neither prunes what the other keeps, but the root bundle's patches and postBuild apply only in its own. A generated Kustomization whose build holds the root node's layout is therefore refused when one of its patches applies to a Source the integration hosts there — a target that selects it the way kustomize selects one, or a target-less strategic-merge patch whose body names its apiVersion, kind, name and effective namespace — or when its postBuild substitution changes one: Flux's own substitution, run offline with the inline substitute vars. When substituteFrom is set, whose values are only in the cluster, a ${...} expression that reads a var the inline vars do not set is refused whatever the offline result; one that reads only inline vars, which override substituteFrom's, is decided by it. Otherwise the two would apply the Source differently and keep overwriting each other. The error names the Kustomization, the Source and the patch index or postBuild; narrow the patch target, move the patch or postBuild to a bundle below the root node, or drop the ${...} from the SourceRef URL. A patch that selects a hosted Source but leaves it unchanged is refused too. A copy the integration did not add, anywhere in the root build (see below), is its owner's: the integration hosts none of its own then and does not check what the root bundle's patches do to it. A sourceRef names the object, not the layout holding it.

The root build also covers the child directories the root's kustomization.yaml lists and the AppFileSingle files written into them. When that build already holds a copy the integration did not add (an earlier integration's, the caller's or an application's), that copy is kept and the integration adds none, since kustomize refuses one object twice. Two copies the integration did not add, in any one build (the root's, a ClusterName wrapper's or a generated Kustomization's spec.path), are refused. So is one such copy in a wrapper whose kustomization.yaml lists the root node's directory: the root's copy cannot stay beside it, and without the root's copy no build the bootstrap applies holds the Source. A copy the caller or an application puts in any other build is theirs to keep, and the integration still hosts its own at the root: that Source then has two owners, one of them the caller's. Kustomizations the caller or an application places are not builds kure answers for. Under FluxSeparate every generated Source goes into flux-system, which is built beside the rest of the tree, so a Source with a generated Source's identity anywhere else in the tree is refused even when it is identical.

Bootstrap Generation

Generate Flux system bootstrap manifests. Two modes are supported:

Mode Description
"flux-operator" Default. Emits a full Flux Operator install bundle (CRDs, Deployment, RBAC). Recommended for new clusters.
"gotk" Legacy mode. Emits the GitOps Toolkit component manifests directly.

When FluxMode is empty, it defaults to "flux-operator".

The "flux-operator" bundle is vendored from the upstream flux-operator release and pinned in lockstep with the github.com/controlplaneio-fluxcd/flux-operator Go module (FluxOperatorVersion, currently v0.58.1). Renovate re-vendors the bundle and updates the constant and this version when it bumps the module (scripts/sync-flux-operator-pin.sh); see flux_operator_install.go for the manual refresh procedure.

The "gotk" components are built from the flux2 release's install manifests base, vendored at the module root (internal/gotk/manifests.tar.gz, shared with the tests that read the toolkit's CustomResourceDefinitions out of it) in lockstep with the github.com/fluxcd/flux2/v2 Go module (GotkVersion, currently v2.9.5), so gotk generation makes no network call and the same input always produces the same manifests. That happens when FluxVersion is empty or names GotkVersion (with or without the leading v). Any other FluxVersion, "latest" included, is an explicit opt-in to the upstream behaviour: a manifests base is downloaded from GitHub at generation time — the named release for a vX.Y.Z value, the latest release for anything else (upstream selects a release only for a v-prefixed version). TestVendoredPinsMatchGoMod fails when GotkVersion or FluxOperatorVersion differs from its go.mod require, or when a controller image in the gotk bundle differs from the matching fluxcd/<controller>/api require (controllers without an API module, such as image-reflector-controller, are not compared); see internal/gotk for the refresh procedure after a flux2 bump.

engine := fluxcd.Engine()
rootNode := &stack.Node{Name: "prod"}

bootstrapConfig := &stack.BootstrapConfig{
    Enabled:     true,
    FluxMode:    "flux-operator", // or "gotk"; empty defaults to "flux-operator"
    FluxVersion: "v2.8.2",
    SourceURL:   "oci://registry.example.com/fleet",
    SourceRef:   "latest",
}

objects, err := engine.GenerateBootstrap(bootstrapConfig, rootNode)
if err != nil {
    panic(err)
}
for _, obj := range objects {
    if obj.GetObjectKind().GroupVersionKind().Kind == "FluxInstance" {
        fmt.Println(obj.GetNamespace(), obj.GetName())
    }
}
Sync name

BootstrapConfig.SyncName becomes the FluxInstance's spec.sync.name: the name flux-operator gives the source and Kustomization it creates for the sync. When it is empty the operator names both after the FluxInstance's namespace (DefaultNamespace). That fallback is the operator's, not kure's, which is why the defaults table above has no row for it. Set SyncName when the Kustomizations you generate reference the sync source by another name; otherwise their sourceRef points at a source nothing creates.

engine := fluxcd.Engine()
rootNode := &stack.Node{Name: "prod"}

bootstrapConfig := &stack.BootstrapConfig{
    Enabled:   true,
    SourceURL: "oci://registry.example.com/fleet",
    SourceRef: "latest",
    SyncName:  "fleet",
}

fi, err := engine.GetBootstrapGenerator().GenerateFluxInstance(bootstrapConfig, rootNode)
if err != nil {
    panic(err)
}
fmt.Println(fi.Name, fi.Spec.Sync.Name, fi.Spec.Sync.Ref)
  • flux-operator mode only: gotk mode ignores it, as flux-operator mode ignores Prune.
  • It needs SourceURL: without one no sync block is emitted, so there is nothing to name.
  • kure passes it through unvalidated. The CRD caps it at 63 characters and makes it immutable once set, so renaming the sync of a live cluster means recreating the FluxInstance.
  • It is not the FluxInstance's own metadata.name, which is always FluxInstanceName (flux): the CRD accepts no other. BootstrapGenerator.BootstrapName names the bootstrap Kustomization only.

Configuration

Kustomization Mode

Controls how kustomization.yaml files reference resources:

  • KustomizationExplicit - Lists all manifest files explicitly
  • KustomizationRecursive - Writes no kustomization.yaml; the Flux Kustomization that builds the directory generates one from every .yaml and .yml file below it. The integrator marks every directory a Kustomization it generated builds (SetFluxBuild), and the writers refuse a marked Recursive directory that holds no file (a bundle with no applications, say), since the Kustomization would name an empty directory, which a Git tree drops, and one whose build would differ from the Explicit mode's: another generated target below it that no kustomization.yaml shields, or a YAML extra file in its build. See "Kustomization Generation" in the layout package README.
Flux Placement

Controls where Flux Kustomization resources are placed:

  • FluxSeparate - Flux resources collected in a separate flux-system/ directory inside the root layout's own directory (where the root's kustomization.yaml references it); children referenced as directories, except those that render bundles, which their own CRs apply
  • FluxIntegratedPerLayout - a Flux Kustomization CR for every layout (incl. augmenter-added child layouts), hosted in its parent layout; the parent's kustomization.yaml lists those CR files as its own resources and references no child directory. Finest granularity.
  • FluxIntegratedPerBundle - Flux Kustomization CRs at bundle boundaries only, each hosted in its parent layout; a bundle's interior (application and augmenter-added child layouts) is a single kustomize build, with those children referenced as directories. A child that renders bundles is not referenced: its own CR applies it. Coarser: Flux reconciles per bundle, kustomize handles the interior.

External augmenters may add child layouts that are not represented in the bundle model; integrated placement discovers those layouts and emits the required Flux resources.

Umbrella Bundles

A Bundle with a non-empty Children slice becomes an umbrella: a parent Flux Kustomization that aggregates the readiness of its children via auto-generated spec.healthChecks. This gives downstream consumers a single stable anchor regardless of how many internal tiers the umbrella contains.

Resource generation

ResourceGenerator.GenerateForBundle detects umbrella bundles and:

  • prepends one HealthChecks entry per direct child (referencing the child's own Kustomization by name/namespace)
  • leaves user-supplied HealthChecks appended after the auto entries

spec.wait is not forced to true here; it is the caller's Bundle.Wait input like any other field. Forcing it was self-defeating: upstream documents that when wait is enabled "the HealthChecks are ignored", so the auto entries the generator had just built were inert. Leaving wait unset is what makes them take effect. A caller who does set Wait=true gets upstream's whole-of-resources health assessment instead, which also gates on the child Kustomizations.

GenerateForBundle(b, path) is strictly self-only — it never recurses into b.Children. GenerateFromLayout and GenerateFromCluster cover the whole umbrella closure, because the walker renders every umbrella child as a layout of its own.

Placement in layouts

LayoutIntegrator places umbrella child Flux CRs at the parent layout node, with spec.path = the child's own directory:

  • Integrated, BundleGrouping: GroupByName: the walker creates a bundle sub-layout under the node layout. Umbrella child Kustomization CRs are appended to the bundle sub-layout's Resources, which that layout lists (it is written in KustomizationExplicit mode); their Source CRs, if the child has a SourceRef.URL, go to the root node's layout like every generated Source. Nested umbrella children are placed at their enclosing umbrella child's layout node.
  • Integrated, BundleGrouping: GroupFlat: there is no intermediate bundle layer, so umbrella children become direct sub-layouts of the node layout, and their Flux CRs sit at the node layout.
  • FluxSeparate: the flux-system layout directory receives every bundle's Kustomization CR, umbrella descendants included, as a flat list.

Under both integrated placements a node bundle's own CR is hosted by the parent of the layout that renders the bundle (the root layout hosts its own): that directory is applied by its own Kustomization only, so the CR that creates it cannot live inside it.

On-disk shape

When a parent layout has an umbrella child, the parent's kustomization.yaml lists the child's Kustomization CR file (it is one of the parent's own resources) instead of the child subdirectory. The child subdirectory still exists and still contains its own kustomization.yaml plus workload YAML files — but no Flux CR files, so Flux does not double-apply the child's resources.

Non-Bundle Child Layout CRs

In FluxIntegratedPerLayout mode every child layout that is not an umbrella child, not AppFileSingle and renders no bundle gets a Kustomization CR in its parent's Resources, with spec.path set to child.FullRepoPath(). (A child that renders a bundle already has that bundle's CR there.) This covers:

  • Application layouts — per-app layouts (ApplicationGrouping: GroupByName, or augmenter apps, which keep a directory under GroupFlat). The CR is named after the layout.
  • Augmenter sub-layouts — hook-group child layouts added by a LayoutAugmenter are children of an app layout. spec.dependsOn is populated from ManifestLayout.DependsOn, enabling ordered reconciliation between hook groups.
  • Bundle-less node layouts — a GroupByName node layout above its bundle layout, or a node without a bundle. The CR is named <path with "/" replaced by "-">-node (with ClusterName: ".", node web's path is web, which is also its bundle's CR name).

The integrator applies this rule at any depth. The CR's spec.sourceRef is the SourceRef of the nearest layout at or above the host that renders bundles; with none, the one SourceRef the URL-less bundles below the child share. A missing, incomplete or ambiguous source is a hard error — a Kustomization without a valid spec.sourceRef is rejected by Flux and must not be emitted silently — and so is a layout whose bundles have different SourceRefs (a NodeGrouping: GroupFlat merge) hosting a layout CR. A CR name used twice (a layout named like a bundle, say) is an error, not a silent skip: Flux Kustomizations share one namespace.

Validation

All cluster-level entry points (GenerateFromCluster, CreateLayoutWithResources) call stack.ValidateCluster before walking the tree, and every generation from a layout runs layout.IndexOrigins (see Kustomization paths). Invalid umbrella configurations — such as a bundle referenced both by a Node and by another bundle's Children, shared umbrella ownership, or multi-package umbrellas — fail fast with a validation error rather than producing malformed output.

CreateLayoutWithResources additionally calls validateSourceRefsForFluxIntegrated for both inline placements (FluxIntegratedPerLayout and FluxIntegratedPerBundle) — both emit bundle/node CRs that carry a spec.sourceRef. (After normalization FluxUnset becomes FluxSeparate, which skips this gate.) This checks that every bundle reachable from the cluster node tree — node bundles and umbrella child bundles recursively — has a complete SourceRef with both Kind and Name set. A nil, zero-value, or partially-populated SourceRef is rejected before layout walking begins. The integrator also enforces this at CR-creation time as defense in depth. FluxSeparate and non-Flux paths are unaffected.

Documentation

Overview

Example (BuilderContractDefaults)
package main

import (
	"fmt"

	"github.com/go-kure/kure/pkg/stack/fluxcd"
)

func main() {
	// declared in pkg/stack/fluxcd/defaults.go
	fmt.Println(fluxcd.DefaultInterval)   // 60 * time.Minute
	fmt.Println(fluxcd.DefaultNamespace)  // "flux-system"
	fmt.Println(fluxcd.DefaultSourceKind) // "OCIRepository"
}
Output:
1h0m0s
flux-system
OCIRepository
Example (FluxWorkflowBootstrapNamespace)
package main

import (
	"fmt"

	"github.com/go-kure/kure/pkg/stack"
	"github.com/go-kure/kure/pkg/stack/fluxcd"
)

func main() {
	engine := fluxcd.Engine()
	rootNode := &stack.Node{Name: "production"}
	bootstrapConfig := &stack.BootstrapConfig{Enabled: true}

	engine.GetBootstrapGenerator().DefaultNamespace = "custom-flux" // default: "flux-system"

	objects, err := engine.GenerateBootstrap(bootstrapConfig, rootNode)
	if err != nil {
		panic(err)
	}
	for _, obj := range objects {
		switch obj.GetObjectKind().GroupVersionKind().Kind {
		case "FluxInstance":
			fmt.Println("FluxInstance in", obj.GetNamespace())
		case "Namespace":
			fmt.Println("Namespace", obj.GetName())
		}
	}
}
Output:
Namespace flux-system
FluxInstance in custom-flux
Example (FluxWorkflowDefine)
package main

import (
	"fmt"

	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/go-kure/kure/pkg/kubernetes"
	"github.com/go-kure/kure/pkg/stack"
)

// workloadApp is the stack.ApplicationConfig the guide's applications use: a
// Deployment and a Service named after the application.
type workloadApp struct{}

func (workloadApp) Generate(app *stack.Application) ([]*client.Object, error) {
	var dep client.Object = kubernetes.CreateDeployment(app.Name, app.Namespace)
	var svc client.Object = kubernetes.CreateService(app.Name, app.Namespace)
	return []*client.Object{&dep, &svc}, nil
}

var (
	certManagerConfig stack.ApplicationConfig = workloadApp{}
	frontendConfig    stack.ApplicationConfig = workloadApp{}
	apiConfig         stack.ApplicationConfig = workloadApp{}
)

func main() {
	// The Flux source every bundle's Kustomization reads from.
	source := &stack.SourceRef{Kind: "GitRepository", Name: "flux-system"}

	certManager, err := stack.NewBundle("cert-manager", []*stack.Application{
		stack.NewApplication("cert-manager", "cert-manager", certManagerConfig),
	}, nil)
	if err != nil {
		panic(err)
	}
	certManager.SourceRef = source
	webTier, err := stack.NewBundle("web-tier", []*stack.Application{
		stack.NewApplication("frontend", "web", frontendConfig),
		stack.NewApplication("api-gateway", "web", apiConfig),
	}, nil)
	if err != nil {
		panic(err)
	}
	webTier.SourceRef = source

	cluster := stack.NewCluster("production", &stack.Node{
		Name: "production",
		Children: []*stack.Node{
			{Name: "infrastructure", Bundle: certManager},
			{Name: "applications", Bundle: webTier},
		},
	})
	for _, node := range cluster.Node.Children {
		b := node.Bundle
		fmt.Println(node.Name, b.Name, len(b.Applications), b.SourceRef.Kind, b.SourceRef.Name)
	}
}
Output:
infrastructure cert-manager 1 GitRepository flux-system
applications web-tier 2 GitRepository flux-system
Example (FluxWorkflowDependsOn)
package main

import (
	"fmt"

	"github.com/go-kure/kure/pkg/stack/layout"
)

func main() {
	preInstall := &layout.ManifestLayout{
		Name: "nginx-00-pre-install",
		// ...
	}
	hooks := &layout.ManifestLayout{
		Name:      "nginx-01-hooks",
		DependsOn: []string{"nginx-00-pre-install"},
		// ...
	}
	fmt.Println(hooks.Name, "after", hooks.DependsOn[0] == preInstall.Name)
}
Output:
nginx-01-hooks after true
Example (FluxWorkflowEngine)
package main

import (
	"fmt"

	"github.com/go-kure/kure/pkg/stack/fluxcd"
)

func main() {
	engine := fluxcd.Engine()
	fmt.Println(engine.GetName())
}
Output:
FluxCD Workflow Engine
Example (FluxWorkflowLayout)
package main

import (
	"fmt"

	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/go-kure/kure/pkg/kubernetes"
	"github.com/go-kure/kure/pkg/stack"
	"github.com/go-kure/kure/pkg/stack/fluxcd"
	"github.com/go-kure/kure/pkg/stack/layout"
)

// workloadApp is the stack.ApplicationConfig the guide's applications use: a
// Deployment and a Service named after the application.
type workloadApp struct{}

func (workloadApp) Generate(app *stack.Application) ([]*client.Object, error) {
	var dep client.Object = kubernetes.CreateDeployment(app.Name, app.Namespace)
	var svc client.Object = kubernetes.CreateService(app.Name, app.Namespace)
	return []*client.Object{&dep, &svc}, nil
}

var (
	certManagerConfig stack.ApplicationConfig = workloadApp{}
	frontendConfig    stack.ApplicationConfig = workloadApp{}
	apiConfig         stack.ApplicationConfig = workloadApp{}
)

// productionCluster is the cluster Example_fluxWorkflowDefine builds, for the
// steps that start from it. Keep the two in step: the Example spells the
// cluster out because its body is the guide's Step 1, so it cannot call this.
func productionCluster() *stack.Cluster {
	source := &stack.SourceRef{Kind: "GitRepository", Name: "flux-system"}
	certManager, err := stack.NewBundle("cert-manager", []*stack.Application{
		stack.NewApplication("cert-manager", "cert-manager", certManagerConfig),
	}, nil)
	if err != nil {
		panic(err)
	}
	certManager.SourceRef = source
	webTier, err := stack.NewBundle("web-tier", []*stack.Application{
		stack.NewApplication("frontend", "web", frontendConfig),
		stack.NewApplication("api-gateway", "web", apiConfig),
	}, nil)
	if err != nil {
		panic(err)
	}
	webTier.SourceRef = source
	return stack.NewCluster("production", &stack.Node{
		Name: "production",
		Children: []*stack.Node{
			{Name: "infrastructure", Bundle: certManager},
			{Name: "applications", Bundle: webTier},
		},
	})
}

func main() {
	cluster := productionCluster()
	engine := fluxcd.Engine()

	// Define layout rules
	rules := layout.LayoutRules{
		NodeGrouping:        layout.GroupByName,
		BundleGrouping:      layout.GroupByName,
		ApplicationGrouping: layout.GroupByName,
		FilePer:             layout.FilePerResource,
		FluxPlacement:       layout.FluxSeparate, // Flux resources in separate tree
	}

	// Generate layout with Flux resources integrated
	ml, err := engine.CreateLayoutWithResources(cluster, rules)
	if err != nil {
		panic(err)
	}
	for _, child := range ml.(*layout.ManifestLayout).Children {
		fmt.Println(child.FullRepoPath())
	}
}
Output:
production/infrastructure
production/applications
production/flux-system
Example (FluxWorkflowUmbrella)
package main

import (
	"fmt"

	kustv1 "github.com/fluxcd/kustomize-controller/api/v1"

	"github.com/go-kure/kure/pkg/stack"
	"github.com/go-kure/kure/pkg/stack/fluxcd"
)

func main() {
	umbrella := &stack.Bundle{
		Name: "platform",
		Children: []*stack.Bundle{
			{Name: "platform-infra"},
			{Name: "platform-services"},
			{Name: "platform-apps"},
		},
	}

	objects, err := fluxcd.Engine().ResourceGen.GenerateForBundle(umbrella, "production/apps/platform")
	if err != nil {
		panic(err)
	}
	ks := objects[0].(*kustv1.Kustomization)
	for _, hc := range ks.Spec.HealthChecks {
		fmt.Println(hc.Kind, hc.Namespace, hc.Name)
	}
}
Output:
Kustomization flux-system platform-infra
Kustomization flux-system platform-services
Kustomization flux-system platform-apps
Example (FluxWorkflowWrite)
ml := productionLayout()
out, err := os.MkdirTemp("", "kure-flux-workflow")
if err != nil {
	panic(err)
}
defer func() { _ = os.RemoveAll(out) }()

err = layout.WriteManifest(out, layout.DefaultLayoutConfig(), ml.(*layout.ManifestLayout))
if err != nil {
	panic(err)
}
printFiles(out)
Output:
clusters/production/applications/kustomization.yaml
clusters/production/applications/web-tier/api-gateway/kustomization.yaml
clusters/production/applications/web-tier/api-gateway/web-deployment-api-gateway.yaml
clusters/production/applications/web-tier/api-gateway/web-service-api-gateway.yaml
clusters/production/applications/web-tier/frontend/kustomization.yaml
clusters/production/applications/web-tier/frontend/web-deployment-frontend.yaml
clusters/production/applications/web-tier/frontend/web-service-frontend.yaml
clusters/production/applications/web-tier/kustomization.yaml
clusters/production/flux-system/flux-system-kustomization-cert-manager.yaml
clusters/production/flux-system/flux-system-kustomization-web-tier.yaml
clusters/production/flux-system/kustomization.yaml
clusters/production/infrastructure/cert-manager/cert-manager/cert-manager-deployment-cert-manager.yaml
clusters/production/infrastructure/cert-manager/cert-manager/cert-manager-service-cert-manager.yaml
clusters/production/infrastructure/cert-manager/cert-manager/kustomization.yaml
clusters/production/infrastructure/cert-manager/kustomization.yaml
clusters/production/infrastructure/kustomization.yaml
clusters/production/kustomization.yaml

Index

Examples

Constants

View Source
const (
	// DefaultNamespace is the namespace generated Flux resources are placed in
	// when the caller names none.
	DefaultNamespace = "flux-system"

	// DefaultSourceName is the name given to a generated GitRepository or
	// OCIRepository when the root node has no name of its own.
	DefaultSourceName = "flux-system"

	// DefaultBootstrapName is the name given to the bootstrap Kustomization. It
	// is not derived from the root node. Override it by assigning
	// [BootstrapGenerator.BootstrapName]. It does not reach the FluxInstance,
	// whose name is fixed — see [FluxInstanceName].
	DefaultBootstrapName = "flux-system"

	// FluxInstanceName is the metadata.name of the FluxInstance emitted in
	// flux-operator mode. It is not a default and has no override: the
	// flux-operator CRD requires metadata.name to be "flux" and rejects any
	// other name at admission (x-kubernetes-validations rule
	// `self.metadata.name == 'flux'` in the vendored flux_operator_install.yaml).
	// The name used to follow [BootstrapGenerator.BootstrapName], so a bundle
	// generated with the default ([DefaultBootstrapName]) or any override other
	// than "flux" was refused with "the only accepted name for a FluxInstance
	// is 'flux'". A test compares this constant against the vendored CRD's rule, so
	// an operator bump that changes the rule fails there rather than at apply.
	FluxInstanceName = "flux"

	// DefaultFluxDirName is the directory a separate Flux layout is placed in,
	// under FluxSeparate placement. It is a path segment, not a namespace: it
	// happens to share [DefaultNamespace]'s value and must not be derived from
	// it, because a caller who renames the namespace does not thereby rename
	// the directory. It has no override.
	DefaultFluxDirName = "flux-system"

	// DefaultBootstrapPathRoot is the first segment of the bootstrap
	// Kustomization's spec.path; the root node's name is joined onto it. It is a
	// fixed segment with no override — rename the root node to change the path.
	DefaultBootstrapPathRoot = "manifests"

	// DefaultFluxMode is the bootstrap mode used when BootstrapConfig.FluxMode
	// is empty. It is also the mode GenerateBootstrap dispatches on and the
	// first entry [BootstrapGenerator.SupportedBootstrapModes] reports, so
	// changing it here changes the default and the accepted spelling together
	// — the alternative left an empty FluxMode resolving to a mode the switch
	// no longer recognised.
	DefaultFluxMode = "flux-operator"

	// ModeGotk is the legacy bootstrap mode. It is not a default — nothing
	// falls back to it — but it is named here so the mode set has one
	// authority alongside [DefaultFluxMode] rather than a literal repeated in
	// the dispatch switch and the supported-mode list.
	ModeGotk = "gotk"

	// DefaultSourceKind is the kind of source object bootstrap emits, and
	// references, when BootstrapConfig.SourceKind does not name "GitRepository".
	// That includes the empty string: an unnamed kind yields an OCIRepository
	// for backward compatibility, which is generateSource's own behaviour.
	//
	// There is deliberately one identifier rather than one per emission site.
	// The bootstrap Kustomization's sourceRef, the source object itself and the
	// FluxInstance sync block previously each decided the kind for themselves,
	// and the first of the three used the opposite polarity to the other two —
	// so an empty SourceKind emitted an OCIRepository under a sourceRef naming a
	// GitRepository that was never created. [resolvedSourceKind] is now the only
	// place that decision is made.
	DefaultSourceKind = "OCIRepository"

	// DefaultSourceRef is the OCI tag used when BootstrapConfig.SourceRef is
	// empty, by both the gotk OCIRepository and the FluxInstance sync
	// ([resolvedSyncRef]). It has no GitRepository equivalent: an empty
	// SourceRef leaves the Git reference unset rather than guessing a branch.
	DefaultSourceRef = "latest"

	// DefaultSyncPath is the path a FluxInstance sync block uses when the root
	// node has no name, and the prefix its name is appended to when it has one.
	// It has no override: it is the prefix a sync path is built from, not a
	// value a caller replaces.
	DefaultSyncPath = "./"
)

The values a generator falls back to when the caller supplies nothing.

pkg/stack is a workflow layer above pkg/kubernetes and may hold opinions — but as declared inputs with names a consumer can read, compare against and override, never as literals buried in a constructor. Every fallback the generators apply is one of the identifiers below; grep for the identifier to find every place the value can reach emitted YAML.

These are defaults, not policy, and they are overridden in one of two ways. DefaultNamespace, DefaultInterval and DefaultBootstrapName are copied into an exported field of ResourceGenerator or BootstrapGenerator by its constructor, and a caller that assigns the field afterwards is never overridden. The rest are applied where they are used, and are overridden by naming the corresponding input on stack.BootstrapConfig, stack.Bundle or the root stack.Node — the per-identifier comments below say which input each one yields to.

Three have no override at all: DefaultSyncPath, DefaultBootstrapPathRoot and DefaultFluxDirName. Each is a fixed structural segment of a path the package builds, not a value a caller replaces, and their comments say so. They are named here anyway so the value is greppable and reviewable rather than a literal inside the function that emits it. FluxInstanceName is likewise fixed but is not a default of any kind: the flux-operator CRD accepts no other value, so it is a constraint the package satisfies, not an opinion it holds.

Interval and namespace are the two that always reach output. KustomizationSpec.Interval is +required upstream with no omitempty (kustomize-controller api/v1/kustomization_types.go:68), so a Kustomization cannot be emitted without one; DefaultInterval is what a caller who names no interval gets.

View Source
const DefaultInterval = 60 * time.Minute

DefaultInterval is the reconciliation interval used when the caller names none, on both generators. It applies only to an empty Bundle.Interval: a non-empty value that does not parse is a validation error, never a reason to fall back to this default.

DefaultMode names the kustomization.yaml listing mode the layout writers use for a layout with no Mode of its own (they treat KustomizationUnset as KustomizationExplicit). It is informational: no generator field is seeded from it, because a Kustomization's spec.path is not a mode of the generator but the directory of the layout that renders the bundle (layout.OriginIndex.KustomizationPath).

View Source
const FluxOperatorVersion = "v0.58.1"

FluxOperatorVersion is the upstream flux-operator release whose install manifest is vendored as fluxOperatorInstallYAML. It is pinned to match the github.com/controlplaneio-fluxcd/flux-operator Go module version in kure's go.mod so that the generated FluxInstance type and the install bundle stay in lockstep.

scripts/sync-flux-operator-pin.sh performs steps 2-4 below from go.mod's version, and Renovate runs it after a module bump. To refresh by hand instead:

  1. Bump github.com/controlplaneio-fluxcd/flux-operator in go.mod.
  2. Download the matching install.yaml from the flux-operator GitHub release page: https://github.com/controlplaneio-fluxcd/flux-operator/releases/download/{version}/install.yaml
  3. Replace pkg/stack/fluxcd/flux_operator_install.yaml with it.
  4. Update this constant, and the version named in pkg/stack/fluxcd/README.md ("currently **vX.Y.Z**"); no test checks it.
  5. Run the tests in this package and confirm the resource inventory in TestFluxOperatorInstallObjects still matches.
View Source
const GotkVersion = gotk.Version

GotkVersion is the upstream flux2 release whose install manifests base (the release's manifests.tar.gz asset) is vendored in internal/gotk, so gotk-mode bootstrap generation emits the controllers whose API types kure compiles against, without any network access. The pin, and the procedure for refreshing it after a flux2 bump, live next to the bundle; TestVendoredPinsMatchGoMod fails when the pin and go.mod disagree.

Variables

This section is empty.

Functions

func FluxOperatorInstallObjects

func FluxOperatorInstallObjects() ([]client.Object, error)

FluxOperatorInstallObjects returns the parsed Flux Operator install manifest: Namespace, CRDs, RBAC, ServiceAccount, Service, and controller Deployment. The bytes are embedded at build time from flux_operator_install.yaml (version FluxOperatorVersion).

The returned slice is cached on first parse; callers must treat it as read-only. To mutate any object, deep-copy first.

The order of objects matches the order in the upstream install.yaml (Namespace → CRDs → RBAC → ServiceAccount → Deployment → Service), which is also a valid apply order.

Types

type BootstrapGenerator

type BootstrapGenerator struct {
	// DefaultNamespace is the namespace where bootstrap resources are created
	DefaultNamespace string
	// DefaultInterval is the default reconciliation interval
	DefaultInterval time.Duration
	// BootstrapName is the name given to the bootstrap Kustomization. It is not
	// derived from the root node, and BootstrapConfig carries no equivalent
	// input, so this field is the only way to override [DefaultBootstrapName].
	// Leaving it empty means the default, not a nameless object — see
	// [BootstrapGenerator.bootstrapName].
	//
	// It does not name the FluxInstance. That object's metadata.name is fixed
	// at [FluxInstanceName] by the flux-operator CRD, which admits no other
	// value; it used to take this field, so any bundle whose BootstrapName was
	// not "flux" — the default included — was rejected at apply
	// (go-kure/kure#847).
	BootstrapName string
}

BootstrapGenerator implements the workflow.BootstrapGenerator interface for Flux. It handles the generation of bootstrap resources for setting up Flux.

func NewBootstrapGenerator

func NewBootstrapGenerator() *BootstrapGenerator

NewBootstrapGenerator creates a FluxCD bootstrap generator seeded with the exported defaults (DefaultNamespace, DefaultInterval, DefaultBootstrapName). Assign the fields afterwards to override any of them.

func (*BootstrapGenerator) GenerateBootstrap

func (bg *BootstrapGenerator) GenerateBootstrap(config *stack.BootstrapConfig, rootNode *stack.Node) ([]client.Object, error)

GenerateBootstrap creates bootstrap resources for setting up Flux. When FluxMode is empty, flux-operator is used as the default.

func (*BootstrapGenerator) GenerateFluxInstance

func (bg *BootstrapGenerator) GenerateFluxInstance(config *stack.BootstrapConfig, rootNode *stack.Node) (*fluxv1.FluxInstance, error)

GenerateFluxInstance returns only the FluxInstance CR configured for the given bootstrap settings, without the full Flux Operator install bundle. Returns (nil, nil) when config is nil. Unlike GenerateBootstrap, this method does not check config.Enabled — the caller is responsible for that gate.

Example
package main

import (
	"fmt"

	"github.com/go-kure/kure/pkg/stack"
	"github.com/go-kure/kure/pkg/stack/fluxcd"
)

func main() {
	engine := fluxcd.Engine()
	rootNode := &stack.Node{Name: "prod"}

	bootstrapConfig := &stack.BootstrapConfig{
		Enabled:   true,
		SourceURL: "oci://registry.example.com/fleet",
		SourceRef: "latest",
		SyncName:  "fleet",
	}

	fi, err := engine.GetBootstrapGenerator().GenerateFluxInstance(bootstrapConfig, rootNode)
	if err != nil {
		panic(err)
	}
	fmt.Println(fi.Name, fi.Spec.Sync.Name, fi.Spec.Sync.Ref)
}
Output:
flux fleet latest

func (*BootstrapGenerator) SupportedBootstrapModes

func (bg *BootstrapGenerator) SupportedBootstrapModes() []string

SupportedBootstrapModes returns the bootstrap modes supported by this generator. DefaultFluxMode is the primary (recommended) mode; ModeGotk is the legacy one. This is the single list: GenerateBootstrap's validation error reports it rather than restating the modes.

type LayoutIntegrator

type LayoutIntegrator struct {
	// ResourceGenerator generates the Flux resources
	Generator *ResourceGenerator
}

LayoutIntegrator implements the workflow.LayoutIntegrator interface for Flux. It handles integration of Flux resources with manifest layouts.

Placement (FluxIntegratedPerLayout vs FluxSeparate) is configured via layout.LayoutRules.FluxPlacement on each call. CreateLayoutWithResources normalizes FluxUnset to FluxSeparate before invoking the SourceRef validation gate, WalkCluster, and IntegrateWithLayout, so all three observers agree on the effective placement.

func NewLayoutIntegrator

func NewLayoutIntegrator(generator *ResourceGenerator) *LayoutIntegrator

NewLayoutIntegrator creates a FluxCD layout integrator.

func (*LayoutIntegrator) CreateLayoutWithResources

func (li *LayoutIntegrator) CreateLayoutWithResources(c *stack.Cluster, rules layout.LayoutRules) (*layout.ManifestLayout, error)

CreateLayoutWithResources creates a new layout that includes Flux resources.

rules.FluxPlacement is normalized once at the top of this method (FluxUnset -> FluxSeparate) and the normalized rules are passed to the SourceRef validation gate, WalkCluster, and IntegrateWithLayout. This guarantees a single placement authority per call.

func (*LayoutIntegrator) IntegrateWithLayout

func (li *LayoutIntegrator) IntegrateWithLayout(ml *layout.ManifestLayout, c *stack.Cluster, rules layout.LayoutRules) error

IntegrateWithLayout adds Flux resources to a manifest layout that layout.WalkCluster built from c.

Placement is driven by rules.FluxPlacement. FluxUnset is treated as FluxSeparate to match DefaultLayoutRules and the walker's normalization.

Every placement first indexes the layout's origins (layout.IndexOrigins): a hand-built, partial or other-cluster tree is refused rather than matched by name, and every Kustomization's spec.path is the directory of the layout that renders its bundle. Integrating the same layout again adds nothing: a CR already present with the same name and spec.path in the layout that would host it is kept; one with the same name elsewhere or with another path is an error.

type ResourceGenerator

type ResourceGenerator struct {
	// DefaultInterval is the default reconciliation interval for generated resources
	DefaultInterval time.Duration
	// DefaultNamespace is the default namespace for generated Flux resources
	DefaultNamespace string
	// Prune is the garbage-collection input for Kustomizations generated from a
	// layout.ManifestLayout (FluxIntegratedPerLayout mode), which carries no
	// prune setting of its own. Kustomizations generated from a stack.Bundle
	// use that bundle's own Prune and ignore this field. nil emits
	// prune: false — see pruneValue.
	Prune *bool
}

ResourceGenerator implements the workflow.ResourceGenerator interface for Flux. It focuses purely on generating Flux CRDs from stack components.

func NewResourceGenerator

func NewResourceGenerator() *ResourceGenerator

NewResourceGenerator creates a FluxCD resource generator seeded with the exported defaults (DefaultInterval, DefaultNamespace). Assign the fields afterwards to override any of them; nothing else is injected into generated resources.

func (*ResourceGenerator) GenerateForBundle

func (g *ResourceGenerator) GenerateForBundle(b *stack.Bundle, path string) ([]client.Object, error)

GenerateForBundle creates the Flux resources for b itself: a Kustomization whose spec.path is path, verbatim, and a Source when b.SourceRef has a URL. Umbrella Children are not recursed. The generator computes no path: take it from the layout that renders b (layout.OriginIndex.KustomizationPath).

func (*ResourceGenerator) GenerateFromCluster

func (g *ResourceGenerator) GenerateFromCluster(c *stack.Cluster) ([]client.Object, error)

GenerateFromCluster creates Flux Kustomizations and Sources from a cluster definition. It runs stack.ValidateCluster first to fail fast on structural errors (umbrella cycles, disjointness violations, etc.), then walks the cluster with layout.DefaultLayoutRules and generates from that layout (see GenerateFromLayout).

The spec.path values are therefore the directories WalkCluster writes under the default rules: the root node at <root>, its children at <root>/<child>. Callers that write the layout with other rules must generate from the layout they write instead — CreateLayoutWithResources, or GenerateFromLayout on their own WalkCluster result. The walk renders every application and runs every LayoutAugmenter, so their errors surface here.

func (*ResourceGenerator) GenerateFromLayout

func (g *ResourceGenerator) GenerateFromLayout(root *layout.ManifestLayout, c *stack.Cluster) ([]client.Object, error)

GenerateFromLayout creates one Kustomization (and, when its SourceRef has a URL, a Source) for every reconciliation unit of the layout tree root — every directory that renders bundles — in layout pre-order. root must have been walked from c (layout.WalkCluster): layout.IndexOrigins refuses anything else. Each spec.path is that directory, relative to the writer's output root; the bundles a grouping axis merged into it share the Kustomization (see generateForUnit).

Example
package main

import (
	"fmt"

	kustv1 "github.com/fluxcd/kustomize-controller/api/v1"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/go-kure/kure/pkg/kubernetes"
	"github.com/go-kure/kure/pkg/stack"
	"github.com/go-kure/kure/pkg/stack/fluxcd"
	"github.com/go-kure/kure/pkg/stack/layout"
)

// configMapApp is a minimal stack.ApplicationConfig: one ConfigMap named after
// the application.
type configMapApp struct{}

func (configMapApp) Generate(app *stack.Application) ([]*client.Object, error) {
	var obj client.Object = kubernetes.CreateConfigMap(app.Name, app.Namespace)
	return []*client.Object{&obj}, nil
}

// exampleCluster is the cluster the examples generate from: a root node
// "apps" whose bundle "web" holds one application.
func exampleCluster() *stack.Cluster {
	cluster, err := stack.NewClusterBuilder("prod").
		WithNode("apps").
		WithBundle("web").
		WithApplication("web", configMapApp{}).
		End().
		End().
		Build()
	if err != nil {
		panic(err)
	}
	return cluster
}

func main() {
	cluster := exampleCluster()
	engine := fluxcd.Engine()
	rules := layout.DefaultLayoutRules()
	bundle := cluster.Node.Bundle

	// From an entire cluster: walks it with layout.DefaultLayoutRules()
	objects, err := engine.GenerateFromCluster(cluster)
	if err != nil {
		panic(err)
	}
	fmt.Println(objects[0].GetName(), objects[0].(*kustv1.Kustomization).Spec.Path)

	// From a layout you walked (and will write) yourself
	ml, err := layout.WalkCluster(cluster, rules)
	if err != nil {
		panic(err)
	}
	objects, err = engine.ResourceGen.GenerateFromLayout(ml, cluster)
	if err != nil {
		panic(err)
	}
	fmt.Println(objects[0].GetName(), objects[0].(*kustv1.Kustomization).Spec.Path)

	// For one bundle, at a path you supply
	objects, err = engine.ResourceGen.GenerateForBundle(bundle, "clusters/prod/apps")
	if err != nil {
		panic(err)
	}
	fmt.Println(objects[0].GetName(), objects[0].(*kustv1.Kustomization).Spec.Path)
}
Output:
web apps
web apps
web clusters/prod/apps

func (*ResourceGenerator) GetName

func (g *ResourceGenerator) GetName() string

GetName returns the name of this resource generator.

func (*ResourceGenerator) GetVersion

func (g *ResourceGenerator) GetVersion() string

GetVersion returns the version of this resource generator.

type WorkflowEngine

type WorkflowEngine struct {
	// ResourceGen handles core resource generation
	ResourceGen *ResourceGenerator
	// LayoutInteg handles layout integration
	LayoutInteg *LayoutIntegrator
	// BootstrapGen handles bootstrap resource generation
	BootstrapGen *BootstrapGenerator
}

WorkflowEngine implements the stack.Workflow interface by composing the specialized generator components. This provides a complete FluxCD workflow implementation with clear separation of concerns.

func Engine

func Engine() *WorkflowEngine

Engine returns a WorkflowEngine initialized with defaults. This is the primary entry point for FluxCD workflow functionality.

Example
package main

import (
	"fmt"

	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/go-kure/kure/pkg/kubernetes"
	"github.com/go-kure/kure/pkg/stack"
	"github.com/go-kure/kure/pkg/stack/fluxcd"
)

// configMapApp is a minimal stack.ApplicationConfig: one ConfigMap named after
// the application.
type configMapApp struct{}

func (configMapApp) Generate(app *stack.Application) ([]*client.Object, error) {
	var obj client.Object = kubernetes.CreateConfigMap(app.Name, app.Namespace)
	return []*client.Object{&obj}, nil
}

// exampleCluster is the cluster the examples generate from: a root node
// "apps" whose bundle "web" holds one application.
func exampleCluster() *stack.Cluster {
	cluster, err := stack.NewClusterBuilder("prod").
		WithNode("apps").
		WithBundle("web").
		WithApplication("web", configMapApp{}).
		End().
		End().
		Build()
	if err != nil {
		panic(err)
	}
	return cluster
}

func main() {
	cluster := exampleCluster()

	// Create engine with defaults. Placement is set on the LayoutRules passed
	// to the layout call, not on the engine — see Layout Integration below.
	engine := fluxcd.Engine()

	// Generate all Flux resources for a cluster (paths of a default-rules walk)
	objects, err := engine.GenerateFromCluster(cluster)
	if err != nil {
		panic(err)
	}
	for _, obj := range objects {
		fmt.Println(obj.GetObjectKind().GroupVersionKind().Kind, obj.GetName())
	}
}
Output:
Kustomization web

func NewWorkflowEngine

func NewWorkflowEngine() *WorkflowEngine

NewWorkflowEngine creates a FluxCD workflow engine with default components.

Example
package main

import (
	"fmt"

	"github.com/go-kure/kure/pkg/stack/fluxcd"
)

func main() {
	// Default engine
	engine := fluxcd.Engine()

	// The same, built from its components
	built := fluxcd.NewWorkflowEngine()
	fmt.Println(engine.GetName() == built.GetName(), built.SupportedBootstrapModes())
}
Output:
true [flux-operator gotk]

func (*WorkflowEngine) CreateLayoutWithResources

func (we *WorkflowEngine) CreateLayoutWithResources(c *stack.Cluster, rules stack.LayoutRulesProvider) (stack.ManifestLayoutResult, error)

CreateLayoutWithResources creates a new layout that includes Flux resources.

Example
package main

import (
	"fmt"
	"io/fs"
	"os"
	"path/filepath"

	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/go-kure/kure/pkg/kubernetes"
	"github.com/go-kure/kure/pkg/stack"
	"github.com/go-kure/kure/pkg/stack/fluxcd"
	"github.com/go-kure/kure/pkg/stack/layout"
)

// configMapApp is a minimal stack.ApplicationConfig: one ConfigMap named after
// the application.
type configMapApp struct{}

func (configMapApp) Generate(app *stack.Application) ([]*client.Object, error) {
	var obj client.Object = kubernetes.CreateConfigMap(app.Name, app.Namespace)
	return []*client.Object{&obj}, nil
}

// exampleCluster is the cluster the examples generate from: a root node
// "apps" whose bundle "web" holds one application.
func exampleCluster() *stack.Cluster {
	cluster, err := stack.NewClusterBuilder("prod").
		WithNode("apps").
		WithBundle("web").
		WithApplication("web", configMapApp{}).
		End().
		End().
		Build()
	if err != nil {
		panic(err)
	}
	return cluster
}

// printFiles prints every file below dir, relative to it.
func printFiles(dir string) {
	err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
		if err == nil && !d.IsDir() {
			rel, _ := filepath.Rel(dir, path)
			fmt.Println(rel)
		}
		return err
	})
	if err != nil {
		panic(err)
	}
}

func main() {
	cluster := exampleCluster()
	engine := fluxcd.Engine()
	rules := layout.DefaultLayoutRules()
	out, err := os.MkdirTemp("", "kure-fluxcd-example")
	if err != nil {
		panic(err)
	}
	defer func() { _ = os.RemoveAll(out) }()

	// Create layout with Flux resources integrated
	ml, err := engine.CreateLayoutWithResources(cluster, rules)
	if err != nil {
		panic(err)
	}

	// Write to disk: every spec.path is relative to <out>/clusters
	err = layout.WriteManifest(out, layout.DefaultLayoutConfig(), ml.(*layout.ManifestLayout))
	if err != nil {
		panic(err)
	}
	printFiles(out)
}
Output:
clusters/apps/cluster-configmap-web.yaml
clusters/apps/flux-system/flux-system-kustomization-web.yaml
clusters/apps/flux-system/kustomization.yaml
clusters/apps/kustomization.yaml

func (*WorkflowEngine) GenerateBootstrap

func (we *WorkflowEngine) GenerateBootstrap(config *stack.BootstrapConfig, rootNode *stack.Node) ([]client.Object, error)

GenerateBootstrap creates bootstrap resources for setting up Flux.

Example
package main

import (
	"fmt"

	"github.com/go-kure/kure/pkg/stack"
	"github.com/go-kure/kure/pkg/stack/fluxcd"
)

func main() {
	engine := fluxcd.Engine()
	rootNode := &stack.Node{Name: "prod"}

	bootstrapConfig := &stack.BootstrapConfig{
		Enabled:     true,
		FluxMode:    "flux-operator", // or "gotk"; empty defaults to "flux-operator"
		FluxVersion: "v2.8.2",
		SourceURL:   "oci://registry.example.com/fleet",
		SourceRef:   "latest",
	}

	objects, err := engine.GenerateBootstrap(bootstrapConfig, rootNode)
	if err != nil {
		panic(err)
	}
	for _, obj := range objects {
		if obj.GetObjectKind().GroupVersionKind().Kind == "FluxInstance" {
			fmt.Println(obj.GetNamespace(), obj.GetName())
		}
	}
}
Output:
flux-system flux

func (*WorkflowEngine) GenerateFromCluster

func (we *WorkflowEngine) GenerateFromCluster(c *stack.Cluster) ([]client.Object, error)

GenerateFromCluster creates Flux resources from a cluster definition, with the spec.path values of a default-rules walk (see ResourceGenerator.GenerateFromCluster).

func (*WorkflowEngine) GetBootstrapGenerator

func (we *WorkflowEngine) GetBootstrapGenerator() *BootstrapGenerator

GetBootstrapGenerator returns the underlying bootstrap generator for advanced configuration.

func (*WorkflowEngine) GetLayoutIntegrator

func (we *WorkflowEngine) GetLayoutIntegrator() *LayoutIntegrator

GetLayoutIntegrator returns the underlying layout integrator for advanced configuration.

func (*WorkflowEngine) GetName

func (we *WorkflowEngine) GetName() string

GetName returns a human-readable name for this workflow engine.

func (*WorkflowEngine) GetResourceGenerator

func (we *WorkflowEngine) GetResourceGenerator() *ResourceGenerator

GetResourceGenerator returns the underlying resource generator for advanced configuration.

func (*WorkflowEngine) GetVersion

func (we *WorkflowEngine) GetVersion() string

GetVersion returns the version of this workflow engine.

func (*WorkflowEngine) IntegrateWithLayout

func (we *WorkflowEngine) IntegrateWithLayout(ml *layout.ManifestLayout, c *stack.Cluster, rules layout.LayoutRules) error

IntegrateWithLayout adds Flux resources to an existing manifest layout.

func (*WorkflowEngine) SupportedBootstrapModes

func (we *WorkflowEngine) SupportedBootstrapModes() []string

SupportedBootstrapModes returns the bootstrap modes supported by this engine.

Jump to

Keyboard shortcuts

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