jarld checks the DoT and DoH listeners every 10 minutes as a client would (page, dashboard, jarl encrypted-dns health, jarl_encrypted_dns_up). The Compare views tab and jarl zone diff show what differs between a zone's views, with Use this version. --local works on unlinked zones. A changed certificates.dir is followed by encrypted DNS's tls statements. Sharing an already shared version replaces other views' overrides. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
105 lines
2.8 KiB
Go
105 lines
2.8 KiB
Go
package engine
|
|
|
|
import (
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
|
|
"jarl/internal/model"
|
|
)
|
|
|
|
// ViewSet is what one view has for a name and type.
|
|
type ViewSet struct {
|
|
View string `json:"view"`
|
|
ZoneID int64 `json:"zone_id"`
|
|
Records []string `json:"records"` // data (with TTL and "(disabled)" when set), sorted
|
|
IDs []int64 `json:"ids"` // the records, in the same order
|
|
Linked string `json:"linked,omitempty"` // shared or local, in a linked zone
|
|
}
|
|
|
|
// CompareRow is one name and type across views.
|
|
type CompareRow struct {
|
|
Name string `json:"name"` // relative to the zone
|
|
FQDN string `json:"fqdn"`
|
|
Type string `json:"type"`
|
|
Same bool `json:"same"`
|
|
Views []ViewSet `json:"views"` // in the order of the zones given
|
|
}
|
|
|
|
// CompareZones lines up the records of zones of the same name in
|
|
// different views (docs/views.md#linked-zones), one row per name and type,
|
|
// sorted like a zone file.
|
|
func CompareZones(st *model.State, zones []*model.Zone) []CompareRow {
|
|
type key struct{ name, typ string }
|
|
sets := make([]map[key][]*model.Record, len(zones))
|
|
var keys []key
|
|
seen := map[key]bool{}
|
|
var all []*model.Record
|
|
for i, z := range zones {
|
|
sets[i] = map[key][]*model.Record{}
|
|
for _, r := range st.ZoneRecords(z.ID) {
|
|
k := key{r.Name, r.Type}
|
|
sets[i][k] = append(sets[i][k], r)
|
|
if !seen[k] {
|
|
seen[k] = true
|
|
keys = append(keys, k)
|
|
all = append(all, r)
|
|
}
|
|
}
|
|
}
|
|
if len(zones) > 0 {
|
|
// Zone file order: the apex first, then by name and type.
|
|
model.SortRecords(all, zones[0])
|
|
keys = keys[:0]
|
|
for _, r := range all {
|
|
keys = append(keys, key{r.Name, r.Type})
|
|
}
|
|
}
|
|
var out []CompareRow
|
|
for _, k := range keys {
|
|
row := CompareRow{FQDN: k.name, Type: k.typ, Same: true}
|
|
if len(zones) > 0 {
|
|
row.Name = relName(k.name, zones[0].Name)
|
|
}
|
|
var first string
|
|
for i, z := range zones {
|
|
vs := ViewSet{View: st.ViewName(z), ZoneID: z.ID, Records: []string{}, IDs: []int64{}}
|
|
rs := slices.Clone(sets[i][k])
|
|
slices.SortFunc(rs, func(a, b *model.Record) int { return strings.Compare(strings.ToLower(a.Data), strings.ToLower(b.Data)) })
|
|
for _, r := range rs {
|
|
d := r.Data
|
|
if r.TTL != nil {
|
|
d = fmt.Sprintf("%s (TTL %d)", d, *r.TTL)
|
|
}
|
|
if r.Disabled {
|
|
d += " (disabled)"
|
|
}
|
|
vs.Records = append(vs.Records, d)
|
|
vs.IDs = append(vs.IDs, r.ID)
|
|
}
|
|
if z.LinkGroup != 0 && len(rs) > 0 {
|
|
vs.Linked = "shared"
|
|
if rs[0].SharedID == 0 {
|
|
vs.Linked = "local"
|
|
}
|
|
}
|
|
sig := strings.Join(vs.Records, "\n")
|
|
if i == 0 {
|
|
first = sig
|
|
} else if sig != first {
|
|
row.Same = false
|
|
}
|
|
row.Views = append(row.Views, vs)
|
|
}
|
|
out = append(out, row)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func relName(name, zone string) string {
|
|
if name == zone {
|
|
return "@"
|
|
}
|
|
return strings.TrimSuffix(name, "."+zone)
|
|
}
|