openova/.github/workflows/pool-domain-manager-build.yaml
hatiyildiz a6fb7410f4 feat(pdm): per-Sovereign PowerDNS zones for #168
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).
2026-04-29 08:36:45 +02:00

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}"