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
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