Group F deliverables — completes the day-2 IaC layer that takes over after OpenTofu's Phase 0 hand-off (per docs/SOVEREIGN-PROVISIONING.md §4).
Three artifacts:
1. platform/crossplane/compositions/ — XRDs + Compositions for canonical Hetzner resources
under the canonical compose.openova.io/v1alpha1 group (per BLUEPRINT-AUTHORING.md §8):
- XHetznerNetwork + composition-network.yaml — wraps hcloud_network + subnet
- XHetznerFirewall + composition-firewall.yaml
- XHetznerServer + composition-server.yaml
- XHetznerLoadBalancer + composition-loadbalancer.yaml (lb11, 80→31080, 443→31443)
- README documenting the canonical pattern
2. clusters/_template/ — the canonical per-Sovereign Flux Kustomization tree.
Copied to clusters/<sovereign-fqdn>/ at provisioning time; cloud-init's
GitRepository points at the result.
- kustomization.yaml (root: flux-system + infrastructure + bootstrap-kit)
- flux-system/ (placeholder for Flux self-config customization)
- infrastructure/ (provider-hcloud + ProviderConfig referencing hcloud-credentials secret OpenTofu writes)
- bootstrap-kit/ — 11 HelmRelease manifests in dependency order:
01-cilium → 02-cert-manager → 03-flux → 04-crossplane → 05-sealed-secrets
→ 06-spire → 07-nats-jetstream → 08-openbao → 09-keycloak → 10-gitea → 11-bp-catalyst-platform
Each pulls from oci://ghcr.io/openova-io/bp-<name>:1.0.0 — the wrapper charts published by blueprint-release CI.
dependsOn declarations enforce the canonical install order at runtime.
3. clusters/omantel.omani.works/ — the first concrete Sovereign instance.
Mirror of _template with SOVEREIGN_FQDN_PLACEHOLDER substituted to omantel.omani.works.
This is what the wizard's first omantel.omani.works run will actually reconcile.
4. products/catalyst/bootstrap/api/cmd/catalyst-dns/main.go — small Go binary the
OpenTofu module's null_resource.dns_pool invokes via local-exec at Phase-0 apply time.
Reads DYNADOT_API_KEY/SECRET/DOMAIN/SUBDOMAIN/LB_IP env vars; calls existing dynadot.Client.AddSovereignRecords. Containerfile already builds + ships it at /usr/local/bin/catalyst-dns.
Architectural compliance (Lesson #24 closed):
- No bespoke Go cloud-API calls (Crossplane Compositions are the canonical day-2 IaC)
- No exec.Command("helm", ...) (Flux HelmReleases are the canonical install unit)
- No kubectl apply from outside (cloud-init kubectl-applies one Flux GitRepository, then Flux owns everything)
After this commit, the path is end-to-end: wizard → catalyst-api → tofu apply (with infra/hetzner/) → cloud-init installs k3s + Flux + applies GitRepository pointing at clusters/omantel.omani.works/ → Flux reconciles bootstrap-kit (11 HelmReleases in dependency order) → Crossplane adopts day-2 management.
81 lines
3.2 KiB
YAML
81 lines
3.2 KiB
YAML
# Composition: hetzner-network.compose.openova.io — default realization for
|
|
# XHetznerNetwork. Renders to provider-hcloud's Network + NetworkSubnet
|
|
# managed resources. Applied by Crossplane every time an XHetznerNetwork or
|
|
# HetznerNetwork claim is created.
|
|
#
|
|
# Per docs/INVIOLABLE-PRINCIPLES.md principle #3 this Composition is the
|
|
# ONLY way day-2 networks get created on a Sovereign. The Phase-0 OpenTofu
|
|
# module creates the bootstrap network exactly once; everything after that
|
|
# is XHetznerNetwork → Crossplane → Hetzner.
|
|
|
|
apiVersion: apiextensions.crossplane.io/v1
|
|
kind: Composition
|
|
metadata:
|
|
name: hetzner-network.compose.openova.io
|
|
labels:
|
|
catalyst.openova.io/component: crossplane
|
|
catalyst.openova.io/composition-family: hetzner
|
|
spec:
|
|
compositeTypeRef:
|
|
apiVersion: compose.openova.io/v1alpha1
|
|
kind: XHetznerNetwork
|
|
|
|
writeConnectionSecretsToNamespace: crossplane-system
|
|
|
|
resources:
|
|
# ── 1. Top-level private network (VPC) ────────────────────────────────
|
|
- name: network
|
|
base:
|
|
apiVersion: network.hcloud.crossplane.io/v1alpha1
|
|
kind: Network
|
|
spec:
|
|
forProvider:
|
|
ipRange: "" # filled by patch from spec.parameters.ipRange
|
|
providerConfigRef:
|
|
name: default-hcloud
|
|
patches:
|
|
- fromFieldPath: spec.parameters.name
|
|
toFieldPath: metadata.name
|
|
- fromFieldPath: spec.parameters.ipRange
|
|
toFieldPath: spec.forProvider.ipRange
|
|
- fromFieldPath: spec.providerConfigRef.name
|
|
toFieldPath: spec.providerConfigRef.name
|
|
- fromFieldPath: spec.parameters.sovereignFQDN
|
|
toFieldPath: metadata.labels[catalyst.openova.io/sovereign]
|
|
- type: ToCompositeFieldPath
|
|
fromFieldPath: metadata.annotations[crossplane.io/external-name]
|
|
toFieldPath: status.networkId
|
|
|
|
# ── 2. Subnet inside the network ──────────────────────────────────────
|
|
- name: subnet
|
|
base:
|
|
apiVersion: network.hcloud.crossplane.io/v1alpha1
|
|
kind: NetworkSubnet
|
|
spec:
|
|
forProvider:
|
|
type: cloud
|
|
networkZone: "" # filled by patch
|
|
ipRange: "" # filled by patch
|
|
networkIdSelector:
|
|
matchControllerRef: true
|
|
providerConfigRef:
|
|
name: default-hcloud
|
|
patches:
|
|
- fromFieldPath: spec.parameters.name
|
|
toFieldPath: metadata.name
|
|
transforms:
|
|
- type: string
|
|
string:
|
|
fmt: "%s-subnet"
|
|
- fromFieldPath: spec.parameters.networkZone
|
|
toFieldPath: spec.forProvider.networkZone
|
|
- fromFieldPath: spec.parameters.subnetIpRange
|
|
toFieldPath: spec.forProvider.ipRange
|
|
- fromFieldPath: spec.providerConfigRef.name
|
|
toFieldPath: spec.providerConfigRef.name
|
|
- fromFieldPath: spec.parameters.sovereignFQDN
|
|
toFieldPath: metadata.labels[catalyst.openova.io/sovereign]
|
|
- type: ToCompositeFieldPath
|
|
fromFieldPath: spec.forProvider.ipRange
|
|
toFieldPath: status.subnetIpRange
|