The launcher trusted a counter only a by-hand reconcile ever cleared and a known-good nothing in the daemon wrote, so no machine could roll its host back; the controller and the node tools were replaced in place with nothing kept. - The launcher runs a delivered host that is not known-good on trial: one that crashes, stops for nothing, or does not report within ten minutes goes back to known-good, once per version, recorded in rolled-back. The host proves itself when the mesh takes a report under its own build, says every standing verdict on its reports, never stands aside for a rolled-back version, and restarts its service once when its launcher was replaced on disk. - The engine keeps the controller's and the node tools' previous build beside the new one and judges the new one: the lease taken by the controller it started (read-only direct get of mesh-controller_lease/holder), or this machine's runtime answering $SRV.PING.node-tools.<node>, within sixty seconds of time it could ask. Not healthy: the previous restored, once, said. Proved: the previous deleted. A build declared not-reversible is never rolled back. - Retire never removes a version newer than the running one.
94 lines
3.4 KiB
Go
94 lines
3.4 KiB
Go
package main
|
|
|
|
import (
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-host/internal/link"
|
|
"github.com/novox/mesh-host/internal/upgrade"
|
|
)
|
|
|
|
// The first account the mesh takes from this build, about a declaration, proves it to the launcher;
|
|
// nothing else does — a refusal, a report about no declaration, another build's report (to-be 45 §8).
|
|
func TestOnlyAnAccountUnderThisBuildProvesIt(t *testing.T) {
|
|
state := filepath.Join(t.TempDir(), "state.json")
|
|
t.Setenv("MESH_HOST_LIBEXEC", t.TempDir())
|
|
var said []string
|
|
p := &proving{statePath: state, version: "2.0", say: func(s string) { said = append(said, s) }}
|
|
|
|
for _, r := range []link.Report{
|
|
{Host: "2.0", Refused: "no"},
|
|
{Host: "2.0"},
|
|
{Host: "1.0", Declared: "abc"},
|
|
} {
|
|
p.heard(r)
|
|
if !unproved(state, "2.0") {
|
|
t.Fatalf("%+v proved this build", r)
|
|
}
|
|
}
|
|
p.heard(link.Report{Host: "2.0", Declared: "abc"})
|
|
if unproved(state, "2.0") {
|
|
t.Fatalf("an account of a declaration under this build did not prove it (%v)", said)
|
|
}
|
|
if got, _ := upgrade.ReadKnownGood(upgrade.KnownGoodPath(state)); got != "2.0" {
|
|
t.Fatalf("known-good is %q", got)
|
|
}
|
|
}
|
|
|
|
// Every verdict that stands is said, and a reconcile that has one says it unasked.
|
|
func TestAStandingRollbackIsSaidOnEveryReport(t *testing.T) {
|
|
state := filepath.Join(t.TempDir(), "state.json")
|
|
line := "2.0\t1.0\t1759744800\trolled-back\tit did not report within 600s of starting\n"
|
|
if err := os.WriteFile(upgrade.RolledBackPath(state), []byte(line), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := standingRollbacks(state)
|
|
if len(got) == 0 || got[0].Component != link.ComponentEngine || got[0].From != "2.0" || got[0].To != "1.0" ||
|
|
got[0].Outcome != link.RolledBack {
|
|
t.Fatalf("what the report says is %+v", got)
|
|
}
|
|
if !worthSaying(link.Report{Rollbacks: got}) {
|
|
t.Fatal("a reconcile with a rollback standing does not say it")
|
|
}
|
|
if adoptionFingerprint(link.Report{Rollbacks: got}) == adoptionFingerprint(link.Report{}) {
|
|
t.Fatal("a rollback reached or ended is not news to the reconcile")
|
|
}
|
|
}
|
|
|
|
// The launcher running this host is known to have been replaced only when it certainly was: a shell
|
|
// reading the launcher, from a file renamed over. Asked of a real process, not a model of one.
|
|
func TestAReplacedLauncherIsSeen(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "launch")
|
|
if err := os.WriteFile(path, []byte("#!/bin/sh\nsleep 5\necho done\n"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
shell := exec.Command("sh", path)
|
|
if err := shell.Start(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = shell.Process.Kill(); _ = shell.Wait() })
|
|
time.Sleep(200 * time.Millisecond)
|
|
if launcherReplaced(shell.Process.Pid, "/proc", path) {
|
|
t.Fatal("a launcher still on disk reads as replaced")
|
|
}
|
|
// Delivered as the mesh writes a file: a new one renamed over it.
|
|
if err := os.WriteFile(path+".new", []byte("#!/bin/sh\necho new\n"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Rename(path+".new", path); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !launcherReplaced(shell.Process.Pid, "/proc", path) {
|
|
fds, _ := os.ReadDir(filepath.Join("/proc", strconv.Itoa(shell.Process.Pid), "fd"))
|
|
t.Skipf("this shell does not keep its script open (%d fds), so a replaced launcher cannot be seen here", len(fds))
|
|
}
|
|
if launcherReplaced(shell.Process.Pid, "/proc", filepath.Join(dir, "other")) {
|
|
t.Fatal("a process reading another script reads as this launcher")
|
|
}
|
|
}
|