Defdo.DDNS.Adoption
(defdo_ddns v0.5.0)
Copy Markdown
Durable holding area for records that exist in Cloudflare but were never declared here.
Discovery is automatic; adoption never is. A record found by
Defdo.DDNS.Reconcile.Inventory enters as pending and stays there until an
operator decides. There is no configuration flag that skips that step — the
whole point is that absorbing someone else's record, or deciding a record is
ours to converge, is a judgement call with consequences.
Three states, and the transitions are one-way:
pending ──accept──▶ accepted
└─reject──▶ rejectedA rejected record must never resurface as pending, or every refresh would ask
the same question again and the answer would stop being read. That is why an
entry is keyed by a derived, stable id rather than something regenerated per
run, and why refresh/1 only ever inserts entries it has never seen.
Accepting records the decision and promotes the record into
Defdo.DDNS.DesiredStateStore, atomically — see accept/2. Adoption therefore
requires the desired-state file to be configured: without somewhere durable to
promote into, "accepted" would be a label with no effect on what DDNS
converges.
Summary
Functions
Accept a pending record: record the decision, then promote it into desired state so the monitor starts converging it.
The stable id for a record: "<type>:<name>", lowercased.
Fetch one entry by id.
Entries in a given state, or all of them. Ordered by id for stable output.
Where the adoption file lives.
Run an inventory and file every undeclared record not already known.
Reject a pending record. Durable: it never returns to pending.
Types
@type entry() :: map()
Functions
Accept a pending record: record the decision, then promote it into desired state so the monitor starts converging it.
Both halves or neither. If the desired-state write fails the decision rolls back to pending, because "accepted but absent from desired state" is indistinguishable from a rejection at the next sync — the record would be silently dropped while the log said it was adopted.
Deciding twice is a no-op returning the existing entry, not an error: the intent is already recorded, and failing would only make an idempotent workflow look broken. Promotion is idempotent too — a record already declared is left alone rather than duplicated.
The stable id for a record: "<type>:<name>", lowercased.
Derived rather than random, so rediscovering the same record maps to the same entry and a decision about it survives.
Fetch one entry by id.
@spec list(:pending | :accepted | :rejected | :all) :: [entry()]
Entries in a given state, or all of them. Ordered by id for stable output.
@spec path() :: String.t()
Where the adoption file lives.
@spec refresh(String.t()) :: {:ok, %{added: non_neg_integer(), unchanged: non_neg_integer()}} | {:error, term()}
Run an inventory and file every undeclared record not already known.
Returns {:ok, %{added: n, unchanged: n}}. An inventory failure propagates
unchanged and writes nothing: a transient Cloudflare error must not be able to
file adoptions, and it must not be able to look like the zone went quiet.
Reject a pending record. Durable: it never returns to pending.