CI workflow (.github/workflows/pool-domain-manager-build.yaml) mirrors
the marketplace-api / catalyst-api shape:
- Triggers on push to core/pool-domain-manager/** + workflow_dispatch
- Runs unit tests (reserved + dynadot — the integration suite needs a
real Postgres which the workflow does not provide; full integration
runs in test-bootstrap-api.yaml against an ephemeral CNPG)
- Builds and pushes ghcr.io/openova-io/openova/pool-domain-manager:<sha>
- Cosign-signs the image via Sigstore keyless OIDC (id-token: write)
- Emits an SBOM attestation tied to the image digest
- Manifest deployment is intentionally NOT in this workflow — PDM
manifests live in the openova-private repo per the issue body, so
the Flux Kustomization there picks up the new SHA via a follow-up
private-repo commit (Phase 6 of #163)
Crossplane composition (platform/crossplane/compositions/xrd-pool-
allocation.yaml + composition-pool-allocation.yaml) wraps PDM as a
declarative Crossplane Resource:
apiVersion: compose.openova.io/v1alpha1
kind: XDynadotPoolAllocation
spec:
parameters:
poolDomain: omani.works
subdomain: omantel
sovereignFQDN: omantel.omani.works
loadBalancerIP: 1.2.3.4
createdBy: crossplane
The Composition uses provider-http (crossplane-contrib/provider-http) to
render the XR into a Reserve → Commit sequence of HTTP calls against
PDM's in-cluster service URL. Per docs/INVIOLABLE-PRINCIPLES.md #3 we use
provider-http rather than bespoke Go to keep the day-2 lifecycle
declarative. Operators who want to pre-allocate a name (e.g. reserve
'omantel.omani.works' for a Sovereign that hasn't been provisioned yet)
commit YAML to Git and Flux+Crossplane converge.
Refs: #163
159 lines
6.8 KiB
YAML
159 lines
6.8 KiB
YAML
# Composition: dynadot-pool-allocation.compose.openova.io — default
|
|
# realization for XDynadotPoolAllocation. Renders to a sequence of
|
|
# provider-http MR calls that mirror the wizard's imperative lifecycle:
|
|
#
|
|
# 1. POST http://pool-domain-manager.../api/v1/pool/{poolDomain}/reserve
|
|
# body: { subdomain, createdBy }
|
|
# → returns reservationToken
|
|
#
|
|
# 2. POST http://pool-domain-manager.../api/v1/pool/{poolDomain}/commit
|
|
# body: { subdomain, reservationToken, sovereignFQDN, loadBalancerIP }
|
|
#
|
|
# 3. (on delete) DELETE http://pool-domain-manager.../api/v1/pool/{poolDomain}/release
|
|
# body: { subdomain }
|
|
#
|
|
# Per docs/INVIOLABLE-PRINCIPLES.md principle #3 we use Crossplane's
|
|
# provider-http so the entire lifecycle is declarative — no bespoke Go,
|
|
# no exec.Command, no out-of-band shell scripts. The operator commits a
|
|
# DynadotPoolAllocation claim to Git and Flux + Crossplane converge.
|
|
#
|
|
# Provider-http reference:
|
|
# https://github.com/crossplane-contrib/provider-http
|
|
# We expect the cluster to have a ProviderConfig named 'pool-domain-manager'
|
|
# pointing at the in-cluster service URL — set up by Phase-0 cluster
|
|
# bootstrap (the Catalyst-Zero install ships this ProviderConfig as part
|
|
# of the platform's openova-system manifests).
|
|
|
|
apiVersion: apiextensions.crossplane.io/v1
|
|
kind: Composition
|
|
metadata:
|
|
name: dynadot-pool-allocation.compose.openova.io
|
|
labels:
|
|
catalyst.openova.io/component: crossplane
|
|
catalyst.openova.io/composition-family: pool-domain-manager
|
|
spec:
|
|
compositeTypeRef:
|
|
apiVersion: compose.openova.io/v1alpha1
|
|
kind: XDynadotPoolAllocation
|
|
|
|
writeConnectionSecretsToNamespace: crossplane-system
|
|
|
|
resources:
|
|
# ── 1. Reserve ────────────────────────────────────────────────────────
|
|
# Calls PDM /reserve. Provider-http stores the response body in the MR's
|
|
# status; the next step's request templating reads reservationToken
|
|
# from there.
|
|
- name: reserve
|
|
base:
|
|
apiVersion: http.crossplane.io/v1alpha2
|
|
kind: Request
|
|
spec:
|
|
forProvider:
|
|
url: "" # filled by patch
|
|
method: POST
|
|
headers:
|
|
Content-Type:
|
|
- application/json
|
|
payload:
|
|
baseUrl: "" # patched
|
|
body: "" # patched (subdomain + createdBy)
|
|
mappings:
|
|
- method: POST
|
|
action: CREATE
|
|
url: "" # patched
|
|
body: "" # patched
|
|
- method: DELETE
|
|
action: REMOVE
|
|
url: "" # patched (release endpoint)
|
|
body: "" # patched
|
|
providerConfigRef:
|
|
name: pool-domain-manager
|
|
patches:
|
|
# PDM URL: '<base>/api/v1/pool/<poolDomain>/reserve' on CREATE,
|
|
# '<base>/api/v1/pool/<poolDomain>/release' on DELETE. We default
|
|
# base to the in-cluster ClusterIP service so a stock Catalyst
|
|
# Sovereign bootstrap doesn't need any per-cluster overrides.
|
|
- fromFieldPath: spec.parameters.poolDomain
|
|
toFieldPath: spec.forProvider.mappings[0].url
|
|
transforms:
|
|
- type: string
|
|
string:
|
|
fmt: "http://pool-domain-manager.openova-system.svc.cluster.local:8080/api/v1/pool/%s/reserve"
|
|
- fromFieldPath: spec.parameters.poolDomain
|
|
toFieldPath: spec.forProvider.mappings[1].url
|
|
transforms:
|
|
- type: string
|
|
string:
|
|
fmt: "http://pool-domain-manager.openova-system.svc.cluster.local:8080/api/v1/pool/%s/release"
|
|
# Body for CREATE: { "subdomain": "<sub>", "createdBy": "crossplane" }
|
|
- fromFieldPath: spec.parameters.subdomain
|
|
toFieldPath: spec.forProvider.mappings[0].body
|
|
transforms:
|
|
- type: string
|
|
string:
|
|
fmt: '{"subdomain":"%s","createdBy":"crossplane"}'
|
|
- fromFieldPath: spec.parameters.subdomain
|
|
toFieldPath: spec.forProvider.mappings[1].body
|
|
transforms:
|
|
- type: string
|
|
string:
|
|
fmt: '{"subdomain":"%s"}'
|
|
# Surface the reservation token back onto the XR status so step 2
|
|
# can read it via fromConnectionSecret OR via direct status patch.
|
|
- type: ToCompositeFieldPath
|
|
fromFieldPath: status.response.body.reservationToken
|
|
toFieldPath: status.reservationToken
|
|
- type: ToCompositeFieldPath
|
|
fromFieldPath: status.response.body.state
|
|
toFieldPath: status.state
|
|
- type: ToCompositeFieldPath
|
|
fromFieldPath: status.response.body.expiresAt
|
|
toFieldPath: status.expiresAt
|
|
|
|
# ── 2. Commit ─────────────────────────────────────────────────────────
|
|
# Calls PDM /commit. Depends on the reservation token surfaced by
|
|
# step 1. provider-http evaluates resources in order, so by the time
|
|
# this MR runs the XR's status.reservationToken is populated.
|
|
- name: commit
|
|
base:
|
|
apiVersion: http.crossplane.io/v1alpha2
|
|
kind: Request
|
|
spec:
|
|
forProvider:
|
|
mappings:
|
|
- method: POST
|
|
action: CREATE
|
|
url: "" # patched
|
|
body: "" # patched
|
|
headers:
|
|
Content-Type:
|
|
- application/json
|
|
providerConfigRef:
|
|
name: pool-domain-manager
|
|
patches:
|
|
- fromFieldPath: spec.parameters.poolDomain
|
|
toFieldPath: spec.forProvider.mappings[0].url
|
|
transforms:
|
|
- type: string
|
|
string:
|
|
fmt: "http://pool-domain-manager.openova-system.svc.cluster.local:8080/api/v1/pool/%s/commit"
|
|
# Body composition: we need subdomain, reservationToken,
|
|
# sovereignFQDN, loadBalancerIP. Crossplane's standard patches
|
|
# don't support multi-source string interpolation in a single
|
|
# transform, so we use a CombineFromComposite block.
|
|
- type: CombineFromComposite
|
|
combine:
|
|
variables:
|
|
- fromFieldPath: spec.parameters.subdomain
|
|
- fromFieldPath: status.reservationToken
|
|
- fromFieldPath: spec.parameters.sovereignFQDN
|
|
- fromFieldPath: spec.parameters.loadBalancerIP
|
|
strategy: string
|
|
string:
|
|
fmt: '{"subdomain":"%s","reservationToken":"%s","sovereignFQDN":"%s","loadBalancerIP":"%s"}'
|
|
toFieldPath: spec.forProvider.mappings[0].body
|
|
# On commit success the row flips to active.
|
|
- type: ToCompositeFieldPath
|
|
fromFieldPath: status.response.body.state
|
|
toFieldPath: status.state
|