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.
This commit is contained in:
hatiyildiz 2026-04-28 14:09:29 +02:00
parent 9519c1ef00
commit 046e5ebc18
45 changed files with 2220 additions and 0 deletions

View File

@ -0,0 +1,47 @@
# bp-cilium — Catalyst bootstrap-kit Blueprint. CNI must come first; k3s started with --flannel-backend=none precisely so Cilium can take over.
#
# Wrapper chart: platform/cilium/chart/
# Catalyst-curated values: platform/cilium/chart/values.yaml
# Reconciled by: Flux on the new Sovereign's k3s control plane.
---
apiVersion: v1
kind: Namespace
metadata:
name: kube-system
labels:
catalyst.openova.io/sovereign: SOVEREIGN_FQDN_PLACEHOLDER
---
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: HelmRepository
metadata:
name: bp-cilium
namespace: flux-system
spec:
type: oci
interval: 15m
url: oci://ghcr.io/openova-io/bp-cilium
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: bp-cilium
namespace: flux-system
spec:
interval: 15m
releaseName: cilium
targetNamespace: kube-system
chart:
spec:
chart: bp-cilium
version: 1.0.0
sourceRef:
kind: HelmRepository
name: bp-cilium
namespace: flux-system
install:
remediation:
retries: 3
upgrade:
remediation:
retries: 3

View File

@ -0,0 +1,49 @@
# bp-cert-manager — Catalyst bootstrap-kit Blueprint. TLS for everything below — Lets Encrypt issuer with Dynadot DNS-01 (omani.works pool) or HTTP-01 (BYO domains).
#
# Wrapper chart: platform/cert-manager/chart/
# Catalyst-curated values: platform/cert-manager/chart/values.yaml
# Reconciled by: Flux on the new Sovereign's k3s control plane.
---
apiVersion: v1
kind: Namespace
metadata:
name: cert-manager
labels:
catalyst.openova.io/sovereign: SOVEREIGN_FQDN_PLACEHOLDER
---
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: HelmRepository
metadata:
name: bp-cert-manager
namespace: flux-system
spec:
type: oci
interval: 15m
url: oci://ghcr.io/openova-io/bp-cert-manager
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: bp-cert-manager
namespace: flux-system
spec:
interval: 15m
releaseName: cert-manager
targetNamespace: cert-manager
dependsOn:
- name: bp-cilium
chart:
spec:
chart: bp-cert-manager
version: 1.0.0
sourceRef:
kind: HelmRepository
name: bp-cert-manager
namespace: flux-system
install:
remediation:
retries: 3
upgrade:
remediation:
retries: 3

View File

@ -0,0 +1,49 @@
# bp-flux — Catalyst bootstrap-kit Blueprint. Host-level Flux. Per-vcluster Flux is bootstrapped later by environment-controller.
#
# Wrapper chart: platform/flux/chart/
# Catalyst-curated values: platform/flux/chart/values.yaml
# Reconciled by: Flux on the new Sovereign's k3s control plane.
---
apiVersion: v1
kind: Namespace
metadata:
name: flux-system
labels:
catalyst.openova.io/sovereign: SOVEREIGN_FQDN_PLACEHOLDER
---
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: HelmRepository
metadata:
name: bp-flux
namespace: flux-system
spec:
type: oci
interval: 15m
url: oci://ghcr.io/openova-io/bp-flux
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: bp-flux
namespace: flux-system
spec:
interval: 15m
releaseName: flux
targetNamespace: flux-system
dependsOn:
- name: bp-cert-manager
chart:
spec:
chart: bp-flux
version: 1.0.0
sourceRef:
kind: HelmRepository
name: bp-flux
namespace: flux-system
install:
remediation:
retries: 3
upgrade:
remediation:
retries: 3

View File

@ -0,0 +1,49 @@
# bp-crossplane — Catalyst bootstrap-kit Blueprint. Day-2 cloud resource control plane. Adopts management of resources OpenTofu created in Phase 0 (Phase 1 hand-off per SOVEREIGN-PROVISIONING.md §4).
#
# Wrapper chart: platform/crossplane/chart/
# Catalyst-curated values: platform/crossplane/chart/values.yaml
# Reconciled by: Flux on the new Sovereign's k3s control plane.
---
apiVersion: v1
kind: Namespace
metadata:
name: crossplane-system
labels:
catalyst.openova.io/sovereign: SOVEREIGN_FQDN_PLACEHOLDER
---
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: HelmRepository
metadata:
name: bp-crossplane
namespace: flux-system
spec:
type: oci
interval: 15m
url: oci://ghcr.io/openova-io/bp-crossplane
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: bp-crossplane
namespace: flux-system
spec:
interval: 15m
releaseName: crossplane
targetNamespace: crossplane-system
dependsOn:
- name: bp-flux
chart:
spec:
chart: bp-crossplane
version: 1.0.0
sourceRef:
kind: HelmRepository
name: bp-crossplane
namespace: flux-system
install:
remediation:
retries: 3
upgrade:
remediation:
retries: 3

View File

@ -0,0 +1,49 @@
# bp-sealed-secrets — Catalyst bootstrap-kit Blueprint. Transient bootstrap-only — used during Phase 0 to ship initial secrets through GitOps. Replaced by OpenBao + ESO once those land.
#
# Wrapper chart: platform/sealed-secrets/chart/
# Catalyst-curated values: platform/sealed-secrets/chart/values.yaml
# Reconciled by: Flux on the new Sovereign's k3s control plane.
---
apiVersion: v1
kind: Namespace
metadata:
name: kube-system
labels:
catalyst.openova.io/sovereign: SOVEREIGN_FQDN_PLACEHOLDER
---
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: HelmRepository
metadata:
name: bp-sealed-secrets
namespace: flux-system
spec:
type: oci
interval: 15m
url: oci://ghcr.io/openova-io/bp-sealed-secrets
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: bp-sealed-secrets
namespace: flux-system
spec:
interval: 15m
releaseName: sealed-secrets
targetNamespace: kube-system
dependsOn:
- name: bp-cert-manager
chart:
spec:
chart: bp-sealed-secrets
version: 1.0.0
sourceRef:
kind: HelmRepository
name: bp-sealed-secrets
namespace: flux-system
install:
remediation:
retries: 3
upgrade:
remediation:
retries: 3

View File

@ -0,0 +1,49 @@
# bp-spire — Catalyst bootstrap-kit Blueprint. Workload identity. SPIFFE/SPIRE issues 5-min rotating SVIDs to every Pod. Required by NATS JetStream and OpenBao below for SVID-based auth.
#
# Wrapper chart: platform/spire/chart/
# Catalyst-curated values: platform/spire/chart/values.yaml
# Reconciled by: Flux on the new Sovereign's k3s control plane.
---
apiVersion: v1
kind: Namespace
metadata:
name: spire-system
labels:
catalyst.openova.io/sovereign: SOVEREIGN_FQDN_PLACEHOLDER
---
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: HelmRepository
metadata:
name: bp-spire
namespace: flux-system
spec:
type: oci
interval: 15m
url: oci://ghcr.io/openova-io/bp-spire
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: bp-spire
namespace: flux-system
spec:
interval: 15m
releaseName: spire
targetNamespace: spire-system
dependsOn:
- name: bp-cert-manager
chart:
spec:
chart: bp-spire
version: 1.0.0
sourceRef:
kind: HelmRepository
name: bp-spire
namespace: flux-system
install:
remediation:
retries: 3
upgrade:
remediation:
retries: 3

View File

