From 697017e496eade2122ddc356232df1bd62a33013 Mon Sep 17 00:00:00 2001 From: Stefan Koelle Date: Sun, 9 Aug 2026 20:26:55 +0200 Subject: [PATCH] Add kctl-tui doctor command for health checks --- README.md | 1 + cmd/kctl-tui/main.go | 115 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+) diff --git a/README.md b/README.md index a87aa76..39caa87 100644 --- a/README.md +++ b/README.md @@ -205,6 +205,7 @@ ln -s /mnt/c/Users//.kube/config ~/.kube/config kctl-tui # start the TUI (full navigation mode) kctl-tui --version # print version kctl-tui --verbose # enable debug logging to stderr +kctl-tui doctor # check if all tools, config and connections are OK kctl-tui config check # validate ~/.kctl-tui/config.yaml kctl-tui panel --context=... --ns=... --team=... # internal (called by tmux) ``` diff --git a/cmd/kctl-tui/main.go b/cmd/kctl-tui/main.go index 3919187..120b008 100644 --- a/cmd/kctl-tui/main.go +++ b/cmd/kctl-tui/main.go @@ -48,6 +48,12 @@ func main() { os.Exit(1) } return + case "doctor": + if err := runDoctor(); err != nil { + fmt.Fprintln(os.Stderr, "kctl-tui doctor error:", err) + os.Exit(1) + } + return } } @@ -110,3 +116,112 @@ func runConfig(args []string) error { } return nil } + +func runDoctor() error { + pass := "ok" + fail := "FAIL" + warn := "WARN" + status := pass + errs := 0 + + check := func(label string, err error) { + if err != nil { + fmt.Printf(" [%s] %s: %v\n", fail, label, err) + status = fail + errs++ + } else { + fmt.Printf(" [%s] %s\n", pass, label) + } + } + + warnCheck := func(label string, err error) { + if err != nil { + fmt.Printf(" [%s] %s: %v\n", warn, label, err) + } else { + fmt.Printf(" [%s] %s\n", pass, label) + } + } + + // --- Tools --- + fmt.Println("\nTools:") + check("kubectl", kubeexec.CheckTool("kubectl")) + check("tmux/psmux", kubeexec.CheckTool("tmux")) + check("k9s", kubeexec.CheckTool("k9s")) + warnCheck("aws CLI (optional)", kubeexec.CheckTool("aws")) + + // --- Config --- + fmt.Println("\nConfig:") + cfgPath, err := config.DefaultPath() + if err != nil { + fmt.Printf(" [%s] config path: %v\n", fail, err) + errs++ + status = fail + } else { + cfg, err := config.Load(cfgPath) + if err != nil { + fmt.Printf(" [%s] load config: %v\n", fail, err) + errs++ + status = fail + } else { + check("config file exists", nil) + if len(cfg.Contexts) == 0 { + fmt.Printf(" [%s] contexts configured\n", fail) + errs++ + status = fail + } else { + fmt.Printf(" [%s] contexts configured (%d)\n", pass, len(cfg.Contexts)) + } + if len(cfg.Envs) == 0 { + fmt.Printf(" [%s] envs configured\n", fail) + errs++ + status = fail + } else { + fmt.Printf(" [%s] envs configured (%d)\n", pass, len(cfg.Envs)) + } + if cfg.ContextTemplate != "" { + ctx := cfg.ResolveContext(cfg.Envs[0], cfg.Contexts[0]) + fmt.Printf(" [%s] context_template resolves to: %s\n", pass, ctx) + } else { + fmt.Printf(" [%s] context_template is empty\n", fail) + errs++ + status = fail + } + if cfg.SecretNameTemplate != "" && len(cfg.Envs) > 0 { + secret := cfg.ResolveSecretName("example-ns", cfg.Envs[0]) + fmt.Printf(" [%s] secret_name_template resolves to: %s\n", pass, secret) + } + } + } + + // --- Connections --- + fmt.Println("\nConnections:") + if kubeexec.CheckTool("kubectl") == nil { + err := kubeexec.CheckAWSAuth() + if err == nil { + fmt.Printf(" [%s] kubectl cluster reachable\n", pass) + } else { + // Not fatal — cluster might be unreachable from this machine + fmt.Printf(" [%s] kubectl cluster: %v\n", warn, err) + } + } + if kubeexec.CheckTool("aws") == nil { + err := kubeexec.CheckAWSAuth() + if err == nil { + fmt.Printf(" [%s] AWS credentials valid\n", pass) + } else { + fmt.Printf(" [%s] AWS credentials: %v\n", warn, err) + } + } else { + fmt.Printf(" [%s] AWS credentials (aws CLI not installed)\n", warn) + } + + // --- Summary --- + fmt.Println() + if status == pass { + fmt.Println("All checks passed. kctl-tui is ready to use.") + } else { + fmt.Printf("%d error(s) found. Fix the issues above and re-run: kctl-tui doctor\n", errs) + os.Exit(1) + } + return nil +}