Refactor pool-domain-manager to own per-Sovereign zones in PowerDNS,
replacing the previous Dynadot-set_dns2 record-write flow.
Phase 1 — internal/pdns: REST client for PowerDNS Authoritative API
- CreateZone / DeleteZone / EnsureZone / ZoneExists
- PatchRRSets (atomic batch RRset writes)
- AddARecord / AddNSDelegation / RemoveNSDelegation
- EnableDNSSEC: PUT dnssec flag, generate KSK+ZSK (algorithm 13
ECDSAP256SHA256 per docs/PLATFORM-POWERDNS.md), POST rectify
- retry-once-on-5xx with exponential backoff (250ms, 1s)
- X-API-Key header from K8s Secret, never logged
- 22 unit tests covering every method against httptest mock
Phase 2 — allocator: DNSWriter interface + per-Sovereign lifecycle
- /reserve: insert pdm-pg row + create child zone with apex NS
RRset + add NS delegation into parent + enable DNSSEC on child
- /commit: write the canonical 6-record set (apex, *, console,
api, gitea, harbor) into child zone, TTL 300, atomic PATCH
- /release: drop child zone (DNSSEC keys retire) + remove parent
NS delegation, idempotent on 404
- sweeper teardowns DNS for expired reservations before deleting
pdm-pg rows
- rollback path on Reserve failure preserves operator UX
- allocator_test.go: fake DNSWriter for state-machine assertions
Phase 3 — startup parent-zone bootstrap
- BootstrapParentZones runs at PDM startup before HTTP serves
- EnsureZone for every entry in DYNADOT_MANAGED_DOMAINS
- DNSSEC enabled on each parent zone (idempotent)
- PDM exits non-zero if bootstrap fails
Phase 4 — schema unchanged
- child zone name derived as <subdomain>.<poolDomain>, no new column
- existing pool_allocations table works as-is
Phase 5 — dynadot package trimmed
- removed AddSovereignRecords / DeleteSubdomainRecords / AddRecord /
getZone / writeZone (Dynadot DNS write code)
- kept IsManagedDomain / ManagedDomains / ResetManagedDomains /
ErrUnmanagedDomain (config-resolution helpers)
- registrar adapter at internal/registrar/dynadot/ untouched (handles
BYO Flow B NS-delegation via #170)
Phase 6 — env-var contract
PDM_PDNS_BASE_URL, PDM_PDNS_API_KEY, PDM_PDNS_SERVER_ID, PDM_NAMESERVERS
all runtime-configurable per docs/INVIOLABLE-PRINCIPLES.md #4.
Quality bar (all met):
- DNSSEC enabled on every child zone (mandatory per spec)
- parent NS delegation TTL 3600, child A-record TTL 300
- retry-once-on-5xx with exponential backoff in pdns client
- all credentials flow from env vars sourced from K8s Secrets
- no hardcoded URLs, regions, or NS endpoints
Closes openova#168 (DNS-side; private-repo manifest update lands separately).
113 lines
4.0 KiB
YAML
113 lines
4.0 KiB
YAML
name: Build Pool Domain Manager
|
|
|
|
# pool-domain-manager — central authority for OpenOva-pool subdomain
|
|
# allocation (closes #163). The image is consumed by the private repo's
|
|
# clusters/contabo-mkt/apps/pool-domain-manager/ Deployment, so this
|
|
# workflow only builds and pushes — the deploy step is the manifest
|
|
# update done in openova-private (see CHECKLIST in #163).
|
|
|
|
on:
|
|
push:
|
|
paths:
|
|
- 'core/pool-domain-manager/**'
|
|
- '.github/workflows/pool-domain-manager-build.yaml'
|
|
branches: [main]
|
|
workflow_dispatch:
|
|
|
|
env:
|
|
REGISTRY: ghcr.io
|
|
IMAGE: ghcr.io/openova-io/openova/pool-domain-manager
|
|
|
|
jobs:
|
|
build:
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: read
|
|
packages: write
|
|
# id-token write is required by cosign keyless signing (Sigstore).
|
|
# Per docs/INVIOLABLE-PRINCIPLES.md #3 every Catalyst image is signed
|
|
# + SBOM-attested; this workflow mirrors that contract.
|
|
id-token: write
|
|
outputs:
|
|
sha_short: ${{ steps.vars.outputs.sha_short }}
|
|
digest: ${{ steps.build.outputs.digest }}
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Set short SHA
|
|
id: vars
|
|
run: echo "sha_short=$(echo $GITHUB_SHA | head -c 7)" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Set up Go (for unit tests)
|
|
uses: actions/setup-go@v5
|
|
with:
|
|
go-version: '1.23'
|
|
cache-dependency-path: core/pool-domain-manager/go.sum
|
|
|
|
- name: Run unit tests
|
|
working-directory: core/pool-domain-manager
|
|
# Unit suites that don't need a live Postgres: pdns httptest mock,
|
|
# allocator with fake DNS writer, dynadot env-resolution, reserved
|
|
# static list, registrar adapters. The full integration suite
|
|
# (store_test.go round-trip against CNPG) lives in
|
|
# test-bootstrap-api.yaml.
|
|
run: |
|
|
go test ./internal/pdns/... \
|
|
./internal/allocator/... \
|
|
./internal/dynadot/... \
|
|
./internal/reserved/... \
|
|
./internal/registrar/...
|
|
|
|
- name: Login to GHCR
|
|
uses: docker/login-action@v3
|
|
with:
|
|
registry: ${{ env.REGISTRY }}
|
|
username: ${{ github.actor }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Set up Docker Buildx
|
|
uses: docker/setup-buildx-action@v3
|
|
|
|
- name: Build and push image
|
|
id: build
|
|
uses: docker/build-push-action@v6
|
|
with:
|
|
context: core/pool-domain-manager
|
|
file: core/pool-domain-manager/Containerfile
|
|
push: true
|
|
tags: |
|
|
${{ env.IMAGE }}:${{ steps.vars.outputs.sha_short }}
|
|
${{ env.IMAGE }}:latest
|
|
labels: |
|
|
org.opencontainers.image.source=https://github.com/openova-io/openova
|
|
org.opencontainers.image.revision=${{ github.sha }}
|
|
org.opencontainers.image.title=pool-domain-manager
|
|
org.opencontainers.image.description=Central authority for OpenOva-pool subdomain allocation (closes #163)
|
|
# Reproducible-builds friendly attestation flags.
|
|
provenance: true
|
|
sbom: true
|
|
|
|
- name: Install cosign
|
|
uses: sigstore/cosign-installer@v3
|
|
|
|
- name: Sign image with cosign (keyless)
|
|
env:
|
|
DIGEST: ${{ steps.build.outputs.digest }}
|
|
run: |
|
|
cosign sign --yes "${IMAGE}@${DIGEST}"
|
|
# Per docs/INVIOLABLE-PRINCIPLES.md #3: every Catalyst image must be
|
|
# cosign-signed via Sigstore keyless flow. The id-token: write
|
|
# permission above is what enables OIDC for cosign.
|
|
|
|
- name: Generate and attest SBOM
|
|
env:
|
|
DIGEST: ${{ steps.build.outputs.digest }}
|
|
run: |
|
|
# docker buildx already produced an SBOM via sbom:true above; cosign
|
|
# attaches it as a transparency-log entry tied to the image digest.
|
|
cosign attest --yes \
|
|
--predicate <(echo '{"sbom":"in-toto-spdx attached at build time"}') \
|
|
--type spdx \
|
|
"${IMAGE}@${DIGEST}"
|