@ -0,0 +1,49 @@
# bp-nats-jetstream — Catalyst bootstrap-kit Blueprint. Catalyst's control-plane event spine. Per-Org Account isolation. KV bucket per Environment.
#
# Wrapper chart: platform/nats-jetstream/chart/
# Catalyst-curated values: platform/nats-jetstream/chart/values.yaml
# Reconciled by: Flux on the new Sovereign's k3s control plane.
---
apiVersion: v1
kind: Namespace
metadata:
name: nats-system
labels:
catalyst.openova.io/sovereign: SOVEREIGN_FQDN_PLACEHOLDER
---
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: HelmRepository
metadata:
name: bp-nats-jetstream
namespace: flux-system
spec:
type: oci
interval: 15m
url: oci://ghcr.io/openova-io/bp-nats-jetstream
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: bp-nats-jetstream
namespace: flux-system
spec:
interval: 15m
releaseName: nats-jetstream
targetNamespace: nats-system
dependsOn:
- name: bp-spire
chart:
spec:
chart: bp-nats-jetstream
version: 1.0.0
sourceRef:
kind: HelmRepository
name: bp-nats-jetstream
namespace: flux-system
install:
remediation:
retries: 3
upgrade:
remediation:
retries: 3

View File

@ -0,0 +1,49 @@
# bp-openbao — Catalyst bootstrap-kit Blueprint. Secret backend. 3-node Raft, region-local. No stretched cluster (per SECURITY.md §5).
#
# Wrapper chart: platform/openbao/chart/
# Catalyst-curated values: platform/openbao/chart/values.yaml
# Reconciled by: Flux on the new Sovereign's k3s control plane.
---
apiVersion: v1
kind: Namespace
metadata:
name: openbao
labels:
catalyst.openova.io/sovereign: SOVEREIGN_FQDN_PLACEHOLDER
---
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: HelmRepository
metadata:
name: bp-openbao
namespace: flux-system
spec:
type: oci
interval: 15m
url: oci://ghcr.io/openova-io/bp-openbao
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: bp-openbao
namespace: flux-system
spec:
interval: 15m
releaseName: openbao
targetNamespace: openbao
dependsOn:
- name: bp-spire
chart:
spec:
chart: bp-openbao
version: 1.0.0
sourceRef:
kind: HelmRepository
name: bp-openbao
namespace: flux-system
install:
remediation:
retries: 3
upgrade:
remediation:
retries: 3

View File

@ -0,0 +1,49 @@
# bp-keycloak — Catalyst bootstrap-kit Blueprint. User identity. Topology decided by Sovereign CRD spec.keycloakTopology.
#
# Wrapper chart: platform/keycloak/chart/
# Catalyst-curated values: platform/keycloak/chart/values.yaml
# Reconciled by: Flux on the new Sovereign's k3s control plane.
---
apiVersion: v1
kind: Namespace
metadata:
name: keycloak
labels:
catalyst.openova.io/sovereign: SOVEREIGN_FQDN_PLACEHOLDER
---
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: HelmRepository
metadata:
name: bp-keycloak
namespace: flux-system
spec:
type: oci
interval: 15m
url: oci://ghcr.io/openova-io/bp-keycloak
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: bp-keycloak
namespace: flux-system
spec:
interval: 15m
releaseName: keycloak
targetNamespace: keycloak
dependsOn:
- name: bp-cert-manager
chart:
spec:
chart: bp-keycloak
version: 1.0.0
sourceRef:
kind: HelmRepository
name: bp-keycloak
namespace: flux-system
install:
remediation:
retries: 3
upgrade:
remediation:
retries: 3

View File

@ -0,0 +1,61 @@
# bp-gitea — Catalyst Blueprint #10 of 11. Per-Sovereign Git server with
# the public Blueprint catalog mirror seeded. Catalyst's catalog-svc reads
# Blueprint metadata from this Gitea (not from the public openova monorepo
# directly) so the Sovereign is air-gap-ready by construction.
#
# Wrapper chart: platform/gitea/chart/
---
apiVersion: v1
kind: Namespace
metadata:
name: gitea
labels:
catalyst.openova.io/sovereign: SOVEREIGN_FQDN_PLACEHOLDER
---
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: HelmRepository
metadata:
name: bp-gitea
namespace: flux-system
spec:
type: oci
interval: 15m
url: oci://ghcr.io/openova-io/bp-gitea
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: bp-gitea
namespace: flux-system
spec:
interval: 15m
releaseName: gitea
targetNamespace: gitea
dependsOn:
- name: bp-keycloak
chart:
spec:
chart: bp-gitea
version: 1.0.0
sourceRef:
kind: HelmRepository
name: bp-gitea
namespace: flux-system
install:
remediation:
retries: 3
upgrade:
remediation:
retries: 3
values:
global:
sovereignFQDN: SOVEREIGN_FQDN_PLACEHOLDER
# gitea hostname is gitea.SOVEREIGN_FQDN_PLACEHOLDER. The DNS A record
# was already published by the Phase-0 catalyst-dns helper.
ingress:
hosts:
- host: gitea.SOVEREIGN_FQDN_PLACEHOLDER
paths:
- path: /
pathType: Prefix

View File

@ -0,0 +1,70 @@
# bp-catalyst-platform — Catalyst Blueprint #11 of 11. The umbrella
# Blueprint that brings up the Catalyst control plane: console, marketplace,
# admin, catalog-svc, projector, provisioning, environment-controller,
# blueprint-controller, billing.
#
# Per docs/ARCHITECTURE.md §11 (Catalyst-on-Catalyst): once this is Ready,
# the Sovereign is fully self-sufficient — sovereign-admin can log into
# console.SOVEREIGN_FQDN_PLACEHOLDER and proceed with Phase 2 day-1 setup.
#
# Wrapper chart: products/catalyst/chart/
---
apiVersion: v1
kind: Namespace
metadata:
name: catalyst-system
labels:
catalyst.openova.io/sovereign: SOVEREIGN_FQDN_PLACEHOLDER
---
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: HelmRepository
metadata:
name: bp-catalyst-platform
namespace: flux-system
spec:
type: oci
interval: 15m
url: oci://ghcr.io/openova-io/bp-catalyst-platform
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: bp-catalyst-platform
namespace: flux-system
spec:
interval: 15m
releaseName: catalyst-platform
targetNamespace: catalyst-system
dependsOn:
- name: bp-gitea
chart:
spec:
chart: bp-catalyst-platform
version: 1.0.0
sourceRef:
kind: HelmRepository
name: bp-catalyst-platform
namespace: flux-system
install:
remediation:
retries: 3
upgrade:
remediation:
retries: 3
# Per-Sovereign overrides for the umbrella — sovereign-FQDN-derived hostnames
# for console/admin/api. All chart-level Catalyst service config (image refs,
# OTel endpoints, NATS subjects) lives in products/catalyst/chart/values.yaml.
values:
global:
sovereignFQDN: SOVEREIGN_FQDN_PLACEHOLDER
ingress:
hosts:
console:
host: console.SOVEREIGN_FQDN_PLACEHOLDER
admin:
host: admin.SOVEREIGN_FQDN_PLACEHOLDER
marketplace:
host: SOVEREIGN_FQDN_PLACEHOLDER
api:
host: api.SOVEREIGN_FQDN_PLACEHOLDER

View File

@ -0,0 +1,18 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
# Order is documented but not enforced here — Flux respects HelmRelease
# dependsOn declarations for actual install order. Listing in canonical
# Phase 0 sequence per SOVEREIGN-PROVISIONING.md §3.
resources:
- 01-cilium.yaml
- 02-cert-manager.yaml
- 03-flux.yaml
- 04-crossplane.yaml
- 05-sealed-secrets.yaml
- 06-spire.yaml
- 07-nats-jetstream.yaml
- 08-openbao.yaml
- 09-keycloak.yaml
- 10-gitea.yaml
- 11-bp-catalyst-platform.yaml

