Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd920ff854 |
@@ -2,9 +2,11 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/novox/mesh-controller/internal/catalogue"
|
||||||
"github.com/novox/mesh-controller/internal/inventory"
|
"github.com/novox/mesh-controller/internal/inventory"
|
||||||
"github.com/novox/mesh-controller/internal/overlay"
|
"github.com/novox/mesh-controller/internal/overlay"
|
||||||
)
|
)
|
||||||
@@ -108,3 +110,69 @@ func TestOnlyAMachineOnThePrivateNetworkIsNamed(t *testing.T) {
|
|||||||
t.Fatalf("a machine that left the network is still named, or the one that stayed is not: %v", names)
|
t.Fatalf("a machine that left the network is still named, or the one that stayed is not: %v", names)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// theResolver is the catalogue's dnsmasq module as it is, or the test is skipped where the
|
||||||
|
// catalogue is not beside this checkout.
|
||||||
|
func theResolver(t *testing.T) catalogue.Manifest {
|
||||||
|
t.Helper()
|
||||||
|
raw, err := os.ReadFile("../../../mesh-catalog/modules/dnsmasq/module.json")
|
||||||
|
if err != nil {
|
||||||
|
t.Skipf("the catalogue is not beside this checkout: %v", err)
|
||||||
|
}
|
||||||
|
m, err := catalogue.ParseManifest(raw)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("dnsmasq does not parse:\n%v", err)
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// The resolver is handed every machine on the private network as a wildcard, the same set and the
|
||||||
|
// same source as the hosts file, and is handed it again when a machine leaves — through the
|
||||||
|
// module's own manifest asking for the fact, with no module of the mesh's own in between (hal
|
||||||
|
// dnsmasq-app conversion, novox/hq 08-connectivity). The runtime on that machine is pointed at the
|
||||||
|
// machine's own address, where the resolver answers for its containers.
|
||||||
|
func TestTheResolverIsToldEveryMachineOnTheNetworkAndToldAgainWhenOneLeaves(t *testing.T) {
|
||||||
|
open := aMesh(t)
|
||||||
|
ctx := t.Context()
|
||||||
|
register(t, open, theResolver(t))
|
||||||
|
if _, err := assign(ctx, open, "anchor", "dnsmasq"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
zones := func() string {
|
||||||
|
t.Helper()
|
||||||
|
for _, r := range composed(t, open, "anchor").Resources {
|
||||||
|
if r["id"] == "dnsmasq.fact-node-zones" {
|
||||||
|
if r["path"] != "/etc/mesh-resolver/nodes.conf" {
|
||||||
|
t.Fatalf("the machines were written somewhere the resolver does not read: %v", r["path"])
|
||||||
|
}
|
||||||
|
return r["content"].(string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Fatal("the resolver was not handed the machines")
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
first := zones()
|
||||||
|
for _, want := range []string{
|
||||||
|
"local=/internal/", "address=/anchor.internal/10.77.0.1\n", "address=/laptop.internal/10.77.0.2\n",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(first, want) {
|
||||||
|
t.Errorf("the resolver's machines lack %q:\n%s", want, first)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, r := range composed(t, open, "anchor").Resources {
|
||||||
|
if r["id"] == "dnsmasq.runtime-dns" {
|
||||||
|
if !strings.Contains(r["content"].(string), `"10.77.0.1"`) || r["into"] != "json" {
|
||||||
|
t.Errorf("the runtime is not pointed at this machine's own address, written into its file: %v", r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The laptop keeps its place and its address, and stops running the network.
|
||||||
|
if err := open.inventory.Unassign(ctx, "laptop", overlay.Name); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
after := zones()
|
||||||
|
if strings.Contains(after, "laptop") || !strings.Contains(after, "address=/anchor.internal/10.77.0.1\n") {
|
||||||
|
t.Fatalf("a machine that left the network is still a wildcard, or the one that stayed is not:\n%s", after)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -495,7 +495,7 @@ func (r Resolution) compose(with Rendering, owner map[string]string) ([]map[stri
|
|||||||
// And what its bindings say, for the half of a connection that is not secret.
|
// And what its bindings say, for the half of a connection that is not secret.
|
||||||
known := knownFor(m, r.Needs, r.Node)
|
known := knownFor(m, r.Needs, r.Node)
|
||||||
// And the machine underneath, which no binding of its own can tell it.
|
// And the machine underneath, which no binding of its own can tell it.
|
||||||
thisMachine := machineFacts(r)
|
thisMachine := machineFacts(r, with.Names)
|
||||||
|
|
||||||
// Which of this module's files carry a secret, for the rule that a container may not read
|
// Which of this module's files carry a secret, for the rule that a container may not read
|
||||||
// one of them as its environment without saying so (ADR 0086, issue 041).
|
// one of them as its environment without saying so (ADR 0086, issue 041).
|
||||||
|
|||||||
@@ -122,10 +122,18 @@ func nodeNames(r Resolution, addresses map[string]string, suffix string) string
|
|||||||
//
|
//
|
||||||
// `*.homer.internal` is homer, which is the whole rule: if homer is at an address, so is anything
|
// `*.homer.internal` is homer, which is the whole rule: if homer is at an address, so is anything
|
||||||
// homer serves. A module wanting this runs the resolver; the mesh only says what is true.
|
// homer serves. A module wanting this runs the resolver; the mesh only says what is true.
|
||||||
|
//
|
||||||
|
// **And the suffix itself, as a local domain.** A resolver that forwards what it cannot answer
|
||||||
|
// would otherwise send a mesh name it does not know — a machine that left, a typo — to a public
|
||||||
|
// resolver, which is a leak of the mesh's names for no answer. `local=` keeps everything under the
|
||||||
|
// suffix here: answered from the lines below or refused. Written in this file rather than in the
|
||||||
|
// resolver's own configuration because the suffix is the mesh's choice (the operator may have
|
||||||
|
// picked another) and this file is the one place the mesh writes what it chose.
|
||||||
func nodeZones(_ Resolution, addresses map[string]string, suffix string) string {
|
func nodeZones(_ Resolution, addresses map[string]string, suffix string) string {
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
b.WriteString("# Generated by the mesh. Do not edit — this file is replaced whenever a machine\n")
|
b.WriteString("# Generated by the mesh. Do not edit — this file is replaced whenever a machine\n")
|
||||||
b.WriteString("# joins or leaves, and an edit would survive until then and vanish.\n\n")
|
b.WriteString("# joins or leaves, and an edit would survive until then and vanish.\n\n")
|
||||||
|
fmt.Fprintf(&b, "local=/%s/\n", strings.TrimPrefix(suffixOr(suffix), "."))
|
||||||
for _, name := range sortedNames(addresses) {
|
for _, name := range sortedNames(addresses) {
|
||||||
internal, _ := meshName(name, suffix)
|
internal, _ := meshName(name, suffix)
|
||||||
fmt.Fprintf(&b, "address=/%s/%s\n", internal, addresses[name])
|
fmt.Fprintf(&b, "address=/%s/%s\n", internal, addresses[name])
|
||||||
@@ -139,16 +147,22 @@ func nodeZones(_ Resolution, addresses map[string]string, suffix string) string
|
|||||||
// suffix is the one the control plane composed those names with, handed down rather than written
|
// suffix is the one the control plane composed those names with, handed down rather than written
|
||||||
// here a second time — the alternative was `homer.internal.internal` on every machine.
|
// here a second time — the alternative was `homer.internal.internal` on every machine.
|
||||||
func meshName(name, suffix string) (internal, bare string) {
|
func meshName(name, suffix string) (internal, bare string) {
|
||||||
if suffix == "" {
|
dotted := "." + strings.TrimPrefix(suffixOr(suffix), ".")
|
||||||
suffix = "internal"
|
|
||||||
}
|
|
||||||
dotted := "." + strings.TrimPrefix(suffix, ".")
|
|
||||||
if strings.HasSuffix(name, dotted) {
|
if strings.HasSuffix(name, dotted) {
|
||||||
return name, strings.TrimSuffix(name, dotted)
|
return name, strings.TrimSuffix(name, dotted)
|
||||||
}
|
}
|
||||||
return name + dotted, name
|
return name + dotted, name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// suffixOr is the suffix given, or the one the mesh composes names with when none was handed down.
|
||||||
|
// The one place the default is written in this file, so a fact and a name cannot disagree about it.
|
||||||
|
func suffixOr(suffix string) string {
|
||||||
|
if suffix == "" {
|
||||||
|
return "internal"
|
||||||
|
}
|
||||||
|
return suffix
|
||||||
|
}
|
||||||
|
|
||||||
func sortedNames(addresses map[string]string) []string {
|
func sortedNames(addresses map[string]string) []string {
|
||||||
out := make([]string, 0, len(addresses))
|
out := make([]string, 0, len(addresses))
|
||||||
for name, at := range addresses {
|
for name, at := range addresses {
|
||||||
|
|||||||
@@ -8,10 +8,14 @@ import (
|
|||||||
// Keyed by the internal name, as the control plane hands them (issue 079).
|
// Keyed by the internal name, as the control plane hands them (issue 079).
|
||||||
var threeMachines = map[string]string{"homer.internal": "10.42.0.1", "marge.internal": "10.42.0.2", "bart.internal": ""}
|
var threeMachines = map[string]string{"homer.internal": "10.42.0.1", "marge.internal": "10.42.0.2", "bart.internal": ""}
|
||||||
|
|
||||||
// **`*.homer.internal` is homer. That is the whole rule.**
|
// **`*.homer.internal` is homer. That is the whole rule.** And the suffix itself is local: a
|
||||||
|
// resolver that forwards what it cannot answer must not send a mesh name it does not know — a
|
||||||
|
// machine that left, a typo — to a public resolver (hal dnsmasq-app conversion, novox/hq
|
||||||
|
// 08-connectivity).
|
||||||
func TestEveryMachineIsAWildcardUnderItsOwnName(t *testing.T) {
|
func TestEveryMachineIsAWildcardUnderItsOwnName(t *testing.T) {
|
||||||
out := nodeZones(Resolution{Node: "homer"}, threeMachines, "")
|
out := nodeZones(Resolution{Node: "homer"}, threeMachines, "")
|
||||||
for _, want := range []string{
|
for _, want := range []string{
|
||||||
|
"local=/internal/",
|
||||||
"address=/homer.internal/10.42.0.1",
|
"address=/homer.internal/10.42.0.1",
|
||||||
"address=/marge.internal/10.42.0.2",
|
"address=/marge.internal/10.42.0.2",
|
||||||
} {
|
} {
|
||||||
@@ -128,6 +132,9 @@ func TestTheFactsWriteTheSuffixTheNamesWereComposedWith(t *testing.T) {
|
|||||||
if !strings.Contains(zones, "address=/homer.lan/10.42.0.1") || strings.Contains(zones, "internal") {
|
if !strings.Contains(zones, "address=/homer.lan/10.42.0.1") || strings.Contains(zones, "internal") {
|
||||||
t.Fatalf("the zones do not carry the operator's suffix as given:\n%s", zones)
|
t.Fatalf("the zones do not carry the operator's suffix as given:\n%s", zones)
|
||||||
}
|
}
|
||||||
|
if !strings.Contains(zones, "local=/lan/") {
|
||||||
|
t.Fatalf("the local domain is not the operator's suffix, so its names would leak upstream:\n%s", zones)
|
||||||
|
}
|
||||||
hosts := nodeNames(Resolution{Node: "homer"}, names, "lan")
|
hosts := nodeNames(Resolution{Node: "homer"}, names, "lan")
|
||||||
if !strings.Contains(hosts, "10.42.0.1\thomer.lan\thomer\t# this machine") {
|
if !strings.Contains(hosts, "10.42.0.1\thomer.lan\thomer\t# this machine") {
|
||||||
t.Fatalf("the hosts line does not carry the operator's suffix as given:\n%s", hosts)
|
t.Fatalf("the hosts line does not carry the operator's suffix as given:\n%s", hosts)
|
||||||
|
|||||||
@@ -22,8 +22,11 @@ import (
|
|||||||
// provides, it does not require. Written as a literal it would be a manifest carrying one
|
// provides, it does not require. Written as a literal it would be a manifest carrying one
|
||||||
// deployment's machine name, which is the shape [ADR 0066] exists to remove.
|
// deployment's machine name, which is the shape [ADR 0066] exists to remove.
|
||||||
//
|
//
|
||||||
// Two facts, both the mesh's own vocabulary — the same `node` and `at` a contribution already
|
// Three facts, all the mesh's own vocabulary — the same `node` and `at` a contribution already
|
||||||
// carries. Nothing about what a machine is *for*: that would be the mesh learning what a module
|
// carries, and the address behind `at`, for software that takes an address and not a name. The
|
||||||
|
// case that found the third is a resolver pointing the container runtime at itself: the runtime's
|
||||||
|
// list of resolvers is addresses, because a name there would have to be resolved by the resolver
|
||||||
|
// it names. Nothing about what a machine is *for*: that would be the mesh learning what a module
|
||||||
// means, which it does not do.
|
// means, which it does not do.
|
||||||
|
|
||||||
// ofMachine is where a module says a fact about the machine underneath it belongs:
|
// ofMachine is where a module says a fact about the machine underneath it belongs:
|
||||||
@@ -49,10 +52,18 @@ func machineUsed(content string) []string {
|
|||||||
// to be reached at an address that does not exist is a misconfiguration, and it is said here —
|
// to be reached at an address that does not exist is a misconfiguration, and it is said here —
|
||||||
// where the module and the machine are both named — rather than discovered later as a certificate
|
// where the module and the machine are both named — rather than discovered later as a certificate
|
||||||
// nobody can verify.
|
// nobody can verify.
|
||||||
func machineFacts(r Resolution) map[string]string {
|
//
|
||||||
|
// `address` is what `at` resolves to, read from the names the control plane composed — the same map
|
||||||
|
// the hosts file and the resolver's wildcards are written from, so a file naming the machine's
|
||||||
|
// address and the file every other machine reaches it by cannot disagree. Absent, like `at`, when
|
||||||
|
// the machine is off the network or the mesh has not placed it.
|
||||||
|
func machineFacts(r Resolution, names map[string]string) map[string]string {
|
||||||
out := map[string]string{"name": r.Node}
|
out := map[string]string{"name": r.Node}
|
||||||
if r.At != "" {
|
if r.At != "" {
|
||||||
out["at"] = r.At
|
out["at"] = r.At
|
||||||
|
if address := names[r.At]; address != "" {
|
||||||
|
out["address"] = address
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,3 +70,36 @@ func TestAnAddressAMachineDoesNotHaveIsRefused(t *testing.T) {
|
|||||||
t.Errorf("the refusal does not name what was asked for: %v", err)
|
t.Errorf("the refusal does not name what was asked for: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A module that must give software the machine's ADDRESS rather than its name — a resolver pointing
|
||||||
|
// the container runtime at itself, whose list of resolvers cannot be a name — says
|
||||||
|
// ${machine:address}, and gets what the machine's name resolves to on the private network: the same
|
||||||
|
// address every other machine's hosts file carries for it.
|
||||||
|
func TestAModuleNamesTheAddressBehindItsMachinesName(t *testing.T) {
|
||||||
|
pointing := Manifest{Module: "pointing", Version: "1", Resources: []map[string]any{{
|
||||||
|
"id": "runtime", "type": "file", "path": "/etc/runtime.json",
|
||||||
|
"content": `{"dns":["${machine:address}"]}`,
|
||||||
|
}}}
|
||||||
|
got, err := Resolve(shelf(pointing), []string{"pointing"}, anchored(), World{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
out, err := got.Declaration(Rendering{Names: map[string]string{
|
||||||
|
"workstation.internal": "10.42.0.7", "anchor.internal": "10.42.0.1"}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if content := plainly(out[0]["content"]); content != `{"dns":["10.42.0.7"]}` {
|
||||||
|
t.Fatalf("the machine's address was not the one its name resolves to: %q", content)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Off the network there is no such address, and the refusal says what the machine does have —
|
||||||
|
// rather than a placeholder written into the runtime's file and read as an address.
|
||||||
|
got, err = Resolve(shelf(pointing), []string{"pointing"}, workstation(), World{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := got.Declaration(Rendering{}); err == nil || !strings.Contains(err.Error(), "${machine:address}") {
|
||||||
|
t.Fatalf("a machine off the network was given an address, or refused for another reason: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,188 @@
|
|||||||
|
package catalogue
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The catalogue's resolver modules as they are, parsed by the real parser and composed as a
|
||||||
|
// machine would receive them (hal dnsmasq-app conversion, novox/hq 08-connectivity).
|
||||||
|
//
|
||||||
|
// The predecessor's resolver answered every name on a machine: the mesh's own itself, the rest
|
||||||
|
// forwarded to two fixed upstreams, with the machine's resolv.conf naming it alone and the
|
||||||
|
// container runtime pointed at its private-network address. These hold the mesh's modules to the
|
||||||
|
// same arrangement, and to the two things a resolver here must never do — read resolv.conf for
|
||||||
|
// its upstreams, or take an address systemd-resolved holds.
|
||||||
|
|
||||||
|
// resolverShelf is the three resolver modules beside something that answers `mesh-addressing`.
|
||||||
|
// The networking module that really does is composed in the controller and cannot be imported
|
||||||
|
// here, so a stand-in offers the same word; what is under test is the manifests, not the network.
|
||||||
|
func resolverShelf(t *testing.T) map[string]Manifest {
|
||||||
|
t.Helper()
|
||||||
|
shelf := map[string]Manifest{
|
||||||
|
"net": {Module: "net", Version: "1", Provides: []Offer{{Name: "mesh-addressing"}}},
|
||||||
|
}
|
||||||
|
for _, name := range []string{"dnsmasq", "resolv-conf", "resolved-split-dns"} {
|
||||||
|
shelf[name] = catalogueManifest(t, name)
|
||||||
|
}
|
||||||
|
return shelf
|
||||||
|
}
|
||||||
|
|
||||||
|
// twoMachines is what the control plane hands a rendering: internal names and their addresses.
|
||||||
|
var twoMachines = map[string]string{"anchor.internal": "10.42.0.1", "laptop.internal": "10.42.0.2"}
|
||||||
|
|
||||||
|
// Its configuration forwards to the upstreams the predecessor's module shipped, and gets them from
|
||||||
|
// nowhere else: `no-resolv` is what makes the documented loop — the resolver finding its own
|
||||||
|
// address in resolv.conf and becoming its own upstream — impossible.
|
||||||
|
func TestTheResolverForwardsToFixedUpstreamsAndNeverReadsResolvConf(t *testing.T) {
|
||||||
|
m := catalogueManifest(t, "dnsmasq")
|
||||||
|
var config string
|
||||||
|
for _, r := range m.Resources {
|
||||||
|
if r["id"] == "config" {
|
||||||
|
config, _ = r["content"].(string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if config == "" {
|
||||||
|
t.Fatal("the resolver has no configuration file")
|
||||||
|
}
|
||||||
|
for _, want := range []string{
|
||||||
|
"\nno-resolv\n", "\nserver=1.1.1.1\n", "\nserver=8.8.8.8\n",
|
||||||
|
"\nlisten-address=127.0.0.1\n", "\ninterface=mesh0\n", "\nbind-dynamic\n",
|
||||||
|
"\ndomain-needed\n", "\nbogus-priv\n",
|
||||||
|
"\nconf-file=" + m.Facts[FactNodeZones] + "\n",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(config, want) {
|
||||||
|
t.Errorf("the resolver's configuration lacks %q:\n%s", strings.TrimSpace(want), config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Not .53 or .54, which systemd-resolved holds; and not .55 any more, which was a convention
|
||||||
|
// beside the one every machine already followed — the predecessor's resolv.conf says .1.
|
||||||
|
for _, taken := range []string{"127.0.0.53", "127.0.0.54", "127.0.0.55"} {
|
||||||
|
if strings.Contains(config, "listen-address="+taken) {
|
||||||
|
t.Errorf("the resolver listens on %s", taken)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// And the file that decides what the machine asks names it there, alone.
|
||||||
|
var resolv string
|
||||||
|
for _, r := range catalogueManifest(t, "resolv-conf").Resources {
|
||||||
|
if r["path"] == "/etc/resolv.conf" {
|
||||||
|
resolv, _ = r["content"].(string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var nameservers []string
|
||||||
|
for _, line := range strings.Split(resolv, "\n") {
|
||||||
|
if strings.HasPrefix(line, "nameserver ") {
|
||||||
|
nameservers = append(nameservers, strings.TrimPrefix(line, "nameserver "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(nameservers) != 1 || nameservers[0] != "127.0.0.1" {
|
||||||
|
t.Errorf("resolv.conf names %v; the predecessor's names the mesh's resolver alone at 127.0.0.1", nameservers)
|
||||||
|
}
|
||||||
|
// The split-DNS alternative points at the same address, or a machine that keeps
|
||||||
|
// systemd-resolved in charge would route the mesh's suffix to nothing.
|
||||||
|
for _, r := range catalogueManifest(t, "resolved-split-dns").Resources {
|
||||||
|
if content, _ := r["content"].(string); content != "" && !strings.Contains(content, "DNS=127.0.0.1\n") {
|
||||||
|
t.Errorf("resolved-split-dns does not point at the resolver's address:\n%s", content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// that file, and the runtime pointed at this machine's own address.
|
||||||
|
func TestTheResolverAndWhatAsksItComposeOnOneMachine(t *testing.T) {
|
||||||
|
got, err := Resolve(resolverShelf(t), []string{"dnsmasq", "resolv-conf"},
|
||||||
|
Node{Name: "anchor", At: "anchor.internal"}, World{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(strings.Join(named(got), " "), "net") {
|
||||||
|
t.Fatalf("the resolver's data is the mesh's addresses, and nothing answering them was taken: %v", named(got))
|
||||||
|
}
|
||||||
|
out, err := got.Declaration(Rendering{
|
||||||
|
Names: twoMachines, Suffix: "internal",
|
||||||
|
Needed: map[string]map[string]string{"dnsmasq": {"broker": "sealed"}},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ids := byID(out)
|
||||||
|
zones := ids["dnsmasq.fact-node-zones"]
|
||||||
|
if zones == nil || zones["path"] != "/etc/mesh-resolver/nodes.conf" {
|
||||||
|
t.Fatalf("the resolver was not given the machines where its configuration reads them: %v", zones)
|
||||||
|
}
|
||||||
|
content, _ := zones["content"].(string)
|
||||||
|
for _, want := range []string{
|
||||||
|
"local=/internal/", "address=/anchor.internal/10.42.0.1", "address=/laptop.internal/10.42.0.2",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(content, want) {
|
||||||
|
t.Errorf("the machines file lacks %q:\n%s", want, content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
service := ids["dnsmasq.service"]
|
||||||
|
if service == nil {
|
||||||
|
t.Fatal("no resolver service composed")
|
||||||
|
}
|
||||||
|
reflects := map[string]bool{}
|
||||||
|
for _, id := range service["restart-on"].([]any) {
|
||||||
|
reflects[id.(string)] = true
|
||||||
|
}
|
||||||
|
if !reflects["dnsmasq.config"] || !reflects["dnsmasq.fact-node-zones"] {
|
||||||
|
t.Errorf("the daemon does not restart on its configuration and the machines file both: %v", service["restart-on"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// The runtime's own file, written into (novox/hq ADR 0102) with the one key this module states.
|
||||||
|
runtime := ids["dnsmasq.runtime-dns"]
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
var keys map[string][]string
|
||||||
|
if err := json.Unmarshal([]byte(runtime["content"].(string)), &keys); err != nil {
|
||||||
|
t.Fatalf("the runtime's keys are not JSON: %v", err)
|
||||||
|
}
|
||||||
|
if len(keys) != 1 || len(keys["dns"]) != 1 || keys["dns"][0] != "10.42.0.1" {
|
||||||
|
t.Errorf("the runtime is pointed at %v; containers resolve at this machine's own private-network address, and nothing else is written", keys)
|
||||||
|
}
|
||||||
|
for _, r := range out {
|
||||||
|
if r["type"] == "service" && r["unit"] == "docker.service" && r["id"] != "" &&
|
||||||
|
strings.HasPrefix(r["id"].(string), "dnsmasq.") {
|
||||||
|
t.Errorf("the resolver orders the runtime restarted or reloaded, which stops every container (ADR 0102) or does nothing for dns: %v", r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolv := ids["resolv-conf.resolv"]
|
||||||
|
if resolv == nil || !strings.Contains(resolv["content"].(string), "\nnameserver 127.0.0.1\n") {
|
||||||
|
t.Fatalf("the machine is not pointed at the resolver: %v", resolv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Two modules deciding what a machine asks are refused on one machine, as before — the claim
|
||||||
|
// exists so they never take turns overwriting each other.
|
||||||
|
func TestTwoThingsDecidingWhatAMachineAsksAreRefused(t *testing.T) {
|
||||||
|
_, err := Resolve(resolverShelf(t), []string{"dnsmasq", "resolv-conf", "resolved-split-dns"},
|
||||||
|
Node{Name: "anchor", At: "anchor.internal"}, World{})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("resolv-conf and resolved-split-dns were both assigned to one machine")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "the-resolver-configuration") {
|
||||||
|
t.Fatalf("the refusal does not say what was claimed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A machine that is not on the private network has no address for the runtime to be pointed at.
|
||||||
|
// Refused where the module and the machine are both named, rather than a placeholder written into
|
||||||
|
// the runtime's file and read as an address.
|
||||||
|
func TestTheResolverOnAMachineOffTheNetworkIsRefused(t *testing.T) {
|
||||||
|
got, err := Resolve(resolverShelf(t), []string{"dnsmasq"}, Node{Name: "anchor"}, World{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
_, err = got.Declaration(Rendering{Names: twoMachines, Suffix: "internal",
|
||||||
|
Needed: map[string]map[string]string{"dnsmasq": {"broker": "sealed"}}})
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "${machine:address}") {
|
||||||
|
t.Fatalf("a machine off the network was composed a resolver, or refused for another reason: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -54,17 +54,13 @@ const Addressing = "mesh-addressing"
|
|||||||
// something that needed the mesh's own addresses. Which is exactly what happened, once.
|
// something that needed the mesh's own addresses. Which is exactly what happened, once.
|
||||||
const TheNetwork = "the-private-network"
|
const TheNetwork = "the-private-network"
|
||||||
|
|
||||||
// Resolver is the module that answers every name under a machine, and the claim it holds.
|
// **A resolver's data went the same way as the names.** Two constants used to sit here — a
|
||||||
//
|
// `mesh-resolver` module that would write the machines as wildcards, and a `resolver-data`
|
||||||
// A claim because a machine has one resolver: two daemons answering the same names on one machine
|
// requirement a daemon would ask for. Nothing ever provided or consumed either: a module that
|
||||||
// is a coin toss about which one a query reaches, and the answer differing between them is the
|
// answers names asks for the `node-zones` fact in its own manifest (catalogue.FactsInto) and
|
||||||
// kind of fault nobody finds by looking at either.
|
// requires Addressing, since the file is made of the mesh's addresses and means nothing off the
|
||||||
const (
|
// network. A requirement nothing provides is refused at resolution, so leaving the names here
|
||||||
Resolver = "mesh-resolver"
|
// would only have documented a mechanism that does not exist.
|
||||||
// ResolverData is what a module running a resolver requires: the mesh's own account of which
|
|
||||||
// machines exist and where, in a file.
|
|
||||||
ResolverData = "resolver-data"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Domain is the module for people who want a network and do not want to choose one.
|
// Domain is the module for people who want a network and do not want to choose one.
|
||||||
//
|
//
|
||||||
|
|||||||
Reference in New Issue
Block a user