package validate import ( "fmt" "strings" "jarl/internal/dnsname" "jarl/internal/model" ) // checkLinked checks linked zones (docs/views.md#linked-zones): what can // be linked, and that their shared records are in step. Jarl keeps them // in step itself; they can only drift when a change was staged before the // zones were linked (or by editing the database). func (c *checker) checkLinked() { groups := map[int64][]*model.Zone{} for _, z := range c.s.SortedZones() { if z.LinkGroup != 0 { groups[z.LinkGroup] = append(groups[z.LinkGroup], z) } } for _, g := range sortedGroupIDs(groups) { zs := c.s.LinkMembers(g) label := func(z *model.Zone) string { if v := c.s.ViewName(z); v != "" { return dnsname.Display(z.Name) + " [" + v + "]" } return dnsname.Display(z.Name) } bad := false for _, z := range zs { switch { case z.Type != model.ZonePrimary: c.errf("linked_zone_type", "zone", z.ID, "zone %s is linked, but only primary zones can be: unlink it", label(z)) bad = true case z.Name != zs[0].Name: c.errf("linked_zone_name", "zone", z.ID, "zone %s is linked with %s; only zones of the same name can be linked", label(z), label(zs[0])) bad = true } } if len(zs) == 1 { c.infof("linked_zone_alone", "zone", zs[0].ID, "zone %s is linked with no other zone; unlink it (jarl zone unlink)", label(zs[0])) continue } if bad { continue } // Shared and local records of one name and type can't mix in a zone. type key struct{ name, typ string } keys := map[key]bool{} var order []key for _, z := range zs { for _, r := range c.s.ZoneRecords(z.ID) { k := key{r.Name, r.Type} if !keys[k] { keys[k] = true order = append(order, k) } } } for _, k := range order { var truth []*model.Record var truthZone *model.Zone for _, z := range zs { rs := c.s.RRset(z.ID, k.name, k.typ) local, shared := 0, 0 for _, r := range rs { if r.SharedID == 0 { local++ } else { shared++ } } if local > 0 && shared > 0 { c.errf("linked_mixed", "zone", z.ID, "%s %s in zone %s has both shared and local records; make the set one or the other", dnsname.Display(k.name), k.typ, label(z)).Zone = z.Name continue } if local > 0 { continue // an override } got := c.s.SharedRecords(z.ID, k.name, k.typ) if truthZone == nil { truth, truthZone = got, z continue } if diff := sharedDiff(truth, got); diff != "" { p := c.errf("linked_out_of_sync", "zone", z.ID, "linked zones %s and %s differ at %s %s (%s). Jarl keeps them in step, so a change was probably staged before they were linked: link them again (jarl zone link) to work out what is shared", label(truthZone), label(z), dnsname.Display(k.name), k.typ, diff) p.Zone = z.Name } } } } } func sortedGroupIDs(m map[int64][]*model.Zone) []int64 { var out []int64 for k := range m { out = append(out, k) } for i := range out { for j := i + 1; j < len(out); j++ { if out[j] < out[i] { out[i], out[j] = out[j], out[i] } } } return out } // sharedDiff says how two zones' copies of a shared set differ ("" when // they don't). func sharedDiff(a, b []*model.Record) string { idx := map[int64]*model.Record{} for _, r := range b { idx[r.SharedID] = r } var out []string for _, r := range a { o := idx[r.SharedID] delete(idx, r.SharedID) switch { case o == nil: out = append(out, "missing "+r.Data) case o.Data != r.Data: out = append(out, r.Data+" vs "+o.Data) case !sameTTLPtr(o.TTL, r.TTL): out = append(out, "TTL of "+r.Data) case o.Disabled != r.Disabled: out = append(out, "disabled "+r.Data) } } for _, r := range idx { out = append(out, "extra "+r.Data) } if len(out) > 3 { out = append(out[:3], fmt.Sprintf("and %d more", len(out)-3)) } return strings.Join(out, ", ") } func sameTTLPtr(a, b *uint32) bool { if a == nil || b == nil { return a == nil && b == nil } return *a == *b }