Files
mesh-catalog/modules/mesh-build-image/cmd/mesh-build-image/helpers_test.go
T
jschoubben d13f74df40 A node that cannot open /dev/net/tun takes no image build, and every node loads tun at boot and keeps its running kernel's modules
An image build node up 24 days had lost its running kernel's modules to a linux-lts upgrade, so tun
could not load and every RUN line failed in pasta. mesh-build-image now proves the tun device and,
whatever its look refuses, judges its node not eligible (capacity 0 with the reason), so the asks go
to a node that can build them. The kernel module loads tun at boot and declares kernel-modules-hook,
which keeps the running kernel's modules across an upgrade. Needs mesh-sdk fix/ace-tun (Puller.Eligible).
2026-10-28 12:44:58 +01:00

210 lines
5.7 KiB
Go

package main
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/forgekey"
)
const commitSHA = "0123456789abcdef0123456789abcdef01234567"
// keptStates is the steps state, kept in memory: every write of a key, and the keys that ended.
type keptStates struct {
mu sync.Mutex
steps map[string][]delivery.Step
ended chan string
}
func newStates() *keptStates {
return &keptStates{steps: map[string][]delivery.Step{}, ended: make(chan string, 16)}
}
func (s *keptStates) putStep(key string, value any) (uint64, error) {
st, err := delivery.DecodeStep(value.(json.RawMessage))
if err != nil {
return 0, err
}
s.mu.Lock()
s.steps[key] = append(s.steps[key], st)
s.mu.Unlock()
if st.State != delivery.StepRunning {
s.ended <- key
}
return 1, nil
}
func (s *keptStates) waitEnd(t *testing.T) delivery.Step {
t.Helper()
select {
case key := <-s.ended:
s.mu.Lock()
defer s.mu.Unlock()
v := s.steps[key]
return v[len(v)-1]
case <-time.After(5 * time.Second):
t.Fatal("the step never ended on its key")
return delivery.Step{}
}
}
// cmdRecord is one command a step ran.
type cmdRecord struct {
env []string
name string
line string
}
// fake stands in for git and buildah at the process boundary and for the registry over HTTP.
type fake struct {
mu sync.Mutex
calls []cmdRecord
digest string // what `buildah images` prints
pushes int
refuse int // the first n pushes answer "connection refused"
failBud forgekey.Ran // what a failing buildah bud answers; a zero Status is a passing bud
// digestFile: "" writes the digest, "none" writes no file, "empty" writes an empty one.
digestFile string
// registry: repository:tag -> digest
registry map[string]string
server *httptest.Server
}
const imageDigest = "sha256:1111111111111111111111111111111111111111111111111111111111111111"
func newFake(t *testing.T) *fake {
f := &fake{digest: imageDigest, registry: map[string]string{}}
f.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
f.mu.Lock()
defer f.mu.Unlock()
key := strings.TrimPrefix(r.URL.Path, "/v2/")
i := strings.Index(key, "/manifests/")
if r.Method != http.MethodHead || i < 0 {
http.NotFound(w, r)
return
}
d, ok := f.registry[key[:i]+":"+key[i+len("/manifests/"):]]
if !ok {
http.NotFound(w, r)
return
}
w.Header().Set("Docker-Content-Digest", d)
}))
t.Cleanup(f.server.Close)
return f
}
func (f *fake) address() string { return f.server.Listener.Addr().String() }
func (f *fake) run(ctx context.Context, env []string, name string, args ...string) forgekey.Ran {
line := name + " " + strings.Join(args, " ")
f.mu.Lock()
f.calls = append(f.calls, cmdRecord{env: env, name: name, line: line})
f.mu.Unlock()
switch {
case name == "git" && strings.Contains(line, " rev-parse HEAD"):
return forgekey.Ran{Stdout: commitSHA + "\n"}
case name == "buildah" && strings.Contains(line, "--version"):
return forgekey.Ran{Stdout: "buildah version 1.45.1 (image-spec 1.1.0, runtime-spec 1.1.0)\n"}
case name == "buildah" && strings.Contains(line, " bud "):
if f.failBud.Status != 0 || f.failBud.Err != "" {
return f.failBud
}
case name == "buildah" && strings.Contains(line, " images "):
return forgekey.Ran{Stdout: f.digest + "\n"}
case name == "buildah" && strings.Contains(line, " push "):
f.mu.Lock()
defer f.mu.Unlock()
if f.refuse > 0 {
f.refuse--
return forgekey.Ran{Status: 125, Stderr: "Error: dial tcp 127.0.0.1:5000: connect: connection refused\n"}
}
f.pushes++
var digestFile, dest string
for i, a := range args {
if a == "--digestfile" {
digestFile = args[i+1]
}
if strings.HasPrefix(a, "docker://") {
dest = strings.TrimPrefix(a, "docker://")
}
}
dest = strings.TrimPrefix(dest, f.address()+"/")
f.registry[dest] = f.digest
switch f.digestFile {
case "":
os.WriteFile(digestFile, []byte(f.digest), 0o644)
case "empty":
os.WriteFile(digestFile, nil, 0o644)
}
}
return forgekey.Ran{}
}
func (f *fake) lines() []string {
f.mu.Lock()
defer f.mu.Unlock()
out := []string{}
for _, c := range f.calls {
out = append(out, c.line)
}
return out
}
func (f *fake) called(verb string) []cmdRecord {
f.mu.Lock()
defer f.mu.Unlock()
var out []cmdRecord
for _, c := range f.calls {
if c.name == "buildah" && strings.Contains(c.line, " "+verb+" ") {
out = append(out, c)
}
}
return out
}
// moduleWith is a module on node ace over the fake, with subordinate ranges for its account, and the registry's
// address written where the manifest would bind it.
func moduleWith(t *testing.T, f *fake, states *keptStates) *Module {
t.Helper()
dir := t.TempDir()
write := func(name, body string) string {
p := filepath.Join(dir, name)
if err := os.WriteFile(p, []byte(body), 0o644); err != nil {
t.Fatal(err)
}
return p
}
m := &Module{
Node: "ace", Account: "mesh-build-image", UID: "1234", Home: t.TempDir(), Work: t.TempDir(), Store: t.TempDir(),
Steps: states.putStep, Ctx: context.Background(), Run: f.run, HTTP: f.server.Client(),
SubUIDFile: write("subuid", "someone:100000:65536\nmesh-build-image:624288:65536\n"),
SubGIDFile: write("subgid", "mesh-build-image:624288:65536\n"),
RegistryFile: write("registry", f.address()+"\n"),
Tun: write("tun", ""),
RegistryWait: 2 * time.Second,
RegistryPause: 5 * time.Millisecond,
}
return m
}
func tool(t *testing.T, m *Module, name string) func(map[string]any) (any, error) {
t.Helper()
for _, tl := range m.Tools(m.StepTools()) {
if tl.Name == name {
return tl.Run
}
}
t.Fatalf("no tool %s", name)
return nil
}