Files
dns-autoresolver/internal/diff/diff.go
T

126 lines
3.6 KiB
Go

package diff
import "github.com/vasyakrg/dns-autoresolver/internal/model"
type ChangeKind string
const (
InSync ChangeKind = "in_sync"
Add ChangeKind = "add"
Update ChangeKind = "update"
Delete ChangeKind = "delete"
)
// RecordDiff describes one RRset's deviation between template and zone.
type RecordDiff struct {
Kind ChangeKind
Type model.RecordType
Name string
Desired *model.Record // nil for Delete
Actual *model.Record // nil for Add
ReadOnly bool // NS/SOA — shown but never applied
}
type Changeset struct {
Diffs []RecordDiff
}
// Actionable returns managed diffs that are not in sync.
func (c Changeset) Actionable() []RecordDiff {
var out []RecordDiff
for _, d := range c.Diffs {
if d.ReadOnly || d.Kind == InSync {
continue
}
out = append(out, d)
}
return out
}
// Updates returns managed (non-ReadOnly) diffs that add or bring a record in
// line with the template (Kind == Add or Kind == Update). It never contains
// Delete diffs, so it is safe for callers to apply automatically without
// risking data loss from an incomplete or empty template.
func (c Changeset) Updates() []RecordDiff {
var out []RecordDiff
for _, d := range c.Diffs {
if d.ReadOnly {
continue
}
if d.Kind == Add || d.Kind == Update {
out = append(out, d)
}
}
return out
}
// Prunes returns managed (non-ReadOnly) diffs where a record exists in the
// zone but is absent from the template (Kind == Delete). These are
// potentially destructive: an incomplete or empty template would surface
// every managed zone record here. Callers should require explicit
// confirmation before applying Prunes(), unlike Updates().
//
// Updates() and Prunes() partition Actionable(): every diff in Actionable()
// appears in exactly one of the two.
func (c Changeset) Prunes() []RecordDiff {
var out []RecordDiff
for _, d := range c.Diffs {
if d.ReadOnly {
continue
}
if d.Kind == Delete {
out = append(out, d)
}
}
return out
}
// Diff compares a template against the actual zone records.
// Records present in the zone but absent from the template yield Delete.
func Diff(template, actual []model.Record) Changeset {
current := index(actual)
seen := make(map[string]bool, len(template))
var diffs []RecordDiff
for _, t := range template {
tt := t
key := tt.Key()
seen[key] = true
ro := !tt.Type.Managed()
if a, ok := current[key]; ok {
ac := a
kind := Update
if tt.Equal(ac) {
kind = InSync
}
diffs = append(diffs, RecordDiff{Kind: kind, Type: tt.Type, Name: tt.Name, Desired: &tt, Actual: &ac, ReadOnly: ro})
} else {
diffs = append(diffs, RecordDiff{Kind: Add, Type: tt.Type, Name: tt.Name, Desired: &tt, ReadOnly: ro})
}
}
for _, a := range actual {
ac := a
if seen[ac.Key()] {
continue
}
diffs = append(diffs, RecordDiff{Kind: Delete, Type: ac.Type, Name: ac.Name, Actual: &ac, ReadOnly: !ac.Type.Managed()})
}
return Changeset{Diffs: diffs}
}
// index builds a lookup of records by Key(). If two records in recs share
// the same Key() (e.g. a provider returning a duplicate RRset entry), this
// is a deliberate, documented choice, not an oversight: the LAST record with
// that Key() wins, since later map assignments overwrite earlier ones for
// the same key during the loop below. Diff() only ever sees the winning
// (last) record for a duplicated key; earlier duplicates are silently
// collapsed. See TestIndexDedupLastWriteWins in diff_test.go, which pins
// this behavior down.
func index(recs []model.Record) map[string]model.Record {
m := make(map[string]model.Record, len(recs))
for _, r := range recs {
m[r.Key()] = r
}
return m
}