Files
mesh-catalog/modules/mesh-build-image/cmd/mesh-build-image/helpers_test.go
T
jschoubben 8c5f27627b
mesh/delivery merged: source.main moved to novox/mesh-catalog@6b6d5cba7195
mesh/check checking 8c5f27627b67 on 6b6d5cba7195
mesh-build-image: give a RUN step pasta's network, since the default netavark bridge cannot be set up rootless (issue 1703)
Every RUN failed before it started: 'setup network: netavark: nftables error ... Could not process rule'. Reproduced on
shanks with buildah 1.45.2, a clean environment and a private XDG_RUNTIME_DIR; --network pasta builds, and reaches the
network from a RUN. The sysrq integration row now fails if the RUN never started.
2026-10-26 15:23:19 +01:00

209 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"),
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
}