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