mesh/delivery merged: source.main moved to novox/mesh-catalog@6b6d5cba7195
mesh/check checking 8c5f27627b67 on 6b6d5cba7195
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.
209 lines
5.7 KiB
Go
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
|
|
}
|