Seven commits landed on main in Flux on 1 and 2 October 2026. The v2.9.6 tag contains none of them. flux diff kustomization now prints skipped for inventory entries the controller will not prune, and a live read that fails for any reason other than NotFound returns an error.
Diff follows the controller prune rules ¶
flux diff kustomization used to mark every inventory entry missing from the local build as deleted. That set included objects with kustomize.toolkit.fluxcd.io/prune: disabled, kustomize.toolkit.fluxcd.io/reconcile: disabled, or kustomize.toolkit.fluxcd.io/ssa: Ignore, plus objects whose owner labels no longer name this Kustomization. The controller leaves those objects in place. The CLI still printed deleted and exited 1.
Respect pruning exclusions in kustomization diff changes the prune loop in internal/build/diff.go. The loop runs only when spec.prune is true and the object diff recorded no errors. For each stale entry the builder calls Get on the live object.
A NotFound result is still a deletion. There is no live object left to protect. Any other Get error, forbidden included, is stored as <kind>/<namespace>/<name> query failed. The command does not print deleted for that object. TestDiffKustomizationPruningForbidden expects exit code 2, and no deletion line, when the caller cannot read the ConfigMap.
If the read succeeds and the owner selector misses, or a label or annotation matches an exclusion, the line is skipped. That path does not set the change bit. Skipped objects by themselves no longer make the command exit 1. With spec.prune set to false, the loop does not run, and the command prints nothing for those inventory entries.
The first version of the exclusion map used string literals, and the SSA value was lowercase ignore. The API constant IgnoreValue is Ignore, with a capital I. Use Flux constants for pruning exclusions builds the keys from controllerGroup, which is kustomize.toolkit.fluxcd.io, and the values from kustomizev1.DisabledValue and kustomizev1.IgnoreValue. The table in cmd/flux/diff_kustomization_test.go expects skipped for a prune annotation, a reconcile label, an SSA Ignore annotation, and a foreign name or namespace owner label.
metadata:
annotations:
kustomize.toolkit.fluxcd.io/prune: disabled
A script that grepped for deleted on a protected ConfigMap will now see skipped, and exit code 0, when that is the only difference. A kubeconfig that can read Kustomization status but cannot get the live objects will fail the diff with exit code 2.
Default install can impersonate any ServiceAccount ¶
Introduce RBAC for ArtifactGenerator cross namespace artifacts adds manifests/rbac/impersonator.yaml and registers it from manifests/rbac/kustomization.yaml. The ClusterRole name is crd-controller-impersonator. The only rule is impersonate on core serviceaccounts. There is no resourceNames list. The ClusterRoleBinding of the same name grants that role to the kustomize-controller, helm-controller, and source-watcher accounts in flux-system. When the install namespace is not flux-system, manifest generation replaces that string in the rendered RBAC file, so the subjects follow the namespace you passed.
This widens the default bundle. The parent of this commit had no impersonate rule in the install manifests. Clusters that set serviceAccountName on a Kustomization or HelmRelease already had to add the verb themselves, often limited to named accounts. The generated role covers every ServiceAccount in the cluster. A token for any of those three controller accounts can act as any ServiceAccount.
The role is there so source-watcher can impersonate a tenant account when an ArtifactGenerator writes an ExternalArtifact in another namespace. The binding lists source-watcher even when that component is not installed. A binding whose account does not exist has no effect until the account appears. cmd/flux/install_test.go checks the role and the three subjects only on the extra components install path. The YAML itself sits in the shared RBAC kustomization, so a default install renders it too.
Workers return after the first error ¶
The diff worker sent an error on errChan and then kept running. The caller reads that channel once. errChan is unbuffered, so a second send blocks with nobody left to receive it, and the late cmd.Print can race the caller reading the same buffer. Stop diff processing after an error prints the diff first in cmd/flux/diff_kustomization.go, sends a status 2 RequestError, and returns. The partial diff stays. The change result is not sent after that error.
Stop build processing after an error adds the same return in cmd/flux/build_kustomization.go after a failed builder.Build and after a failed ssautil.ObjectsToYAML. Build does not print a partial manifest on that path. The new test runs build kustomization missing and expects failed to get kustomization object plus a not found error for kustomizations.kustomize.toolkit.fluxcd.io. Both patches are small. Lookup failures from the prune change go through this same error path, so the worker has to stop after the first one.
Plugin archives with a leading dot slash ¶
Plugin install matches archive entry names against extractPath. An archive built with tar -C dir . stores the entry as ./bin/foo. matchArchiveEntry compared the raw strings with == whenever the target contained a slash, so bin/foo did not match ./bin/foo. Install then failed with binary not found in archive.
Match plugin archive entries with a leading ./ compares path.Clean of both names in internal/plugin/install.go. Targets with no slash still use filepath.Base. path.Clean("./bin/foo") is bin/foo. Cleaning also drops repeated slashes and . segments, which is a wider match than the ./ prefix alone. TestMatchArchiveEntry adds ./bin/flux-operator against bin/flux-operator, a bare ./flux-operator, and ./other/flux-operator, which must still miss.
What to watch ¶
Update toolkit components moves go.mod, the helm controller base, and the kustomize controller base. helm-controller goes from v1.6.4 to v1.6.5. kustomize-controller and source-controller go from v1.9.5 to v1.9.6. Flux v2.9.6 already ships those image versions, from the release branch, through a different commit. The CLI changes in this post are absent from that tag. flux install from v2.9.6 does not render crd-controller-impersonator, and that CLI still reports protected objects as deleted.
Pipelines that treat exit code 1 as drift should retest protected objects. They now exit 0 when nothing else changed, and a denied get exits 2. If the cluster already grants impersonate with resourceNames, compare that binding with the new cluster wide role before installing from main. Plugin packs created with tar -C can drop a workaround that stripped a leading ./, once the CLI is built from this main.