1
0
mirror of synced 2026-04-14 18:56:56 +00:00

feat: add --ignore-not-found flag to 'flux diff ks' command

Signed-off-by: rycli <cyril@ryc.li>
Assisted-by: claude-code/claude-opus-4-6
This commit is contained in:
rycli
2026-04-12 09:19:33 +02:00
parent ac7f72b62b
commit d349ffe37d
11 changed files with 200 additions and 5 deletions

View File

@@ -63,6 +63,7 @@ type diffKsFlags struct {
recursive bool recursive bool
localSources map[string]string localSources map[string]string
inMemoryBuild bool inMemoryBuild bool
ignoreNotFound bool
} }
var diffKsArgs diffKsFlags 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().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, diffKsCmd.Flags().BoolVar(&diffKsArgs.inMemoryBuild, "in-memory-build", true,
"Use in-memory filesystem during build.") "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) diffCmd.AddCommand(diffKsCmd)
} }
@@ -117,6 +120,7 @@ func diffKsCmdRun(cmd *cobra.Command, args []string) error {
build.WithLocalSources(diffKsArgs.localSources), build.WithLocalSources(diffKsArgs.localSources),
build.WithSingleKustomization(), build.WithSingleKustomization(),
build.WithInMemoryBuild(diffKsArgs.inMemoryBuild), build.WithInMemoryBuild(diffKsArgs.inMemoryBuild),
build.WithIgnoreNotFound(diffKsArgs.ignoreNotFound),
) )
} else { } else {
builder, err = build.NewBuilder(name, diffKsArgs.path, builder, err = build.NewBuilder(name, diffKsArgs.path,
@@ -129,6 +133,7 @@ func diffKsCmdRun(cmd *cobra.Command, args []string) error {
build.WithLocalSources(diffKsArgs.localSources), build.WithLocalSources(diffKsArgs.localSources),
build.WithSingleKustomization(), build.WithSingleKustomization(),
build.WithInMemoryBuild(diffKsArgs.inMemoryBuild), build.WithInMemoryBuild(diffKsArgs.inMemoryBuild),
build.WithIgnoreNotFound(diffKsArgs.ignoreNotFound),
) )
} }

View File

@@ -48,7 +48,7 @@ func TestDiffKustomization(t *testing.T) {
name: "diff nothing deployed", name: "diff nothing deployed",
args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo --progress-bar=false", args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo --progress-bar=false",
objectFile: "", 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", 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", 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", args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo --progress-bar=false --kustomization-file ./testdata/diff-kustomization/flux-kustomization-multiobj.yaml",
objectFile: "", objectFile: "",
assert: assertGoldenFile("./testdata/diff-kustomization/nothing-is-deployed.golden"), assert: assertGoldenFile("./testdata/diff-kustomization/diff-new-kustomization.golden"),
}, },
{ {
name: "diff with recursive", 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 { func createObjectFromFile(objectFile string, templateValues map[string]string, t *testing.T) []*unstructured.Unstructured {
buf, err := os.ReadFile(objectFile) buf, err := os.ReadFile(objectFile)
if err != nil { if err != nil {

View File

@@ -0,0 +1,7 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: existing-config
namespace: default
data:
key: value

View File

@@ -0,0 +1,5 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ./existing.yaml
- ./new.yaml

View File

@@ -0,0 +1,7 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: new-config
namespace: default
data:
key: value

View File

@@ -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

View File

@@ -0,0 +1,7 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: existing-config
namespace: default
data:
key: value

View File

@@ -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

View File

@@ -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

View File

@@ -146,8 +146,9 @@ type Builder struct {
strictSubst bool strictSubst bool
recursive bool recursive bool
localSources map[string]string 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 singleKustomization bool
ignoreNotFound bool
fsBackend fsBackend 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 // WithIgnore sets ignore field
func WithIgnore(ignore []string) BuilderOptionFunc { func WithIgnore(ignore []string) BuilderOptionFunc {
return func(b *Builder) error { 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") 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 { if !b.dryRun && b.client == nil {
return nil, fmt.Errorf("client is required for live run") return nil, fmt.Errorf("client is required for live run")
} }
@@ -443,10 +457,11 @@ func (b *Builder) build() (m resmap.ResMap, err error) {
} else { } else {
liveKus, err = b.getKustomization(ctx) liveKus, err = b.getKustomization(ctx)
if err != nil { 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) return nil, fmt.Errorf("failed to get kustomization object: %w", err)
} }
// use provided Kustomization
liveKus = b.kustomization liveKus = b.kustomization
} }
} }