Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e56416fa8c |
@@ -254,11 +254,10 @@ func theResolver(t *testing.T) catalogue.Manifest {
|
|||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
// The resolver is handed every machine on the private network as a wildcard, the same set and the
|
// The resolver is handed every machine on the private network as a wildcard, and is handed it again
|
||||||
// same source as the hosts file, and is handed it again when a machine leaves — through the
|
// when a machine leaves — through the module's own manifest asking for the fact, with no module of the
|
||||||
// module's own manifest asking for the fact, with no module of the mesh's own in between (hal
|
// mesh's own in between (hal dnsmasq-app conversion, novox/hq 08-connectivity). It is the mesh's one
|
||||||
// dnsmasq-app conversion, novox/hq 08-connectivity). The runtime on that machine is pointed at the
|
// resolver (ADR 0194), and the container runtime is given no resolver of its own (ADR 0196).
|
||||||
// machine's own address, where the resolver answers for its containers.
|
|
||||||
func TestTheResolverIsToldEveryMachineOnTheNetworkAndToldAgainWhenOneLeaves(t *testing.T) {
|
func TestTheResolverIsToldEveryMachineOnTheNetworkAndToldAgainWhenOneLeaves(t *testing.T) {
|
||||||
open := aMesh(t)
|
open := aMesh(t)
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
@@ -293,11 +292,12 @@ func TestTheResolverIsToldEveryMachineOnTheNetworkAndToldAgainWhenOneLeaves(t *t
|
|||||||
t.Errorf("the resolver's machines lack %q:\n%s", want, first)
|
t.Errorf("the resolver's machines lack %q:\n%s", want, first)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// The container runtime is given no resolver of its own (novox/hq ADR 0196): it copies its
|
||||||
|
// machine's, which name the mesh's resolver first. A `dns` key would be a second account of where a
|
||||||
|
// container asks, read only when the runtime starts.
|
||||||
for _, r := range composed(t, open, "anchor").Resources {
|
for _, r := range composed(t, open, "anchor").Resources {
|
||||||
if r["id"] == "dnsmasq.runtime-dns" {
|
if r["id"] == "dnsmasq.runtime-dns" {
|
||||||
if !strings.Contains(r["content"].(string), `"10.77.0.1"`) || r["into"] != "json" {
|
t.Errorf("the resolver still writes the runtime's own dns: %v", r)
|
||||||
t.Errorf("the runtime is not pointed at this machine's own address, written into its file: %v", r)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -657,6 +657,13 @@ func renderingFor(ctx context.Context, open *stores, node string,
|
|||||||
machines[name] = at
|
machines[name] = at
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// And every zone a module in the mesh answers itself (novox/hq ADR 0199), for the mesh's resolver
|
||||||
|
// to forward.
|
||||||
|
zones, err := zonesInTheMesh(ctx, open)
|
||||||
|
if err != nil {
|
||||||
|
return catalogue.Rendering{}, inventory.Node{}, err
|
||||||
|
}
|
||||||
|
|
||||||
// **The bus is never public** (novox/hq ADR 0169). It was a foundation port — widened from the
|
// **The bus is never public** (novox/hq ADR 0169). It was a foundation port — widened from the
|
||||||
// broker's own `from: mesh` to from-anywhere on the broker's host, so a machine could enrol
|
// broker's own `from: mesh` to from-anywhere on the broker's host, so a machine could enrol
|
||||||
// before it had an address on the private network. A machine joins through the tunnel now, and
|
// before it had an address on the private network. A machine joins through the tunnel now, and
|
||||||
@@ -726,14 +733,79 @@ func renderingFor(ctx context.Context, open *stores, node string,
|
|||||||
BusMembership: memberships[node],
|
BusMembership: memberships[node],
|
||||||
Settings: settings, Generators: gens, Grants: grants, Needed: needed, Ports: ports,
|
Settings: settings, Generators: gens, Grants: grants, Needed: needed, Ports: ports,
|
||||||
Certificate: certificate, Authority: authority, Mesh: private, Names: names,
|
Certificate: certificate, Authority: authority, Mesh: private, Names: names,
|
||||||
Machines: machines,
|
Machines: machines, Zones: zones,
|
||||||
Suffix: overlay.Suffix(), MeshRange: meshRange, TunnelInterface: overlay.Interface, Accounts: accounts, Foundation: foundation,
|
Suffix: overlay.Suffix(), MeshRange: meshRange, TunnelInterface: overlay.Interface, Accounts: accounts, Foundation: foundation,
|
||||||
Kept: kept, Adopted: record.Adopted, OutwardLinks: outwardLinks,
|
Kept: kept, Adopted: record.Adopted, OutwardLinks: outwardLinks,
|
||||||
Given: given, Taken: taken, Seats: seats, ArtifactStore: artifactStore, Built: built,
|
Given: given, Taken: taken, Seats: seats, ArtifactStore: artifactStore, Built: built,
|
||||||
BusUsers: busUsers,
|
BusUsers: busUsers,
|
||||||
}, record, nil
|
}, record, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// zonesInTheMesh is every zone a module in the mesh declares, where the mesh placed it (novox/hq ADR
|
||||||
|
// 0199): the zone settled from that node's settings, the node's private address, the port the
|
||||||
|
// answering listen is published on there.
|
||||||
|
//
|
||||||
|
// Read across every machine's resolution, as the roster once read routed names: a node whose set does
|
||||||
|
// not compose declares nothing and is passed over, so one broken machine does not cost the rest their
|
||||||
|
// zones; a store that cannot be read is raised, naming the machine, because returning the zones
|
||||||
|
// without it would withdraw them from the resolver as if the operator had (novox/hq 04-ISSUES/152).
|
||||||
|
// What the mesh refuses about the zones together — one declared twice, one shadowing the mesh's
|
||||||
|
// suffix or a node's public domain — is refused here, by name.
|
||||||
|
func zonesInTheMesh(ctx context.Context, open *stores) ([]catalogue.ZoneAt, error) {
|
||||||
|
inv := open.inventory
|
||||||
|
places, err := inv.Overlays(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("where the machines are cannot be read: %w", err)
|
||||||
|
}
|
||||||
|
address := map[string]string{}
|
||||||
|
for _, p := range places {
|
||||||
|
if strings.TrimSpace(p.Address) != "" {
|
||||||
|
address[p.Name] = p.Address
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodes, err := inv.Nodes(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("which machines the mesh has cannot be read: %w", err)
|
||||||
|
}
|
||||||
|
var zones []catalogue.ZoneAt
|
||||||
|
var public []string
|
||||||
|
for _, n := range nodes {
|
||||||
|
plan, _, err := planFor(ctx, open, n.Name)
|
||||||
|
switch {
|
||||||
|
case unresolvable(err):
|
||||||
|
continue
|
||||||
|
case err != nil:
|
||||||
|
return nil, fmt.Errorf("the zones %s answers cannot be read: %w", n.Name, err)
|
||||||
|
}
|
||||||
|
if plan.PublicDomain != "" {
|
||||||
|
public = append(public, plan.PublicDomain)
|
||||||
|
}
|
||||||
|
for _, m := range plan.Modules {
|
||||||
|
if m.Zone == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
at := address[n.Name]
|
||||||
|
if at == "" {
|
||||||
|
// Not on the private network yet: nothing could reach its answerer.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
published, layers, err := portsGivenOn(ctx, inv, n.Name, m)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("the zone %s declares on %s cannot be read: %w", m.Module, n.Name, err)
|
||||||
|
}
|
||||||
|
z, err := catalogue.ZoneOn(m, layers, published, n.Name, at)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
zones = append(zones, *z)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if problems := catalogue.ZonesProblems(zones, overlay.Suffix(), public); len(problems) > 0 {
|
||||||
|
return nil, fmt.Errorf("the mesh's zones cannot be forwarded:\n - %s", strings.Join(problems, "\n - "))
|
||||||
|
}
|
||||||
|
return zones, nil
|
||||||
|
}
|
||||||
|
|
||||||
// certificateFor is what the mesh certifies about one machine's internal name.
|
// certificateFor is what the mesh certifies about one machine's internal name.
|
||||||
//
|
//
|
||||||
// It reaches across two contexts and reads neither one's store from the other: `inventory` knows
|
// It reaches across two contexts and reads neither one's store from the other: `inventory` knows
|
||||||
|
|||||||
@@ -21,8 +21,9 @@ import (
|
|||||||
// formats and gains no fields.
|
// formats and gains no fields.
|
||||||
//
|
//
|
||||||
// **It stays name-agnostic** ([ADR 0027]). The mesh does not learn what a `postgres-database` is:
|
// **It stays name-agnostic** ([ADR 0027]). The mesh does not learn what a `postgres-database` is:
|
||||||
// `at`, `as` and `from` are facts about any provision at all, and everything else comes from what
|
// `at`, `as`, `from` and `address` (the providing machine's private address, novox/hq ADR 0194) are
|
||||||
// the provider said it serves — whose keys are agreed by the requirement's name, not by this file.
|
// facts about any provision at all, and everything else comes from what the provider said it
|
||||||
|
// serves — whose keys are agreed by the requirement's name, not by this file.
|
||||||
|
|
||||||
// bound is where a module says a value from one of its bindings belongs:
|
// bound is where a module says a value from one of its bindings belongs:
|
||||||
// ${bound:<provision>.<key>}.
|
// ${bound:<provision>.<key>}.
|
||||||
|
|||||||
@@ -232,3 +232,34 @@ func TestTwoModulesOnOneNodeAreTwoIdentities(t *testing.T) {
|
|||||||
t.Fatal("one module on two machines shares an identity")
|
t.Fatal("one module on two machines shares an identity")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A machine's resolver configuration must name its resolver by address — it cannot resolve the name
|
||||||
|
// of the thing it resolves names with (novox/hq ADR 0194). So a binding offers the providing
|
||||||
|
// machine's private address beside its name, and only when the machine has one.
|
||||||
|
func TestABindingOffersTheProvidersAddress(t *testing.T) {
|
||||||
|
consumer := func() Resolution {
|
||||||
|
return Resolution{
|
||||||
|
Node: "workstation",
|
||||||
|
Modules: []Manifest{{
|
||||||
|
Module: "resolv-conf",
|
||||||
|
Requires: []string{"wildcard-resolution"},
|
||||||
|
Resources: []map[string]any{{
|
||||||
|
"id": "resolv", "type": "file", "path": "/etc/resolv.conf", "mode": "0644",
|
||||||
|
"content": "nameserver ${bound:wildcard-resolution:address}\n",
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
Needs: []Needed{{Name: "wildcard-resolution", From: "anchor", At: "anchor.internal", For: "resolv-conf"}},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out, err := consumer().Declaration(Rendering{Machines: map[string]string{"anchor.internal": "10.77.0.1"}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got := fileNamed(out, "resolv-conf.resolv")["content"]; got != "nameserver 10.77.0.1\n" {
|
||||||
|
t.Fatalf("the resolver is not named by its address: %q", got)
|
||||||
|
}
|
||||||
|
// A machine with no address yet: refused, never written with a blank where the address belongs.
|
||||||
|
if _, err := consumer().Declaration(Rendering{}); err == nil {
|
||||||
|
t.Fatal("a file naming an address the mesh does not have was composed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -155,6 +155,10 @@ type Rendering struct {
|
|||||||
// standing beside the machines and looking as real as they do.
|
// standing beside the machines and looking as real as they do.
|
||||||
Machines map[string]string
|
Machines map[string]string
|
||||||
|
|
||||||
|
// Zones is every zone a module in the mesh answers itself, where it is answered (novox/hq ADR
|
||||||
|
// 0199): the mesh's resolver forwards each one there.
|
||||||
|
Zones []ZoneAt
|
||||||
|
|
||||||
Settings SettingsBy
|
Settings SettingsBy
|
||||||
Generators map[string]Generator
|
Generators map[string]Generator
|
||||||
// Grants are the credentials this node must create, for the provisions it offers. Passed in
|
// Grants are the credentials this node must create, for the provisions it offers. Passed in
|
||||||
@@ -732,6 +736,16 @@ func (r Resolution) compose(with Rendering, owner map[string]string,
|
|||||||
known[provision] = values
|
known[provision] = values
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// And the providing machine's private address, beside its name (novox/hq ADR 0194). A name is
|
||||||
|
// what nearly every consumer wants; the one that cannot use it is a machine's resolver
|
||||||
|
// configuration, which must reach the resolver before it can resolve anything — the resolver's
|
||||||
|
// own name included. Absent when the machine has no address yet, so a file naming it is refused
|
||||||
|
// rather than written with a blank where an address belongs.
|
||||||
|
for _, values := range known {
|
||||||
|
if address := with.Machines[values["at"]]; address != "" {
|
||||||
|
values["address"] = address
|
||||||
|
}
|
||||||
|
}
|
||||||
// And what the module is called through each requirement it contributes to (novox/hq
|
// And what the module is called through each requirement it contributes to (novox/hq
|
||||||
// 04-ISSUES/122) — the same composition its binding file carries.
|
// 04-ISSUES/122) — the same composition its binding file carries.
|
||||||
for provision, values := range known {
|
for provision, values := range known {
|
||||||
@@ -901,7 +915,7 @@ func (r Resolution) compose(with Rendering, owner map[string]string,
|
|||||||
// plane's; making a name resolve is the module's software. Emitted as ordinary files under
|
// plane's; making a name resolve is the module's software. Emitted as ordinary files under
|
||||||
// this module's name, so they are applied, reported and removed exactly as anything else
|
// this module's name, so they are applied, reported and removed exactly as anything else
|
||||||
// it declares.
|
// it declares.
|
||||||
given, err := FactsInto(m, r, with.Names, with.Machines, with.Accounts, with.Suffix)
|
given, err := FactsWithZonesInto(m, r, with.Names, with.Machines, with.Accounts, with.Suffix, with.Zones)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -540,6 +540,10 @@ type Manifest struct {
|
|||||||
// `restart-on` names to restart when the roster changes.
|
// `restart-on` names to restart when the roster changes.
|
||||||
Facts map[string]RosterFile `json:"facts,omitempty"`
|
Facts map[string]RosterFile `json:"facts,omitempty"`
|
||||||
|
|
||||||
|
// Zone is the zone of names this module answers itself, and the listen that answers it (novox/hq
|
||||||
|
// ADR 0199). The mesh's resolver forwards the zone to it; nothing here names an address.
|
||||||
|
Zone *Zone `json:"zone,omitempty"`
|
||||||
|
|
||||||
// Certificate is where this module wants a certificate for its machine's name inside the
|
// Certificate is where this module wants a certificate for its machine's name inside the
|
||||||
// mesh, and where the key that goes with it can be found.
|
// mesh, and where the key that goes with it can be found.
|
||||||
//
|
//
|
||||||
@@ -1742,6 +1746,7 @@ func ParseManifest(raw []byte) (Manifest, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
problems = append(problems, zoneProblems(m)...)
|
||||||
if len(problems) > 0 {
|
if len(problems) > 0 {
|
||||||
sort.Strings(problems)
|
sort.Strings(problems)
|
||||||
return Manifest{}, fmt.Errorf("this manifest cannot be used:\n - %s",
|
return Manifest{}, fmt.Errorf("this manifest cannot be used:\n - %s",
|
||||||
|
|||||||
@@ -48,9 +48,12 @@ func TestTheResolverForwardsToFixedUpstreamsAndNeverReadsResolvConf(t *testing.T
|
|||||||
}
|
}
|
||||||
for _, want := range []string{
|
for _, want := range []string{
|
||||||
"\nno-resolv\n", "\nserver=1.1.1.1\n", "\nserver=8.8.8.8\n",
|
"\nno-resolv\n", "\nserver=1.1.1.1\n", "\nserver=8.8.8.8\n",
|
||||||
// Loopback is the mesh-wide setting's default; a machine answering its own LAN adds its
|
// The private address and loopback, never a LAN's (novox/hq ADR 0194): a device that is not a
|
||||||
// address there (novox/hq issue 198).
|
// member cannot reach what the mesh's names point at.
|
||||||
"\nlisten-address=${setting:listen-addresses}\n", "\nlisten-address=${machine:address}\n", "\nbind-dynamic\n",
|
"\nlisten-address=127.0.0.1\n", "\nlisten-address=${machine:address}\n", "\nbind-dynamic\n",
|
||||||
|
// No hosts file and no operator's files: the mesh's resolver answers every node (ADR 0199).
|
||||||
|
"\nno-hosts\n",
|
||||||
|
"\nconf-file=" + m.Facts["zones"].Path + "\n",
|
||||||
"\ndomain-needed\n", "\nbogus-priv\n",
|
"\ndomain-needed\n", "\nbogus-priv\n",
|
||||||
"\nconf-file=" + m.Facts["node-zones"].Path + "\n",
|
"\nconf-file=" + m.Facts["node-zones"].Path + "\n",
|
||||||
} {
|
} {
|
||||||
@@ -73,7 +76,15 @@ func TestTheResolverForwardsToFixedUpstreamsAndNeverReadsResolvConf(t *testing.T
|
|||||||
t.Errorf("the resolver listens on %s", taken)
|
t.Errorf("the resolver listens on %s", taken)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// And the file that decides what the machine asks names it there, alone.
|
// Never a directory or a file the operator keeps: a line written for one machine's programs would
|
||||||
|
// become an answer for every node (ADR 0199).
|
||||||
|
for _, never := range []string{"conf-dir=", "addn-hosts=", "listen-address=${setting:"} {
|
||||||
|
if strings.Contains(config, never) {
|
||||||
|
t.Errorf("the mesh's resolver still reads or listens on %q", never)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// And the file that decides what the machine asks names the mesh's resolver first, by address,
|
||||||
|
// and a public one second, asked only when the first is silent (ADR 0196).
|
||||||
var resolv string
|
var resolv string
|
||||||
for _, r := range catalogueManifest(t, "resolv-conf").Resources {
|
for _, r := range catalogueManifest(t, "resolv-conf").Resources {
|
||||||
if r["path"] == "/etc/resolv.conf" {
|
if r["path"] == "/etc/resolv.conf" {
|
||||||
@@ -86,13 +97,16 @@ func TestTheResolverForwardsToFixedUpstreamsAndNeverReadsResolvConf(t *testing.T
|
|||||||
nameservers = append(nameservers, strings.TrimPrefix(line, "nameserver "))
|
nameservers = append(nameservers, strings.TrimPrefix(line, "nameserver "))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(nameservers) != 1 || nameservers[0] != "127.0.0.1" {
|
if len(nameservers) != 2 || nameservers[0] != "${bound:wildcard-resolution:address}" || nameservers[1] != "1.1.1.1" {
|
||||||
t.Errorf("resolv.conf names %v; the predecessor's names the mesh's resolver alone at 127.0.0.1", nameservers)
|
t.Errorf("resolv.conf names %v; the mesh's resolver by address first, a public one second", nameservers)
|
||||||
}
|
}
|
||||||
// The split-DNS alternative points at the same address, or a machine that keeps
|
if !strings.Contains(resolv, "\noptions timeout:1 attempts:1") {
|
||||||
|
t.Errorf("the fallback is not reached after one short attempt:\n%s", resolv)
|
||||||
|
}
|
||||||
|
// The split-DNS alternative points at the same resolver, or a machine that keeps
|
||||||
// systemd-resolved in charge would route the mesh's suffix to nothing.
|
// systemd-resolved in charge would route the mesh's suffix to nothing.
|
||||||
for _, r := range catalogueManifest(t, "resolved-split-dns").Resources {
|
for _, r := range catalogueManifest(t, "resolved-split-dns").Resources {
|
||||||
if content, _ := r["content"].(string); content != "" && !strings.Contains(content, "DNS=127.0.0.1\n") {
|
if content, _ := r["content"].(string); content != "" && !strings.Contains(content, "DNS=${bound:wildcard-resolution:address}\n") {
|
||||||
t.Errorf("resolved-split-dns does not point at the resolver's address:\n%s", content)
|
t.Errorf("resolved-split-dns does not point at the resolver's address:\n%s", content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -100,7 +114,8 @@ func TestTheResolverForwardsToFixedUpstreamsAndNeverReadsResolvConf(t *testing.T
|
|||||||
|
|
||||||
// The resolver and what points the machine at it compose on one machine, and what arrives is the
|
// The resolver and what points the machine at it compose on one machine, and what arrives is the
|
||||||
// mesh's account of every machine as a wildcard, the suffix kept local, the daemon restarting on
|
// mesh's account of every machine as a wildcard, the suffix kept local, the daemon restarting on
|
||||||
// that file, and the runtime pointed at this machine's own address.
|
// that file, the machine pointed at the resolver by address, and the runtime given no resolver of
|
||||||
|
// its own but kept running across a restart (ADR 0196).
|
||||||
func TestTheResolverAndWhatAsksItComposeOnOneMachine(t *testing.T) {
|
func TestTheResolverAndWhatAsksItComposeOnOneMachine(t *testing.T) {
|
||||||
got, err := Resolve(resolverShelf(t), []string{"dnsmasq", "resolv-conf"},
|
got, err := Resolve(resolverShelf(t), []string{"dnsmasq", "resolv-conf"},
|
||||||
Node{Name: "anchor", At: "anchor.internal"}, World{})
|
Node{Name: "anchor", At: "anchor.internal"}, World{})
|
||||||
@@ -115,6 +130,7 @@ func TestTheResolverAndWhatAsksItComposeOnOneMachine(t *testing.T) {
|
|||||||
// issue 111) — the resolver's zones read only the second, and in this scenario the two
|
// issue 111) — the resolver's zones read only the second, and in this scenario the two
|
||||||
// happen to be the same map, since nothing routed is part of it.
|
// happen to be the same map, since nothing routed is part of it.
|
||||||
Names: twoMachines, Machines: twoMachines, Suffix: "internal",
|
Names: twoMachines, Machines: twoMachines, Suffix: "internal",
|
||||||
|
Zones: []ZoneAt{{Zone: "incus", Address: "10.42.0.2", Port: 5353}},
|
||||||
Needed: map[string]map[string]string{"dnsmasq": {"broker": "sealed"}},
|
Needed: map[string]map[string]string{"dnsmasq": {"broker": "sealed"}},
|
||||||
Settings: SettingsBy{"dnsmasq": {{From: "the mesh", Values: map[string]any{"listen-addresses": "127.0.0.1"}}}},
|
Settings: SettingsBy{"dnsmasq": {{From: "the mesh", Values: map[string]any{"listen-addresses": "127.0.0.1"}}}},
|
||||||
})
|
})
|
||||||
@@ -144,24 +160,29 @@ func TestTheResolverAndWhatAsksItComposeOnOneMachine(t *testing.T) {
|
|||||||
for _, id := range service["restart-on"].([]any) {
|
for _, id := range service["restart-on"].([]any) {
|
||||||
reflects[id.(string)] = true
|
reflects[id.(string)] = true
|
||||||
}
|
}
|
||||||
if !reflects["dnsmasq.config"] || !reflects["dnsmasq.fact-node-zones"] {
|
if !reflects["dnsmasq.config"] || !reflects["dnsmasq.fact-node-zones"] || !reflects["dnsmasq.fact-zones"] {
|
||||||
t.Errorf("the daemon does not restart on its configuration and the machines file both: %v", service["restart-on"])
|
t.Errorf("the daemon does not restart on its configuration, the machines and the zones: %v", service["restart-on"])
|
||||||
|
}
|
||||||
|
if z, _ := ids["dnsmasq.fact-zones"]["content"].(string); !strings.Contains(z, "server=/incus/10.42.0.2#5353\n") {
|
||||||
|
t.Errorf("the resolver was not told to forward the zone to its answerer:\n%s", z)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The runtime's own file, written into (novox/hq ADR 0102) with the keys this module states:
|
// The runtime's own file, written into (novox/hq ADR 0102) with one key, by what decides how the
|
||||||
// where containers resolve, and that a restart keeps them running — because the runtime reads
|
// machine resolves: a restart keeps every container running. No `dns` — a container copies its
|
||||||
// `dns` only when it starts, and the one restart that needs is the operator's (issue 110).
|
// machine's resolvers (ADR 0196), and the mesh's resolver is not written into the runtime twice.
|
||||||
runtime := ids["dnsmasq.runtime-dns"]
|
if ids["dnsmasq.runtime-dns"] != nil {
|
||||||
|
t.Errorf("the resolver still writes the runtime's dns: %v", ids["dnsmasq.runtime-dns"])
|
||||||
|
}
|
||||||
|
runtime := ids["resolv-conf.runtime-config"]
|
||||||
if runtime == nil || runtime["path"] != "/etc/docker/daemon.json" || runtime["into"] != "json" {
|
if runtime == nil || runtime["path"] != "/etc/docker/daemon.json" || runtime["into"] != "json" {
|
||||||
t.Fatalf("the runtime's dns is not written into its file: %v", runtime)
|
t.Fatalf("live-restore is not written into the runtime's file: %v", runtime)
|
||||||
}
|
}
|
||||||
var keys map[string]any
|
var keys map[string]any
|
||||||
if err := json.Unmarshal([]byte(runtime["content"].(string)), &keys); err != nil {
|
if err := json.Unmarshal([]byte(runtime["content"].(string)), &keys); err != nil {
|
||||||
t.Fatalf("the runtime's keys are not JSON: %v", err)
|
t.Fatalf("the runtime's keys are not JSON: %v", err)
|
||||||
}
|
}
|
||||||
dns, _ := keys["dns"].([]any)
|
if len(keys) != 1 || keys["live-restore"] != true {
|
||||||
if len(keys) != 2 || len(dns) != 1 || dns[0] != "10.42.0.1" || keys["live-restore"] != true {
|
t.Errorf("the runtime is given %v; live-restore and nothing else", keys)
|
||||||
t.Errorf("the runtime is given %v; containers resolve at this machine's own private-network address, a restart keeps them, and nothing else is written", keys)
|
|
||||||
}
|
}
|
||||||
// The runtime is reloaded when that file changes, and never restarted: a restart stops every
|
// The runtime is reloaded when that file changes, and never restarted: a restart stops every
|
||||||
// container on the machine (ADR 0102), and a reload is what turns live-restore on.
|
// container on the machine (ADR 0102), and a reload is what turns live-restore on.
|
||||||
@@ -171,10 +192,10 @@ func TestTheResolverAndWhatAsksItComposeOnOneMachine(t *testing.T) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if _, restarts := r["restart-on"]; restarts {
|
if _, restarts := r["restart-on"]; restarts {
|
||||||
t.Errorf("the resolver orders the runtime restarted, which stops every container (ADR 0102): %v", r)
|
t.Errorf("the runtime is ordered restarted, which stops every container (ADR 0102): %v", r)
|
||||||
}
|
}
|
||||||
for _, on := range asStrings(r["reload-on"]) {
|
for _, on := range asStrings(r["reload-on"]) {
|
||||||
if on == "dnsmasq.runtime-dns" {
|
if on == "resolv-conf.runtime-config" {
|
||||||
reloaded = true
|
reloaded = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -184,8 +205,8 @@ func TestTheResolverAndWhatAsksItComposeOnOneMachine(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resolv := ids["resolv-conf.resolv"]
|
resolv := ids["resolv-conf.resolv"]
|
||||||
if resolv == nil || !strings.Contains(resolv["content"].(string), "\nnameserver 127.0.0.1\n") {
|
if resolv == nil || !strings.Contains(resolv["content"].(string), "\nnameserver 10.42.0.1\nnameserver 1.1.1.1\n") {
|
||||||
t.Fatalf("the machine is not pointed at the resolver: %v", resolv)
|
t.Fatalf("the machine is not pointed at the resolver by address, with the public fallback: %v", resolv)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -61,6 +61,16 @@ type rosterView struct {
|
|||||||
Suffix string
|
Suffix string
|
||||||
Names []rosterEntry
|
Names []rosterEntry
|
||||||
Machines []rosterEntry
|
Machines []rosterEntry
|
||||||
|
// Zones is every zone a module in the mesh answers itself, with where its answerer is (novox/hq
|
||||||
|
// ADR 0199) — what the mesh's resolver forwards. Ordered by zone.
|
||||||
|
Zones []rosterZone
|
||||||
|
}
|
||||||
|
|
||||||
|
// rosterZone is one zone as a template sees it: the zone, and the address and port answering it.
|
||||||
|
type rosterZone struct {
|
||||||
|
Zone string
|
||||||
|
Address string
|
||||||
|
Port int
|
||||||
}
|
}
|
||||||
|
|
||||||
// rosterEntry is one machine as a template sees it: its bare name, its full mesh name, its address,
|
// rosterEntry is one machine as a template sees it: its bare name, its full mesh name, its address,
|
||||||
@@ -80,6 +90,12 @@ type rosterEntry struct {
|
|||||||
// `machines` is only the machines — the two must not be confused (novox/hq 04-ISSUES/111), so both
|
// `machines` is only the machines — the two must not be confused (novox/hq 04-ISSUES/111), so both
|
||||||
// are given and the template chooses.
|
// are given and the template chooses.
|
||||||
func FactsInto(m Manifest, r Resolution, every, machines, accounts map[string]string, suffix string) ([]map[string]any, error) {
|
func FactsInto(m Manifest, r Resolution, every, machines, accounts map[string]string, suffix string) ([]map[string]any, error) {
|
||||||
|
return FactsWithZonesInto(m, r, every, machines, accounts, suffix, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FactsWithZonesInto is FactsInto with the mesh's zones in the view, for a template that ranges them.
|
||||||
|
func FactsWithZonesInto(m Manifest, r Resolution, every, machines, accounts map[string]string, suffix string,
|
||||||
|
zones []ZoneAt) ([]map[string]any, error) {
|
||||||
if len(m.Facts) == 0 {
|
if len(m.Facts) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
@@ -94,6 +110,7 @@ func FactsInto(m Manifest, r Resolution, every, machines, accounts map[string]st
|
|||||||
Suffix: strings.TrimPrefix(suffixOr(suffix), "."),
|
Suffix: strings.TrimPrefix(suffixOr(suffix), "."),
|
||||||
Names: entriesFrom(every, accounts, suffix),
|
Names: entriesFrom(every, accounts, suffix),
|
||||||
Machines: entriesFrom(machines, accounts, suffix),
|
Machines: entriesFrom(machines, accounts, suffix),
|
||||||
|
Zones: zonesFrom(zones),
|
||||||
}
|
}
|
||||||
|
|
||||||
out := make([]map[string]any, 0, len(names))
|
out := make([]map[string]any, 0, len(names))
|
||||||
@@ -223,3 +240,13 @@ func sortedNames(addresses map[string]string) []string {
|
|||||||
sort.Strings(out)
|
sort.Strings(out)
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// zonesFrom is the zones a template ranges, ordered by zone so two renderings of one mesh are one file.
|
||||||
|
func zonesFrom(zones []ZoneAt) []rosterZone {
|
||||||
|
out := make([]rosterZone, 0, len(zones))
|
||||||
|
for _, z := range zones {
|
||||||
|
out = append(out, rosterZone{Zone: z.Zone, Address: z.Address, Port: z.Port})
|
||||||
|
}
|
||||||
|
sort.Slice(out, func(i, j int) bool { return out[i].Zone < out[j].Zone })
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|||||||
@@ -107,7 +107,31 @@ var defaultSeats = []Seat{
|
|||||||
// that machine unresolvable in the meantime. Deleted once no registered manifest claims it.
|
// that machine unresolvable in the meantime. Deleted once no registered manifest claims it.
|
||||||
{Name: "mesh-build-machine", Scope: ScopeMesh,
|
{Name: "mesh-build-machine", Scope: ScopeMesh,
|
||||||
Accepts: []string{"build"}, Emits: []string{"started", "built", "log.*"}, Decision: "novox/hq ADR 0190"},
|
Accepts: []string{"build"}, Emits: []string{"started", "built", "log.*"}, Decision: "novox/hq ADR 0190"},
|
||||||
|
// **The mesh's one resolver** (novox/hq ADR 0194, 0196): every node's internal domain, held in one
|
||||||
|
// place, and every node and container asks it first. Delivers what a machine's resolver
|
||||||
|
// configuration requires, so that requirement resolves to the holder wherever it is placed.
|
||||||
|
{Name: "mesh-dns-resolver", Scope: ScopeMesh, Delivers: "wildcard-resolution", Decision: "novox/hq ADR 0194"},
|
||||||
|
// **Retired by ADR 0194, kept while a manifest still claims it** — the same reason as
|
||||||
|
// mesh-build-machine above: a machine still holds it until the mesh's resolver replaces it, and
|
||||||
|
// removing the row first would make that machine unresolvable. Deleted once nothing claims it.
|
||||||
{Name: "node-dns-resolver", Scope: ScopeNode, Decision: "novox/hq ADR 0121"},
|
{Name: "node-dns-resolver", Scope: ScopeNode, Decision: "novox/hq ADR 0121"},
|
||||||
|
// **A machine's /etc/hosts is one module's** (novox/hq ADR 0199): its holder writes the machine's
|
||||||
|
// own lines and keeps every other line as the operator's, changed through these three verbs on that
|
||||||
|
// machine alone. The controller holds none of it.
|
||||||
|
{Name: "node-hosts-file", Scope: ScopeNode, Decision: "novox/hq ADR 0199",
|
||||||
|
Serves: []Verb{
|
||||||
|
{Name: "entries", Description: "Every line of this machine's /etc/hosts, each marked whose it is: " +
|
||||||
|
"the operator's, or the block of the module or tool that writes it.",
|
||||||
|
Input: schema(map[string]string{}, nil)},
|
||||||
|
{Name: "add", Description: "Add one address and its names to the operator's lines of this machine's " +
|
||||||
|
"/etc/hosts — a name for this machine's own programs, not the mesh's.",
|
||||||
|
Input: schema(map[string]string{"address": "the IPv4 or IPv6 address",
|
||||||
|
"names": "the names for it, separated by spaces"}, []string{"address", "names"})},
|
||||||
|
{Name: "remove", Description: "Remove one name, or every line of one address, from the operator's " +
|
||||||
|
"lines of this machine's /etc/hosts. A line a module writes is refused, naming the module.",
|
||||||
|
Input: schema(map[string]string{"name": "a host name, or an address to remove every line of"},
|
||||||
|
[]string{"name"})},
|
||||||
|
}},
|
||||||
// The intrusion prevention's verbs (novox/hq ADR 0179): what a person asks a machine's ban list
|
// The intrusion prevention's verbs (novox/hq ADR 0179): what a person asks a machine's ban list
|
||||||
// whatever keeps it — who is banned and why, ban one address, let one go. Every holder serves all
|
// whatever keeps it — who is banned and why, ban one address, let one go. Every holder serves all
|
||||||
// four; the jails themselves are composed from the modules the machine runs (to-be 31).
|
// four; the jails themselves are composed from the modules the machine runs (to-be 31).
|
||||||
|
|||||||
@@ -44,10 +44,10 @@ func TestTheSeatsAreAClosedSetAndEachNamesItsDecision(t *testing.T) {
|
|||||||
delivered[s.Delivers] = s.Name
|
delivered[s.Delivers] = s.Name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Seventeen since node-build-agent (novox/hq ADR 0190) — sixteen once the retired
|
// Nineteen since mesh-dns-resolver (novox/hq ADR 0194) and node-hosts-file (ADR 0199) — two fewer
|
||||||
// mesh-build-machine row goes, when no registered manifest claims it any more.
|
// once the retired mesh-build-machine and node-dns-resolver rows go, when no manifest claims either.
|
||||||
if len(Seats()) != 17 {
|
if len(Seats()) != 19 {
|
||||||
t.Errorf("the mesh defines %d seats rather than 17; the set is closed, so a change here is "+
|
t.Errorf("the mesh defines %d seats rather than 19; the set is closed, so a change here is "+
|
||||||
"a decision (novox/hq ADR 0110): %s", len(Seats()), seatNames())
|
"a decision (novox/hq ADR 0110): %s", len(Seats()), seatNames())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
package catalogue
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Zones: names a module answers itself (novox/hq ADR 0199).
|
||||||
|
//
|
||||||
|
// The mesh's resolver holds each node's internal domain and nothing else (ADR 0191, 0194). A module
|
||||||
|
// whose names are its own — the lab's scenario machines, known only while a scenario runs — declares
|
||||||
|
// the zone it answers and the listen that answers it; the controller hands the resolver's holder every
|
||||||
|
// zone with the declaring node's private address and the port that listen is published on, and the
|
||||||
|
// holder forwards the zone there. **A definition names no address** (ADR 0112): the zone is a setting,
|
||||||
|
// the listen is the module's own, and where they are is the mesh's fact.
|
||||||
|
|
||||||
|
// Zone is the manifest's declaration that a module answers the names in one zone.
|
||||||
|
type Zone struct {
|
||||||
|
// Name is the zone: a label or a dotted name, normally `${setting:<key>}`, so the operator chooses
|
||||||
|
// it and the definition does not.
|
||||||
|
Name string `json:"name"`
|
||||||
|
// Listen names one of the module's listens: the DNS answerer for the zone.
|
||||||
|
Listen string `json:"listen"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZoneAt is a declared zone where the mesh placed it: what the resolver's holder forwards, and where.
|
||||||
|
type ZoneAt struct {
|
||||||
|
Zone string
|
||||||
|
Node string
|
||||||
|
Module string
|
||||||
|
Address string
|
||||||
|
Port int
|
||||||
|
}
|
||||||
|
|
||||||
|
// zoneProblems is what is wrong with a module's zone declaration on its own, before any node.
|
||||||
|
func zoneProblems(m Manifest) []string {
|
||||||
|
if m.Zone == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var problems []string
|
||||||
|
if strings.TrimSpace(m.Zone.Name) == "" {
|
||||||
|
problems = append(problems, fmt.Sprintf("%s declares a zone with no name", m.Module))
|
||||||
|
}
|
||||||
|
if !m.hasListen(m.Zone.Listen) {
|
||||||
|
problems = append(problems, fmt.Sprintf(
|
||||||
|
"%s declares zone %q answered by listen %q, and has no listen of that name",
|
||||||
|
m.Module, m.Zone.Name, m.Zone.Listen))
|
||||||
|
}
|
||||||
|
return problems
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Manifest) hasListen(name string) bool {
|
||||||
|
if name == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, l := range m.Listens {
|
||||||
|
if l.Name == name {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZoneOn is one module's zone as one node places it: the name settled from the node's settings, the
|
||||||
|
// port its answering listen is published on there. Nothing when the module declares no zone.
|
||||||
|
func ZoneOn(m Manifest, layers []Layer, published map[int]int, node, address string) (*ZoneAt, error) {
|
||||||
|
if m.Zone == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
settled, err := Settle(map[string]any{"zone": m.Zone.Name}, layers)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s's zone on %s: %w", m.Module, node, err)
|
||||||
|
}
|
||||||
|
zone := strings.Trim(strings.ToLower(fmt.Sprint(settled["zone"])), ".")
|
||||||
|
var port int
|
||||||
|
for _, l := range m.Listens {
|
||||||
|
if l.Name == m.Zone.Listen {
|
||||||
|
port = l.Port
|
||||||
|
if at, given := published[l.Port]; given {
|
||||||
|
port = at
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &ZoneAt{Zone: zone, Node: node, Module: m.Module, Address: address, Port: port}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZonesProblems is what the mesh refuses about its zones together: one zone declared twice, a zone
|
||||||
|
// that is the mesh's suffix or under it, a zone that is a node's public domain or under one. A module
|
||||||
|
// may not shadow names the mesh's resolver or the public DNS answers.
|
||||||
|
func ZonesProblems(zones []ZoneAt, suffix string, publicDomains []string) []string {
|
||||||
|
var problems []string
|
||||||
|
under := func(zone, domain string) bool {
|
||||||
|
domain = strings.Trim(strings.ToLower(domain), ".")
|
||||||
|
return domain != "" && (zone == domain || strings.HasSuffix(zone, "."+domain))
|
||||||
|
}
|
||||||
|
seen := map[string]ZoneAt{}
|
||||||
|
for _, z := range zones {
|
||||||
|
if other, twice := seen[z.Zone]; twice && (other.Node != z.Node || other.Module != z.Module) {
|
||||||
|
problems = append(problems, fmt.Sprintf("zone %q is declared by %s on %s and by %s on %s; one module answers a zone",
|
||||||
|
z.Zone, other.Module, other.Node, z.Module, z.Node))
|
||||||
|
}
|
||||||
|
seen[z.Zone] = z
|
||||||
|
if under(z.Zone, suffix) {
|
||||||
|
problems = append(problems, fmt.Sprintf("%s on %s declares zone %q, which is the mesh's own suffix or under it",
|
||||||
|
z.Module, z.Node, z.Zone))
|
||||||
|
}
|
||||||
|
for _, d := range publicDomains {
|
||||||
|
if under(z.Zone, d) {
|
||||||
|
problems = append(problems, fmt.Sprintf("%s on %s declares zone %q, which is the public domain %q or under it",
|
||||||
|
z.Module, z.Node, z.Zone, d))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Strings(problems)
|
||||||
|
return problems
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package catalogue
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A zone names the listen that answers it, or there is nothing to forward to (novox/hq ADR 0199).
|
||||||
|
func TestAZoneMustNameOneOfTheModulesListens(t *testing.T) {
|
||||||
|
_, err := ParseManifest([]byte(`{"module":"lab","version":"1",
|
||||||
|
"listens":[{"name":"dns","port":5353,"protocol":"udp","from":"mesh","why":"the lab's names"}],
|
||||||
|
"zone":{"name":"${setting:zone}","listen":"web"}}`))
|
||||||
|
if err == nil || !strings.Contains(err.Error(), `has no listen of that name`) {
|
||||||
|
t.Fatalf("a zone answered by a listen the module does not have was accepted: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := ParseManifest([]byte(`{"module":"lab","version":"1",
|
||||||
|
"listens":[{"name":"dns","port":5353,"protocol":"udp","from":"mesh","why":"the lab's names"}],
|
||||||
|
"zone":{"name":"${setting:zone}","listen":"dns"}}`)); err != nil {
|
||||||
|
t.Fatalf("a well-formed zone was refused: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The zone is the operator's (a setting) and the port is where this machine publishes the listen —
|
||||||
|
// neither is the definition's to state.
|
||||||
|
func TestAZoneIsPlacedFromTheNodesSettingAndPublishedPort(t *testing.T) {
|
||||||
|
m := Manifest{Module: "lab", Zone: &Zone{Name: "${setting:zone}", Listen: "dns"},
|
||||||
|
Listens: []Listening{{Name: "dns", Port: 5353, From: FromMesh}}}
|
||||||
|
z, err := ZoneOn(m, []Layer{{From: "node", Values: map[string]any{"zone": "Incus."}}},
|
||||||
|
map[int]int{5353: 15353}, "workstation", "10.77.0.3")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if z.Zone != "incus" || z.Address != "10.77.0.3" || z.Port != 15353 || z.Node != "workstation" {
|
||||||
|
t.Fatalf("the zone was placed as %+v", *z)
|
||||||
|
}
|
||||||
|
if _, err := ZoneOn(m, nil, nil, "workstation", "10.77.0.3"); err == nil {
|
||||||
|
t.Fatal("a zone nobody named was placed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// One module answers a zone, and none may shadow the mesh's names or a public domain.
|
||||||
|
func TestTheMeshRefusesAZoneTwiceOrOneThatShadows(t *testing.T) {
|
||||||
|
one := ZoneAt{Zone: "incus", Node: "workstation", Module: "lab", Address: "10.77.0.3", Port: 53}
|
||||||
|
if p := ZonesProblems([]ZoneAt{one}, "internal", []string{"example.tld"}); len(p) != 0 {
|
||||||
|
t.Fatalf("one ordinary zone was refused: %v", p)
|
||||||
|
}
|
||||||
|
twice := one
|
||||||
|
twice.Node, twice.Module = "laptop", "other"
|
||||||
|
cases := map[string][]ZoneAt{
|
||||||
|
"declared by": {one, twice},
|
||||||
|
"mesh's own suffix": {{Zone: "lab.internal", Node: "a", Module: "m"}},
|
||||||
|
"public domain": {{Zone: "dev.example.tld", Node: "a", Module: "m"}},
|
||||||
|
}
|
||||||
|
for want, zones := range cases {
|
||||||
|
p := strings.Join(ZonesProblems(zones, "internal", []string{"example.tld"}), "\n")
|
||||||
|
if !strings.Contains(p, want) {
|
||||||
|
t.Errorf("not refused for %q: %q", want, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The resolver's template sees every zone with where it is answered, in zone order.
|
||||||
|
func TestTheResolversTemplateRangesTheZones(t *testing.T) {
|
||||||
|
m := Manifest{Module: "dnsmasq", Facts: map[string]RosterFile{"zones": {
|
||||||
|
Path: "/etc/mesh-resolver/zones.conf",
|
||||||
|
Template: "{{range .Zones}}server=/{{.Zone}}/{{.Address}}#{{.Port}}\n{{end}}",
|
||||||
|
}}}
|
||||||
|
out, err := FactsWithZonesInto(m, Resolution{Node: "anchor"}, nil, nil, nil, "", []ZoneAt{
|
||||||
|
{Zone: "zeta", Address: "10.77.0.2", Port: 53},
|
||||||
|
{Zone: "incus", Address: "10.77.0.3", Port: 15353},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got := out[0]["content"]; got != "server=/incus/10.77.0.3#15353\nserver=/zeta/10.77.0.2#53\n" {
|
||||||
|
t.Fatalf("the resolver was told %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user