View File

@ -0,0 +1,9 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
# Flux's own components were applied directly by cloud-init. The
# GitRepository + Kustomization that point at clusters/<sovereign-fqdn>/
# (this directory) are what start the GitOps loop and are also written
# by cloud-init. This entry is a stub for future Flux config customization
# (pull intervals, registry credentials, etc.) — empty by default.
resources: []

View File

@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- provider-hcloud.yaml
- provider-config-hcloud.yaml

View File

@ -0,0 +1,15 @@
# ProviderConfig for provider-hcloud. Token source = the K8s secret
# `hcloud-credentials` in `crossplane-system`, which the OpenTofu module's
# cloud-init writes at Phase-0 time so Crossplane can adopt resources
# immediately after install.
apiVersion: hcloud.crossplane.io/v1beta1
kind: ProviderConfig
metadata:
name: default
spec:
credentials:
source: Secret
secretRef:
namespace: crossplane-system
name: hcloud-credentials
key: token

View File

@ -0,0 +1,8 @@
# Crossplane provider-hcloud — installed AFTER bp-crossplane lands core.
# Adopts Phase-0 OpenTofu-created resources and manages day-2 changes.
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
name: provider-hcloud
spec:
package: xpkg.upbound.io/crossplane-contrib/provider-hcloud:v0.4.0

View File

@ -0,0 +1,15 @@
# Per-Sovereign Flux Kustomization root.
#
# Copied from clusters/_template/ → clusters/<sovereign-fqdn>/ at provisioning
# time, with SOVEREIGN_FQDN_PLACEHOLDER substituted. The Sovereign's k3s
# control plane (cloud-init bootstrap) installs Flux core, then applies a
# GitRepository pointing at this Sovereign's directory. From there Flux owns
# everything.
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- flux-system
- infrastructure
- bootstrap-kit

View File

@ -0,0 +1,47 @@
# bp-cilium — Catalyst bootstrap-kit Blueprint. CNI must come first; k3s started with --flannel-backend=none precisely so Cilium can take over.
#
# Wrapper chart: platform/cilium/chart/
# Catalyst-curated values: platform/cilium/chart/values.yaml
# Reconciled by: Flux on the new Sovereign's k3s control plane.
---
apiVersion: v1
kind: Namespace
metadata:
name: kube-system
labels:
catalyst.openova.io/sovereign: omantel.omani.works
---
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: HelmRepository
metadata:
name: bp-cilium
namespace: flux-system
spec:
type: oci
interval: 15m
url: oci://ghcr.io/openova-io/bp-cilium
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: bp-cilium
namespace: flux-system
spec:
interval: 15m
releaseName: cilium
targetNamespace: kube-system
chart:
spec:
chart: bp-cilium
version: 1.0.0
sourceRef:
kind: HelmRepository
name: bp-cilium
namespace: flux-system
install:
remediation:
retries: 3
upgrade:
remediation:
retries: 3

View File

@ -0,0 +1,49 @@
# bp-cert-manager — Catalyst bootstrap-kit Blueprint. TLS for everything below — Lets Encrypt issuer with Dynadot DNS-01 (omani.works pool) or HTTP-01 (BYO domains).
#
# Wrapper chart: platform/cert-manager/chart/
# Catalyst-curated values: platform/cert-manager/chart/values.yaml
# Reconciled by: Flux on the new Sovereign's k3s control plane.
---
apiVersion: v1
kind: Namespace
metadata:
name: cert-manager
labels:
catalyst.openova.io/sovereign: omantel.omani.works
---
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: HelmRepository
metadata:
name: bp-cert-manager
namespace: flux-system
spec:
type: oci
interval: 15m
url: oci://ghcr.io/openova-io/bp-cert-manager
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: bp-cert-manager
namespace: flux-system
spec:
interval: 15m
releaseName: cert-manager
targetNamespace: cert-manager
dependsOn:
- name: bp-cilium
chart:
spec:
chart: bp-cert-manager
version: 1.0.0
sourceRef:
kind: HelmRepository
name: bp-cert-manager
namespace: flux-system
install:
remediation:
retries: 3
upgrade:
remediation:
retries: 3

View File

@ -0,0 +1,49 @@
# bp-flux — Catalyst bootstrap-kit Blueprint. Host-level Flux. Per-vcluster Flux is bootstrapped later by environment-controller.
#
# Wrapper chart: platform/flux/chart/
# Catalyst-curated values: platform/flux/chart/values.yaml
# Reconciled by: Flux on the new Sovereign's k3s control plane.
---
apiVersion: v1
kind: Namespace
metadata:
name: flux-system
labels:
catalyst.openova.io/sovereign: omantel.omani.works
---
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: HelmRepository
metadata:
name: bp-flux
namespace: flux-system
spec:
type: oci
interval: 15m
url: oci://ghcr.io/openova-io/bp-flux
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: bp-flux
namespace: flux-system
spec:
interval: 15m
releaseName: flux
targetNamespace: flux-system
dependsOn:
- name: bp-cert-manager
chart:
spec:
chart: bp-flux
version: 1.0.0
sourceRef:
kind: HelmRepository
name: bp-flux
namespace: flux-system
install:
remediation:
retries: 3
upgrade:
remediation:
retries: 3

View File

@ -0,0 +1,49 @@
# bp-crossplane — Catalyst bootstrap-kit Blueprint. Day-2 cloud resource control plane. Adopts management of resources OpenTofu created in Phase 0 (Phase 1 hand-off per SOVEREIGN-PROVISIONING.md §4).
#
# Wrapper chart: platform/crossplane/chart/
# Catalyst-curated values: platform/crossplane/chart/values.yaml
# Reconciled by: Flux on the new Sovereign's k3s control plane.
---
apiVersion: v1
kind: Namespace
metadata:
name: crossplane-system
labels:
catalyst.openova.io/sovereign: omantel.omani.works
---
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: HelmRepository
metadata:
name: bp-crossplane
namespace: flux-system
spec:
type: oci
interval: 15m
url: oci://ghcr.io/openova-io/bp-crossplane
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: bp-crossplane
namespace: flux-system
spec:
interval: 15m
releaseName: crossplane
targetNamespace: crossplane-system
dependsOn:
- name: bp-flux
chart:
spec:
chart: bp-crossplane
version: 1.0.0
sourceRef:
kind: HelmRepository
name: bp-crossplane
namespace: flux-system
install:
remediation:
retries: 3
upgrade:
remediation:
retries: 3

View File

@ -0,0 +1,49 @@
# bp-sealed-secrets — Catalyst bootstrap-kit Blueprint. Transient bootstrap-only — used during Phase 0 to ship initial secrets through GitOps. Replaced by OpenBao + ESO once those land.
#
# Wrapper chart: platform/sealed-secrets/chart/
# Catalyst-curated values: platform/sealed-secrets/chart/values.yaml
# Reconciled by: Flux on the new Sovereign's k3s control plane.
---
apiVersion: v1
kind: Namespace
metadata:
name: kube-system
labels:
catalyst.openova.io/sovereign: omantel.omani.works
---
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: HelmRepository
metadata:
name: bp-sealed-secrets
namespace: flux-system
spec:
type: oci
interval: 15m
url: oci://ghcr.io/openova-io/bp-sealed-secrets
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: bp-sealed-secrets
namespace: flux-system
spec:
interval: 15m
releaseName: sealed-secrets
targetNamespace: kube-system
dependsOn:
- name: bp-cert-manager
chart:
spec:
chart: bp-sealed-secrets
version: 1.0.0
sourceRef:
kind: HelmRepository
name: bp-sealed-secrets
namespace: flux-system
install:
remediation:
retries: 3
upgrade:
remediation:
retries: 3

