From 787ce4edfaff48fd797e9c98a4be06f0dfd40bd6 Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Fri, 19 Jun 2026 09:09:34 +0000 Subject: [PATCH] =?UTF-8?q?homelab:=20v0.3.1=20=E2=80=94=20fix=20k8s=20db?= =?UTF-8?q?=20PG=20target=20(resolve=20CNPG=20primary=20pod,=20not=20the?= =?UTF-8?q?=20Service)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `k8s db ` (Postgres path) execed `pg-cluster-rw`, which is the CNPG read-write SERVICE, not a pod — so kubectl exec failed with `pods "pg-cluster-rw" not found`. The unit test only checked the plan; the verb was never fired at live state (the gap flagged in v0.2), so it shipped broken. Fix: the PG plan now carries a label selector (cnpg.io/instanceRole=primary) instead of a pod name, and k8s db resolves the actual primary POD via `kubectl get pod -l ` before exec. MySQL path (real pod mysql-standalone-0) unchanged. Live-verified both paths (psql + mysql). Co-Authored-By: Claude Opus 4.8 --- cli/VERSION | 2 +- cli/cmd_k8s.go | 10 +++++++++- cli/k8s.go | 15 ++++++++++++--- cli/k8s_test.go | 4 +++- 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/cli/VERSION b/cli/VERSION index 268b0334..937cd784 100644 --- a/cli/VERSION +++ b/cli/VERSION @@ -1 +1 @@ -v0.3.0 +v0.3.1 diff --git a/cli/cmd_k8s.go b/cli/cmd_k8s.go index ae47c940..80f8f62d 100644 --- a/cli/cmd_k8s.go +++ b/cli/cmd_k8s.go @@ -152,11 +152,19 @@ func k8sDB(args []string) error { return fmt.Errorf(`usage: homelab k8s db [--mysql] [--db NAME] -- ""`) } p := planDBExec(app, dbName, sql, mysql) + pod := p.pod + if pod == "" && p.selector != "" { + resolved, err := kubectlCapture(p.ns, "get", "pod", "-l", p.selector, "-o", "jsonpath={.items[0].metadata.name}") + if err != nil || resolved == "" { + return fmt.Errorf("could not resolve db pod in %s (selector %q): %v", p.ns, p.selector, err) + } + pod = resolved + } exec := []string{"exec"} if sql == "" { exec = append(exec, "-it") // interactive client when no SQL given } - exec = append(exec, p.pod) + exec = append(exec, pod) if p.container != "" { exec = append(exec, "-c", p.container) } diff --git a/cli/k8s.go b/cli/k8s.go index e7ee201e..3a2d0a5d 100644 --- a/cli/k8s.go +++ b/cli/k8s.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "os/exec" "strings" ) @@ -19,6 +20,12 @@ func kubectlStream(ns string, args ...string) error { return runStreamingIn("", "kubectl", kubectlBase(ns, args...)...) } +// kubectlCapture runs kubectl and returns trimmed stdout (for resolving pods). +func kubectlCapture(ns string, args ...string) (string, error) { + out, err := exec.Command("kubectl", kubectlBase(ns, args...)...).Output() + return strings.TrimSpace(string(out)), err +} + // k8sTarget is the parsed `` + selectors shared by the k8s verbs. type k8sTarget struct { app string @@ -96,13 +103,15 @@ func (t k8sTarget) objectRef() string { type dbPlan struct { ns string - pod string + pod string // explicit pod (e.g. mysql-standalone-0) + selector string // resolve the pod by this label when pod == "" (CNPG primary) container string // "" = default container argv []string // command + args to run inside the pod } // planDBExec builds the in-pod command to run sql against app's database. -// PG (default): CNPG primary pg-cluster-rw, psql -U postgres -d . +// PG (default): CNPG primary POD (resolved by label — pg-cluster-rw is a +// Service, not an exec target), psql -U postgres -d . // MySQL: mysql-standalone-0, password from env (never on the command line). // dbName defaults to app. sql empty => interactive client. func planDBExec(app, dbName, sql string, mysql bool) dbPlan { @@ -120,7 +129,7 @@ func planDBExec(app, dbName, sql string, mysql bool) dbPlan { if sql != "" { argv = append(argv, "-tAc", sql) } - return dbPlan{ns: "dbaas", pod: "pg-cluster-rw", container: "postgres", argv: argv} + return dbPlan{ns: "dbaas", selector: "cnpg.io/instanceRole=primary", container: "postgres", argv: argv} } // shellQuote single-quotes s for safe embedding in a bash -c string. diff --git a/cli/k8s_test.go b/cli/k8s_test.go index 471fc917..cfa356bc 100644 --- a/cli/k8s_test.go +++ b/cli/k8s_test.go @@ -34,7 +34,9 @@ func TestK8sTargetObjectRef(t *testing.T) { func TestPlanDBExecPostgresDefault(t *testing.T) { p := planDBExec("fire-planner", "", "SELECT 1", false) - if p.ns != "dbaas" || p.pod != "pg-cluster-rw" || p.container != "postgres" { + // pg-cluster-rw is a Service, so the PG plan resolves the primary POD by + // label rather than naming an (un-exec-able) Service. + if p.ns != "dbaas" || p.pod != "" || p.selector != "cnpg.io/instanceRole=primary" || p.container != "postgres" { t.Fatalf("unexpected pg target: %+v", p) } // db name defaults to the app; SQL passed via -tAc