* feat(catalyst): Hetzner Object Storage credential pattern (Phase 0b, #371) Adds the per-Sovereign Hetzner Object Storage credential capture + bucket provisioning Phase 0b path described in the omantel handover WBS §5. Hybrid Option A+B: wizard collects operator-issued S3 credentials (Hetzner exposes no Cloud API to mint them — they're issued once in the Hetzner Console and the secret half is shown exactly once), and OpenTofu auto-provisions the per-Sovereign bucket via the aminueza/minio provider + writes a flux-system/hetzner-object-storage Secret into the new Sovereign at cloud-init time so Harbor (#383) and Velero (#384) find their backing-store credentials already in the cluster from Phase 1 onwards. Extends the EXISTING canonical seam at every layer (per the founder's anti-duplication rule for #371's session): the existing Tofu module at infra/hetzner/, the existing handler/credentials.go validator, the existing provisioner.Request struct, the existing store.Redact path, and the existing wizard StepCredentials. No parallel binaries / scripts / operators introduced. infra/hetzner/ (Tofu module — Phase 0): - versions.tf: declare aminueza/minio provider (Hetzner's official recommendation for S3-compatible bucket creation per docs.hetzner.com/storage/object-storage/getting-started/...) - variables.tf: 4 sensitive vars — region (validated against fsn1/nbg1/hel1, the European-only OS regions as of 2026-04), access_key, secret_key, bucket_name (RFC-compliant S3 naming) - main.tf: minio_s3_bucket.main resource — idempotent on re-apply, no force_destroy (Velero archive must survive a control-plane reinstall), object_locking=false (content-addressed digests are the immutability guarantee for Harbor; Velero uses S3 versioning) - cloudinit-control-plane.tftpl: write flux-system/hetzner-object-storage Secret with the canonical s3-endpoint/s3-region/s3-bucket/s3-access-key/s3-secret-key keys Harbor + Velero charts consume via existingSecret refs - outputs.tf: surface endpoint/region/bucket back to catalyst-api for the deployment record (credentials NEVER returned) products/catalyst/bootstrap/api/ (Go): - internal/hetzner/objectstorage.go: NEW — minio-go/v7-based ListBuckets validator. Distinguishes auth failure ("rejected") from network failure ("unreachable") so the wizard renders the right error card. NOT a parallel cloud-resource path — the existing purge.go handles hcloud purge; objectstorage.go handles a separate API surface (S3-compatible) that has no equivalent client today. - internal/handler/credentials.go: extend with ValidateObjectStorageCredentials handler — same wire shape (200 valid:true / 200 valid:false / 503 unreachable / 400 bad input) as the existing token validator so the wizard's failure- card machinery handles both without per-endpoint switches. - cmd/api/main.go: wire POST /api/v1/credentials/object-storage/validate - internal/provisioner/provisioner.go: extend Request with ObjectStorageRegion/AccessKey/SecretKey/Bucket; Validate() rejects empty/malformed values fail-fast at /api/v1/deployments POST time; writeTfvars() emits the 4 new tfvars. - internal/handler/deployments.go: derive bucket name from FQDN slug pre-Validate (catalyst-<fqdn-with-dots-replaced-by-dashes>) so Hetzner's globally-namespaced bucket pool gets a deterministic, collision-resistant per-Sovereign name without operator input. - internal/store/store.go: redact access/secret keys; preserve region+bucket plain (they're public in tofu outputs anyway). products/catalyst/bootstrap/ui/ (TypeScript / React): - entities/deployment/model.ts + store.ts: 4 new wizard fields (objectStorageRegion/AccessKey/SecretKey/Validated) with merge() coercion for legacy persisted state. - pages/wizard/steps/StepCredentials.tsx: ObjectStorageSection — region picker (fsn1/nbg1/hel1), masked secret-key input, Validate button gating Next. Same FailureCard taxonomy (rejected/too-short/unreachable/network/parse/http) the existing TokenSection uses, so the operator UX is consistent. Section only renders when Hetzner is among chosen providers — non-Hetzner Sovereigns skip Phase 0b until their own backing-store path lands. - pages/wizard/steps/StepReview.tsx: include objectStorageRegion/AccessKey/SecretKey in the POST /v1/deployments payload (bucket derived server-side). Tests: - api: 7 new provisioner Validate tests (region/keys/bucket required + RFC-compliant + valid-region acceptance), 5 handler tests for the new endpoint (bad JSON / missing region / invalid region / short keys), 4 hetzner/objectstorage_test.go tests (endpoint composition + early input rejection), 1 handler test for the bucket-name derivation. Existing tests updated to supply the new required fields. - ui: StepCredentials.test.tsx pre-populates objectStorageValidated in beforeEach so the existing 11 SSH-section tests aren't gated on Object Storage validation. DoD: a fresh Sovereign provision results in a usable S3 endpoint URL + access/secret keys available as a K8s Secret in the Sovereign's home cluster (flux-system/hetzner-object-storage), ready for consumption by Harbor + Velero charts via existingSecret references. Closes #371. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(wbs): #371 done — Hetzner Object Storage Phase 0b shipped (#409) Marks #371 done with the architectural rationale (hybrid Option A + B — Hetzner exposes no Cloud API to mint S3 keys, so the wizard MUST capture them; OpenTofu auto-provisions the bucket + cloud-init writes the flux-system/hetzner-object-storage Secret with the canonical s3-* keys Harbor + Velero consume). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: hatiyildiz <269457768+hatiyildiz@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
70 lines
3.2 KiB
HCL
70 lines
3.2 KiB
HCL
terraform {
|
|
required_version = ">= 1.6.0"
|
|
|
|
required_providers {
|
|
hcloud = {
|
|
source = "hetznercloud/hcloud"
|
|
version = "~> 1.49"
|
|
}
|
|
|
|
# Hetzner's own Cloud API does NOT expose Object Storage as a managed
|
|
# resource (the upstream `hetznercloud/terraform-provider-hcloud` v1.49
|
|
# supports certificate, datacenter, firewall, floatingip, image,
|
|
# loadbalancer, network, primaryip, server, sshkey, storagebox, volume,
|
|
# zone — but no object_storage / bucket / s3 resource). Hetzner ALSO
|
|
# exposes no API to mint S3 access keys programmatically — credentials
|
|
# are operator-issued exactly once via the Hetzner Console.
|
|
#
|
|
# Buckets, however, ARE creatable via standard S3 once credentials
|
|
# exist. Hetzner officially recommends the `aminueza/minio` Terraform
|
|
# provider for this path (https://docs.hetzner.com/storage/object-storage/
|
|
# getting-started/creating-a-bucket-minio-terraform/). The provider
|
|
# speaks both the MinIO admin API (irrelevant to us) AND the underlying
|
|
# S3 bucket API (which is what Hetzner's Object Storage exposes).
|
|
#
|
|
# The bucket is a per-Sovereign Phase-0 resource — Harbor and Velero
|
|
# consume the `s3-*` keys of the K8s Secret cloud-init writes into the
|
|
# cluster. Without this provider declared here, OpenTofu's plan stage
|
|
# cannot create the bucket and the operator would be forced to bring
|
|
# up the Sovereign and then create the bucket out-of-band — violating
|
|
# the principle that Phase 0 is end-to-end declarative.
|
|
minio = {
|
|
source = "aminueza/minio"
|
|
version = "~> 3.5"
|
|
}
|
|
}
|
|
}
|
|
|
|
# Provider configured from the hcloud_token variable. Per Catalyst the token
|
|
# comes from the wizard's StepCredentials, never from environment variables
|
|
# in the catalyst-api process — every Sovereign provisioning runs with the
|
|
# requesting customer's own token, never with a shared OpenOva token.
|
|
provider "hcloud" {
|
|
token = var.hcloud_token
|
|
}
|
|
|
|
# Hetzner Object Storage — S3-compatible bucket layer. The endpoint format is
|
|
# `<region>.your-objectstorage.com` (region ∈ fsn1 / nbg1 / hel1 — Object
|
|
# Storage availability is European-only as of 2026-04, see
|
|
# https://docs.hetzner.com/storage/object-storage/overview/). For Sovereigns
|
|
# whose compute region is ash/hil (USA), the operator selects a European
|
|
# Object Storage region in the wizard; Velero/Harbor backup latency is
|
|
# acceptable for the use case (asynchronous tarball push).
|
|
#
|
|
# `minio_ssl = true` is mandatory — Hetzner Object Storage rejects plaintext
|
|
# HTTP. The minio provider uses the AWS S3 SDK under the hood and works
|
|
# against any S3-compatible service whose region+endpoint pair is set.
|
|
#
|
|
# Credentials come from the wizard's StepCredentials object-storage section,
|
|
# operator-issued in the Hetzner Console (Console → Object Storage →
|
|
# Manage Credentials). Like hcloud_token, they never live in environment
|
|
# variables on the catalyst-api process — every Sovereign apply uses its
|
|
# own tenant's credentials.
|
|
provider "minio" {
|
|
minio_server = "${var.object_storage_region}.your-objectstorage.com"
|
|
minio_user = var.object_storage_access_key
|
|
minio_password = var.object_storage_secret_key
|
|
minio_region = var.object_storage_region
|
|
minio_ssl = true
|
|
}
|