Guidance for AI coding assistants working in fluxcd/kustomize-controller. Read this file before making changes.
These rules come from fluxcd/flux2/CONTRIBUTING.md and apply to every Flux repository.
- Do not add
Signed-off-byorCo-authored-bytrailers with your agent name. Only a human can legally certify the DCO. - Disclose AI assistance with an
Assisted-bytrailer naming your agent and model:Thegit commit -s -m "Add support for X" --trailer "Assisted-by: <agent-name>/<model-id>"
-sflag adds the human'sSigned-off-byfrom their git config — do not remove it. - Commit message format: Subject in imperative mood ("Add feature X" instead of "Adding feature X"), capitalized, no trailing period, ≤50 characters. Body wrapped at 72 columns, explaining what and why. No
@mentionsor#123issue references in the commit — put those in the PR description. - Trim verbiage: in PR descriptions, commit messages, and code comments. No marketing prose, no restating the diff, no emojis.
- Rebase, don't merge: Never merge
maininto the feature branch; rebase onto the latestmainand push with--force-with-lease. Squash before merge when asked. - Pre-PR gate:
make tidy fmt vet && make testmust pass and the working tree must be clean after codegen. Commit regenerated files in the same PR. - Flux is GA: Backward compatibility is mandatory. Breaking changes to CRD fields, status, CLI flags, metrics, or observable behavior will be rejected. Design additive changes and keep older API versions round-tripping.
- Copyright: All new
.gofiles must begin with the boilerplate fromhack/boilerplate.go.txt(Apache 2.0). Update the year to the current year when copying. - Spec docs: New features and API changes must be documented in
docs/spec/v1/kustomizations.md. Update it in the same PR that introduces the change. - Tests: New features, improvements and fixes must have test coverage. Add unit tests in
internal/controller/*_test.goand otherinternal/*packages as appropriate. Follow the existing patterns for test organization, fixtures, and assertions. Run tests locally before pushing.
Before submitting code, review your changes for the following:
- No secrets in logs or events. Never surface decryption keys, auth tokens, passwords, or credential URLs in error messages, conditions, events, or log lines. SOPS key material (PGP, age, KMS credentials) must never be logged or returned in errors.
- No unchecked I/O. Close HTTP response bodies, file handles, and archive readers in
deferstatements. Check and propagate errors fromio.Copy,os.Remove,os.Rename. - No path traversal. Validate and sanitize file paths extracted from source artifacts. Kustomize builds must stay within the source root; respect
--no-remote-basesto block remote base fetches. - No unbounded reads. Use
io.LimitReaderwhen reading from network or archive sources. Respect existing size limits; do not introduce new reads without bounds. - No command injection. Do not shell out via
os/exec. Kustomize builds go throughfluxcd/pkg/kustomize, not thekustomizeCLI. - No hardcoded defaults for security settings. TLS verification must remain enabled by default; impersonation and cross-namespace ref restrictions must be honored.
- SSA field manager integrity. Always use the controller's field manager via
fluxcd/pkg/ssa. Never hard-code a different manager or strip fields out of band — changing it orphans fields on every managed object. - Error handling. Wrap errors with
%wfor chain inspection. Do not swallow errors silently. Return actionable error messages that help users diagnose the issue without leaking internal state. - Resource cleanup. Ensure temporary files, directories, and fetched artifacts are cleaned up on all code paths (success and error). Use
deferandt.TempDir()in tests. - Concurrency safety. Do not introduce shared mutable state without synchronization. Reconcilers run concurrently; per-object work must be isolated.
- No panics. Never use
panicin runtime code paths. Return errors and let the reconciler handle them gracefully. - Minimal surface. Keep new exported APIs, flags, and environment variables to the minimum needed. Every export is a backward-compatibility commitment.
kustomize-controller is a core component of the Flux GitOps Toolkit. It reconciles the Kustomization CRD (kustomize.toolkit.fluxcd.io). For each object it:
- Fetches a source artifact produced by source-controller from a
GitRepository,Bucket,OCIRepository, or (feature-gated) genericExternalArtifact. - Runs a Kustomize build against the referenced path, with optional
postBuildvariable substitution (envsubst). - Decrypts SOPS-encrypted Kubernetes secrets inline using keys from PGP, age, Hashicorp Vault, AWS KMS, GCP KMS, or Azure Key Vault.
- Applies the resulting manifests with server-side apply using a stable field manager, detects drift, prunes objects removed from source via a resource inventory, and waits on health checks using
cli-utilskstatus. - Optionally impersonates a
ServiceAccountand/or targets a remote cluster via akubeconfigsecret.
It depends on source-controller only at runtime for artifact delivery; it never talks to Git or OCI directly.
main.go— controller entrypoint: flag parsing, feature gate wiring, manager setup,KustomizationReconcilerconstruction.api/— separate Go module (github.com/fluxcd/kustomize-controller/api) with CRD types. Imported viareplacefrom the root module. Containsv1(storage),v1beta1,v1beta2.api/v1/—kustomization_types.go,inventory_types.go,reference_types.go, generatedzz_generated.deepcopy.go.
internal/controller/—kustomization_controller.go(reconcile loop),kustomization_manager.go(SetupWithManager / watches),kustomization_indexers.go,source_predicate.go,constants.go,utils.go,suite_test.go, per-feature*_test.go.testdata/holds manifest fixtures.internal/decryptor/— SOPS integration. Resolves PGP, age, Vault token, AWS/GCP/Azure KMS credentials (including workload identity) and decrypts secrets inside the Kustomize resource tree.internal/inventory/— translatesssa.ChangeSetresults intoResourceInventoryonKustomization.statusand back for pruning/diffing.internal/sops/awskms/,internal/sops/azkv/,internal/sops/keyservice/— provider-specific SOPS plumbing plus an in-process key service used by the decryptor to avoid shelling out tosops.internal/features/— feature gate definitions and defaults (DisableStatusPollerCache,StrictPostBuildSubstitutions,CancelHealthCheckOnNewRevision, etc.).config/— Kustomize overlays.config/crd/bases/holds the generated controller CRD plus source-controller CRDs downloaded for envtest.config/manager/,config/rbac/,config/default/,config/samples/,config/testdata/.hack/—boilerplate.go.txtlicense header andapi-docs/templates.docs/—spec/v1/user-facing API spec,api/v1/generated reference,diagrams/,internal/notes.
- Group:
kustomize.toolkit.fluxcd.io. Kind:Kustomization. - Storage/served version:
v1.v1beta1andv1beta2remain for conversion compatibility — do not remove them. - Types in
api/<version>/kustomization_types.go,reference_types.go,inventory_types.go. Deep-copy methods are generated intozz_generated.deepcopy.go— never hand-edit. config/crd/bases/kustomize.toolkit.fluxcd.io_kustomizations.yamlis generated bymake manifests. Other files in that directory (gitrepositories.yaml,buckets.yaml,ocirepositories.yaml,externalartifacts.yaml) are downloaded from the pinned source-controller version bymake download-crd-deps— do not edit them.api/is its own Go module. When you change types, runmake generate manifests api-docsfrom the repo root; those targets drivecontroller-geninsideapi/via thereplacedirective.
All targets live in the root Makefile. Tool binaries land in build/gobin/.
make tidy— tidy both the root andapi/modules.make fmt/make vet— run in both modules.make generate—controller-gen objectagainstapi/(deepcopy).make manifests— regenerate CRDs and RBAC underconfig/crd/basesfrom both modules.make api-docs— regeneratedocs/api/v1/kustomize.md.make manager— build the binary tobuild/bin/manager.make run— run the controller locally. RequiresSOURCE_CONTROLLER_LOCALHOSTif source-controller is port-forwarded (seeDEVELOPMENT.md).make test— canonical test target. Chainstidy generate fmt vet manifests api-docs download-crd-deps install-envtestand a localsopsbinary, then runsgo test ./... -v -coverprofile cover.out. HonorsGO_TEST_ARGS,ENVTEST_KUBERNETES_VERSION,ENVTEST_ARCH.make install-envtest— download envtest binaries viasetup-envtest.make install/make deploy/make dev-deploy/make docker-build/make docker-push— cluster workflows.make verify— runsfmtand fails if the working tree is dirty.
Check go.mod and the Makefile for current dependency and tool versions. After changing API types or kubebuilder markers, regenerate and commit the results:
make generate manifests api-docsGenerated files (never hand-edit):
api/*/zz_generated.deepcopy.goconfig/crd/bases/kustomize.toolkit.fluxcd.io_kustomizations.yamlconfig/crd/bases/{gitrepositories,buckets,ocirepositories,externalartifacts}.yaml(downloaded from source-controller)docs/api/v1/kustomize.md
Load-bearing replace directives in go.mod — do not remove:
sigs.k8s.io/kustomize/api+kyamlpinned viareplace. Do not bump without coordinating with maintainers; kustomize upgrades are breaking.opencontainers/go-digestpinned to a master snapshot for BLAKE3 support.
The fluxcd/source-controller/api version in go.mod drives SOURCE_VER in the Makefile and determines which source CRDs are downloaded for envtest. Bumping it requires re-running make download-crd-deps (or deleting build/.src-crd-*).
Bump fluxcd/pkg/* modules as a set — version skew breaks go.sum. Run make tidy after any bump.
- Standard
gofmt. All exported types, functions, and package declarations need doc comments; non-trivial unexported declarations too. Error messages lowercase, wrapped with%w. - Kustomize builds go through
github.com/fluxcd/pkg/kustomize(imported asgenerator). Respect--no-remote-basesviaKustomizationReconciler.NoRemoteBases. - Server-side apply goes through
github.com/fluxcd/pkg/ssa. Two field managers are used:gotk-kustomize-controllerfor status and finalizers (set asStatusManagerinmain.go), andkustomize-controllerfor managed objects applied via SSA. - Inventory and pruning. Owned objects are persisted on
Kustomization.status.inventoryasResourceRef{ID, Version}whereIDis acli-utilsObjMetadatastring. Helpers live ininternal/inventory. Pruning diffs the previous inventory against the new change set; corrupting the format breaks upgrades. - Drift detection / correction. SSA reapply with force corrects drift. Resources marked
ignoreor without the diff filter must be skipped — reuse the existing helpers. - Impersonation. If
spec.serviceAccountNameis set (or--default-service-accountis configured), the reconciler builds a scoped REST config viafluxcd/pkg/runtime/clientandfluxcd/pkg/auth. When--no-cross-namespace-refsis set, cross-namespace references are rejected — honorNoCrossNamespaceRefsin any new ref-resolving code. - Multi-tenancy lockdown flags:
--default-service-account,--default-decryption-service-account,--default-kubeconfig-service-account,--no-remote-bases,--no-cross-namespace-refs, and theObjectLevelWorkloadIdentityfeature gate. Don't invent parallel mechanisms. - Field-manager override.
--override-managerconfiguresDisallowedFieldManagers; conflicting managers get their fields reclaimed. - Feature gates. Add new gates to
internal/features/features.gowith a default and a doc comment. Check them inmain.goduring setup and propagate booleans intoKustomizationReconcilerfields — do not callfeatures.Enabledfrom hot reconcile paths. - Post-build substitution (
spec.postBuild.substitute/substituteFrom) runs envsubst after kustomize build.StrictPostBuildSubstitutionscontrols whether a missing variable without a default is an error. - Dependency ordering.
spec.dependsOnrequires referenced Kustomizations to beReady(and withAdditiveCELDependencyCheck, to satisfy a CEL expression) before reconciling. RespectrequeueDependency; never bypass the check.
- Primary tests run with envtest.
make testdownloads envtest binaries viasetup-envtestintobuild/testbin/and exportsKUBEBUILDER_ASSETS. - Controller test suite lives in
internal/controller/.suite_test.gowires up afluxcd/pkg/runtime/testenv.Environment, registers the reconciler, starts an in-processfluxcd/pkg/testserverartifact server, and (for SOPS/Vault tests) a Vault container viaory/dockertest. Tests use Gomega. - Fixtures:
internal/controller/testdata/,internal/decryptor/testdata/,internal/inventory/testdata/,config/testdata/. - Run a single test:
make test GO_TEST_ARGS='-run TestKustomizationReconciler_Foo'. - Vault and SOPS tests spin up Docker containers — a working Docker daemon is required.
DEBUG_TEST=1enables verbose zap logging insidesuite_test.go.
api/is a separate Go module with its owngo.mod. Forgetting to tidy it (done bymake tidy) breaks downstream consumers.go.modpinssigs.k8s.io/kustomize/apiandkyamlviareplace. Do not remove the replace or bump these independently.source-controllerCRDs underconfig/crd/bases/are downloaded and version-tracked viabuild/.src-crd-<version>. If you changefluxcd/source-controller/apiingo.mod, delete that marker (or runmake cleanup-crd-deps) so the nextmake testpulls fresh CRDs.- SOPS age keys for fallback decryption come from the secret named by
--sops-age-secretin the controller's runtime namespace, not from theKustomization's namespace. Per-Kustomizationkeys come fromspec.decryption.secretRef. - The managed-object field manager name (
kustomize-controller) is load-bearing for drift detection across restarts. Similarly,gotk-kustomize-controllerowns status and finalizer fields. See Code quality for consequences of changing either. ResourceInventoryformat must stay backward-compatible:IDis acli-utilsObjMetadatastring andVersionis the apiVersion at apply time. Do not reorder or re-encode.- Post-build substitution uses envsubst semantics, not Go templates.
$VARand${VAR}are substituted; escape with$$. dependsOnis evaluated every reconcile; a Kustomization that depends on itself transitively wedges the requeue behavior.fluxcd/pkgversion skew: controllers in the Flux distribution expect compatiblepkg/runtime,pkg/ssa, andpkg/kustomize. When bumping one, check the others and run the fullmake test.main.goregisters onlykustomizev1andsourcev1with the scheme. A new watched type means updatinginit()and theSetupWithManagerwatches ininternal/controller/kustomization_manager.go, and honoring--watch-label-selectorviamgrConfig.Cache.ByObject.