Feature Specification: Effect octodns-zone
Created: 2026-10-07
Status: Implemented
Input: Reconstructed from the code at 093e68b6f (core/effects/octodns/effect.nix, core/effects/octodns/octodns_fediversity.py, core/effects/octodns/netbox-zone.py, core/effects/octodns/kv-txt.py, core/effects/octodns/kv-secret.py, core/effects/octodns/served-zone.py, core/render/catalog.nix, core/setups/hosts-common.nix).
The octodns-zone effect publishes the DNS records of one group into one
zone. The zone lives at an external DNS service or at the group's own named.
The effect deploys no nodes. The tf-incus-hosts setup builds it as
setup.octodns (see ../009-deployment-methods/tf-incus.md). The family
behavior of standalone effects is in spec.md. The program grammar, the verb
names and the state rules are in ../004-deployment-programs/spec.md.
User Scenarios & Testing (mandatory)
User Story 1 - Publish the group zone (Priority: P1)
An operator deploys a group. The operator runs plan to see the record
diff. Then the operator runs sync to push the records into the DNS service.
The service then answers for every node and every served name.
Why this priority: Without the records, no user reaches a deployed service by name.
Independent Test: integrations-octodns-powerdns (a whole zone pushed
into an empty PowerDNS zone) and integrations-octodns-bind (the group zone
pushed into the group's own named over RFC2136, then transferred to the
second nameserver).
Acceptance Scenarios:
- Given a rendered zone and a provider token, When the operator runs
plan, Then the effect prints the diff and changes nothing. - Given the same inputs, When the operator runs
sync, Then the DNS service serves the records of the rendered zone. - Given a provider secret that neither
secretFilesnorkvSecretssupplies, When the effect evaluates, Then the eval fails and names the secret.
User Story 2 - Render the zone from NetBox at run time (Priority: P1)
The group uses external IPAM. At eval time every address is a placeholder. The effect asks NetBox for the allocated addresses at run time and renders the zone from them.
Why this priority: Under external IPAM, an eval-time zone holds no real address, so a push from it is wrong.
Independent Test: integrations-octodns-netbox. The choice between
netboxZone and zone is asserted in core/setups/zone-publication-test.nix
(suite nix-unit-setups).
Acceptance Scenarios:
- Given
netboxZonewith a cluster name, Whensyncruns, Then the zone holds one A/AAAA record per NetBox address of that cluster, at a 300 s TTL. - Given a cluster name that NetBox does not hold, When
syncruns, Then the render fails withno NetBox cluster named <name>.
User Story 3 - Share a zone that other parties also write (Priority: P2)
A third party holds the zone and writes records into it. With ownership
on, the effect changes only the records it wrote before. It does not delete
or adopt the other records.
Why this priority: A push into a shared zone must not remove what another party publishes.
Independent Test: integrations-octodns-powerdns (partial-zone
ownership over zones that already hold unowned records).
Acceptance Scenarios:
- Given
ownership = trueand an unowned record in the zone, Whensyncruns, Then the unowned record stays and stderr names it. - Given
ownership = trueand a rendered CNAME or same-type record at an unowned name, Whensyncruns, Then the push fails withUnownedRecordCollision. - Given
ownership = trueand a plan that deletes more than 5% of the records, Whensyncruns, Then OctoDNS refuses the plan.
User Story 4 - Publish a TXT value that a node minted (Priority: P3)
A node writes a public value to OpenBao KV, for example a key that a mail relay generated. No eval can know that value. The effect reads it at run time and appends it as a TXT record.
Why this priority: Some records hold data that exists only after the node starts.
Independent Test: integrations-octodns-kv-txt (every way that the KV
read fails).
Acceptance Scenarios:
- Given
kvTxtandopenbao, When the KV store holds the value, Then the zone holds the TXT record. - Given
kvTxtandopenbao, When one value is missing, Then the render writes the other records and the verb exits non-zero. - Given
OCTODNS_KV_TXT_OPTIONAL=1, When one value is missing, Then the effect skips that record and the verb succeeds.
Edge Cases
netboxZone.clusterNameis empty:netbox-zone.pylooks up addresses without a cluster scope and prints a note on stderr.kvSecretsis set withoutopenbao: the eval fails.kvSecretsandsecretFilesname the same secret: the eval fails.OCTODNS_NO_WITHDRAWALSis set: theNoWithdrawalsprocessor drops every delete from the plan and prints the dropped deletes. The CI publish script sets it for its additive fallback.- The plan changes the apex
NSset:ApexAuthorityaccepts that change but keeps the percentage guards for all other changes. verifycannot reach a nameserver over IPv6, but another address of that nameserver answers:served-zone.pysets the IPv6 failure aside.
Requirements (mandatory)
Functional Requirements
- FR-001: The catalog MUST offer
octodns-zonefor a group only when the group DNS routing is external or the group has DNS nodes. - FR-002: The effect MUST read its coordinates from the env keys of the
tf-incusmethod (envKeysFor "tf-incus"). It MUST also declare the run-time switchesOCTODNS_NO_WITHDRAWALSandOCTODNS_KV_TXT_OPTIONAL, from theoctodnsreader ofcore/effects/common/standalone-env.nix. - FR-003: The effect MUST offer the verbs
validate,plan,syncandverify, andrunMUST besync. - FR-004:
validateMUST runoctodns-validate --allon the rendered configuration. - FR-005:
planMUST runoctodns-syncwithout--doit, andsyncMUST run it with--doit. - FR-006:
verifyMUST ask every nameserver of the zone, without recursion, for every rendered record exceptSOA, and MUST exit 0 when all records are served, 1 on a shortfall and 2 when it cannot ask. - FR-007: The effect MUST take the zone from
zoneat eval time, or fromzoneCommandat run time into$OCTODNS_ZONE_DIR/<domain>. - FR-008: When
netboxZoneorkvTxtis set, the defaultzoneCommandMUST runnetbox-zone.pyand thenkv-txt.py. - FR-009:
netbox-zone.pyMUST write node records at TTL 300, the apexNSat TTL 300, other records at TTL 86400, and a placeholderSOA. - FR-010: The effect MUST pass each provider secret to OctoDNS as the env var
<PROVIDER>_<SECRET>, read from a file at run time, and never put it in the store. - FR-011:
kv-secret.pyMUST read eachkvSecretsentry from OpenBao KV atsecret/<instance>/<file>before OctoDNS runs. - FR-012: The effect MUST support the providers
desec,hetzner,powerdnsandbind(RFC2136 with a TSIG key,hmac-sha256by default). - FR-013: The provider MUST carry
update_pcent_threshold = 1.0, and underownershipalsodelete_pcent_threshold = 0.05. - FR-014: Under
ownership, the processors MUST beownershipand thenno-withdrawals; otherwise they MUST beno-withdrawalsand thenapex-authority. - FR-015: Under
ownership, the effect MUST NOT change, delete or adopt a record that it did not write. - FR-016: When
openbaois set,kv-txt.pyMUST process everykvTxtlabel and then exit non-zero if one read failed. - FR-017: The effect MUST remove its scratch directory on exit.
- FR-018:
zoneMUST have the typedns.lib.types.subzoneof dns.nix: the records of the zone apex and itssubdomains, with noSOAand no zoneTTL. The effect adds theSOAwhen it renders the zone file fordomain. An unknown record type MUST fail at evaluation. - FR-019:
providers.<name>.configurationMUST holdmkOptiondeclarations. Whenprovideris<name>, they are the options of the top-levelconfiguration, which types each value. A value in place of a declaration MUST fail at evaluation.
Key Entities
- Rendered zone: an RFC1035 zone file for
<domain>, fromzoneor fromzoneCommand. - Provider: the DNS service that
syncwrites to, with its secrets. - Ownership marker: the OctoDNS ownership records that mark the records this effect wrote.
- KV TXT value: a public value that a node wrote to OpenBao KV under
kvMount/kvPrefix.
Success Criteria (mandatory)
Measurable Outcomes
- SC-001: After
sync,verifyexits 0 for the rendered zone. - SC-002: A second
planaftersyncshows no change. - SC-003: Under
ownership, the count of unowned records is the same before and aftersync. - SC-004: No provider secret occurs in the store or in the rendered configuration.
- SC-005:
nix-unit-env-configpasses, so the verbs read no env key that the effect does not declare.
Assumptions
- The DNS service API is reachable from the machine that runs the verb.
- Under
netboxZone, the apply of thetf-incusmethod already recorded the addresses in NetBox. - Under
kvTxt, the nodes already wrote their values to OpenBao KV. - The OctoDNS version keeps the percentage thresholds on the provider. Zone-level thresholds are discarded.