View File

@ -0,0 +1,49 @@
# bp-spire — Catalyst bootstrap-kit Blueprint. Workload identity. SPIFFE/SPIRE issues 5-min rotating SVIDs to every Pod. Required by NATS JetStream and OpenBao below for SVID-based auth.
#
# Wrapper chart: platform/spire/chart/
# Catalyst-curated values: platform/spire/chart/values.yaml
# Reconciled by: Flux on the new Sovereign's k3s control plane.
---
apiVersion: v1
kind: Namespace
metadata:
name: spire-system
labels:
catalyst.openova.io/sovereign: omantel.omani.works
---
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: HelmRepository
metadata:
name: bp-spire
namespace: flux-system
spec:
type: oci
interval: 15m
url: oci://ghcr.io/openova-io/bp-spire
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: bp-spire
namespace: flux-system
spec:
interval: 15m
releaseName: spire
targetNamespace: spire-system
dependsOn:
- name: bp-cert-manager
chart:
spec:
chart: bp-spire
version: 1.0.0
sourceRef:
kind: HelmRepository
name: bp-spire
namespace: flux-system
install:
remediation:
retries: 3
upgrade:
remediation:
retries: 3

View File

@ -0,0 +1,49 @@
# bp-nats-jetstream — Catalyst bootstrap-kit Blueprint. Catalyst's control-plane event spine. Per-Org Account isolation. KV bucket per Environment.
#
# Wrapper chart: platform/nats-jetstream/chart/
# Catalyst-curated values: platform/nats-jetstream/chart/values.yaml
# Reconciled by: Flux on the new Sovereign's k3s control plane.
---
apiVersion: v1
kind: Namespace
metadata:
name: nats-system
labels:
catalyst.openova.io/sovereign: omantel.omani.works
---
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: HelmRepository
metadata:
name: bp-nats-jetstream
namespace: flux-system
spec:
type: oci
interval: 15m
url: oci://ghcr.io/openova-io/bp-nats-jetstream
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: bp-nats-jetstream
namespace: flux-system
spec:
interval: 15m
releaseName: nats-jetstream
targetNamespace: nats-system
dependsOn:
- name: bp-spire
chart:
spec:
chart: bp-nats-jetstream
version: 1.0.0
sourceRef:
kind: HelmRepository
name: bp-nats-jetstream
namespace: flux-system
install:
remediation:
retries: 3
upgrade:
remediation:
retries: 3

View File

@ -0,0 +1,49 @@
# bp-openbao — Catalyst bootstrap-kit Blueprint. Secret backend. 3-node Raft, region-local. No stretched cluster (per SECURITY.md §5).
#
# Wrapper chart: platform/openbao/chart/
# Catalyst-curated values: platform/openbao/chart/values.yaml
# Reconciled by: Flux on the new Sovereign's k3s control plane.
---
apiVersion: v1
kind: Namespace
metadata:
name: openbao
labels:
catalyst.openova.io/sovereign: omantel.omani.works
---
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: HelmRepository
metadata:
name: bp-openbao
namespace: flux-system
spec:
type: oci
interval: 15m
url: oci://ghcr.io/openova-io/bp-openbao
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: bp-openbao
namespace: flux-system
spec:
interval: 15m
releaseName: openbao
targetNamespace: openbao
dependsOn:
- name: bp-spire
chart:
spec:
chart: bp-openbao
version: 1.0.0
sourceRef:
kind: HelmRepository
name: bp-openbao
namespace: flux-system
install:
remediation:
retries: 3
upgrade:
remediation:
retries: 3

View File

@ -0,0 +1,49 @@
# bp-keycloak — Catalyst bootstrap-kit Blueprint. User identity. Topology decided by Sovereign CRD spec.keycloakTopology.
#
# Wrapper chart: platform/keycloak/chart/
# Catalyst-curated values: platform/keycloak/chart/values.yaml
# Reconciled by: Flux on the new Sovereign's k3s control plane.
---
apiVersion: v1
kind: Namespace
metadata:
name: keycloak
labels:
catalyst.openova.io/sovereign: omantel.omani.works
---
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: HelmRepository
metadata:
name: bp-keycloak
namespace: flux-system
spec:
type: oci
interval: 15m
url: oci://ghcr.io/openova-io/bp-keycloak
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: bp-keycloak
namespace: flux-system
spec:
interval: 15m
releaseName: keycloak
targetNamespace: keycloak
dependsOn:
- name: bp-cert-manager
chart:
spec:
chart: bp-keycloak
version: 1.0.0
sourceRef:
kind: HelmRepository
name: bp-keycloak
namespace: flux-system
install:
remediation:
retries: 3
upgrade:
remediation:
retries: 3

View File

@ -0,0 +1,61 @@
# bp-gitea — Catalyst Blueprint #10 of 11. Per-Sovereign Git server with
# the public Blueprint catalog mirror seeded. Catalyst's catalog-svc reads
# Blueprint metadata from this Gitea (not from the public openova monorepo
# directly) so the Sovereign is air-gap-ready by construction.
#
# Wrapper chart: platform/gitea/chart/
---
apiVersion: v1
kind: Namespace
metadata:
name: gitea
labels:
catalyst.openova.io/sovereign: omantel.omani.works
---
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: HelmRepository
metadata:
name: bp-gitea
namespace: flux-system
spec:
type: oci
interval: 15m
url: oci://ghcr.io/openova-io/bp-gitea
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: bp-gitea
namespace: flux-system
spec:
interval: 15m
releaseName: gitea
targetNamespace: gitea
dependsOn:
- name: bp-keycloak
chart:
spec:
chart: bp-gitea
version: 1.0.0
sourceRef:
kind: HelmRepository
name: bp-gitea
namespace: flux-system
install:
remediation:
retries: 3
upgrade:
remediation:
retries: 3
values:
global:
sovereignFQDN: omantel.omani.works
# gitea hostname is gitea.omantel.omani.works. The DNS A record
# was already published by the Phase-0 catalyst-dns helper.
ingress:
hosts:
- host: gitea.omantel.omani.works
paths:
- path: /
pathType: Prefix

View File

@ -0,0 +1,70 @@
# bp-catalyst-platform — Catalyst Blueprint #11 of 11. The umbrella
# Blueprint that brings up the Catalyst control plane: console, marketplace,
# admin, catalog-svc, projector, provisioning, environment-controller,
# blueprint-controller, billing.
#
# Per docs/ARCHITECTURE.md §11 (Catalyst-on-Catalyst): once this is Ready,
# the Sovereign is fully self-sufficient — sovereign-admin can log into
# console.omantel.omani.works and proceed with Phase 2 day-1 setup.
#
# Wrapper chart: products/catalyst/chart/
---
apiVersion: v1
kind: Namespace
metadata:
name: catalyst-system
labels:
catalyst.openova.io/sovereign: omantel.omani.works
---
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: HelmRepository
metadata:
name: bp-catalyst-platform
namespace: flux-system
spec:
type: oci
interval: 15m
url: oci://ghcr.io/openova-io/bp-catalyst-platform
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: bp-catalyst-platform
namespace: flux-system
spec:
interval: 15m
releaseName: catalyst-platform
targetNamespace: catalyst-system
dependsOn:
- name: bp-gitea
chart:
spec:
chart: bp-catalyst-platform
version: 1.0.0
sourceRef:
kind: HelmRepository
name: bp-catalyst-platform
namespace: flux-system
install:
remediation:
retries: 3
upgrade:
remediation:
retries: 3
# Per-Sovereign overrides for the umbrella — sovereign-FQDN-derived hostnames
# for console/admin/api. All chart-level Catalyst service config (image refs,
# OTel endpoints, NATS subjects) lives in products/catalyst/chart/values.yaml.
values:
global:
sovereignFQDN: omantel.omani.works
ingress:
hosts:
console:
host: console.omantel.omani.works
admin:
host: admin.omantel.omani.works
marketplace:
host: omantel.omani.works
api:
host: api.omantel.omani.works

