mesh/delivery merged: source.main moved to novox/mesh-catalog@683ce7e37f29
mesh/check being carried to 3f552a17f697 on 683ce7e37f29 | from 3feeefc70f7c
125 lines
4.2 KiB
Go
125 lines
4.2 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// The module's directory, where its manifest and its files/ are.
|
|
const moduleDir = "../.."
|
|
|
|
// templateText is the text a file resource places on the machine: its template's, in the module's files/
|
|
// (novox/hq ADR 0349 §2). A resource that names no template, or a template that is not there, fails the test.
|
|
func templateText(t *testing.T, r map[string]any) string {
|
|
t.Helper()
|
|
name, _ := r["template"].(string)
|
|
if name == "" {
|
|
t.Fatalf("resource %v names no template", r["id"])
|
|
}
|
|
raw, err := os.ReadFile(filepath.Join(moduleDir, filepath.FromSlash(name)))
|
|
if err != nil {
|
|
t.Fatalf("resource %v names %s: %v", r["id"], name, err)
|
|
}
|
|
return string(raw)
|
|
}
|
|
|
|
// No file resource carries its text inline: each names its template in files/, and the build packs files/
|
|
// into the one archive the controller reads the templates back from (novox/hq ADR 0349 §2).
|
|
func TestEveryFileTakesItsTextFromATemplate(t *testing.T) {
|
|
raw, err := os.ReadFile(filepath.Join(moduleDir, "module.json"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var m struct {
|
|
Resources []map[string]any `json:"resources"`
|
|
Build struct {
|
|
Artifacts []map[string]any `json:"artifacts"`
|
|
} `json:"build"`
|
|
}
|
|
if err := json.Unmarshal(raw, &m); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, r := range m.Resources {
|
|
if _, inline := r["content"]; inline {
|
|
t.Errorf("resource %v keeps its text inline: keep it in files/ and name it as the template", r["id"])
|
|
}
|
|
if r["type"] == "file" {
|
|
templateText(t, r)
|
|
}
|
|
}
|
|
packs := 0
|
|
for _, a := range m.Build.Artifacts {
|
|
if a["kind"] == "archive" && a["from"] == "files" {
|
|
packs++
|
|
if a["name"] != "files" {
|
|
t.Errorf("the templates archive is named %v, not files", a["name"])
|
|
}
|
|
}
|
|
}
|
|
if packs != 1 {
|
|
t.Errorf("the build packs files/ into %d archives, not one", packs)
|
|
}
|
|
}
|
|
|
|
// The packaged dnsmasq.service orders itself Before= network-online.target and nss-lookup.target. A drop-in
|
|
// that orders it After= either of them makes an ordering cycle, and systemd drops the start job: the mesh
|
|
// resolver stayed down for 90 minutes at a boot (novox/hq issue 1126). Wants=, Requires= and BindsTo= are
|
|
// refused too: they pull the target in, and with the packaged ordering they are the same mistake. Because the
|
|
// tunnel is ordered after those targets, the drop-in cannot wait for the address either: a start that finds it
|
|
// missing fails at once and Restart=on-failure brings it up (see the next test).
|
|
func TestDropInDoesNotOrderAfterATargetThePackagedUnitOrdersBefore(t *testing.T) {
|
|
raw, err := os.ReadFile(filepath.Join(moduleDir, "files", "mesh.conf"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
forbidden := []string{"network-online.target", "nss-lookup.target"}
|
|
for n, line := range strings.Split(string(raw), "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if strings.HasPrefix(line, "#") {
|
|
continue
|
|
}
|
|
key, val, ok := strings.Cut(line, "=")
|
|
key, val = strings.TrimSpace(key), strings.TrimSpace(val)
|
|
if !ok || (key != "After" && key != "Requires" && key != "Wants" && key != "BindsTo") {
|
|
continue
|
|
}
|
|
for _, target := range forbidden {
|
|
if strings.Contains(val, target) {
|
|
t.Errorf("mesh.conf line %d: %s=%s orders dnsmasq after a target its packaged unit orders itself before: a cycle", n+1, key, val)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// The wait for the address in ExecStartPre is at most two seconds: the tunnel that brings the address is ordered
|
|
// after network-online.target, which the resolver is ordered before, so a longer wait would hold the tunnel back.
|
|
func TestAddressWaitIsAtMostTwoSeconds(t *testing.T) {
|
|
raw, err := os.ReadFile(filepath.Join(moduleDir, "files", "mesh.conf"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
found := false
|
|
for _, line := range strings.Split(string(raw), "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if !strings.HasPrefix(line, "ExecStartPre=") {
|
|
continue
|
|
}
|
|
m := regexp.MustCompile(`seq 1 (\d+)`).FindStringSubmatch(line)
|
|
if m == nil {
|
|
t.Fatalf("ExecStartPre has no bounded wait (seq 1 N): %s", line)
|
|
}
|
|
found = true
|
|
if n, _ := strconv.Atoi(m[1]); n > 2 {
|
|
t.Errorf("ExecStartPre waits up to %d s for the address, more than 2 s", n)
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatal("mesh.conf has no ExecStartPre")
|
|
}
|
|
}
|