Commit Graph

2 Commits

Author SHA1 Message Date
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
hatiyildiz
31b03ce02a ci(pdm)+platform(crossplane): build workflow + XDynadotPoolAllocation composition (Phase 3+4 of #163)
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
2026-04-29 06:46:11 +02:00