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:
1 parent
6313fa51f8
commit
057433c198
31 files changed
+1729
-55
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import { h, mount, table, modal, field, input, select, textarea, lines, toast, errorBox, display, badge, viewLink, lastList } from "../ui.js";
|
||||
import { h, mount, table, modal, field, input, select, textarea, lines, toast, errorBox, display, badge, viewLink, lastList, checkbox } from "../ui.js";
|
||||
import { get, change, changeMany, describeResult, can, draftQuery } from "../api.js";
|
||||
import { rerender } from "../app.js";
|
||||
import { checkDomain, reverseZoneFor } from "../names.js";
|
||||
@@ -72,12 +72,17 @@ export async function newZoneDialog(views, zones, settings, preset = {}) {
|
||||
const file = input({ placeholder: "/usr/share/dns/root.hints", class: "mono" });
|
||||
const inView = select(views.map((v) => [v.name, v.name]), "");
|
||||
const cloneSel = select([["", "– don't copy –"]], "");
|
||||
const linkBox = checkbox("Keep them linked: shared records stay in step, each view can add its own", true);
|
||||
const cloneWrap = h("div", null, cloneSel, linkBox);
|
||||
const syncLink = () => { linkBox.style.display = cloneSel.value ? "" : "none"; };
|
||||
cloneSel.addEventListener("change", syncLink);
|
||||
const comment = input();
|
||||
const updateClones = () => {
|
||||
// Records can only be copied into a single zone.
|
||||
const n = (kind === "reverse" ? reverseZoneFor(name.value).name || "-" : name.value.trim().toLowerCase().replace(/\.$/, "")) + ".";
|
||||
mount(cloneSel, h("option", { value: "" }, "– don't copy –"),
|
||||
zones.filter((z) => z.type === "primary" && z.name === n).map((z) => h("option", { value: z.id }, z.display_name + " [" + (z.view || "no view") + "]")));
|
||||
syncLink();
|
||||
};
|
||||
// check validates the name as typed and returns the zone name (or null);
|
||||
// a network that needs several reverse zones gives a list of names.
|
||||
@@ -152,7 +157,7 @@ export async function newZoneDialog(views, zones, settings, preset = {}) {
|
||||
field("Name server addresses", nsAddr, "Optional, for name servers inside this zone: 'ns1 192.0.2.1' per line (glue)."),
|
||||
field("SOA contact", contact, "Email address of the zone's administrator."),
|
||||
field("Default TTL", ttl, "Seconds, used by records without their own TTL."),
|
||||
field("Copy records from", cloneSel, "For split-horizon setups: start with the records of this zone in another view.")],
|
||||
field("Copy records from", cloneWrap, "For split-horizon setups: start with the records of this zone in another view. Linked, Jarl keeps the shared records the same in both (docs/views.md#linked-zones).")],
|
||||
secondary: [field("Primary servers", primaries, "One per line: address [port N] [key NAME].")],
|
||||
stub: [field("Primary servers", primaries, "One per line.")],
|
||||
mirror: [field("Primary servers", primaries, "One per line (not needed for the root zone).")],
|
||||
@@ -196,7 +201,12 @@ export async function newZoneDialog(views, zones, settings, preset = {}) {
|
||||
data.nameservers = lines(ns.value);
|
||||
if (contact.value.trim()) data.soa = { rname: contact.value.trim() };
|
||||
data.default_ttl = Number(ttl.value) || undefined;
|
||||
if (cloneSel.value) { data.clone_from = Number(cloneSel.value); delete data.nameservers; }
|
||||
if (cloneSel.value) {
|
||||
const src = zones.find((z) => z.id === Number(cloneSel.value));
|
||||
if (linkBox.input.checked && src && src.view) data.link_with = src.view;
|
||||
else data.clone_from = Number(cloneSel.value);
|
||||
delete data.nameservers;
|
||||
}
|
||||
data.records = lines(nsAddr.value).map((l) => {
|
||||
const [n, a] = l.split(/\s+/);
|
||||
return { name: n, type: a && a.includes(":") ? "AAAA" : "A", data: a };
|
||||
|
||||
Reference in new issue
Block a user