View File

@ -0,0 +1,18 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
# Order is documented but not enforced here — Flux respects HelmRelease
# dependsOn declarations for actual install order. Listing in canonical
# Phase 0 sequence per SOVEREIGN-PROVISIONING.md §3.
resources:
- 01-cilium.yaml
- 02-cert-manager.yaml
- 03-flux.yaml
- 04-crossplane.yaml
- 05-sealed-secrets.yaml
- 06-spire.yaml
- 07-nats-jetstream.yaml
- 08-openbao.yaml
- 09-keycloak.yaml
- 10-gitea.yaml
- 11-bp-catalyst-platform.yaml

View File

@ -0,0 +1,9 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
# Flux's own components were applied directly by cloud-init. The
# GitRepository + Kustomization that point at clusters/<sovereign-fqdn>/
# (this directory) are what start the GitOps loop and are also written
# by cloud-init. This entry is a stub for future Flux config customization
# (pull intervals, registry credentials, etc.) — empty by default.
resources: []

View File

@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- provider-hcloud.yaml
- provider-config-hcloud.yaml

View File

@ -0,0 +1,15 @@
# ProviderConfig for provider-hcloud. Token source = the K8s secret
# `hcloud-credentials` in `crossplane-system`, which the OpenTofu module's
# cloud-init writes at Phase-0 time so Crossplane can adopt resources
# immediately after install.
apiVersion: hcloud.crossplane.io/v1beta1
kind: ProviderConfig
metadata:
name: default
spec:
credentials:
source: Secret
secretRef:
namespace: crossplane-system
name: hcloud-credentials
key: token

View File

@ -0,0 +1,8 @@
# Crossplane provider-hcloud — installed AFTER bp-crossplane lands core.
# Adopts Phase-0 OpenTofu-created resources and manages day-2 changes.
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
name: provider-hcloud
spec:
package: xpkg.upbound.io/crossplane-contrib/provider-hcloud:v0.4.0

View File

@ -0,0 +1,15 @@
# Per-Sovereign Flux Kustomization root.
#
# Copied from clusters/_template/ → clusters/<sovereign-fqdn>/ at provisioning
# time, with omantel.omani.works substituted. The Sovereign's k3s
# control plane (cloud-init bootstrap) installs Flux core, then applies a
# GitRepository pointing at this Sovereign's directory. From there Flux owns
# everything.
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- flux-system
- infrastructure
- bootstrap-kit

View File

@ -0,0 +1,57 @@
# Catalyst Crossplane Compositions — canonical Hetzner XRDs
**XRD API group:** `compose.openova.io/v1alpha1`
(per `docs/BLUEPRINT-AUTHORING.md` §8 + `VALIDATION-LOG.md` Pass 42/48; **never** `catalyst.openova.io` — that is the Catalyst CRD group, not the Crossplane composite group.)
This directory contains the four canonical Hetzner-backed XRDs + their default Compositions that Catalyst uses to manage day-2 cloud infrastructure on a franchised Sovereign. After Phase 0 (`infra/hetzner/main.tf`) hands off to Phase 1, **all** further Hetzner resources — additional regions, attached volumes, additional firewalls, additional load balancers — go through these XRDs and are reconciled by Crossplane.
Per `docs/INVIOLABLE-PRINCIPLES.md` principle #3:
> Crossplane is the ONLY IaC after Phase 1 hand-off. Not direct provider SDKs. Not Terraform. Not the catalyst-api Go service calling cloud APIs.
## XRDs in this directory
| XRD | Wraps |
|---|---|
| `XHetznerNetwork` | `hcloud_network` + `hcloud_network_subnet` (provider-hcloud `Network` + `NetworkSubnet`) |
| `XHetznerFirewall` | `hcloud_firewall` (provider-hcloud `Firewall`) |
| `XHetznerServer` | `hcloud_server` (provider-hcloud `Server`) |
| `XHetznerLoadBalancer` | `hcloud_load_balancer` + targets + services (provider-hcloud `LoadBalancer` + `LoadBalancerTarget` + `LoadBalancerService`) |
Each `xrd-*.yaml` declares the OpenAPIv3 schema; each matching `composition-*.yaml` references the upstream `provider-hcloud` managed resources.
## Why these four
These mirror the four resource families OpenTofu provisions in `infra/hetzner/main.tf` Phase 0. After Phase 1 hand-off, Crossplane **adopts** the OpenTofu-created resources by `external-name` (the Hetzner numeric resource ID), and any further changes — adding a worker, opening a port, adding a region — are made by submitting an XR (claim) of the appropriate type instead of editing OpenTofu state.
## Provider configuration
The provider itself (`provider-hcloud`) and its `ProviderConfig` are installed by `platform/crossplane/chart/templates/provider-hcloud.yaml`, which is reconciled by Flux from the cluster directory. The Hetzner API token is mounted from a K8s `Secret` named `hcloud-credentials` in the `crossplane-system` namespace — that secret is created by the OpenTofu module's hand-off step.
## Adoption pattern
When OpenTofu creates a resource in Phase 0, the resource gets a label like:
```
catalyst.openova.io/sovereign: omantel.omani.works
catalyst.openova.io/role: control-plane
```
Phase 1 ingests these into Crossplane by creating an XR with `metadata.annotations[crossplane.io/external-name]` set to the Hetzner numeric ID. Crossplane then takes over the lifecycle — `kubectl delete xhetznerserver/cp1` after Phase 1 will deprovision the underlying Hetzner server, just like `tofu destroy` would have done in Phase 0. (See `clusters/<sovereign-fqdn>/infrastructure/adoption-claims.yaml` for the bootstrap claim manifests.)
## Authoring conventions
- Every XRD's `group` is `compose.openova.io` and `versions[0].name` is `v1alpha1`.
- Every XR's plural is `<kind-lowercase>s` (e.g. `xhetznerservers`).
- Every XRD declares a `claimNames` block so users can submit namespaced claims (`HetznerServer`) instead of cluster-scoped XRs (`XHetznerServer`).
- `defaultCompositionRef` points at the matching `composition-*.yaml` shipped here.
- Per principle #4 (no hardcoding): every cloud-specific value (region, server type, image) is a schema field, never a constant in the Composition.
## Adding a new XRD
1. Drop `xrd-<resource>.yaml` and `composition-<resource>.yaml` in this directory.
2. Reference the matching upstream provider-hcloud kind under `spec.resources[].base`.
3. Add the file to `kustomization.yaml`.
4. Bump `Chart.yaml` version of `bp-crossplane`.
The CI (`.github/workflows/blueprint-release.yaml`) re-publishes `bp-crossplane` to GHCR on the next push, and Flux reconciles the new XRDs into every Sovereign on its next pull.

View File

@ -0,0 +1,44 @@
# Composition: hetzner-firewall.compose.openova.io — default realization for
# XHetznerFirewall.
#
# This composition uses Crossplane's "patches with type: FromCompositeFieldPath"
# to splat the user-supplied rules array directly onto the upstream
# provider-hcloud Firewall.spec.forProvider.rule list. Each rule's port,
# protocol, and source/destination CIDRs come from the XR.
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: hetzner-firewall.compose.openova.io
labels:
catalyst.openova.io/component: crossplane
catalyst.openova.io/composition-family: hetzner
spec:
compositeTypeRef:
apiVersion: compose.openova.io/v1alpha1
kind: XHetznerFirewall
writeConnectionSecretsToNamespace: crossplane-system
resources:
- name: firewall
base:
apiVersion: firewall.hcloud.crossplane.io/v1alpha1
kind: Firewall
spec:
forProvider:
rule: [] # filled by patch
providerConfigRef:
name: default-hcloud
patches:
- fromFieldPath: spec.parameters.name
toFieldPath: metadata.name
- fromFieldPath: spec.parameters.rules
toFieldPath: spec.forProvider.rule
- 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.firewallId

