Linked zones: shared records kept in step across views

Zones of the same name in different views can be linked. Shared records
are kept in step: a change in one view reaches the others in the same
changeset, as explicit operations, so the diff, permission checks,
history and undo cover every view. Records local to a view override the
shared records of that name and type there; removing the override brings
the shared ones back, and sharing a view's version makes it every view's.
Each zone keeps its own options, serial and file.

Linking existing zones changes no answer: what is the same everywhere
becomes shared, the rest stays local. Unlinking keeps every record, all
local. ACME challenges are written once per link, and --view all changes
a link once. jarl zone link/unlink, zone create --link-with, record
--local/--shared, LINK column, web UI controls, POST /zones/{zone}/link
and /unlink, validation checks linked_*, store migration 11, docs and
e2e.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adamandClaude Opus 5.5 committed 2026-10-12 15:53:58 +01:00
1 parent 6313fa51f8
commit 057433c198
31 files changed
+1729 -55

No files matched your search

+37 -11
View File
@@ -12,34 +12,60 @@ import (
// zoneViews expands a --view value for a record command: "" or one view
// as it is, "a,b" to those views, and "all" to every view that has the
// zone.
// localFanOut is set by commands making local changes (--local), which
// go to every view named even when the zones are linked.
var localFanOut bool
func zoneViews(c *client.Client, zone, view string) ([]string, error) {
if view != "all" && !strings.Contains(view, ",") {
return []string{view}, nil
}
if view != "all" {
var out []string
for _, v := range strings.Split(view, ",") {
if v = strings.TrimSpace(v); v != "" {
out = append(out, v)
}
}
return out, nil
}
var zones []api.Zone
if err := c.Get(ctx(), "/zones", nil, &zones); err != nil {
return nil, err
}
want := strings.ToLower(strings.TrimSuffix(zone, "."))
link := map[string]int64{}
var out []string
for _, z := range zones {
if strings.ToLower(strings.TrimSuffix(z.Display, ".")) == want && z.View != "" {
out = append(out, z.View)
link[z.View] = z.LinkGroup
if view == "all" {
out = append(out, z.View)
}
}
}
if view != "all" {
for _, v := range strings.Split(view, ",") {
if v = strings.TrimSpace(v); v != "" {
out = append(out, v)
}
}
}
if len(out) == 0 {
return nil, fmt.Errorf("zone %s is in no view", zone)
}
return out, nil
// Linked zones (docs/views.md#linked-zones) get a shared change once:
// Jarl copies it to the others. A local change goes to each.
if localFanOut {
return out, nil
}
var dedup []string
first := map[int64]string{}
for _, v := range out {
g := link[v]
if g != 0 {
if f, ok := first[g]; ok {
if !flagQuiet {
fmt.Fprintf(os.Stderr, "view %s: linked with %s, so it gets the change from there\n", v, f)
}
continue
}
first[g] = v
}
dedup = append(dedup, v)
}
return dedup, nil
}
// perView runs a record command once per view. With several views, the