Activates the previously-templated `letsencrypt-dns01-prod` ClusterIssuer
in bp-cert-manager by shipping the missing piece — a Go binary that
satisfies cert-manager's external webhook contract
(`webhook.acme.cert-manager.io/v1alpha1`) against the Dynadot api3.json.
Architecture
============
* `core/pkg/dynadot-client/` — canonical Dynadot HTTP client (shared with
pool-domain-manager and catalyst-dns). Encapsulates the api3.json
transport, command builders, response decoding, and the safe
read-modify-write semantics required to never accidentally wipe a
zone (memory: feedback_dynadot_dns.md). Destructive `set_dns2`
variant is unexported.
* `core/cmd/cert-manager-dynadot-webhook/` — the cert-manager webhook
binary. Implements `Solver.Present` via the client's append-only
`AddRecord` path and `Solver.CleanUp` via the read-modify-write
`RemoveSubRecord` path. Domain allowlist (`DYNADOT_MANAGED_DOMAINS`)
rejects challenges for unmanaged apexes BEFORE any Dynadot call.
* `platform/cert-manager-dynadot-webhook/` — Catalyst-authored Helm
wrapper. Templates Deployment + Service + APIService + serving
Certificate (CA chain via cert-manager Issuer self-signing) +
RBAC + ServiceAccount. Mirrors the standard cert-manager external-
webhook deployment shape.
* `platform/cert-manager/chart/` — flips `dns01.enabled: true` so the
paired ClusterIssuer activates. The interim http01 issuer remains
templated as the rollback path.
Test results
============
core/pkg/dynadot-client — 7 tests PASS (race-clean)
core/cmd/cert-manager-dynadot-... — 9 tests PASS (race-clean)
Test coverage includes a Present/CleanUp round-trip against an
httptest fixture that models Dynadot's zone state, an explicit
unmanaged-domain rejection, a regression preserving a pre-existing
CNAME across the DNS-01 round-trip (the zone-wipe defence), and a
typed-error propagation test that surfaces `ErrInvalidToken` to
cert-manager so the controller will retry.
Helm template smoke render
==========================
`helm template` against the new chart with default values yields 12
resources / 424 lines (APIService, Certificate, ClusterRoleBinding,
Deployment, Issuer, Role, RoleBinding, Service, ServiceAccount). The
modified bp-cert-manager chart still renders both ClusterIssuers
(`letsencrypt-dns01-prod` + `letsencrypt-http01-prod`) with default
values; flipping `certManager.issuers.dns01.enabled=false` is the
clean rollback.
Smoke command (post-deploy)
===========================
kubectl get apiservices.apiregistration.k8s.io \
v1alpha1.acme.dynadot.openova.io
# Issue a *.<sovereign>.<pool> wildcard cert and watch the
# Order/Challenge progress through cert-manager.
CI
==
`.github/workflows/build-cert-manager-dynadot-webhook.yaml` mirrors the
pool-domain-manager-build pattern (cosign keyless signing, SBOM
attestation, GHCR push at `ghcr.io/openova-io/openova/cert-manager-
dynadot-webhook:<sha>`). Triggered by changes to either the binary or
the shared dynadot-client package.
Closes #159
Co-authored-by: hatiyildiz <hatice.yildiz@openova.io>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
43 lines
2.2 KiB
Go
43 lines
2.2 KiB
Go
// Package dynadot is the shared OpenOva client for the Dynadot api3.json
|
|
// REST endpoint. It is the single ground-truth implementation of the
|
|
// Dynadot HTTP transport, command builders, response decoding, and the
|
|
// safe read-modify-write semantics required for record management.
|
|
//
|
|
// The client is consumed by every Catalyst service that has to talk to
|
|
// api.dynadot.com:
|
|
//
|
|
// - core/cmd/cert-manager-dynadot-webhook (DNS-01 wildcard TLS)
|
|
// - core/pool-domain-manager (NS-flip during BYO-domain provisioning)
|
|
// - products/catalyst/bootstrap/api/cmd/catalyst-dns (Sovereign A-record set)
|
|
//
|
|
// Why a separate Go module under core/pkg/:
|
|
//
|
|
// - The legacy clients live under each consumer's `internal/` tree, so
|
|
// they cannot be imported across module boundaries (Go's internal-
|
|
// package rule). Hosting the canonical client at core/pkg/ makes it
|
|
// visible to every service module without breaking the convention
|
|
// that service-private code stays in `internal/`.
|
|
// - A standalone module keeps the dependency surface tiny: this package
|
|
// uses only the standard library, so the consumers don't transitively
|
|
// pick up Postgres / chi / etc. when all they need is a single API
|
|
// call.
|
|
// - Per docs/INVIOLABLE-PRINCIPLES.md #3 (one canonical implementation
|
|
// per concern), all future Dynadot work should land here. The legacy
|
|
// copies under products/catalyst/bootstrap/api/internal/dynadot and
|
|
// core/pool-domain-manager/internal/registrar/dynadot remain in
|
|
// place at the time of writing — they will be migrated to this
|
|
// package in a follow-up. Do not extend them; extend this package.
|
|
//
|
|
// Safety contract — the package enforces the operator-memory rule that
|
|
// `set_dns2` calls without `add_dns_to_current_setting=yes` wipe the
|
|
// entire zone. Every record-mutating helper in this package either:
|
|
//
|
|
// 1. Uses `add_dns_to_current_setting=yes` (append-only path), or
|
|
// 2. Performs a read-modify-write against `domain_info` and writes the
|
|
// reconstructed full record set so no record is ever silently
|
|
// dropped.
|
|
//
|
|
// The caller cannot accidentally invoke the destructive variant — that
|
|
// command builder is unexported.
|
|
package dynadot
|