From d349ffe37da90e1fbad0521a98d0bb73de247ce6 Mon Sep 17 00:00:00 2001 From: rycli Date: Sun, 12 Apr 2026 09:19:33 +0200 Subject: [PATCH 1/2] feat: add --ignore-not-found flag to 'flux diff ks' command Signed-off-by: rycli Assisted-by: claude-code/claude-opus-4-6 --- cmd/flux/diff_kustomization.go | 5 + cmd/flux/diff_kustomization_test.go | 116 +++++++++++++++++- .../configmaps/existing.yaml | 7 ++ .../configmaps/kustomization.yaml | 5 + .../build-kustomization/configmaps/new.yaml | 7 ++ ...d.golden => diff-new-kustomization.golden} | 0 .../diff-taking-ownership.golden | 9 ++ .../existing-configmap.yaml | 7 ++ .../flux-kustomization-configmaps.yaml | 14 +++ .../flux-kustomization-local-only.yaml | 14 +++ internal/build/build.go | 21 +++- 11 files changed, 200 insertions(+), 5 deletions(-) create mode 100644 cmd/flux/testdata/build-kustomization/configmaps/existing.yaml create mode 100644 cmd/flux/testdata/build-kustomization/configmaps/kustomization.yaml create mode 100644 cmd/flux/testdata/build-kustomization/configmaps/new.yaml rename cmd/flux/testdata/diff-kustomization/{nothing-is-deployed.golden => diff-new-kustomization.golden} (100%) create mode 100644 cmd/flux/testdata/diff-kustomization/diff-taking-ownership.golden create mode 100644 cmd/flux/testdata/diff-kustomization/existing-configmap.yaml create mode 100644 cmd/flux/testdata/diff-kustomization/flux-kustomization-configmaps.yaml create mode 100644 cmd/flux/testdata/diff-kustomization/flux-kustomization-local-only.yaml diff --git a/cmd/flux/diff_kustomization.go b/cmd/flux/diff_kustomization.go index bc8164f0..9e1ec770 100644 --- a/cmd/flux/diff_kustomization.go +++ b/cmd/flux/diff_kustomization.go @@ -63,6 +63,7 @@ type diffKsFlags struct { recursive bool localSources map[string]string inMemoryBuild bool + ignoreNotFound bool } var diffKsArgs diffKsFlags @@ -78,6 +79,8 @@ func init() { diffKsCmd.Flags().StringToStringVar(&diffKsArgs.localSources, "local-sources", nil, "Comma-separated list of repositories in format: Kind/namespace/name=path") diffKsCmd.Flags().BoolVar(&diffKsArgs.inMemoryBuild, "in-memory-build", true, "Use in-memory filesystem during build.") + diffKsCmd.Flags().BoolVar(&diffKsArgs.ignoreNotFound, "ignore-not-found", false, + "Ignore Kustomization not found errors on the cluster when diffing.") diffCmd.AddCommand(diffKsCmd) } @@ -117,6 +120,7 @@ func diffKsCmdRun(cmd *cobra.Command, args []string) error { build.WithLocalSources(diffKsArgs.localSources), build.WithSingleKustomization(), build.WithInMemoryBuild(diffKsArgs.inMemoryBuild), + build.WithIgnoreNotFound(diffKsArgs.ignoreNotFound), ) } else { builder, err = build.NewBuilder(name, diffKsArgs.path, @@ -129,6 +133,7 @@ func diffKsCmdRun(cmd *cobra.Command, args []string) error { build.WithLocalSources(diffKsArgs.localSources), build.WithSingleKustomization(), build.WithInMemoryBuild(diffKsArgs.inMemoryBuild), + build.WithIgnoreNotFound(diffKsArgs.ignoreNotFound), ) } diff --git a/cmd/flux/diff_kustomization_test.go b/cmd/flux/diff_kustomization_test.go index 33cea70e..69577dd0 100644 --- a/cmd/flux/diff_kustomization_test.go +++ b/cmd/flux/diff_kustomization_test.go @@ -48,7 +48,7 @@ func TestDiffKustomization(t *testing.T) { name: "diff nothing deployed", args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo --progress-bar=false", objectFile: "", - assert: assertGoldenFile("./testdata/diff-kustomization/nothing-is-deployed.golden"), + assert: assertGoldenFile("./testdata/diff-kustomization/diff-new-kustomization.golden"), }, { name: "diff with a deployment object", @@ -96,7 +96,7 @@ func TestDiffKustomization(t *testing.T) { name: "diff where kustomization file has multiple objects with the same name", args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo --progress-bar=false --kustomization-file ./testdata/diff-kustomization/flux-kustomization-multiobj.yaml", objectFile: "", - assert: assertGoldenFile("./testdata/diff-kustomization/nothing-is-deployed.golden"), + assert: assertGoldenFile("./testdata/diff-kustomization/diff-new-kustomization.golden"), }, { name: "diff with recursive", @@ -138,6 +138,118 @@ func TestDiffKustomization(t *testing.T) { } } +// TestDiffKustomizationNotDeployed tests `flux diff ks` when the Kustomization +// CR does not exist in the cluster but is provided via --kustomization-file. +// Reproduces https://github.com/fluxcd/flux2/issues/5439 +func TestDiffKustomizationNotDeployed(t *testing.T) { + // Use a dedicated namespace with NO setup() -- the Kustomization CR + // intentionally does not exist in the cluster. + tmpl := map[string]string{ + "fluxns": allocateNamespace("flux-system"), + } + setupTestNamespace(tmpl["fluxns"], t) + + tests := []struct { + name string + args string + assert assertFunc + }{ + { + name: "fails without --ignore-not-found", + args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo --progress-bar=false " + + "--kustomization-file ./testdata/diff-kustomization/flux-kustomization-local-only.yaml", + assert: assertError("failed to get kustomization object: kustomizations.kustomize.toolkit.fluxcd.io \"podinfo\" not found"), + }, + { + name: "succeeds with --ignore-not-found and --kustomization-file", + args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo --progress-bar=false " + + "--kustomization-file ./testdata/diff-kustomization/flux-kustomization-local-only.yaml " + + "--ignore-not-found", + assert: assertGoldenFile("./testdata/diff-kustomization/diff-new-kustomization.golden"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := cmdTestCase{ + args: tt.args + " -n " + tmpl["fluxns"], + assert: tt.assert, + } + cmd.runTestCmd(t) + }) + } +} + +// TestDiffKustomizationTakeOwnership tests `flux diff ks` when taking ownership +// of existing resources on the cluster. A "pre-existing" configmap is applied +// to the cluster, and the kustomization contains a matching configmap; the +// diff should show the labels added by flux +func TestDiffKustomizationTakeOwnership(t *testing.T) { + tmpl := map[string]string{ + "fluxns": allocateNamespace("flux-system"), + } + setupTestNamespace(tmpl["fluxns"], t) + + b, _ := build.NewBuilder("configmaps", "", build.WithClientConfig(kubeconfigArgs, kubeclientOptions)) + resourceManager, err := b.Manager() + if err != nil { + t.Fatal(err) + } + + // Pre-create the "existing" configmap in the cluster without Flux labels + if _, err := resourceManager.ApplyAll(context.Background(), createObjectFromFile("./testdata/diff-kustomization/existing-configmap.yaml", tmpl, t), ssa.DefaultApplyOptions()); err != nil { + t.Fatal(err) + } + + cmd := cmdTestCase{ + args: "diff kustomization configmaps --path ./testdata/build-kustomization/configmaps --progress-bar=false " + + "--kustomization-file ./testdata/diff-kustomization/flux-kustomization-configmaps.yaml " + + "--ignore-not-found" + + " -n " + tmpl["fluxns"], + assert: assertGoldenFile("./testdata/diff-kustomization/diff-taking-ownership.golden"), + } + cmd.runTestCmd(t) +} + +// TestDiffKustomizationNewNamespaceAndConfigmap runs `flux diff ks` when the +// kustomization creates a new namespace and resources inside it. The server-side +// dry-run cannot resolve resources in a namespace that doesn't exist yet, +// consistent with `kubectl diff --server-side` behavior. +func TestDiffKustomizationNewNamespaceAndConfigmap(t *testing.T) { + tmpl := map[string]string{ + "fluxns": allocateNamespace("flux-system"), + } + setupTestNamespace(tmpl["fluxns"], t) + + cmd := cmdTestCase{ + args: "diff kustomization new-namespace-and-configmap --path ./testdata/build-kustomization/new-namespace-and-configmap --progress-bar=false " + + "--kustomization-file ./testdata/diff-kustomization/flux-kustomization-new-namespace-and-configmap.yaml " + + "--ignore-not-found" + + " -n " + tmpl["fluxns"], + assert: assertError("ConfigMap/new-ns/app-config not found: namespaces \"new-ns\" not found"), + } + cmd.runTestCmd(t) +} + +// TestDiffKustomizationNewNamespaceOnly runs `flux diff ks` when the +// kustomization creates only a new namespace. The diff should show the +// namespace as created. +func TestDiffKustomizationNewNamespaceOnly(t *testing.T) { + tmpl := map[string]string{ + "fluxns": allocateNamespace("flux-system"), + } + setupTestNamespace(tmpl["fluxns"], t) + + cmd := cmdTestCase{ + args: "diff kustomization new-namespace-only --path ./testdata/build-kustomization/new-namespace-only --progress-bar=false " + + "--kustomization-file ./testdata/diff-kustomization/flux-kustomization-new-namespace-only.yaml " + + "--ignore-not-found" + + " -n " + tmpl["fluxns"], + assert: assertGoldenFile("./testdata/diff-kustomization/diff-new-namespace-only.golden"), + } + cmd.runTestCmd(t) +} + func createObjectFromFile(objectFile string, templateValues map[string]string, t *testing.T) []*unstructured.Unstructured { buf, err := os.ReadFile(objectFile) if err != nil { diff --git a/cmd/flux/testdata/build-kustomization/configmaps/existing.yaml b/cmd/flux/testdata/build-kustomization/configmaps/existing.yaml new file mode 100644 index 00000000..e23da200 --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/configmaps/existing.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: existing-config + namespace: default +data: + key: value diff --git a/cmd/flux/testdata/build-kustomization/configmaps/kustomization.yaml b/cmd/flux/testdata/build-kustomization/configmaps/kustomization.yaml new file mode 100644 index 00000000..2d84cfd3 --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/configmaps/kustomization.yaml @@ -0,0 +1,5 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: +- ./existing.yaml +- ./new.yaml diff --git a/cmd/flux/testdata/build-kustomization/configmaps/new.yaml b/cmd/flux/testdata/build-kustomization/configmaps/new.yaml new file mode 100644 index 00000000..e33bb513 --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/configmaps/new.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: new-config + namespace: default +data: + key: value diff --git a/cmd/flux/testdata/diff-kustomization/nothing-is-deployed.golden b/cmd/flux/testdata/diff-kustomization/diff-new-kustomization.golden similarity index 100% rename from cmd/flux/testdata/diff-kustomization/nothing-is-deployed.golden rename to cmd/flux/testdata/diff-kustomization/diff-new-kustomization.golden diff --git a/cmd/flux/testdata/diff-kustomization/diff-taking-ownership.golden b/cmd/flux/testdata/diff-kustomization/diff-taking-ownership.golden new file mode 100644 index 00000000..50bb1819 --- /dev/null +++ b/cmd/flux/testdata/diff-kustomization/diff-taking-ownership.golden @@ -0,0 +1,9 @@ +► ConfigMap/default/existing-config drifted + +metadata ++ one map entry added: + labels: + kustomize.toolkit.fluxcd.io/name: configmaps + kustomize.toolkit.fluxcd.io/namespace: + +► ConfigMap/default/new-config created diff --git a/cmd/flux/testdata/diff-kustomization/existing-configmap.yaml b/cmd/flux/testdata/diff-kustomization/existing-configmap.yaml new file mode 100644 index 00000000..e23da200 --- /dev/null +++ b/cmd/flux/testdata/diff-kustomization/existing-configmap.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: existing-config + namespace: default +data: + key: value diff --git a/cmd/flux/testdata/diff-kustomization/flux-kustomization-configmaps.yaml b/cmd/flux/testdata/diff-kustomization/flux-kustomization-configmaps.yaml new file mode 100644 index 00000000..e516bf65 --- /dev/null +++ b/cmd/flux/testdata/diff-kustomization/flux-kustomization-configmaps.yaml @@ -0,0 +1,14 @@ +--- +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: configmaps +spec: + interval: 5m0s + path: ./kustomize + force: true + prune: true + sourceRef: + kind: GitRepository + name: configmaps + targetNamespace: default diff --git a/cmd/flux/testdata/diff-kustomization/flux-kustomization-local-only.yaml b/cmd/flux/testdata/diff-kustomization/flux-kustomization-local-only.yaml new file mode 100644 index 00000000..1c9a215c --- /dev/null +++ b/cmd/flux/testdata/diff-kustomization/flux-kustomization-local-only.yaml @@ -0,0 +1,14 @@ +--- +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: podinfo +spec: + interval: 5m0s + path: ./kustomize + force: true + prune: true + sourceRef: + kind: GitRepository + name: podinfo + targetNamespace: default diff --git a/internal/build/build.go b/internal/build/build.go index 6f1b44c5..7010c3b6 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -146,8 +146,9 @@ type Builder struct { strictSubst bool recursive bool localSources map[string]string - // diff needs to handle kustomizations one by one + // diff needs to handle kustomizations one by one, and opt-in to ignore kustomizations missing on cluster singleKustomization bool + ignoreNotFound bool fsBackend fsBackend } @@ -235,6 +236,15 @@ func WithStrictSubstitute(strictSubstitute bool) BuilderOptionFunc { } } +// WithIgnoreNotFound ignores NotFound errors from the cluster kustomization +// lookup as long as a local kustomization file is provided +func WithIgnoreNotFound(ignore bool) BuilderOptionFunc { + return func(b *Builder) error { + b.ignoreNotFound = ignore + return nil + } +} + // WithIgnore sets ignore field func WithIgnore(ignore []string) BuilderOptionFunc { return func(b *Builder) error { @@ -345,6 +355,10 @@ func NewBuilder(name, resources string, opts ...BuilderOptionFunc) (*Builder, er return nil, fmt.Errorf("kustomization file is required for dry-run") } + if b.ignoreNotFound && b.kustomizationFile == "" { + return nil, fmt.Errorf("kustomization file is required when assuming new kustomizations") + } + if !b.dryRun && b.client == nil { return nil, fmt.Errorf("client is required for live run") } @@ -443,10 +457,11 @@ func (b *Builder) build() (m resmap.ResMap, err error) { } else { liveKus, err = b.getKustomization(ctx) if err != nil { - if !apierrors.IsNotFound(err) || b.kustomization == nil { + unknownError := !apierrors.IsNotFound(err) + hasLocalFallback := b.kustomization != nil || b.ignoreNotFound + if unknownError || !hasLocalFallback { return nil, fmt.Errorf("failed to get kustomization object: %w", err) } - // use provided Kustomization liveKus = b.kustomization } } From e9bcccfede649ebc4025c3e1492ae62a98e935e3 Mon Sep 17 00:00:00 2001 From: rycli Date: Mon, 13 Apr 2026 12:54:58 +0200 Subject: [PATCH 2/2] test: add 'flux diff ks' tests for cases that involve new namespaces Signed-off-by: rycli Assisted-by: claude-code/claude-opus-4-6 --- .../new-namespace-and-configmap/configmap.yaml | 7 +++++++ .../new-namespace-and-configmap/kustomization.yaml | 5 +++++ .../new-namespace-and-configmap/namespace.yaml | 4 ++++ .../new-namespace-only/kustomization.yaml | 4 ++++ .../new-namespace-only/namespace.yaml | 4 ++++ .../diff-new-namespace-only.golden | 1 + ...x-kustomization-new-namespace-and-configmap.yaml | 13 +++++++++++++ .../flux-kustomization-new-namespace-only.yaml | 13 +++++++++++++ 8 files changed, 51 insertions(+) create mode 100644 cmd/flux/testdata/build-kustomization/new-namespace-and-configmap/configmap.yaml create mode 100644 cmd/flux/testdata/build-kustomization/new-namespace-and-configmap/kustomization.yaml create mode 100644 cmd/flux/testdata/build-kustomization/new-namespace-and-configmap/namespace.yaml create mode 100644 cmd/flux/testdata/build-kustomization/new-namespace-only/kustomization.yaml create mode 100644 cmd/flux/testdata/build-kustomization/new-namespace-only/namespace.yaml create mode 100644 cmd/flux/testdata/diff-kustomization/diff-new-namespace-only.golden create mode 100644 cmd/flux/testdata/diff-kustomization/flux-kustomization-new-namespace-and-configmap.yaml create mode 100644 cmd/flux/testdata/diff-kustomization/flux-kustomization-new-namespace-only.yaml diff --git a/cmd/flux/testdata/build-kustomization/new-namespace-and-configmap/configmap.yaml b/cmd/flux/testdata/build-kustomization/new-namespace-and-configmap/configmap.yaml new file mode 100644 index 00000000..078b4c15 --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/new-namespace-and-configmap/configmap.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: app-config + namespace: new-ns +data: + key: value diff --git a/cmd/flux/testdata/build-kustomization/new-namespace-and-configmap/kustomization.yaml b/cmd/flux/testdata/build-kustomization/new-namespace-and-configmap/kustomization.yaml new file mode 100644 index 00000000..491d68b1 --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/new-namespace-and-configmap/kustomization.yaml @@ -0,0 +1,5 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: +- ./namespace.yaml +- ./configmap.yaml diff --git a/cmd/flux/testdata/build-kustomization/new-namespace-and-configmap/namespace.yaml b/cmd/flux/testdata/build-kustomization/new-namespace-and-configmap/namespace.yaml new file mode 100644 index 00000000..4755a448 --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/new-namespace-and-configmap/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: new-ns diff --git a/cmd/flux/testdata/build-kustomization/new-namespace-only/kustomization.yaml b/cmd/flux/testdata/build-kustomization/new-namespace-only/kustomization.yaml new file mode 100644 index 00000000..73029636 --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/new-namespace-only/kustomization.yaml @@ -0,0 +1,4 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: +- ./namespace.yaml diff --git a/cmd/flux/testdata/build-kustomization/new-namespace-only/namespace.yaml b/cmd/flux/testdata/build-kustomization/new-namespace-only/namespace.yaml new file mode 100644 index 00000000..4755a448 --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/new-namespace-only/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: new-ns diff --git a/cmd/flux/testdata/diff-kustomization/diff-new-namespace-only.golden b/cmd/flux/testdata/diff-kustomization/diff-new-namespace-only.golden new file mode 100644 index 00000000..77493d65 --- /dev/null +++ b/cmd/flux/testdata/diff-kustomization/diff-new-namespace-only.golden @@ -0,0 +1 @@ +► Namespace/new-ns created diff --git a/cmd/flux/testdata/diff-kustomization/flux-kustomization-new-namespace-and-configmap.yaml b/cmd/flux/testdata/diff-kustomization/flux-kustomization-new-namespace-and-configmap.yaml new file mode 100644 index 00000000..e045b495 --- /dev/null +++ b/cmd/flux/testdata/diff-kustomization/flux-kustomization-new-namespace-and-configmap.yaml @@ -0,0 +1,13 @@ +--- +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: new-namespace-and-configmap +spec: + interval: 5m0s + path: ./kustomize + force: true + prune: true + sourceRef: + kind: GitRepository + name: new-namespace-and-configmap diff --git a/cmd/flux/testdata/diff-kustomization/flux-kustomization-new-namespace-only.yaml b/cmd/flux/testdata/diff-kustomization/flux-kustomization-new-namespace-only.yaml new file mode 100644 index 00000000..56401cf8 --- /dev/null +++ b/cmd/flux/testdata/diff-kustomization/flux-kustomization-new-namespace-only.yaml @@ -0,0 +1,13 @@ +--- +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: new-namespace-only +spec: + interval: 5m0s + path: ./kustomize + force: true + prune: true + sourceRef: + kind: GitRepository + name: new-namespace-only