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) } } }