Files
mesh-catalog/modules/systemd-resolved/cmd/resolver-tools/manifest_test.go
T
jschoubben d22a7f3a41
mesh/delivery merged: source.main moved to novox/mesh-catalog@a60728e6f09d
mesh/delivery-group group fix/1748-resolver-wait merged: every member is merged
mesh/check checking d22a7f3a412f on a60728e6f09d | novox/mesh-catalog#671 in group fix/1748-resolver-wait
fix-1748-own-resolver-wait: the own resolver's file waits 3 s a try
The resolver file of systemd-resolved (resolvers and kept facts) carries options timeout:3 attempts:2 edns0 in place of timeout:1; manifest test asserts both facts, guard test's literal follows.

Builds: design 50 resolver-wait §1, §4 (and §2 of the main file, line 73)
2026-11-12 18:09:17 +01:00

126 lines
4.5 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)
}
// Design 50 resolver-wait §1: both facts carry the longer wait, never the uplink files' timeout:1.
for _, fact := range []string{"resolvers", "kept"} {
if tpl := m.Facts[fact].Template; !strings.Contains(tpl, "\noptions timeout:3 attempts:2 edns0\n") || strings.Contains(tpl, "timeout:1") {
t.Errorf("fact %s does not carry options timeout:3 attempts:2 edns0: %q", fact, tpl)
}
}
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)
}
}
}