Phase 5 is done when the replays of 236, 262, 263 and 266 fail on the commit before their fix and pass after. The register names every replay with its issue and fix; the bus replay (266) runs a consumer filtered like the controller's against a bus of a given release, the resolver replay (262) renders the catalogue's machine list and asks every machine's name by getaddrinfo under musl and glibc, and the prover runs each at both commits: all five (236, 262, 263, 266, 273) proved.
134 lines
5.0 KiB
Go
134 lines
5.0 KiB
Go
package replays
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"text/template"
|
|
"time"
|
|
)
|
|
|
|
// **R262 — every machine's name resolves under musl and under glibc** (novox/hq issue 262; to-be 45 §9,
|
|
// "the resolver module's tests run under musl and glibc").
|
|
//
|
|
// After the mesh moved to one resolver, an Alpine container crash-looped on `getaddrinfo ENOTFOUND` for
|
|
// a machine's mesh name that `getent hosts` found: the resolver answered the name only through a
|
|
// wildcard, so asked for its IPv6 address it said there is no such name, and musl — which asks for both
|
|
// and takes NXDOMAIN as final — gave up; glibc did not. The replay renders the machine list the
|
|
// catalogue's resolver module reads (its `node-zones` fact, from the manifest at MESH_REPLAY_CATALOGUE),
|
|
// runs the resolver with it, and asks for each machine by `getaddrinfo` from a program under each C
|
|
// library, on the runtime's default bridge as the incident's container was. Needs a container runtime
|
|
// (its socket); its images come from the public registry.
|
|
var libcs = map[string]string{"musl": "python:3.13-alpine", "glibc": "python:3.13-slim"}
|
|
|
|
// resolverImage is where the resolver runs: the distribution's own dnsmasq, installed as the module
|
|
// installs it on a machine.
|
|
const resolverImage = "alpine:3.20"
|
|
|
|
func TestReplay262EveryMachineResolvesUnderMuslAndGlibc(t *testing.T) {
|
|
catalogue := os.Getenv("MESH_REPLAY_CATALOGUE")
|
|
if catalogue == "" {
|
|
catalogue = filepath.Join("..", "..", "mesh-catalog")
|
|
}
|
|
raw, err := os.ReadFile(filepath.Join(catalogue, "modules", "dnsmasq", "module.json"))
|
|
if err != nil {
|
|
t.Skipf("no catalogue to read the resolver's configuration from (%v); MESH_REPLAY_CATALOGUE names one", err)
|
|
}
|
|
docker, ok := DockerFromEnv()
|
|
if !ok {
|
|
t.Skip("no container runtime: the resolver and the two C libraries run in containers")
|
|
}
|
|
var manifest struct {
|
|
Facts map[string]struct {
|
|
Template string `json:"template"`
|
|
} `json:"facts"`
|
|
}
|
|
if err := json.Unmarshal(raw, &manifest); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
zones, ok := manifest.Facts["node-zones"]
|
|
if !ok {
|
|
t.Fatal("the resolver module renders no machine list (`facts.node-zones`)")
|
|
}
|
|
type entry struct{ Name, FQDN, Address, Account string }
|
|
machines := []entry{{Name: "anchor", FQDN: "anchor.internal", Address: "10.88.0.1"},
|
|
{Name: "homeserver", FQDN: "homeserver.internal", Address: "10.88.0.2"}}
|
|
var rendered bytes.Buffer
|
|
tpl, err := template.New("node-zones").Parse(zones.Template)
|
|
if err == nil {
|
|
err = tpl.Execute(&rendered, map[string]any{"Node": "anchor", "Suffix": "internal", "Names": machines,
|
|
"Machines": machines, "Zones": nil, "Holders": map[string][]entry{}})
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("the machine list does not render: %v", err)
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Minute)
|
|
defer cancel()
|
|
for _, image := range append([]string{resolverImage}, libcs["musl"], libcs["glibc"]) {
|
|
if err := docker.Pull(ctx, image); err != nil {
|
|
t.Fatalf("cannot fetch %s: %v", image, err)
|
|
}
|
|
}
|
|
// The resolver: nothing but the machine list, answering on its address on the bridge.
|
|
conf := base64.StdEncoding.EncodeToString(rendered.Bytes())
|
|
script := fmt.Sprintf(`apk add -q --no-cache dnsmasq >/dev/null && echo %s | base64 -d > /etc/nodes.conf && `+
|
|
`exec dnsmasq -k --no-resolv --no-hosts --log-queries --log-facility=- --conf-file=/etc/nodes.conf`, conf)
|
|
resolver, address, err := docker.Start(ctx, Run{Image: resolverImage, Cmd: []string{"sh", "-c", script}, Network: "bridge"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer docker.Remove(context.Background(), resolver)
|
|
if address == "" {
|
|
t.Fatal("the resolver has no address on the bridge")
|
|
}
|
|
|
|
// Each machine, asked by getaddrinfo as a program does — both families, as musl asks — a few times
|
|
// while the resolver comes up.
|
|
ask := fmt.Sprintf(`
|
|
import socket, sys, time
|
|
names = %q.split()
|
|
for attempt in range(30):
|
|
failed = []
|
|
for n in names:
|
|
try:
|
|
socket.getaddrinfo(n, 80)
|
|
except socket.gaierror as e:
|
|
failed.append("%%s: %%s" %% (n, e))
|
|
if not failed:
|
|
print("resolved", " ".join(names)); sys.exit(0)
|
|
time.sleep(1)
|
|
print("\n".join(failed)); sys.exit(1)
|
|
`, machines[0].FQDN+" "+machines[1].FQDN)
|
|
for libc, image := range libcs {
|
|
id, _, err := docker.Start(ctx, Run{Image: image, Cmd: []string{"python3", "-c", ask}, Network: "bridge",
|
|
DNS: []string{address}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
code, said, err := docker.Wait(ctx, id)
|
|
docker.Remove(context.Background(), id)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if code != 0 {
|
|
t.Errorf("under %s (%s) a machine's mesh name does not resolve through the catalogue's resolver — "+
|
|
"issue 262:\n%s\nthe resolver said:\n%s", libc, image, strings.TrimSpace(said), tail(docker.Logs(ctx, resolver), 12))
|
|
}
|
|
}
|
|
}
|
|
|
|
func tail(s string, n int) string {
|
|
lines := strings.Split(strings.TrimSpace(s), "\n")
|
|
if len(lines) > n {
|
|
lines = lines[len(lines)-n:]
|
|
}
|
|
return strings.Join(lines, "\n")
|
|
}
|