From 3ec7aa6a072cf20d67b0b7b28e43aab99af2d6d3 Mon Sep 17 00:00:00 2001 From: Said Boudjelda Date: Sun, 18 Oct 2026 15:55:49 +0200 Subject: [PATCH] fix(auto): kates auto --dry-run introspects the cluster read-only `kates auto --dry-run` previews the Helm install, but its introspection ran every write probe: a namespace and a Secret to audit Secret creation, prober pods to measure inter-zone latency, and a dns-detect pod. kates deploy --dry-run sets the collector ReadOnly for that; auto never did. A failed preflight also ended the process with os.Exit(1) from inside RunE. The collector is ReadOnly under --dry-run, and auto takes its executor from defaultExecutor, as deploy does, so a test can stand in for kubectl. A failed preflight is returned instead; the exit code stays 1. A failed introspection keeps os.Exit(2), the code the CLI reference documents for it. TestAutoDryRunCollectsReadOnly records every command: with --dry-run none writes, and without it the same introspection does. --- cli/cmd/auto.go | 9 ++++++--- cli/cmd/auto_test.go | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 cli/cmd/auto_test.go diff --git a/cli/cmd/auto.go b/cli/cmd/auto.go index db33b20f..9ffef057 100644 --- a/cli/cmd/auto.go +++ b/cli/cmd/auto.go @@ -48,12 +48,14 @@ func init() { func runAuto(cmd *cobra.Command, args []string) error { output.Header("Kates Auto-Deploy") - executor := detect.NewOSExecutor() + executor := defaultExecutor collector := detect.NewCollector(executor) + // Introspection creates a namespace, a Secret and prober pods. A dry run + // collects without them, as kates deploy --dry-run does. + collector.ReadOnly = autoDryRun if err := collector.Preflight(); err != nil { - output.Error(fmt.Sprintf("Preflight failed: %v", err)) - os.Exit(1) + return cmdErr(fmt.Sprintf("Preflight failed: %v", err)) } // 1. Detect Phase @@ -62,6 +64,7 @@ func runAuto(cmd *cobra.Command, args []string) error { report, collectErr := collector.Collect(context.Background()) if collectErr != nil { output.Error(fmt.Sprintf("Cluster introspection failed: %v", collectErr)) + // The CLI reference documents exit 2 here; an error from RunE exits 1. os.Exit(2) } diff --git a/cli/cmd/auto_test.go b/cli/cmd/auto_test.go new file mode 100644 index 00000000..13a7d5e3 --- /dev/null +++ b/cli/cmd/auto_test.go @@ -0,0 +1,40 @@ +package cmd + +import ( + "path/filepath" + "strings" + "testing" +) + +// kates auto --dry-run introspected the cluster with every write probe on: a +// namespace, a Secret and prober pods, where kates deploy --dry-run writes +// nothing. The run without --dry-run is the control: the same introspection +// does write. +func TestAutoDryRunCollectsReadOnly(t *testing.T) { + origExecutor, origDryRun, origChart := defaultExecutor, autoDryRun, autoChartDir + t.Cleanup(func() { defaultExecutor, autoDryRun, autoChartDir = origExecutor, origDryRun, origChart }) + // No chart there, so the run stops before helm, right after introspection. + autoChartDir = filepath.Join(t.TempDir(), "no-chart") + + writes := func(dryRun bool) []string { + rec := &recordingExecutor{} + defaultExecutor, autoDryRun = rec, dryRun + if err := runAuto(autoCmd, nil); err == nil || !strings.Contains(err.Error(), "Helm chart not found") { + t.Fatalf("dry run %v: err = %v, want the missing chart", dryRun, err) + } + var out []string + for _, c := range rec.commands() { + if clusterWrite.MatchString(c) { + out = append(out, c) + } + } + return out + } + + if len(writes(false)) == 0 { + t.Fatal("control: introspection without --dry-run wrote nothing, so the dry run proves nothing") + } + if got := writes(true); len(got) != 0 { + t.Errorf("auto --dry-run wrote to the cluster:\n %s", strings.Join(got, "\n ")) + } +}