mesh/merge-gate fail: builds new: modules/systemd-resolved, sent nowhere; no bus step; a manifest the change touches fails the module check: modules/system…
mesh/repo-check fail: its merge-check.sh failed: long-running resources without health: 70
mesh/delivery superseded: a newer head of the same pull request
Holds node-resolver and provides split-dns at the machine's reach, for a machine whose VPN client pushes resolvers of its own. It writes the resolver file naming the machine's private address, gives resolved the mesh's resolvers as the default route, and serves routes, route and unroute on the mesh and, over a root-only socket, on the machine. Its guard keeps an outside write of the file for the module that handles it and puts the module's file back: at once when taken, after 90 s otherwise, so a write nothing declared to handle is still raised by the node-engine.
120 lines
4.1 KiB
Go
120 lines
4.1 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// The manifest and this code say one thing: the paths the guard keeps are the files the mesh renders, the
|
|
// process runs this binary as the guard, and the resolver file and its kept copy are one template.
|
|
|
|
type manifest struct {
|
|
Claims []struct {
|
|
Name string `json:"name"`
|
|
Serves []string `json:"serves"`
|
|
} `json:"claims"`
|
|
Provides []struct {
|
|
Name string `json:"name"`
|
|
Reach string `json:"reach"`
|
|
} `json:"provides"`
|
|
Upgrade struct {
|
|
Policy string `json:"policy"`
|
|
Why string `json:"why"`
|
|
} `json:"upgrade"`
|
|
Facts map[string]struct {
|
|
Path string `json:"path"`
|
|
Template string `json:"template"`
|
|
} `json:"facts"`
|
|
Resources []map[string]any `json:"resources"`
|
|
Build struct {
|
|
Artifacts []struct {
|
|
Binary string `json:"binary"`
|
|
} `json:"artifacts"`
|
|
} `json:"build"`
|
|
}
|
|
|
|
func theManifest(t *testing.T) manifest {
|
|
t.Helper()
|
|
raw, err := os.ReadFile("../../module.json")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var m manifest
|
|
if err := json.Unmarshal(raw, &m); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return m
|
|
}
|
|
|
|
func TestTheManifestSaysWhatTheGuardKeeps(t *testing.T) {
|
|
m := theManifest(t)
|
|
if m.Facts["resolvers"].Path != ResolvConf || m.Facts["kept"].Path != KeptPath || m.Facts["suffix"].Path != SuffixPath {
|
|
t.Errorf("the rendered paths are not the ones the guard reads: %+v", m.Facts)
|
|
}
|
|
if m.Facts["resolvers"].Template != m.Facts["kept"].Template {
|
|
t.Error("the resolver file and the copy the guard puts back are not one template")
|
|
}
|
|
// Sorted before the live file, so the mesh writes the copy first and the guard never puts back the
|
|
// file it is about to be given.
|
|
if !("kept" < "resolvers") {
|
|
t.Error("the kept copy is not rendered before the file")
|
|
}
|
|
if !strings.HasPrefix(m.Facts["resolvers"].Template, "# Managed by the mesh") {
|
|
t.Error("the resolver file does not begin as the mesh's own does, which is how the uplink's verb reads it")
|
|
}
|
|
var lines []string
|
|
for _, l := range strings.Split(m.Facts["resolvers"].Template, "\n") {
|
|
if strings.Contains(l, "nameserver") && !strings.HasPrefix(l, "#") {
|
|
lines = append(lines, l)
|
|
}
|
|
}
|
|
if len(lines) != 1 || strings.Contains(m.Facts["resolvers"].Template, "search ") {
|
|
t.Errorf("the resolver file lists more than this machine's own resolver: %v", lines)
|
|
}
|
|
conf := m.Facts["resolved"].Template
|
|
for _, want := range []string{`DNS={{range index .Holders "mesh-dns-resolver"}}`, "\nDomains=~.\n", "\nFallbackDNS=\n",
|
|
"DNSStubListenerExtra={{$own}}\n", "\nCache=no\n", "\nLLMNR=no\n", "\nMulticastDNS=no\n"} {
|
|
if !strings.Contains(conf, want) {
|
|
t.Errorf("resolved's drop-in lacks %q", want)
|
|
}
|
|
}
|
|
guard, service := false, false
|
|
for _, r := range m.Resources {
|
|
run, _ := r["run"].([]any)
|
|
if r["type"] == "process" && len(run) == 2 && run[0] == "./"+m.Build.Artifacts[0].Binary && run[1] == "guard" {
|
|
guard = r["user"] == nil && r["run-once"] == nil && r["health"] != nil
|
|
}
|
|
if r["type"] == "service" && r["unit"] == "systemd-resolved.service" {
|
|
on, _ := r["restart-on"].([]any)
|
|
service = r["state"] == "running" && len(on) == 1 && on[0] == "systemd-resolved.fact-resolved" && r["health"] != nil
|
|
}
|
|
}
|
|
if !guard || !service {
|
|
t.Errorf("the guard (root, long-running, its health said) %v; resolved (running, restarted on its drop-in) %v", guard, service)
|
|
}
|
|
if len(m.Claims) != 1 || m.Claims[0].Name != Seat || strings.Join(m.Claims[0].Serves, " ") != "routes route unroute" {
|
|
t.Errorf("the claim is %+v", m.Claims)
|
|
}
|
|
if len(m.Provides) != 1 || m.Provides[0].Name != "split-dns" || m.Provides[0].Reach != "machine" {
|
|
t.Errorf("split-dns is not provided at the machine's reach: %+v", m.Provides)
|
|
}
|
|
if m.Upgrade.Policy != "record" || m.Upgrade.Why == "" {
|
|
t.Error("a build of the machine's names rolls out on its own")
|
|
}
|
|
}
|
|
|
|
// Every verb the claim serves is a tool of the bundle, by the seat's name.
|
|
func TestTheBundleServesTheClaimedVerbs(t *testing.T) {
|
|
have := map[string]bool{}
|
|
for _, tool := range tools(&Resolver{}, "") {
|
|
have[tool.Name] = true
|
|
}
|
|
for _, v := range theManifest(t).Claims[0].Serves {
|
|
if !have[Seat+"."+v] {
|
|
t.Errorf("%s.%s is claimed and not served", Seat, v)
|
|
}
|
|
}
|
|
}
|