Documentation
¶
Overview ¶
Package apply is the write path, and the only thing that changes zone data.
A client submits a Command. The applier works out what it amounts to against the zone as it stands, writes it, and records it as one journal.Commit in the same transaction. Nothing else advances a zone serial, and no record is written without a commit explaining it (invariant 4).
Three cases are returned to the caller rather than logged:
- the address already answers with another name (D3). The existing entry stays and Conflict reports it.
- no zone exists to hold the entry (D6). MissingZone names the zone that would be needed, since creating one asserts authority over a namespace.
- a different entry is wanted (D4). Editing a generated record is refused; ActionDetach hands it over instead.
RFC 2317 is handled on both sides. An entry for an address inside a classless child goes under that child's apex, and where this server also holds the parent, the CNAME pointing there is written too (D7). Provenance is a chain, delegation to entry to address record, and removals are ordered from the links rather than assumed.
A reverse zone created for a network already in use has no change to react to. Applier.Reconcile fills it, and moves what the arrival of a more specific zone has overtaken. It only adds: obsolete entries were removed by the change that obsoleted them, and a detached entry (D4) is not the automation's to touch.
Index ¶
- Constants
- func SetStoredPolicy(ctx context.Context, w store.Writer, p Policy) error
- type Applier
- func (a *Applier) Apply(ctx context.Context, cmd Command) (*Result, error)
- func (a *Applier) CreateZone(ctx context.Context, z *zone.Zone, records []zone.Record, meta Meta) (*Result, error)
- func (a *Applier) DeleteZone(ctx context.Context, zid zone.ZoneID, meta Meta) (*Result, error)
- func (a *Applier) Import(ctx context.Context, in Import, meta Meta) (*Result, error)
- func (a *Applier) Policy(ctx context.Context, r store.Reader) (Policy, error)
- func (a *Applier) Reconcile(ctx context.Context, zid zone.ZoneID, meta Meta) (*Result, error)
- func (a *Applier) Rollback(ctx context.Context, zid zone.ZoneID, target zone.Serial, meta Meta) (*Result, error)
- func (a *Applier) UpdateZone(ctx context.Context, z *zone.Zone, meta Meta) (*Result, error)
- type Command
- type Conflict
- type Import
- type Meta
- type MissingZone
- type OpAction
- type Options
- type Policy
- type RecordOp
- type Result
- type Skipped
Constants ¶
const PolicySetting = "reverse_policy"
PolicySetting is the key the server-wide reverse conflict policy is stored under.
It lives in the database rather than in the configuration file because an operator changes it and every client has to be able to reach it (docs/decisions.md D11). Reading it inside the write transaction is what makes a change take effect on the next write instead of the next restart.
Variables ¶
This section is empty.
Functions ¶
Types ¶
type Applier ¶
type Applier struct {
// contains filtered or unexported fields
}
Applier is the only thing that changes zone data.
Every write goes through it, and every write it makes is recorded as a commit in the same transaction (architecture invariant 4). Nothing else advances a zone serial. That is what lets the audit log, the diff view, rollback and incremental transfer all read from one structure instead of four.
func (*Applier) CreateZone ¶
func (a *Applier) CreateZone( ctx context.Context, z *zone.Zone, records []zone.Record, meta Meta, ) (*Result, error)
CreateZone brings a zone into existence together with its first records, and records it as one commit.
The zone has to be usable when the call returns: at least one NS record at the apex (RFC 1034 §4.2.1). A zone with none names no authority for itself and no parent can delegate to it, and refusing it here means every zone in the database is one that can actually be answered from. Supplying a sensible default, usually an NS pointing at the SOA's primary, is the job of whatever is talking to the person, not of the write path.
The zone's own serial is where the journal starts counting. An import therefore keeps the serial the zone already had elsewhere, which its secondaries have seen; see docs/decisions.md D2.
func (*Applier) DeleteZone ¶
DeleteZone removes a zone and everything in it, and records that it happened.
func (*Applier) Import ¶
Import brings a whole zone in at once.
The zone must not already exist. A file is the complete contents of a zone, so importing into a zone that already holds records is a replacement rather than an import, and doing that silently would be the difference between gaining a zone and losing one.
func (*Applier) Policy ¶
Policy returns the reverse conflict policy in force, which is what a client asking to see the settings is told.
func (*Applier) Reconcile ¶
Reconcile writes the reverse entries a zone's records imply but does not yet have.
It exists because reverse automation reacts to changes, and a zone that arrives after the records is a case with no change to react to. Creating a reverse zone for a network whose addresses are already in use is exactly that: the entries would appear one at a time as each address record happened to be edited, which is no answer at all. So the zone is created (never as a side effect, always because somebody asked (D6)) and then filled.
It only adds. A generated entry that should no longer exist is already taken away by the change that made it obsolete, and one that somebody detached is theirs to keep (D4): removing it here would make detaching mean nothing.
func (*Applier) Rollback ¶
func (a *Applier) Rollback( ctx context.Context, zid zone.ZoneID, target zone.Serial, meta Meta, ) (*Result, error)
Rollback restores a zone to the state it had at a serial.
It moves forward to that state rather than rewinding to it: the difference between the zone as it now stands and the zone as it stood then is written as a new commit, marked as a rollback and naming the serial it restores. History stays append-only, which is not tidiness but correctness: a secondary that has already seen serial 90 will never accept a jump back to 42, because RFC 1982 arithmetic makes 42 older and RFC 1995 has no way to express going backwards. See data model §3.7.
Records the server generated are not restored. They are derived from other records, and the automation puts them back from whatever those now say; a rollback that also wrote the derived copies would be racing the thing that owns them.
func (*Applier) UpdateZone ¶
UpdateZone replaces a zone's own settings, leaving its records alone.
It returns nil when nothing changed. Where something did, the serial advances like any other change: a zone's settings include the SOA timers, which are served, and whether the zone answers at all.
type Command ¶
type Command struct {
ZoneID zone.ZoneID
// Ops are the authored operations, applied in order.
Ops []RecordOp
// ExpectedSerial enables optimistic concurrency: the command is refused if
// the zone has moved on since it was read. Nil skips the check. It is a
// pointer because serial 0 is a legal serial and could not otherwise be
// told apart from "do not check".
ExpectedSerial *zone.Serial
Kind journal.Kind
Source journal.Source
Actor string
Comment string
}
Command is the intent a client submits, before validation against the zone and before reverse automation expands it.
It is the unit Raft will replicate; see docs/adr/0002-journal-as-command-log.md. That is why the applier fills in everything undetermined, identifiers above all, before it starts, rather than while applying: a command has to produce the same result on every node that applies it.
type Conflict ¶
type Conflict struct {
// Source is the address record whose reverse entry was not created.
Source zone.RecordID
// SourceName is the name that record sits at, and Address what it points at.
SourceName zone.Name
Address netip.Addr
// ReverseZone and ReverseName are where the entry would have gone.
ReverseZone zone.ZoneID
ReverseName zone.Name
// Existing is the name the address currently reverses to, and Generated
// says whether the record holding it is one the server made. An entry
// someone wrote by hand is never taken away from them.
Existing zone.Name
Generated bool
Policy Policy
}
Conflict is a reverse entry that was not created because the address already answers with a different name.
It is returned to the caller rather than logged: several names pointing at one address is the normal case (virtual hosts, a load balancer, a service alias) and a conflict only visible in the server log is the same as no conflict detection at all (D3).
type Import ¶
type Import struct {
// Name is the zone apex.
Name zone.Name
// SOA is the zone's own settings, serial included. The serial is the one
// the zone had wherever it came from, and it is kept: starting a migrated
// zone at 1 would make every existing secondary consider our copy older
// than what it already has (RFC 1982 §3.2) and refuse to transfer it. See
// docs/decisions.md D2.
SOA zone.SOA
// Records is everything the zone holds, apart from the SOA.
Records []zone.Record
// DefaultTTL is what a record added later gets when it names no TTL. Zero
// takes the SOA's own TTL, which for a file is what its $TTL directive
// most likely said.
DefaultTTL zone.TTL
}
Import is a whole zone arriving at once.
It is deliberately not a zonefile. A file is one way a zone arrives and a declarative configuration is another, and neither belongs in the write path: zonefiles are an import and export format, not a storage format (architecture invariant 5). What arrives here is a name, the zone's own settings and its records.
type Meta ¶
Meta is who asked for a change and why. Every commit carries it, because an audit log that cannot say who did something is a list of mysteries.
type MissingZone ¶
type MissingZone struct {
Source zone.RecordID
SourceName zone.Name
Address netip.Addr
// Suggested is the reverse zone that would cover the address, at the
// boundary such zones are conventionally delegated on: a /24 for IPv4 and a
// /64 for IPv6.
Suggested zone.Name
Prefix netip.Prefix
}
MissingZone reports that an address has no reverse zone to put an entry in.
Creating one is an assertion of authority over a namespace, and doing that as a side effect of adding a record would be a surprise: for public address space it would be wrong. So the caller is told which zone would be needed and decides (D6).
type OpAction ¶
type OpAction string
OpAction is what one step of a Command asks for.
const ( // ActionAdd adds one record. ActionAdd OpAction = "add" // ActionUpdate replaces one record's content, keeping its identity so that // its comment, its provenance and the diff line pointing at it survive. ActionUpdate OpAction = "update" // ActionDelete removes one record. ActionDelete OpAction = "delete" // ActionReplaceRRset makes an RRset exactly the given set of records. An // empty set removes it. This is what a record editor submits, because it // is the unit DNS answers with (RFC 2181 §5). ActionReplaceRRset OpAction = "replace_rrset" // ActionDetach turns a generated record into an authored one. It keeps its // data and its identity, loses its link to the record it came from, and the // automation stops touching it. See docs/decisions.md D4. ActionDetach OpAction = "detach" )
type Options ¶
type Options struct {
// Now supplies the time a commit is stamped with. Nil uses [time.Now];
// tests pass their own so that the stamp is theirs to predict.
Now func() time.Time
// AutoReverse is whether reverse entries are generated for zones that do
// not say either way. Nil means on, so a caller that does not mention the
// automation gets it: it is the product's headline feature, and a zero
// value that switched it off would turn every forgotten option into a
// silently half-working server. Only a caller that means to switch it off
// writes it down. A zone overrides this on itself.
AutoReverse *bool
// Policy is what to do when an address already answers a reverse lookup
// with another name. Empty means [PolicyFirstWins].
Policy Policy
}
Options configure an applier.
type Policy ¶
type Policy string
Policy says what to do when an address already answers a reverse lookup with some other name. See docs/decisions.md D3.
const ( // PolicyFirstWins keeps the reverse entry that is already there and reports // a conflict. It is the default, and the only setting that never changes an // answer nobody asked to change. PolicyFirstWins Policy = "first-wins" // PolicyLastWins replaces a generated entry with the new one, still // reporting the conflict. An entry someone wrote by hand is never replaced. PolicyLastWins Policy = "last-wins" // PolicyMulti keeps both. It is the most literal reading of "generate the // reverse entry", and it turns a routine change into a multi-record PTR set // that reverse-lookup checks are not built for. PolicyMulti Policy = "multi" // PolicyReject refuses the whole change. PolicyReject Policy = "reject" )
func StoredPolicy ¶
StoredPolicy returns the policy the database holds, or the empty policy when nothing has been set.
An unreadable or unknown value is an error rather than a silent fallback: a server that quietly ignores the policy an operator asked for is worse than one that refuses to write until the value is fixed.
type RecordOp ¶
type RecordOp struct {
Action OpAction
// RecordID addresses the record for [ActionUpdate] and [ActionDelete].
RecordID zone.RecordID
// Record is the content for [ActionAdd] and [ActionUpdate].
Record *zone.Record
// Key names the RRset for [ActionReplaceRRset]. It is given separately from
// Records because removing an RRset entirely leaves no record to read it
// from.
Key zone.RRsetKey
// Records is what the RRset should hold after [ActionReplaceRRset]. Empty
// removes the set.
Records []zone.Record
}
RecordOp is one authored change inside a Command.
type Result ¶
type Result struct {
// Commits are the commits written, one per zone the change reached, in the
// order the zones were touched. It is empty when nothing changed.
Commits []*journal.Commit
Conflicts []Conflict
MissingZones []MissingZone
// Skipped are records an import left out because the zone could never
// answer with them. Empty for every other kind of change.
Skipped []Skipped
}
Result is what a change produced.
Conflicts and missing zones are returned rather than logged, because both are things a person has to decide about and neither is an error: a conflict means the address already answers with another name, and a missing zone means there is nowhere to put a reverse entry. See docs/decisions.md D3 and D6.
type Skipped ¶
Skipped is a record an import did not write, and the reason.
It travels as data rather than as an error, for the reason docs/decisions.md D3 and D6 give about conflicts and missing reverse zones: this is something a person has to decide about, and refusing the whole file over it would mean nobody can migrate a zone that has an oddity in it.