Skip to content

feat(cli): add nself server for Hetzner server lifecycle (G-011) - #410

Merged
acamarata merged 2 commits into
mainfrom
p6/g011-server-lifecycle
Sep 12, 2026
Merged

acamarata merged 2 commits into
mainfrom
p6/g011-server-lifecycle

Conversation

@acamarata

Copy link
Copy Markdown
Collaborator

Summary

Closes gap G-011: nself server (provision / list / resize / destroy), backed by a new internal/server package talking to the Hetzner Cloud API. Previously an operator provisioning or destroying a server had no CLI path — nself access only manages SSH keys on an already-deployed host, and nself security only audits one — and fell back to raw hcloud server create / hcloud server delete, with none of the safety a manual snapshot-and-verify procedure gives.

Design requirements -> implementation

  1. destroy refuses to run without a verified backup. internal/server/destroy.go's ErrNoBackup gate requires --snapshot (takes one via internal/server/snapshot.go's TakeVerifiedSnapshot, polling until the image reaches status=available before returning) or an explicit --force-no-backup. A snapshot that fails or times out aborts destroy before the server is ever deleted.
  2. destroy protects the primary IP by default. internal/server/primaryip.go's ProtectOrReleaseIPs sets auto_delete=false on the server's primary IP(s) before deletion, unless --release-ip is passed (which explicitly sets auto_delete=true instead of merely "leaving it alone"). The command prints which IPs were retained vs. released, read back from what was actually set — never assumed from the flag.
  3. resize explains disk-shrink instead of raw-erroring. internal/server/resize.go compares the current and target server types' disk sizes before calling the provider and returns ErrDiskShrink with the snapshot -> new server -> restore workaround spelled out, when Hetzner itself would only return invalid_input.
  4. Token resolution, never hardcoded. internal/server/token.go's ResolveToken checks --token > --token-env (defaults to the existing HETZNER_NSELF_TOKEN vault var, so any other project's own token var works via --token-env without a code change) > HCLOUD_TOKEN.

Other changes

  • +1 on the CLI-R11 command-surface budget (50 -> 51, .github/command-surface-budget.txt), the same deliberate-exception pattern already used for access (nself access: manage SSH keys/users on already-deployed servers #238).
  • Regenerated .github/command-inventory.json, .github/wiki/Commands.md, .github/surface-parity.{md,json}, and SPORT F02-COMMAND-INVENTORY.md.
  • Added .github/wiki/cmd-server.md (T03 template).
  • cmd/commands/groups.go + error_harness_test.go: registered server in the Advanced & Enterprise help group and the error-state harness.

Test plan

  • internal/server: 35 unit tests against a hand-written fake Client (no network) plus 4 httptest.Server-backed tests proving the real Hetzner HTTP request/response wire format — covers the backup gate, IP protection, disk-shrink detection, snapshot polling (success/error/timeout), and token resolution.
  • cmd/commands: 8 tests against a fake server.Client injected via the newServerClient indirection (mirrors newAccessTransport) — no network, no real infrastructure ever touched.
  • go build ./..., go vet ./..., gofmt -l . clean.
  • golangci-lint run clean (fixed 3 staticcheck S1016 findings).
  • Full go test ./... green, including internal/repoqa's command-inventory/parity/surface-budget/wiki-parity gates.

Provisioning, resizing, and destroying a server had no CLI surface: nself
access only manages SSH keys on an already-deployed host, and nself
security only audits one. An operator building or tearing down a CI box
tonight had to fall back to raw hcloud server create / hcloud server
delete, with none of the safety a manual snapshot-and-verify procedure
provides.

Add nself server provision/list/resize/destroy, backed by a new
internal/server package (Client interface over the Hetzner Cloud API,
mockable in tests):

- destroy refuses to run without --snapshot (taken and verified
  status=available before anything is deleted) or --force-no-backup.
- destroy sets auto_delete=false on the server's primary IP(s) before
  deleting it, unless --release-ip is passed, and reports which IPs were
  retained/released. Hetzner primary IPs default to auto_delete=true, so
  deleting a server otherwise permanently destroys its IP too.
- resize detects a disk-shrinking type change before calling the
  provider and explains the snapshot -> new server -> restore path,
  instead of surfacing Hetzner's raw invalid_input error.
- the API token is resolved from --token, --token-env (defaults to the
  existing HETZNER_NSELF_TOKEN vault var), or HCLOUD_TOKEN — never
  hardcoded.

+1 on the CLI-R11 command-surface budget (50 -> 51), same deliberate
exception pattern used for `access` (#238): a new top-level command
closing a real capability gap named in G-011, not surface creep.

Regenerated the command inventory, wiki command index, and the
surface-parity matrix; added .github/wiki/cmd-server.md.
@acamarata
acamarata merged commit 2a68bcb into main Sep 12, 2026
31 checks passed
@acamarata
acamarata deleted the p6/g011-server-lifecycle branch September 12, 2026 14:09
@acamarata acamarata mentioned this pull request Sep 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant