homelab: add k8s verb-group (v0.2) — the biggest remaining surface
Mining the post-v0.1 corpus showed kubectl is the dominant remaining domain by far: 11,291 commands across 243 sessions (more than everything else combined). This adds the full k8s verb-group built on an app→namespace→pod resolver (most namespaces hold one app, so <app> defaults to the namespace and the target defaults to deploy/<app>, letting kubectl resolve the pod; -n/--pod/-c/-l/--tty override). Read: status (pods + non-Normal events), get, logs, describe, debug (one-shot triage), pf, rollout-status. Write/operational: db (the dbaas psql/mysql exec pattern — PG via pg-cluster-rw -c postgres, MySQL via mysql-standalone-0 with the env-password bash wrapper, never inline), exec, rm-pod (pods/jobs ONLY), restart. Config-mutation verbs (apply/edit/patch/scale/create) are deliberately NOT exposed — they stay raw per the Terraform-only policy. Smoke-verified read verbs against the live cluster (get/logs/rollout-status); write verbs are unit-tested (resolver, db-plan, shell-quoting) but not fired at live state. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
66caa0bf7f
commit
1f7438bb18
4 changed files with 473 additions and 0 deletions
280
cli/cmd_k8s.go
Normal file
280
cli/cmd_k8s.go
Normal file
|
|
@ -0,0 +1,280 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func k8sCommands() []Command {
|
||||||
|
return []Command{
|
||||||
|
{Path: []string{"k8s", "status"}, Tier: TierRead,
|
||||||
|
Summary: "pods (wide) + recent non-Normal events for a namespace (or -A)", Run: k8sStatus},
|
||||||
|
{Path: []string{"k8s", "get"}, Tier: TierRead,
|
||||||
|
Summary: "kubectl get in a namespace: k8s get <ns> <resource> [args]", Run: k8sGet},
|
||||||
|
{Path: []string{"k8s", "logs"}, Tier: TierRead,
|
||||||
|
Summary: "logs for <app> (deploy/<app>; --tail/-c/--previous/--since/-l)", Run: k8sLogs},
|
||||||
|
{Path: []string{"k8s", "describe"}, Tier: TierRead,
|
||||||
|
Summary: "describe <app>'s deployment (or an explicit resource)", Run: k8sDescribe},
|
||||||
|
{Path: []string{"k8s", "debug"}, Tier: TierRead,
|
||||||
|
Summary: "one-shot triage for <app>: pods+deploy+describe+logs+events", Run: k8sDebug},
|
||||||
|
{Path: []string{"k8s", "pf"}, Tier: TierRead,
|
||||||
|
Summary: "port-forward: k8s pf <app> <local:remote> [svc/pod target]", Run: k8sPortForward},
|
||||||
|
{Path: []string{"k8s", "db"}, Tier: TierWrite,
|
||||||
|
Summary: `query a dbaas DB: k8s db <app> [--mysql] [--db N] -- "<SQL>"`, Run: k8sDB},
|
||||||
|
{Path: []string{"k8s", "exec"}, Tier: TierWrite,
|
||||||
|
Summary: "exec in <app>'s pod: k8s exec <app> [--tty] -- <cmd>", Run: k8sExec},
|
||||||
|
{Path: []string{"k8s", "rm-pod"}, Tier: TierWrite,
|
||||||
|
Summary: "delete a stuck pod/job ONLY: k8s rm-pod <name> -n <ns> [--job] [--force]", Run: k8sRmPod},
|
||||||
|
{Path: []string{"k8s", "rollout-status"}, Tier: TierRead,
|
||||||
|
Summary: "rollout status of deploy/<app>", Run: k8sRolloutStatus},
|
||||||
|
{Path: []string{"k8s", "restart"}, Tier: TierWrite,
|
||||||
|
Summary: "rollout restart deploy/<app> then wait for status", Run: k8sRestart},
|
||||||
|
{Path: []string{"k8s", "probe"}, Tier: TierRead,
|
||||||
|
Summary: "in-cluster reachability: ephemeral curl pod to <app>.<ns>.svc", Run: k8sProbe},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func k8sStatus(args []string) error {
|
||||||
|
t := parseK8sTarget(args)
|
||||||
|
ns := t.namespace() // "" when no app/ns given → cluster-wide
|
||||||
|
get := []string{"get", "pods", "-o", "wide"}
|
||||||
|
ev := []string{"get", "events", "--field-selector", "type!=Normal", "--sort-by=.lastTimestamp"}
|
||||||
|
if ns == "" {
|
||||||
|
get = append(get, "-A")
|
||||||
|
ev = append(ev, "-A")
|
||||||
|
}
|
||||||
|
if err := kubectlStream(ns, get...); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Fprintln(os.Stderr, "\n--- recent events (type!=Normal) ---")
|
||||||
|
_ = kubectlStream(ns, ev...) // best-effort
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func k8sGet(args []string) error {
|
||||||
|
t := parseK8sTarget(args)
|
||||||
|
if t.app == "" || len(t.rest) == 0 {
|
||||||
|
return fmt.Errorf("usage: homelab k8s get <ns> <resource> [args]")
|
||||||
|
}
|
||||||
|
return kubectlStream(t.app, append([]string{"get"}, t.rest...)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func k8sLogs(args []string) error {
|
||||||
|
t := parseK8sTarget(args)
|
||||||
|
if t.app == "" {
|
||||||
|
return fmt.Errorf("usage: homelab k8s logs <app> [--tail N] [-c ctr] [--previous] [--since 1h] [-l sel]")
|
||||||
|
}
|
||||||
|
a := []string{"logs"}
|
||||||
|
if t.selector != "" {
|
||||||
|
a = append(a, "-l", t.selector)
|
||||||
|
} else {
|
||||||
|
a = append(a, t.objectRef())
|
||||||
|
}
|
||||||
|
if t.container != "" {
|
||||||
|
a = append(a, "-c", t.container)
|
||||||
|
}
|
||||||
|
if !containsPrefix(t.rest, "--tail") {
|
||||||
|
a = append(a, "--tail=200")
|
||||||
|
}
|
||||||
|
a = append(a, t.rest...)
|
||||||
|
return kubectlStream(t.namespace(), a...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func k8sDescribe(args []string) error {
|
||||||
|
t := parseK8sTarget(args)
|
||||||
|
if t.app == "" {
|
||||||
|
return fmt.Errorf("usage: homelab k8s describe <app> [resource]")
|
||||||
|
}
|
||||||
|
if len(t.rest) > 0 {
|
||||||
|
return kubectlStream(t.namespace(), append([]string{"describe"}, t.rest...)...)
|
||||||
|
}
|
||||||
|
return kubectlStream(t.namespace(), "describe", t.objectRef())
|
||||||
|
}
|
||||||
|
|
||||||
|
func k8sDebug(args []string) error {
|
||||||
|
t := parseK8sTarget(args)
|
||||||
|
if t.app == "" {
|
||||||
|
return fmt.Errorf("usage: homelab k8s debug <app>")
|
||||||
|
}
|
||||||
|
ns := t.namespace()
|
||||||
|
sec := func(title string) { fmt.Fprintf(os.Stderr, "\n=== %s ===\n", title) }
|
||||||
|
sec("pods")
|
||||||
|
_ = kubectlStream(ns, "get", "pods", "-o", "wide")
|
||||||
|
sec("workloads")
|
||||||
|
_ = kubectlStream(ns, "get", "deploy,sts,ds", "-o", "wide")
|
||||||
|
sec("describe "+t.objectRef())
|
||||||
|
_ = kubectlStream(ns, "describe", t.objectRef())
|
||||||
|
sec("recent logs (--tail=50)")
|
||||||
|
_ = kubectlStream(ns, "logs", t.objectRef(), "--tail=50")
|
||||||
|
sec("events (type!=Normal)")
|
||||||
|
_ = kubectlStream(ns, "get", "events", "--field-selector", "type!=Normal", "--sort-by=.lastTimestamp")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func k8sPortForward(args []string) error {
|
||||||
|
t := parseK8sTarget(args)
|
||||||
|
if t.app == "" || len(t.rest) == 0 {
|
||||||
|
return fmt.Errorf("usage: homelab k8s pf <app> <local:remote> [svc/pod target]")
|
||||||
|
}
|
||||||
|
ports := t.rest[0]
|
||||||
|
target := "svc/" + t.app
|
||||||
|
if len(t.rest) > 1 {
|
||||||
|
target = t.rest[1]
|
||||||
|
}
|
||||||
|
return kubectlStream(t.namespace(), "port-forward", target, ports)
|
||||||
|
}
|
||||||
|
|
||||||
|
func k8sDB(args []string) error {
|
||||||
|
var app, dbName, sql string
|
||||||
|
mysql := false
|
||||||
|
for i := 0; i < len(args); i++ {
|
||||||
|
a := args[i]
|
||||||
|
if a == "--" {
|
||||||
|
sql = strings.Join(args[i+1:], " ")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case a == "--mysql":
|
||||||
|
mysql = true
|
||||||
|
case a == "--db":
|
||||||
|
if i+1 < len(args) {
|
||||||
|
dbName = args[i+1]
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
case strings.HasPrefix(a, "--db="):
|
||||||
|
dbName = strings.TrimPrefix(a, "--db=")
|
||||||
|
case !strings.HasPrefix(a, "-") && app == "":
|
||||||
|
app = a
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if app == "" {
|
||||||
|
return fmt.Errorf(`usage: homelab k8s db <app> [--mysql] [--db NAME] -- "<SQL>"`)
|
||||||
|
}
|
||||||
|
p := planDBExec(app, dbName, sql, mysql)
|
||||||
|
exec := []string{"exec"}
|
||||||
|
if sql == "" {
|
||||||
|
exec = append(exec, "-it") // interactive client when no SQL given
|
||||||
|
}
|
||||||
|
exec = append(exec, p.pod)
|
||||||
|
if p.container != "" {
|
||||||
|
exec = append(exec, "-c", p.container)
|
||||||
|
}
|
||||||
|
exec = append(exec, "--")
|
||||||
|
exec = append(exec, p.argv...)
|
||||||
|
return kubectlStream(p.ns, exec...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func k8sExec(args []string) error {
|
||||||
|
t := parseK8sTarget(args)
|
||||||
|
if t.app == "" {
|
||||||
|
return fmt.Errorf("usage: homelab k8s exec <app> [--pod p] [-c ctr] [--tty] -- <cmd>")
|
||||||
|
}
|
||||||
|
if len(t.rest) == 0 {
|
||||||
|
return fmt.Errorf("provide a command after --, e.g. homelab k8s exec %s -- env", t.app)
|
||||||
|
}
|
||||||
|
a := []string{"exec"}
|
||||||
|
if t.tty {
|
||||||
|
a = append(a, "-it")
|
||||||
|
}
|
||||||
|
a = append(a, t.objectRef())
|
||||||
|
if t.container != "" {
|
||||||
|
a = append(a, "-c", t.container)
|
||||||
|
}
|
||||||
|
a = append(a, "--")
|
||||||
|
a = append(a, t.rest...)
|
||||||
|
return kubectlStream(t.namespace(), a...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func k8sRmPod(args []string) error {
|
||||||
|
var pod, ns, grace string
|
||||||
|
force, job := false, false
|
||||||
|
for i := 0; i < len(args); i++ {
|
||||||
|
a := args[i]
|
||||||
|
switch {
|
||||||
|
case a == "-n" || a == "--namespace":
|
||||||
|
if i+1 < len(args) {
|
||||||
|
ns = args[i+1]
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
case a == "--force":
|
||||||
|
force = true
|
||||||
|
case a == "--job":
|
||||||
|
job = true
|
||||||
|
case a == "--grace":
|
||||||
|
if i+1 < len(args) {
|
||||||
|
grace = args[i+1]
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
case !strings.HasPrefix(a, "-") && pod == "":
|
||||||
|
pod = a
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if pod == "" || ns == "" {
|
||||||
|
return fmt.Errorf("usage: homelab k8s rm-pod <name> -n <ns> [--job] [--force] [--grace N] (pods/jobs only)")
|
||||||
|
}
|
||||||
|
kind := "pod"
|
||||||
|
if job {
|
||||||
|
kind = "job"
|
||||||
|
}
|
||||||
|
a := []string{"delete", kind, pod}
|
||||||
|
if grace != "" {
|
||||||
|
a = append(a, "--grace-period="+grace)
|
||||||
|
}
|
||||||
|
if force {
|
||||||
|
a = append(a, "--force")
|
||||||
|
}
|
||||||
|
return kubectlStream(ns, a...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func k8sRolloutStatus(args []string) error {
|
||||||
|
t := parseK8sTarget(args)
|
||||||
|
if t.app == "" {
|
||||||
|
return fmt.Errorf("usage: homelab k8s rollout-status <app>")
|
||||||
|
}
|
||||||
|
return kubectlStream(t.namespace(), "rollout", "status", "deploy/"+t.app)
|
||||||
|
}
|
||||||
|
|
||||||
|
func k8sRestart(args []string) error {
|
||||||
|
t := parseK8sTarget(args)
|
||||||
|
if t.app == "" {
|
||||||
|
return fmt.Errorf("usage: homelab k8s restart <app>")
|
||||||
|
}
|
||||||
|
ns := t.namespace()
|
||||||
|
if err := kubectlStream(ns, "rollout", "restart", "deploy/"+t.app); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return kubectlStream(ns, "rollout", "status", "deploy/"+t.app)
|
||||||
|
}
|
||||||
|
|
||||||
|
func k8sProbe(args []string) error {
|
||||||
|
t := parseK8sTarget(args)
|
||||||
|
if t.app == "" {
|
||||||
|
return fmt.Errorf("usage: homelab k8s probe <app> [path] [--port N]")
|
||||||
|
}
|
||||||
|
ns := t.namespace()
|
||||||
|
url := "http://" + t.app + "." + ns + ".svc.cluster.local"
|
||||||
|
if port := flagValue(args, "--port"); port != "" {
|
||||||
|
url += ":" + port
|
||||||
|
}
|
||||||
|
if len(t.rest) > 0 {
|
||||||
|
p := t.rest[0]
|
||||||
|
if !strings.HasPrefix(p, "/") {
|
||||||
|
p = "/" + p
|
||||||
|
}
|
||||||
|
url += p
|
||||||
|
}
|
||||||
|
return kubectlStream(ns, "run", "homelab-probe", "--rm", "-i", "--restart=Never",
|
||||||
|
"--image=curlimages/curl:latest", "--",
|
||||||
|
"curl", "-sS", "--max-time", "10", "-w", "\n[%{http_code}] %{time_total}s\n", url)
|
||||||
|
}
|
||||||
|
|
||||||
|
// containsPrefix reports whether any arg starts with prefix.
|
||||||
|
func containsPrefix(args []string, prefix string) bool {
|
||||||
|
for _, a := range args {
|
||||||
|
if strings.HasPrefix(a, prefix) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
@ -14,6 +14,7 @@ func buildRegistry() []Command {
|
||||||
reg = append(reg, claimCommands()...)
|
reg = append(reg, claimCommands()...)
|
||||||
reg = append(reg, tfCommands()...)
|
reg = append(reg, tfCommands()...)
|
||||||
reg = append(reg, workCommands()...)
|
reg = append(reg, workCommands()...)
|
||||||
|
reg = append(reg, k8sCommands()...)
|
||||||
return reg
|
return reg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
129
cli/k8s.go
Normal file
129
cli/k8s.go
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// kubectl helpers use the ambient kubeconfig (no per-call auth flags).
|
||||||
|
|
||||||
|
func kubectlBase(ns string, args ...string) []string {
|
||||||
|
var full []string
|
||||||
|
if ns != "" {
|
||||||
|
full = append(full, "-n", ns)
|
||||||
|
}
|
||||||
|
return append(full, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func kubectlStream(ns string, args ...string) error {
|
||||||
|
return runStreamingIn("", "kubectl", kubectlBase(ns, args...)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// k8sTarget is the parsed `<app>` + selectors shared by the k8s verbs.
|
||||||
|
type k8sTarget struct {
|
||||||
|
app string
|
||||||
|
ns string
|
||||||
|
pod string
|
||||||
|
container string
|
||||||
|
selector string
|
||||||
|
tty bool
|
||||||
|
rest []string // passthrough flags and, after `--`, the exec command
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseK8sTarget reads `<app> [-n ns] [--pod p] [-c ctr] [-l sel] [flags] [-- cmd]`.
|
||||||
|
// The first bare token is the app; unknown flags pass through in rest.
|
||||||
|
func parseK8sTarget(args []string) k8sTarget {
|
||||||
|
t := k8sTarget{}
|
||||||
|
i := 0
|
||||||
|
take := func() string {
|
||||||
|
if i+1 < len(args) {
|
||||||
|
i++
|
||||||
|
return args[i]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
for i = 0; i < len(args); i++ {
|
||||||
|
a := args[i]
|
||||||
|
switch {
|
||||||
|
case a == "--":
|
||||||
|
t.rest = append(t.rest, args[i+1:]...)
|
||||||
|
return t
|
||||||
|
case a == "-n" || a == "--namespace":
|
||||||
|
t.ns = take()
|
||||||
|
case strings.HasPrefix(a, "--namespace="):
|
||||||
|
t.ns = strings.TrimPrefix(a, "--namespace=")
|
||||||
|
case a == "--pod":
|
||||||
|
t.pod = take()
|
||||||
|
case strings.HasPrefix(a, "--pod="):
|
||||||
|
t.pod = strings.TrimPrefix(a, "--pod=")
|
||||||
|
case a == "-c" || a == "--container":
|
||||||
|
t.container = take()
|
||||||
|
case strings.HasPrefix(a, "--container="):
|
||||||
|
t.container = strings.TrimPrefix(a, "--container=")
|
||||||
|
case a == "-l" || a == "--selector":
|
||||||
|
t.selector = take()
|
||||||
|
case strings.HasPrefix(a, "--selector="):
|
||||||
|
t.selector = strings.TrimPrefix(a, "--selector=")
|
||||||
|
case a == "--tty" || a == "-it" || a == "-ti":
|
||||||
|
t.tty = true
|
||||||
|
case !strings.HasPrefix(a, "-") && t.app == "":
|
||||||
|
t.app = a
|
||||||
|
default:
|
||||||
|
t.rest = append(t.rest, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
// namespace defaults to the app name (most namespaces hold exactly one app).
|
||||||
|
func (t k8sTarget) namespace() string {
|
||||||
|
if t.ns != "" {
|
||||||
|
return t.ns
|
||||||
|
}
|
||||||
|
return t.app
|
||||||
|
}
|
||||||
|
|
||||||
|
// objectRef is the kubectl object for logs/exec: an explicit pod, else
|
||||||
|
// deploy/<app> (kubectl resolves a pod from the Deployment).
|
||||||
|
func (t k8sTarget) objectRef() string {
|
||||||
|
if t.pod != "" {
|
||||||
|
return "pod/" + t.pod
|
||||||
|
}
|
||||||
|
return "deploy/" + t.app
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- database access (the dbaas exec pattern) ---
|
||||||
|
|
||||||
|
type dbPlan struct {
|
||||||
|
ns string
|
||||||
|
pod string
|
||||||
|
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 <db>.
|
||||||
|
// 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 {
|
||||||
|
if dbName == "" {
|
||||||
|
dbName = app
|
||||||
|
}
|
||||||
|
if mysql {
|
||||||
|
inner := fmt.Sprintf(`mysql -u root -p"$MYSQL_ROOT_PASSWORD" %s`, shellQuote(dbName))
|
||||||
|
if sql != "" {
|
||||||
|
inner += " -e " + shellQuote(sql)
|
||||||
|
}
|
||||||
|
return dbPlan{ns: "dbaas", pod: "mysql-standalone-0", argv: []string{"bash", "-c", inner}}
|
||||||
|
}
|
||||||
|
argv := []string{"psql", "-U", "postgres", "-d", dbName}
|
||||||
|
if sql != "" {
|
||||||
|
argv = append(argv, "-tAc", sql)
|
||||||
|
}
|
||||||
|
return dbPlan{ns: "dbaas", pod: "pg-cluster-rw", container: "postgres", argv: argv}
|
||||||
|
}
|
||||||
|
|
||||||
|
// shellQuote single-quotes s for safe embedding in a bash -c string.
|
||||||
|
func shellQuote(s string) string {
|
||||||
|
return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'"
|
||||||
|
}
|
||||||
63
cli/k8s_test.go
Normal file
63
cli/k8s_test.go
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseK8sTarget(t *testing.T) {
|
||||||
|
got := parseK8sTarget([]string{"tripit", "-n", "prod", "--pod", "x-123", "-c", "app", "-l", "k=v", "--tail=50", "--", "ls", "-la"})
|
||||||
|
want := k8sTarget{app: "tripit", ns: "prod", pod: "x-123", container: "app", selector: "k=v", rest: []string{"--tail=50", "ls", "-la"}}
|
||||||
|
if !reflect.DeepEqual(got, want) {
|
||||||
|
t.Fatalf("parseK8sTarget =\n %+v\nwant\n %+v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestK8sTargetNamespaceDefaultsToApp(t *testing.T) {
|
||||||
|
if ns := parseK8sTarget([]string{"immich"}).namespace(); ns != "immich" {
|
||||||
|
t.Errorf("namespace() = %q, want immich", ns)
|
||||||
|
}
|
||||||
|
if ns := parseK8sTarget([]string{"immich", "-n", "dbaas"}).namespace(); ns != "dbaas" {
|
||||||
|
t.Errorf("namespace() = %q, want dbaas", ns)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestK8sTargetObjectRef(t *testing.T) {
|
||||||
|
if r := parseK8sTarget([]string{"tripit"}).objectRef(); r != "deploy/tripit" {
|
||||||
|
t.Errorf("objectRef() = %q, want deploy/tripit", r)
|
||||||
|
}
|
||||||
|
if r := parseK8sTarget([]string{"tripit", "--pod", "tripit-abc"}).objectRef(); r != "pod/tripit-abc" {
|
||||||
|
t.Errorf("objectRef() = %q, want pod/tripit-abc", r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPlanDBExecPostgresDefault(t *testing.T) {
|
||||||
|
p := planDBExec("fire-planner", "", "SELECT 1", false)
|
||||||
|
if p.ns != "dbaas" || p.pod != "pg-cluster-rw" || p.container != "postgres" {
|
||||||
|
t.Fatalf("unexpected pg target: %+v", p)
|
||||||
|
}
|
||||||
|
// db name defaults to the app; SQL passed via -tAc
|
||||||
|
joined := strings.Join(p.argv, " ")
|
||||||
|
if !strings.Contains(joined, "-d fire-planner") || !strings.Contains(joined, "-tAc") {
|
||||||
|
t.Fatalf("pg argv missing db/sql: %v", p.argv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPlanDBExecMysqlEnvPassword(t *testing.T) {
|
||||||
|
p := planDBExec("wrongmove", "wrongmove", "SHOW TABLES", true)
|
||||||
|
if p.pod != "mysql-standalone-0" {
|
||||||
|
t.Fatalf("unexpected mysql pod: %+v", p)
|
||||||
|
}
|
||||||
|
inner := strings.Join(p.argv, " ")
|
||||||
|
// password must come from the env var, never inline
|
||||||
|
if !strings.Contains(inner, `-p"$MYSQL_ROOT_PASSWORD"`) {
|
||||||
|
t.Fatalf("mysql must use env password wrapper: %v", p.argv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShellQuoteEscapes(t *testing.T) {
|
||||||
|
if got := shellQuote("a'b"); got != `'a'\''b'` {
|
||||||
|
t.Fatalf("shellQuote = %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue