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:
2026-07-09 18:38:24 +02:00
parent 996d403248
commit 44ec06b50f
16 changed files with 1279 additions and 1 deletions
+20
View File
@@ -9,13 +9,16 @@ import (
"os"
"os/signal"
"path/filepath"
"strconv"
"syscall"
"time"
"gitea.dooplex.hu/admin/felhom-hub/internal/api"
"gitea.dooplex.hu/admin/felhom-hub/internal/assets"
"gitea.dooplex.hu/admin/felhom-hub/internal/gitea"
"gitea.dooplex.hu/admin/felhom-hub/internal/hetznerapi"
"gitea.dooplex.hu/admin/felhom-hub/internal/mailrelay"
"gitea.dooplex.hu/admin/felhom-hub/internal/offsite"
"gitea.dooplex.hu/admin/felhom-hub/internal/monitor"
"gitea.dooplex.hu/admin/felhom-hub/internal/notify"
"gitea.dooplex.hu/admin/felhom-hub/internal/store"
@@ -264,6 +267,23 @@ func main() {
logger.Printf("[INFO] Gitea artifact browser enabled (Day-0 version dropdowns)")
}
// Offsite provisioning (SLICE 1): enabled when a Hetzner storage-box token is provided out-of-band.
// PREREQUISITE: this MUST be a token scoped to a DEDICATED Hetzner project (the shared project token can
// delete ep0 — SPIKE §6). Base is api.hetzner.com (NOT api.hetzner.cloud). Absent token → offsite UI
// still renders, but saving with offsite enabled returns "not configured".
if tok := os.Getenv("HETZNER_TOKEN"); tok != "" {
poolBoxID, _ := strconv.ParseInt(os.Getenv("HETZNER_POOL_BOX_ID"), 10, 64)
location := os.Getenv("HETZNER_LOCATION")
if location == "" {
location = "fsn1"
}
client := hetznerapi.NewClient(func() string { return os.Getenv("HETZNER_TOKEN") })
webServer.SetOffsiteProvisioner(&offsite.Provisioner{
API: client, Store: dataStore, PoolBoxID: poolBoxID, Location: location, Logger: logger,
})
logger.Printf("[INFO] Offsite provisioning enabled (pool_box=%d, location=%s)", poolBoxID, location)
}
// Build HTTP mux
mux := http.NewServeMux()