Linked zones: shared records kept in step across views
Zones of the same name in different views can be linked. Shared records
are kept in step: a change in one view reaches the others in the same
changeset, as explicit operations, so the diff, permission checks,
history and undo cover every view. Records local to a view override the
shared records of that name and type there; removing the override brings
the shared ones back, and sharing a view's version makes it every view's.
Each zone keeps its own options, serial and file.
Linking existing zones changes no answer: what is the same everywhere
becomes shared, the rest stays local. Unlinking keeps every record, all
local. ACME challenges are written once per link, and --view all changes
a link once. jarl zone link/unlink, zone create --link-with, record
--local/--shared, LINK column, web UI controls, POST /zones/{zone}/link
and /unlink, validation checks linked_*, store migration 11, docs and
e2e.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6313fa51f8
commit
057433c198
31 files changed
+1729
-55
No files matched your search
@@ -0,0 +1,101 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// Linked zones (docs/views.md#linked-zones): zones of the same name in
|
||||
// different views whose shared records Jarl keeps in step. Each zone
|
||||
// keeps its own copy of every shared record (with the same SharedID), so
|
||||
// named, validation and rendering see ordinary zones. A record with
|
||||
// SharedID 0 in a linked zone is local to its view: an override of the
|
||||
// shared records of the same name and type there.
|
||||
|
||||
// LinkMembers returns the zones of a link group in view order.
|
||||
func (s *State) LinkMembers(group int64) []*Zone {
|
||||
if group == 0 {
|
||||
return nil
|
||||
}
|
||||
var out []*Zone
|
||||
for _, z := range s.Zones {
|
||||
if z.LinkGroup == group {
|
||||
out = append(out, z)
|
||||
}
|
||||
}
|
||||
s.SortByView(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// LinkedWith returns the other zones z is linked with, in view order.
|
||||
func (s *State) LinkedWith(z *Zone) []*Zone {
|
||||
var out []*Zone
|
||||
for _, o := range s.LinkMembers(z.LinkGroup) {
|
||||
if o.ID != z.ID {
|
||||
out = append(out, o)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// AnyLinked reports whether any zone is linked.
|
||||
func (s *State) AnyLinked() bool {
|
||||
for _, z := range s.Zones {
|
||||
if z.LinkGroup != 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// SortByView sorts zones in view order.
|
||||
func (s *State) SortByView(zs []*Zone) {
|
||||
pos := func(z *Zone) int {
|
||||
if v := s.Views[z.ViewID]; v != nil {
|
||||
return v.Position
|
||||
}
|
||||
return -1
|
||||
}
|
||||
sort.Slice(zs, func(i, j int) bool {
|
||||
if a, b := pos(zs[i]), pos(zs[j]); a != b {
|
||||
return a < b
|
||||
}
|
||||
return zs[i].ID < zs[j].ID
|
||||
})
|
||||
}
|
||||
|
||||
// RRset returns the records of one name and type in a zone, by ID.
|
||||
func (s *State) RRset(zoneID int64, name, typ string) []*Record {
|
||||
var out []*Record
|
||||
for _, r := range s.Records {
|
||||
if r.ZoneID == zoneID && r.Name == name && r.Type == typ {
|
||||
out = append(out, r)
|
||||
}
|
||||
}
|
||||
slices.SortFunc(out, func(a, b *Record) int { return int(a.ID - b.ID) })
|
||||
return out
|
||||
}
|
||||
|
||||
// HasLocal reports whether a linked zone overrides the shared records of
|
||||
// one name and type with records of its own.
|
||||
func (s *State) HasLocal(zoneID int64, name, typ string) bool {
|
||||
for _, r := range s.Records {
|
||||
if r.ZoneID == zoneID && r.Name == name && r.Type == typ && r.SharedID == 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// SharedRecords returns a zone's shared records of one name and type,
|
||||
// by SharedID.
|
||||
func (s *State) SharedRecords(zoneID int64, name, typ string) []*Record {
|
||||
var out []*Record
|
||||
for _, r := range s.Records {
|
||||
if r.ZoneID == zoneID && r.Name == name && r.Type == typ && r.SharedID != 0 {
|
||||
out = append(out, r)
|
||||
}
|
||||
}
|
||||
slices.SortFunc(out, func(a, b *Record) int { return int(a.SharedID - b.SharedID) })
|
||||
return out
|
||||
}
|
||||
@@ -166,6 +166,10 @@ type Zone struct {
|
||||
// Disabled zones stay in Jarl, records and all, but named doesn't
|
||||
// load them: their zone statement is left out of named.conf.
|
||||
Disabled bool `json:"disabled,omitempty"`
|
||||
// LinkGroup links zones of the same name in different views: Jarl
|
||||
// keeps their shared records in step (docs/views.md#linked-zones). 0
|
||||
// for a zone that isn't linked; linked zones share the ID.
|
||||
LinkGroup int64 `json:"link_group,omitempty"`
|
||||
Meta
|
||||
}
|
||||
|
||||
@@ -195,6 +199,10 @@ type Record struct {
|
||||
// Disabled records stay in Jarl but are left out of the zone (the
|
||||
// zone file lists them as comments).
|
||||
Disabled bool `json:"disabled,omitempty"`
|
||||
// SharedID ties the copies of a shared record in linked zones
|
||||
// together (the same in every copy). 0 for a record of a zone that
|
||||
// isn't linked, and for a record local to its view in one that is.
|
||||
SharedID int64 `json:"shared_id,omitempty"`
|
||||
Meta
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user