package engine import ( "fmt" "reflect" "sort" "jarl/internal/dnsname" "jarl/internal/model" ) // DiffOps returns operations that turn from into to. It is used to record // how to undo an applied changeset and to turn an imported configuration // into a changeset. Create operations keep the object IDs of to. func DiffOps(from, to *model.State) []model.Op { var ops []model.Op add := func(kind string, id int64, base int, data any, summary string) { ops = append(ops, newOp(kind, id, base, data, summary)) } if !reflect.DeepEqual(stripVersion(from.Settings), stripVersion(to.Settings)) { s := to.Settings add(OpSettingsUpdate, 0, 0, SettingsInput{DefaultTTL: &s.DefaultTTL, Nameservers: model.Ptr(append([]string{}, s.Nameservers...)), RName: &s.RName, Refresh: &s.Refresh, Retry: &s.Retry, Expire: &s.Expire, NegativeTTL: &s.NegativeTTL, SerialPolicy: &s.SerialPolicy}, "restore settings") } for _, k := range sortedKeys(to.Keys) { n := to.Keys[k] in := TSIGInput{Name: &n.Name, Algorithm: &n.Algorithm, Secret: &n.Secret, Comment: &n.Comment} if o, ok := from.Keys[k]; !ok { add(OpTSIGCreate, k, 0, in, "create TSIG key "+n.Name) } else if !sameObject(o, n) { add(OpTSIGUpdate, k, o.Version, in, "restore TSIG key "+n.Name) } } for _, k := range sortedKeys(to.ACLs) { n := to.ACLs[k] in := ACLInput{Name: &n.Name, Elements: model.Ptr(append([]string{}, n.Elements...)), Comment: &n.Comment} if o, ok := from.ACLs[k]; !ok { add(OpACLCreate, k, 0, in, "create ACL "+n.Name) } else if !sameObject(o, n) { add(OpACLUpdate, k, o.Version, in, "restore ACL "+n.Name) } } for _, k := range sortedKeys(to.Views) { n := to.Views[k] in := ViewInput{Name: &n.Name, MatchClients: model.Ptr(append([]string{}, n.MatchClients...)), MatchDestinations: model.Ptr(append([]string{}, n.MatchDestinations...)), Comment: &n.Comment, Position: model.Ptr(n.Position)} if n.MatchRecursiveOnly != nil { in.MatchRecursiveOnly = model.Some(*n.MatchRecursiveOnly) } else { in.MatchRecursiveOnly = model.Opt[bool]{Set: true, Null: true} } if o, ok := from.Views[k]; !ok { add(OpViewCreate, k, 0, in, "create view "+n.Name) } else if !sameObject(o, n) { add(OpViewUpdate, k, o.Version, in, "restore view "+n.Name) } } for _, k := range sortedKeys(to.Zones) { n := to.Zones[k] s := n.SOA in := ZoneInput{Name: &n.Name, ViewID: &n.ViewID, Type: &n.Type, InView: &n.InView, File: &n.File, DefaultTTL: &n.DefaultTTL, SerialPolicy: &n.SerialPolicy, DNSSECPolicy: &n.DNSSECPolicy, Comment: &n.Comment} if n.Type == model.ZonePrimary { in.SOA = &SOAInput{MName: &s.MName, RName: &s.RName, Serial: &s.Serial, Refresh: &s.Refresh, Retry: &s.Retry, Expire: &s.Expire, Minimum: &s.Minimum} } if o, ok := from.Zones[k]; !ok { add(OpZoneCreate, k, 0, in, "create zone "+zoneDesc(to, n)) } else if !sameObject(o, n) { in.Name = nil // zones can't be renamed if n.Type == model.ZonePrimary && o.SOA.Serial != s.Serial { // Never move a serial backwards: let the normal bump // happen instead. in.SOA.Serial = nil } add(OpZoneUpdate, k, o.Version, in, "restore zone "+zoneDesc(to, n)) } } for _, k := range sortedKeys(to.Options) { n := to.Options[k] in := OptionInput{Scope: &n.Scope, ScopeID: &n.ScopeID, Name: &n.Name, Value: &n.Value, Comment: &n.Comment, Position: &n.Position} if o, ok := from.Options[k]; !ok { add(OpOptionCreate, k, 0, in, "set "+optionLabel(to, n)+" "+n.Value) } else if !sameObject(o, n) { in.Scope, in.ScopeID = nil, nil add(OpOptionUpdate, k, o.Version, in, "restore "+optionLabel(to, n)) } } for _, k := range sortedKeys(to.Records) { n := to.Records[k] in := RecordInput{ZoneID: n.ZoneID, Name: &n.Name, Type: &n.Type, Data: &n.Data, Comment: &n.Comment, Disabled: &n.Disabled} if n.TTL != nil { in.TTL = model.Some(*n.TTL) } else { in.TTL = model.Opt[uint32]{Set: true, Null: true} } if o, ok := from.Records[k]; !ok { add(OpRecordCreate, k, 0, in, fmt.Sprintf("add %s %s %s", n.Name, n.Type, n.Data)) } else if !sameObject(o, n) { in.ZoneID = 0 add(OpRecordUpdate, k, o.Version, in, fmt.Sprintf("restore %s %s %s", n.Name, n.Type, n.Data)) } } // Deletions, most dependent objects first. for _, k := range sortedKeys(from.Records) { if _, ok := to.Records[k]; !ok { o := from.Records[k] // Records of zones that are deleted go with the zone. if _, zoneStays := to.Zones[o.ZoneID]; zoneStays { add(OpRecordDelete, k, o.Version, nil, fmt.Sprintf("delete %s %s %s", o.Name, o.Type, o.Data)) } } } for _, k := range sortedKeys(from.Options) { if _, ok := to.Options[k]; !ok { o := from.Options[k] stays := true switch o.Scope { case model.ScopeZone: _, stays = to.Zones[o.ScopeID] case model.ScopeView: _, stays = to.Views[o.ScopeID] } if stays { add(OpOptionDelete, k, o.Version, nil, "delete "+optionLabel(from, o)) } } } for _, k := range sortedKeys(from.Zones) { if _, ok := to.Zones[k]; !ok { o := from.Zones[k] add(OpZoneDelete, k, o.Version, nil, "delete zone "+dnsname.Display(o.Name)) } } for _, k := range sortedKeys(from.Views) { if _, ok := to.Views[k]; !ok { o := from.Views[k] add(OpViewDelete, k, o.Version, DeleteInput{Cascade: true}, "delete view "+o.Name) } } for _, k := range sortedKeys(from.ACLs) { if _, ok := to.ACLs[k]; !ok { add(OpACLDelete, k, from.ACLs[k].Version, nil, "delete ACL "+from.ACLs[k].Name) } } for _, k := range sortedKeys(from.Keys) { if _, ok := to.Keys[k]; !ok { add(OpTSIGDelete, k, from.Keys[k].Version, nil, "delete TSIG key "+from.Keys[k].Name) } } for i := range ops { ops[i].Seq = i + 1 } return ops } func stripVersion(s model.Settings) model.Settings { s.Version = 0 return s } func sortedKeys[T any](m map[int64]*T) []int64 { keys := make([]int64, 0, len(m)) for k := range m { keys = append(keys, k) } sort.Slice(keys, func(i, j int) bool { return keys[i] < keys[j] }) return keys } // sameObject compares two objects ignoring their meta data. func sameObject[T any](a, b *T) bool { ca, cb := *a, *b clearMeta(&ca) clearMeta(&cb) return reflect.DeepEqual(ca, cb) } func clearMeta(v any) { rv := reflect.ValueOf(v).Elem() if f := rv.FieldByName("Meta"); f.IsValid() { f.Set(reflect.Zero(f.Type())) } }