openova/platform/crossplane/compositions/composition-network.yaml
hatiyildiz 046e5ebc18 feat(day2-iac): Crossplane Compositions + per-Sovereign Flux cluster tree + catalyst-dns binary
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.
2026-04-28 14:09:29 +02:00

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