TestTheForgesSshPortIsGivenByTheNumberTheForgeCallsIt exercised a settings override from '2222' to 222 — but 2222 was never a real port anywhere, just a mistake in gitea's own manifest (fixed alongside this: listens.port is now 22, the container's real internal sshd port, matching every other module's convention, and ports declares 222:22 directly — 222 has always been the real, fixed public git-ssh port, needing no per-node override). Split into two tests: the fixed default with no override, and a genuine override case for a hypothetical node whose predecessor used a different number, keyed correctly by 22. TestTheResolverAndWhatAsksItComposeOnOneMachine set Rendering.Names but FactNodeZones reads Rendering.Machines (novox/hq issue 111 split the two apart: every name the mesh serves vs. the machines subset) — a loose end from that merge, not exercised until now. Both are the same map in this test's scenario, so both fields are set.
192 lines
8.4 KiB
Go
192 lines
8.4 KiB
Go
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 is every name the mesh serves; Machines is the subset that is a node (novox/hq
|
|
// 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.
|
|
Names: twoMachines, Machines: 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)
|
|
}
|
|
}
|