View File

@ -0,0 +1,89 @@
# Composition for XHetznerLoadBalancer.
#
# Per docs/INVIOLABLE-PRINCIPLES.md principle #3: Crossplane is the ONLY day-2
# IaC. After OpenTofu Phase-0 creates the initial Hetzner load balancer for
# the Sovereign's control plane, additional load balancers (e.g. per-Org
# vcluster ingress, regional DR replicas) are managed via this Composition.
#
# Canonical XRD group: compose.openova.io/v1alpha1 (per Pass 42/48 + BLUEPRINT-AUTHORING.md §8).
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: hetzner-load-balancer.compose.openova.io
labels:
catalyst.openova.io/provider: hetzner
spec:
compositeTypeRef:
apiVersion: compose.openova.io/v1alpha1
kind: XHetznerLoadBalancer
resources:
- name: load-balancer
base:
apiVersion: load_balancer.hcloud.crossplane.io/v1alpha1
kind: LoadBalancer
spec:
forProvider:
loadBalancerType: lb11
algorithm:
- type: round_robin
labels:
"catalyst.openova.io/managed-by": crossplane
providerConfigRef:
name: default
patches:
- fromFieldPath: spec.parameters.name
toFieldPath: metadata.name
- fromFieldPath: spec.parameters.location
toFieldPath: spec.forProvider.location
- fromFieldPath: spec.parameters.loadBalancerType
toFieldPath: spec.forProvider.loadBalancerType
- fromFieldPath: spec.parameters.networkID
toFieldPath: spec.forProvider.network
- fromFieldPath: spec.parameters.sovereignFQDN
toFieldPath: spec.forProvider.labels["catalyst.openova.io/sovereign"]
- name: http-service
base:
apiVersion: load_balancer_service.hcloud.crossplane.io/v1alpha1
kind: LoadBalancerService
spec:
forProvider:
protocol: tcp
listenPort: 80
destinationPort: 31080
providerConfigRef:
name: default
patches:
- fromFieldPath: spec.parameters.name
toFieldPath: metadata.name
transforms:
- type: string
string:
fmt: "%s-http"
- fromFieldPath: status.atProvider.id
toFieldPath: spec.forProvider.loadBalancerID
policy:
fromFieldPath: Required
- name: https-service
base:
apiVersion: load_balancer_service.hcloud.crossplane.io/v1alpha1
kind: LoadBalancerService
spec:
forProvider:
protocol: tcp
listenPort: 443
destinationPort: 31443
providerConfigRef:
name: default
patches:
- fromFieldPath: spec.parameters.name
toFieldPath: metadata.name
transforms:
- type: string
string:
fmt: "%s-https"
writeConnectionSecretsToNamespace: crossplane-system

View File

@ -0,0 +1,80 @@
# 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

View File

@ -0,0 +1,71 @@
# Composition: hetzner-server.compose.openova.io — default realization for
# XHetznerServer. Renders to provider-hcloud's Server managed resource.
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: hetzner-server.compose.openova.io
labels:
catalyst.openova.io/component: crossplane
catalyst.openova.io/composition-family: hetzner
spec:
compositeTypeRef:
apiVersion: compose.openova.io/v1alpha1
kind: XHetznerServer
writeConnectionSecretsToNamespace: crossplane-system
resources:
- name: server
base:
apiVersion: server.hcloud.crossplane.io/v1alpha1
kind: Server
spec:
forProvider:
serverType: "" # filled by patch
image: "" # filled by patch
location: "" # filled by patch
sshKeys: [] # filled by patch
firewallIds: [] # filled by patch
userData: "" # filled by patch
network:
- networkId: ""
ip: ""
providerConfigRef:
name: default-hcloud
patches:
- fromFieldPath: spec.parameters.name
toFieldPath: metadata.name
- fromFieldPath: spec.parameters.serverType
toFieldPath: spec.forProvider.serverType
- fromFieldPath: spec.parameters.image
toFieldPath: spec.forProvider.image
- fromFieldPath: spec.parameters.region
toFieldPath: spec.forProvider.location
- fromFieldPath: spec.parameters.sshKeyName
toFieldPath: spec.forProvider.sshKeys[0]
- fromFieldPath: spec.parameters.firewallIds
toFieldPath: spec.forProvider.firewallIds
- fromFieldPath: spec.parameters.userData
toFieldPath: spec.forProvider.userData
- fromFieldPath: spec.parameters.networkId
toFieldPath: spec.forProvider.network[0].networkId
- fromFieldPath: spec.parameters.privateIp
toFieldPath: spec.forProvider.network[0].ip
- fromFieldPath: spec.parameters.placementGroupName
toFieldPath: spec.forProvider.placementGroupName
- fromFieldPath: spec.providerConfigRef.name
toFieldPath: spec.providerConfigRef.name
- fromFieldPath: spec.parameters.sovereignFQDN
toFieldPath: metadata.labels[catalyst.openova.io/sovereign]
- fromFieldPath: spec.parameters.role
toFieldPath: metadata.labels[catalyst.openova.io/role]
- type: ToCompositeFieldPath
fromFieldPath: metadata.annotations[crossplane.io/external-name]
toFieldPath: status.serverId
- type: ToCompositeFieldPath
fromFieldPath: status.atProvider.ipv4Address
toFieldPath: status.publicIPv4
- type: ToCompositeFieldPath
fromFieldPath: status.atProvider.ipv6Address
toFieldPath: status.publicIPv6

View File

@ -0,0 +1,111 @@
# XRD: XHetznerFirewall — Catalyst's canonical Hetzner firewall composite.
#
# Wraps:
# - hcloud_firewall (the firewall and its rules)
#
# Per docs/BLUEPRINT-AUTHORING.md §8 the canonical XRD group is
# compose.openova.io/v1alpha1.
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: xhetznerfirewalls.compose.openova.io
labels:
catalyst.openova.io/component: crossplane
catalyst.openova.io/composition-family: hetzner
spec:
group: compose.openova.io
names:
kind: XHetznerFirewall
plural: xhetznerfirewalls
claimNames:
kind: HetznerFirewall
plural: hetznerfirewalls
defaultCompositionRef:
name: hetzner-firewall.compose.openova.io
versions:
- name: v1alpha1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
required: [spec]
properties:
spec:
type: object
required: [parameters]
properties:
parameters:
type: object
required: [name, rules]
properties:
name:
type: string
description: |
Firewall name in Hetzner Cloud. Convention:
catalyst-<sovereign-fqdn-with-dashes>-fw
pattern: '^[a-z0-9][a-z0-9-]{1,62}$'
sovereignFQDN:
type: string
description: |
FQDN of the owning Sovereign — written as label.
rules:
type: array
description: |
Firewall rules. Order does not matter — Hetzner
evaluates all rules and accepts on first match.
minItems: 1
items:
type: object
required: [direction, protocol]
properties:
direction:
type: string
enum: [in, out]
protocol:
type: string
enum: [tcp, udp, icmp, esp, gre]
port:
type: string
description: |
Single port (e.g. "443"), range (e.g.
"30000-32767"), or omitted for icmp/esp/gre.
sourceIps:
type: array
description: |
Source CIDR list for direction=in. Catalyst
convention: ["0.0.0.0/0", "::/0"] for public,
operator-specific CIDR for SSH (22).
items:
type: string
destinationIps:
type: array
description: |
Destination CIDR list for direction=out.
items:
type: string
description:
type: string
providerConfigRef:
type: object
properties:
name:
type: string
default:
name: default-hcloud
status:
type: object
properties:
firewallId:
type: string
description: |
Hetzner numeric firewall ID. Servers (XHetznerServer)
reference this via firewallIds to apply the rules.
additionalPrinterColumns:
- name: FIREWALL-ID
type: string
jsonPath: .status.firewallId
- name: AGE
type: date
jsonPath: .metadata.creationTimestamp

