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 }