openova/infra/hetzner/versions.tf
e3mrah 1e17668055
feat(catalyst): Hetzner Object Storage credential pattern — Phase 0b (#371) (#409)
* 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>
2026-05-01 16:54:22 +04:00

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
}