hub v0.37.0: offsite provisioning SLICE 1 — Cloud-API client + provisioning core
Hetzner storage-box provisioning against api.hetzner.com/v1 (NOT .cloud).
internal/hetznerapi (typed client + CloudAPI interface + Fake + WaitAction);
internal/offsite (Provisioner.ProvisionOffsite — idempotent by label, shared
sub-account/dedicated box, transient password, non-secret Descriptor,
fail-closed); one_time_secrets store (single-use Save/Consume); POST
/offsite/consume-password/{id} (customer-key auth, once); config-form Offsite
section → applyOffsite (502+no-save on error) → descriptor in ConfigJSON →
version bump. Token/passwords never logged/committed/in ConfigJSON. Tested vs a
faked Cloud API + fail-closed red-proof. NOT yet live-provisioned (needs the
dedicated-project scoped token; current token can delete ep0).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
@@ -1,5 +1,40 @@
|
||||
# Felhom Hub — Changelog
|
||||
|
||||
## v0.37.0 — offsite provisioning SLICE 1: Hetzner Cloud-API client + provisioning core (2026-07-09)
|
||||
|
||||
Slice 1 of the offsite-provisioning epic. On operator enable, the hub provisions a Hetzner storage-box
|
||||
sub-account (shared) or dedicated box, generates the transient password, stores it one-time-consumable, and
|
||||
serves the non-secret target descriptor to the controller via `ConfigJSON`. Coded against the API shapes
|
||||
measured live in `documentation/audits/SPIKE-hetzner-api-provisioning-2026-07-09.md` (996d403). The
|
||||
controller apply-bridge (SLICE 2), escrow auto-confirm (SLICE 3), and soft-quota enforcement (SLICE 4) are
|
||||
separate slices.
|
||||
|
||||
- **`internal/hetznerapi`:** a typed client for the storage-box surface at **`https://api.hetzner.com/v1`**
|
||||
(NOT `api.hetzner.cloud` — the classic Cloud API 404s for storage boxes). Sub-account + box
|
||||
create/reset/access/change_type/delete/list-by-label + `WaitAction` (poll to `success`, bounded). A
|
||||
`CloudAPI` interface + an exported `Fake` so provisioning is unit-tested with **no live Hetzner calls**.
|
||||
Bearer token from an injected func (out-of-band secret; never logged).
|
||||
- **`internal/offsite`:** `Provisioner.ProvisionOffsite` — **idempotent by `label_selector`
|
||||
(felhom-customer=<id>)** (names aren't unique); shared → sub-account on the pool box, dedicated → box;
|
||||
generates a 4-class transient password → `WaitAction` → `Store.SaveOneTimeSecret` → builds the NON-SECRET
|
||||
`Descriptor{enabled,type,host,user,port:23,repo_path:/home/felhom-repo, quota_gb|box_type}`. **Fail-closed:**
|
||||
any API/action error returns without a provisioned resource, one-time password, or descriptor.
|
||||
`MergeDescriptor` merges it under the `offsite` key of `ConfigJSON` (never a secret).
|
||||
- **`internal/store`:** `one_time_secrets` table + `SaveOneTimeSecret` / `ConsumeOneTimeSecret` (single-use,
|
||||
return-and-mark in one tx). The transient password NEVER rides `ConfigJSON`.
|
||||
- **`internal/api`:** `POST /offsite/consume-password/{id}` — serves the one-time password to the
|
||||
authenticated customer (same API-key auth as config-pull) EXACTLY once, then 404s. Never logged.
|
||||
- **`internal/web`:** the config form gains an **Offsite backup** section (enable / type / soft-quota / box
|
||||
type); save → `applyOffsite` provisions (fail-closed: a provisioning error returns 502 and does NOT save)
|
||||
and merges the descriptor → `ConfigVersion` bump → controller re-pulls. Optional dep
|
||||
(`SetOffsiteProvisioner`), wired in `cmd/hub/main.go` from `HETZNER_TOKEN`/`HETZNER_POOL_BOX_ID`/`HETZNER_LOCATION`.
|
||||
- Tests (faked Cloud API, no live calls): shared/dedicated provision + descriptor + one-time-password-stored
|
||||
+ **password-absent-from-ConfigJSON**; idempotent re-save (no 2nd resource); fail-closed **+ companion
|
||||
red-proof** (swallow the create error → offsite marked enabled despite failure → test fails); one-time
|
||||
consume-once; `WaitAction` success/error/timeout; the consume endpoint (auth + single-use).
|
||||
- **NOT yet live-provisioned** — awaiting the **dedicated-project scoped token** (the current token can delete
|
||||
ep0 — SPIKE §6); a live create is a supervised validation. Unit tests are this slice's proof.
|
||||
|
||||
## v0.36.0 — customer page: passphrase hardening + interactive install-command generator (TASK GL-7) (2026-07-09)
|
||||
|
||||
Two coupled, security-first changes to the operator-facing customer page (`customer_unified.html` +
|
||||
|
||||
Reference in New Issue
Block a user