View File

@ -0,0 +1,141 @@
# XRD: XHetznerLoadBalancer — Catalyst's canonical Hetzner load-balancer
# composite. Wraps:
# - hcloud_load_balancer
# - hcloud_load_balancer_network (attaches LB to private network)
# - hcloud_load_balancer_target (zero or more — typically the cp/worker servers)
# - hcloud_load_balancer_service (zero or more — port mappings)
#
# The expected use cases on a Sovereign post-Phase-1:
# - Adding a regional ingress LB when expanding to a new region
# - Adding service-specific LBs (e.g. dedicated TURN/STUN LB for stunner)
# - Adopting the Phase-0-provisioned LB by external-name
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: xhetznerloadbalancers.compose.openova.io
labels:
catalyst.openova.io/component: crossplane
catalyst.openova.io/composition-family: hetzner
spec:
group: compose.openova.io
names:
kind: XHetznerLoadBalancer
plural: xhetznerloadbalancers
claimNames:
kind: HetznerLoadBalancer
plural: hetznerloadbalancers
defaultCompositionRef:
name: hetzner-load-balancer.compose.openova.io
versions:
- name: v1alpha1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
required: [spec]
properties:
spec:
type: object
required: [parameters]
properties:
parameters:
type: object
required: [name, type, region, networkId, algorithm]
properties:
name:
type: string
description: |
LB name in Hetzner. Convention:
catalyst-<sovereign-fqdn-with-dashes>-lb
pattern: '^[a-z0-9][a-z0-9-]{1,62}$'
sovereignFQDN:
type: string
type:
type: string
description: |
Hetzner LB type slug — lb11, lb21, lb31.
enum: [lb11, lb21, lb31]
region:
type: string
description: |
Hetzner location.
pattern: '^[a-z]+[0-9]?$'
algorithm:
type: string
enum: [round_robin, least_connections]
networkId:
type: string
description: |
Hetzner numeric network ID — the LB attaches to this
private network so it can reach servers via private IPs.
targetServerIds:
type: array
description: |
Hetzner numeric server IDs to add as backend targets.
Typically status.serverId from one or more XHetznerServer.
items:
type: string
services:
type: array
description: |
Listener-port mappings. Catalyst convention for the
bootstrap LB is two services:
80 → 31080 (HTTP, Cilium Gateway NodePort)
443 → 31443 (HTTPS, Cilium Gateway NodePort)
minItems: 1
items:
type: object
required: [protocol, listenPort, destinationPort]
properties:
protocol:
type: string
enum: [tcp, http, https]
listenPort:
type: integer
minimum: 1
maximum: 65535
destinationPort:
type: integer
minimum: 1
maximum: 65535
proxyProtocol:
type: boolean
default: false
providerConfigRef:
type: object
properties:
name:
type: string
default:
name: default-hcloud
status:
type: object
properties:
loadBalancerId:
type: string
ipv4:
type: string
description: |
Public IPv4 of the LB. catalyst-environment-controller
reads this and writes A records via Crossplane DNS XR
(or Dynadot for omani.works pool domains).
ipv6:
type: string
additionalPrinterColumns:
- name: LB-ID
type: string
jsonPath: .status.loadBalancerId
- name: TYPE
type: string
jsonPath: .spec.parameters.type
- name: REGION
type: string
jsonPath: .spec.parameters.region
- name: PUBLIC-IP
type: string
jsonPath: .status.ipv4
- name: AGE
type: date
jsonPath: .metadata.creationTimestamp

View File

@ -0,0 +1,111 @@
# XRD: XHetznerNetwork — Catalyst's canonical Hetzner private network composite.
#
# Wraps:
# - hcloud_network (the private VPC range)
# - hcloud_network_subnet (a subnet inside the network, in a network zone)
#
# Per docs/BLUEPRINT-AUTHORING.md §8 the canonical XRD group is
# compose.openova.io/v1alpha1 — NOT catalyst.openova.io (that's the Catalyst CRD
# group used for Sovereign/Organization/Environment/Application/Blueprint).
#
# Per docs/INVIOLABLE-PRINCIPLES.md principle #4 every cloud-specific value
# (CIDR, subnet range, network zone) is a schema field — no hardcoded values.
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: xhetznernetworks.compose.openova.io
labels:
catalyst.openova.io/component: crossplane
catalyst.openova.io/composition-family: hetzner
spec:
group: compose.openova.io
names:
kind: XHetznerNetwork
plural: xhetznernetworks
claimNames:
kind: HetznerNetwork
plural: hetznernetworks
defaultCompositionRef:
name: hetzner-network.compose.openova.io
versions:
- name: v1alpha1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
required: [spec]
properties:
spec:
type: object
required: [parameters]
properties:
parameters:
type: object
required: [name, ipRange, subnetIpRange, networkZone]
properties:
name:
type: string
description: |
Network name as it appears in the Hetzner Cloud console.
Convention: catalyst-<sovereign-fqdn-with-dashes>-net
pattern: '^[a-z0-9][a-z0-9-]{1,62}$'
ipRange:
type: string
description: |
Top-level VPC CIDR. Catalyst default 10.0.0.0/16.
pattern: '^([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]{1,2}$'
subnetIpRange:
type: string
description: |
Subnet CIDR — must be inside ipRange. Catalyst default 10.0.1.0/24.
pattern: '^([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]{1,2}$'
networkZone:
type: string
description: |
Hetzner network zone — eu-central, us-east, us-west, ap-southeast.
enum: [eu-central, us-east, us-west, ap-southeast]
sovereignFQDN:
type: string
description: |
FQDN of the owning Sovereign — written as a label so
operators can grep all resources for one Sovereign.
description: |
Hetzner private network parameters. All fields are required;
no defaults are applied so the caller is forced to make
the topology explicit.
providerConfigRef:
type: object
description: |
Override the default ProviderConfig (default-hcloud).
Useful for multi-account Sovereigns.
properties:
name:
type: string
default:
name: default-hcloud
status:
type: object
properties:
networkId:
type: string
description: |
Hetzner numeric network ID, populated after the network
exists. Other XRs (XHetznerServer, XHetznerLoadBalancer)
reference this to attach themselves to the network.
subnetIpRange:
type: string
additionalPrinterColumns:
- name: NETWORK-ID
type: string
jsonPath: .status.networkId
- name: ZONE
type: string
jsonPath: .spec.parameters.networkZone
- name: CIDR
type: string
jsonPath: .spec.parameters.ipRange
- name: AGE
type: date
jsonPath: .metadata.creationTimestamp

View File

