6.3 KiB
kmsvc-cli — CLI wrapper implementation plan
Context
Fourth sibling repo, alongside kafaka_management_service (server), kmsvc-proto (wire contract), and kmsvc-sdk (Go client SDK). Per design.md §11a, this is a thin CLI over kmsvc-sdk. Used by operators to send/receive/delete messages and inspect/redrive DLQ contents from the terminal.
Queue lifecycle (create/delete/configure) is managed via the Queue CRD on the cluster (kubectl apply/kubectl get queues), not exposed over gRPC by kmsvc-proto — so this CLI has no queue subcommand, only message and dlq.
Dependency: kmsvc-cli imports kmsvc-sdk as a normal Go module dependency — go get forgejo.riotpiao.homelab.com/rock/[email protected] (module path matches the self-hosted Forgejo host, same pattern as kmsvc-proto). While kmsvc-sdk isn't pushed/tagged yet, this repo uses a local replace directive in go.mod pointing at the sibling ../kmsvc-sdk checkout — remove it once a real tag exists.
CLI framework: the user's existing talos-cli (homelab cluster control) is a Rust/clap binary, not Go — there's no existing Go CLI convention in this homelab to literally reuse. We use cobra (the de facto standard for Go CLIs, same family kubectl/helm use) and mirror talos-cli's behavioral conventions instead: simple subcommand tree, env-var-driven config with sane defaults, no mandatory config file.
Repo layout
kmsvc-cli/
go.mod # module forgejo.riotpiao.homelab.com/rock/kmsvc-cli
cmd/kmsvc/main.go # entrypoint
internal/cli/root.go # root command, global flags (--server, --token, --output)
internal/cli/config.go # ~/.kmsvc/config.yaml (optional) + env var overrides, flag overrides env
internal/cli/client.go # MessageClient interface + buildClient (lazy kmsvc-sdk.Client construction)
internal/cli/messages.go # kmsvc message send|receive|delete|change-visibility
internal/cli/dlq.go # kmsvc dlq peek|redrive
internal/cli/output.go # table/json rendering, shared across subcommands
internal/cli/version.go # kmsvc version (ldflags-injected at release build time)
internal/cli/*_test.go
.forgejo/workflows/ci.yaml
.forgejo/workflows/release.yaml
README.md
Implementation steps
Step 1 — Command skeleton + config loading ✅ done
cobraroot commandkmsvcwith persistent flags--server/--token/--output(table|json), each falling back toKMSVC_SERVER/KMSVC_TOKEN/KMSVC_OUTPUT, falling back to an optional~/.kmsvc/config.yaml(server:,output:), falling back to defaults (output: table). Precedence: flag > env > file > default.- Verify:
kmsvc --helplistsmessage/dlq/version; a table-driven test asserts file+env+flag precedence resolves correctly.
Step 2 — Auth wiring + client construction ✅ done (revised)
- Commands use the concrete
*kmsvc.Clientdirectly — noMessageClientinterface orclientFactoryabstraction (course-corrected: re-wrapping the SDK's own methods behind an interface purely for test substitution was unnecessary duplication). buildClientconstructs a real*kmsvc.Clientfrom resolved config (kmsvc.WithTokenSource(kmsvc.StaticToken(token))when--token/KMSVC_TOKENis set), errors if--server/KMSVC_SERVERis unset.- Tests spin up a real loopback-TCP
grpc.Serverbacked by a hand-rolled fake ofkafkamgmtv1.QueueServiceServer(testserver_test.go), exercised through the unmodified SDKNew/Client— fully offline, no interface needed on the CLI side.
Step 3 — Message commands ✅ done
kmsvc message send --queue --body [--group-id] [--dedup-id] [--delay],receive --queue [--wait] [--max-messages] [--visibility-timeout],delete --queue --receipt-handle,change-visibility --queue --receipt-handle --timeout.- Verify: per-command tests against a hand-rolled fake
MessageClient;--output jsonproduces machine-parseable output forreceive.
Step 4 — DLQ inspection commands ✅ done
kmsvc dlq peek --queue <dlq-name> [--max-messages] [--visibility-timeout]: receives without deleting (a short visibility timeout lets it become re-visible) — non-destructive inspect.kmsvc dlq redrive --queue <dlq-name> --to <source-queue> [--max-messages]: for each received message —SendMessageto--to, thenDeleteMessagefrom the DLQ. Not atomic (3+ SDK calls): if send succeeds but delete fails, the command reports that message as "sent but not removed from DLQ — may be redelivered" rather than silently continuing, and the command exits non-zero if any entry had a partial failure.- Verify: fake-backed test asserting call order (send before delete) and that a forced delete failure surfaces the duplicate-risk warning and a non-zero exit.
Step 5 — Output formatting ✅ done
internal/cli/output.go: sharedtable/jsonrenderer for messages and batch results.- Verify: tests for both formats against a fixed sample response.
Step 6 — Packaging + CI ✅ done (workflows written, release untested — no tag pushed yet)
.forgejo/workflows/ci.yaml:go build ./...,go vet ./...,go test ./...on every push/PR..forgejo/workflows/release.yaml: on a tag push, cross-compile (darwin/linux × amd64/arm64) with-ldflags -X .../internal/cli.version=<tag>, attach to a Forgejo release.- Verify: a real tag push produces downloadable binaries;
kmsvc versionin the built binary reports the tag. — deferred until first tag.
Acceptance criteria (overall)
- All
message/dlqsubcommands work against a fake gRPC server in CI — no real network dependency. - Config file + env var + flag precedence is flag > env > file > default, tested explicitly.
--output jsonis machine-parseable for every read command (receive,dlq peek).dlq redrivepartial-failure (send ok, delete fails) is surfaced, not silently swallowed, and causes non-zero exit.- A tagged release produces real downloadable binaries via Forgejo Actions.
- Manual end-to-end smoke test (send → receive → delete → dlq redrive) passes against a real running server, once one exists.
Sequencing note
Once kmsvc-sdk is pushed and tagged on Forgejo, swap the go.mod replace directive for a real go get forgejo.riotpiao.homelab.com/rock/kmsvc-sdk@<tag>.