188 lines
7.2 KiB
Go
188 lines
7.2 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
"git.novox.be/novox/mesh-sdk/go/forgekey"
|
|
"git.novox.be/novox/mesh-sdk/go/health"
|
|
)
|
|
|
|
// 1703a: a machine where buildah runs, the account holds ranges in both files and a namespace can be made is healthy,
|
|
// and the health names buildah and the ranges (ADR 0487 §2: proven, not declared).
|
|
func TestHealthIsHealthyWithBuildahRangesAndANamespace(t *testing.T) {
|
|
f := newFake(t)
|
|
m := moduleWith(t, f, newStates())
|
|
a := m.Health(context.Background())
|
|
if a.State != health.StateHealthy || !a.Healthy {
|
|
t.Fatalf("health = %+v, want healthy", a)
|
|
}
|
|
for _, want := range []string{"buildah 1.45.1", "mesh-build-image holds 65536 subordinate uids and 65536 gids"} {
|
|
if !strings.Contains(a.Why, want) {
|
|
t.Errorf("health says %q, lacks %q", a.Why, want)
|
|
}
|
|
}
|
|
if _, err := os.Stat(m.Store + "/xdg"); err != nil {
|
|
t.Errorf("the runtime directory inside the store was not made: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestHealthIsUnhealthyWithoutBuildah(t *testing.T) {
|
|
f := newFake(t)
|
|
m := moduleWith(t, f, newStates())
|
|
m.Run = func(ctx context.Context, env []string, name string, args ...string) forgekey.Ran {
|
|
return forgekey.Ran{Status: 127, Err: "exec: \"buildah\": executable file not found in $PATH"}
|
|
}
|
|
a := m.Health(context.Background())
|
|
if a.Healthy || !strings.Contains(a.Why, "buildah") {
|
|
t.Errorf("health = %+v, want unhealthy naming buildah", a)
|
|
}
|
|
}
|
|
|
|
// The ranges are read from the two files, one each: without one in /etc/subuid or in /etc/subgid the account has no ids to
|
|
// map, and the health says which file. And without /dev/net/tun (a build node on 2026-10-28: its running kernel's modules were
|
|
// gone, so tun could not be loaded) buildah's network for a RUN line cannot be made: the health says the node is not
|
|
// eligible, and the puller judges by 0 so its asks go to a node that can build them.
|
|
func TestHealthIsUnhealthyWithoutWhatABuildNeeds(t *testing.T) {
|
|
for _, c := range []struct {
|
|
name string
|
|
set func(*Module, string)
|
|
says []string
|
|
}{
|
|
{"subuid", func(m *Module, p string) { m.SubUIDFile = p }, []string{"/subuid", "mesh-build-image"}},
|
|
{"subgid", func(m *Module, p string) { m.SubGIDFile = p }, []string{"/subgid", "mesh-build-image"}},
|
|
} {
|
|
f := newFake(t)
|
|
m := moduleWith(t, f, newStates())
|
|
p := t.TempDir() + "/" + c.name
|
|
os.WriteFile(p, []byte("someone-else:100000:65536\n"), 0o644)
|
|
c.set(m, p)
|
|
a := m.Health(context.Background())
|
|
for _, want := range c.says {
|
|
if a.Healthy || !strings.Contains(a.Why, want) {
|
|
t.Errorf("%s: health = %+v, want unhealthy saying %q", c.name, a, want)
|
|
}
|
|
}
|
|
// Whatever the look refuses, the node is not eligible, with the same words.
|
|
if got := m.Eligible(); got != a.Why {
|
|
t.Errorf("%s: eligible says %q, the health %q", c.name, got, a.Why)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 1126: a node that cannot build here now (no tun device) stays healthy, saying "not eligible" and why; the assigner reads
|
|
// unhealthy as a failed delivery and held the new build for every node. The eligibility, the status and the log say it too.
|
|
func TestANodeWithoutTunIsHealthyAndSaysItIsNotEligible(t *testing.T) {
|
|
f := newFake(t)
|
|
m := moduleWith(t, f, newStates())
|
|
var said []string
|
|
m.Say = func(format string, a ...any) { said = append(said, fmt.Sprintf(format, a...)) }
|
|
m.Tun = t.TempDir() + "/dev/net/tun"
|
|
a := m.Health(context.Background())
|
|
if a.State != health.StateHealthy || !a.Healthy {
|
|
t.Fatalf("health = %+v, want healthy", a)
|
|
}
|
|
for _, want := range []string{"not eligible", "/dev/net/tun cannot be opened", "tun"} {
|
|
if !strings.Contains(a.Why, want) {
|
|
t.Errorf("health says %q, lacks %q", a.Why, want)
|
|
}
|
|
}
|
|
if got := m.Eligible(); !strings.Contains(got, "not eligible: ") {
|
|
t.Errorf("eligible says %q", got)
|
|
}
|
|
if s := m.Status(context.Background()); s["eligible"] != false || s["refusing"] != true {
|
|
t.Errorf("status = %v, want not eligible", s)
|
|
}
|
|
// One log line for the change, none for the same look again; one when it can build again.
|
|
m.Health(context.Background())
|
|
if len(said) != 1 || !strings.Contains(said[0], "takes no image build now") {
|
|
t.Errorf("said = %q, want one line for the change", said)
|
|
}
|
|
m.Tun = filepath.Join(t.TempDir(), "tun")
|
|
os.WriteFile(m.Tun, nil, 0o644)
|
|
if a := m.Health(context.Background()); !a.Healthy || strings.Contains(a.Why, "not eligible") {
|
|
t.Errorf("health = %+v, want plain healthy", a)
|
|
}
|
|
if len(said) != 2 || !strings.Contains(said[1], "eligible to build images again") {
|
|
t.Errorf("said = %q, want the return said", said)
|
|
}
|
|
// A step still ends refused with the cause named.
|
|
m.Tun = t.TempDir() + "/none"
|
|
if err := m.Check(context.Background()); err == nil || !strings.Contains(err.Error(), "not eligible") {
|
|
t.Errorf("check = %v", err)
|
|
}
|
|
}
|
|
|
|
// 1126: what cannot run stays unhealthy even beside a missing tun: buildah missing is not "not eligible".
|
|
func TestWithoutBuildahTheNodeIsUnhealthyNotIneligible(t *testing.T) {
|
|
m := moduleWith(t, newFake(t), newStates())
|
|
m.Tun = t.TempDir() + "/none"
|
|
m.Run = func(ctx context.Context, env []string, name string, args ...string) forgekey.Ran {
|
|
return forgekey.Ran{Status: 127, Err: "exec: \"buildah\": executable file not found in $PATH"}
|
|
}
|
|
if a := m.Health(context.Background()); a.Healthy {
|
|
t.Errorf("health = %+v, want unhealthy", a)
|
|
}
|
|
}
|
|
|
|
func TestHealthIsUnhealthyWhenANamespaceCannotBeMade(t *testing.T) {
|
|
f := newFake(t)
|
|
m := moduleWith(t, f, newStates())
|
|
run := m.Run
|
|
m.Run = func(ctx context.Context, env []string, name string, args ...string) forgekey.Ran {
|
|
if len(args) >= 2 && args[len(args)-2] == "unshare" {
|
|
return forgekey.Ran{Status: 125, Stderr: "Error: cannot clone: Operation not permitted\n"}
|
|
}
|
|
return run(ctx, env, name, args...)
|
|
}
|
|
a := m.Health(context.Background())
|
|
if a.Healthy || !strings.Contains(a.Why, "user namespace") {
|
|
t.Errorf("health = %+v, want unhealthy naming the user namespace", a)
|
|
}
|
|
}
|
|
|
|
func TestSubRangeSumsTheAccountsLinesByNameOrUid(t *testing.T) {
|
|
p := t.TempDir() + "/subuid"
|
|
os.WriteFile(p, []byte("a:1:10\nmesh-build-image:100:65536\n1234:200000:100\nb:5:5\nbad line\n"), 0o644)
|
|
if n, err := SubRange(p, "mesh-build-image", "1234"); err != nil || n != 65636 {
|
|
t.Errorf("range = %d, %v, want 65636", n, err)
|
|
}
|
|
if n, _ := SubRange(p, "nobody", ""); n != 0 {
|
|
t.Errorf("nobody has %d", n)
|
|
}
|
|
}
|
|
|
|
// A step on a machine that cannot build ends refused with the cause named, and nothing else runs.
|
|
func TestAStepOnAMachineThatCannotBuildEndsRefused(t *testing.T) {
|
|
f := newFake(t)
|
|
states := newStates()
|
|
m := moduleWith(t, f, states)
|
|
m.SubGIDFile = "/nonexistent/subgid"
|
|
if _, err := tool(t, m, FetchTool)(map[string]any{"repository": "novox/mesh-catalog", "commit": commitSHA}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
st := states.waitEnd(t)
|
|
if st.State != delivery.StepRefused || !strings.Contains(st.Reason, "/nonexistent/subgid") {
|
|
t.Errorf("step = %+v, want refused naming the file", st)
|
|
}
|
|
for _, l := range f.lines() {
|
|
if strings.HasPrefix(l, "git ") {
|
|
t.Errorf("a refused step ran %q", l)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestStatusSaysWhatTheMachineHas(t *testing.T) {
|
|
f := newFake(t)
|
|
m := moduleWith(t, f, newStates())
|
|
s := m.Status(context.Background())
|
|
if s["refusing"] != false || s["buildah"] != "1.45.1" || s["subuids"] != 65536 || s["account"] != "mesh-build-image" {
|
|
t.Errorf("status = %v", s)
|
|
}
|
|
}
|