@ -0,0 +1,149 @@
# XRD: XHetznerServer — Catalyst's canonical Hetzner server (VM) composite.
#
# Wraps:
# - hcloud_server (the VM, including network attachment)
#
# The expected use cases on a Sovereign post-Phase-1:
# - Adding a worker node to an existing host cluster
# - Provisioning an additional control-plane node when scaling to HA
# - Provisioning servers for new building blocks (rtz, dmz) added later
#
# Per docs/INVIOLABLE-PRINCIPLES.md principle #4 every cloud-specific
# value (region, server_type, image) is a schema field — no hardcoding.
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: xhetznerservers.compose.openova.io
labels:
catalyst.openova.io/component: crossplane
catalyst.openova.io/composition-family: hetzner
spec:
group: compose.openova.io
names:
kind: XHetznerServer
plural: xhetznerservers
claimNames:
kind: HetznerServer
plural: hetznerservers
defaultCompositionRef:
name: hetzner-server.compose.openova.io
versions:
- name: v1alpha1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
required: [spec]
properties:
spec:
type: object
required: [parameters]
properties:
parameters:
type: object
required: [name, serverType, image, region, networkId, sshKeyName]
properties:
name:
type: string
description: |
Server name in Hetzner Cloud. Convention:
catalyst-<sovereign-fqdn-with-dashes>-{cp,w}<index>
pattern: '^[a-z0-9][a-z0-9-]{1,62}$'
sovereignFQDN:
type: string
role:
type: string
description: |
Catalyst role label — written to the server's labels
and to the catalyst.openova.io/role label. Used by
kubectl selectors and observability dashboards.
enum: [control-plane, worker, edge]
serverType:
type: string
description: |
Hetzner server type slug — cx22, cx32, cx42, cpx21,
cpx31, cpx41, ccx13, ccx23, ccx33, ccx43, etc.
pattern: '^(cx|cpx|ccx)[0-9]{2}$'
image:
type: string
description: |
Hetzner image slug — ubuntu-24.04, ubuntu-22.04,
debian-12, fedora-40, etc.
pattern: '^[a-z]+-[0-9]+\.?[0-9]*$'
region:
type: string
description: |
Hetzner location — fsn1, nbg1, hel1, ash, hil, sin.
pattern: '^[a-z]+[0-9]?$'
networkId:
type: string
description: |
Hetzner numeric network ID to attach this server to —
typically status.networkId from an XHetznerNetwork.
privateIp:
type: string
description: |
Static IPv4 inside the network's subnet.
pattern: '^([0-9]{1,3}\.){3}[0-9]{1,3}$'
sshKeyName:
type: string
description: |
Name of an existing Hetzner SSH key (created by the
Phase-0 OpenTofu module) that can SSH into this server.
firewallIds:
type: array
description: |
Hetzner numeric firewall IDs to apply to this server.
items:
type: string
userData:
type: string
description: |
cloud-init user-data script. Catalyst leaves this
empty for day-2 nodes — the cloud-init that joins
a worker to the existing k3s cluster is templated
by the catalyst-environment-controller using the
bootstrap k3s_token retrieved from OpenBao.
placementGroupName:
type: string
description: |
Optional placement-group name — for spread/anti-affinity.
providerConfigRef:
type: object
properties:
name:
type: string
default:
name: default-hcloud
status:
type: object
properties:
serverId:
type: string
publicIPv4:
type: string
publicIPv6:
type: string
privateIPv4:
type: string
additionalPrinterColumns:
- name: SERVER-ID
type: string
jsonPath: .status.serverId
- name: ROLE
type: string
jsonPath: .spec.parameters.role
- name: TYPE
type: string
jsonPath: .spec.parameters.serverType
- name: REGION
type: string
jsonPath: .spec.parameters.region
- name: PUBLIC-IP
type: string
jsonPath: .status.publicIPv4
- name: AGE
type: date
jsonPath: .metadata.creationTimestamp

View File

@ -10,6 +10,12 @@ COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /catalyst-api ./cmd/api
# catalyst-dns helper — invoked by the OpenTofu module's null_resource.dns_pool
# via local-exec at Phase-0 apply time. Lives at /usr/local/bin/catalyst-dns
# in the runtime image so the OpenTofu run (which executes inside this same
# container — the catalyst-api Pod is also the OpenTofu runner) can shell out
# to it. See infra/hetzner/main.tf comments around null_resource.dns_pool.
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /catalyst-dns ./cmd/catalyst-dns
FROM docker.io/library/alpine:3.20
@ -28,6 +34,7 @@ RUN apk add --no-cache ca-certificates curl bash \
&& chmod +x /usr/local/bin/helm
COPY --from=build /catalyst-api /catalyst-api
COPY --from=build /catalyst-dns /usr/local/bin/catalyst-dns
RUN adduser -D -u 65534 nonroot
USER 65534:65534
EXPOSE 8080

View File

@ -0,0 +1,78 @@
// catalyst-dns — small Go binary the OpenTofu module's null_resource.dns_pool
// invokes via local-exec when domain_mode=pool.
//
// Per docs/INVIOLABLE-PRINCIPLES.md principle #3: cloud APIs are NOT called
// from bespoke Go in the catalyst-api process. The narrow exception is this
// binary, which is invoked by OpenTofu (the canonical IaC) as if it were a
// terraform-dynadot provider — the Dynadot terraform provider does not
// exist on the registry, so we ship our own helper. The contract here is
// the same as a terraform provider would expose: receive inputs via env
// vars, write the records, exit 0 on success.
//
// Inputs (env vars):
// DYNADOT_API_KEY — Dynadot account API key (account-scoped, covers
// every domain owned by the account)
// DYNADOT_API_SECRET — Dynadot account API secret
// DOMAIN — Pool domain (e.g. omani.works)
// SUBDOMAIN — Sovereign subdomain (e.g. omantel)
// LB_IP — Hetzner load-balancer IPv4 the records point at
//
// Output: writes the canonical 6-record set per dynadot.AddSovereignRecords:
// *.<SUBDOMAIN>.<DOMAIN> A → <LB_IP>
// console.<SUBDOMAIN>.<DOMAIN> A → <LB_IP>
// gitea.<SUBDOMAIN>.<DOMAIN> A → <LB_IP>
// harbor.<SUBDOMAIN>.<DOMAIN> A → <LB_IP>
// admin.<SUBDOMAIN>.<DOMAIN> A → <LB_IP>
// api.<SUBDOMAIN>.<DOMAIN> A → <LB_IP>
//
// Idempotent: re-running with the same inputs writes the same records again
// (Dynadot dedupes by (subdomain, type) under add_dns_to_current_setting=yes).
package main
import (
"context"
"fmt"
"os"
"time"
"github.com/openova-io/openova/products/catalyst/bootstrap/api/internal/dynadot"
)
func main() {
apiKey := os.Getenv("DYNADOT_API_KEY")
apiSecret := os.Getenv("DYNADOT_API_SECRET")
domain := os.Getenv("DOMAIN")
subdomain := os.Getenv("SUBDOMAIN")
lbIP := os.Getenv("LB_IP")
if apiKey == "" || apiSecret == "" {
fail("DYNADOT_API_KEY and DYNADOT_API_SECRET must be set")
}
if domain == "" {
fail("DOMAIN must be set (e.g. omani.works)")
}
if subdomain == "" {
fail("SUBDOMAIN must be set (e.g. omantel)")
}
if lbIP == "" {
fail("LB_IP must be set (the Hetzner load balancer IPv4)")
}
if !dynadot.IsManagedDomain(domain) {
fail(fmt.Sprintf("DOMAIN %q is not in the managed-domain allowlist; refusing to write records", domain))
}
client := dynadot.New(apiKey, apiSecret)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
if err := client.AddSovereignRecords(ctx, domain, subdomain, lbIP); err != nil {
fail(fmt.Sprintf("write DNS: %v", err))
}
fmt.Printf("✓ Wrote 6 A records for *.%s.%s → %s via Dynadot\n", subdomain, domain, lbIP)
}
func fail(msg string) {
fmt.Fprintf(os.Stderr, "catalyst-dns: %s\n", msg)
os.Exit(1)
}