v0.3.0 8f98fd3

sandboxtest.go RawHistoryFolder

1// SPDX-License-Identifier: MIT
2
3// Package sandboxtest has helpers shared by the tests of the sandbox drivers.
4package sandboxtest
5
6import (
7	"context"
8	"encoding/binary"
9	"io"
10	"net"
11	"net/http"
12	"net/netip"
13	"os"
14	"path/filepath"
15	"sync"
16	"testing"
17	"time"
18
19	"bnema.dev/ermite-runner/internal/cachesvc"
20	"bnema.dev/ermite-runner/internal/cmdrun"
21	"bnema.dev/ermite-runner/internal/netpolicy"
22	"bnema.dev/ermite-runner/internal/sandbox"
23)
24
25// NoMountRunner runs real commands except mount and umount, which succeed
26// without doing anything: loop mounts need root, and the tests that use it
27// replace them by plain directories (mkfs still runs).
28type NoMountRunner struct{}
29
30// Run implements cmdrun.Runner.
31func (NoMountRunner) Run(ctx context.Context, in io.Reader, name string, args ...string) ([]byte, error) {
32	if name == "mount" || name == "umount" {
33		return nil, nil
34	}
35	return cmdrun.Exec{}.Run(ctx, in, name, args...)
36}
37
38// WriteStaticELF writes a minimal statically linked ELF64 file (one PT_LOAD, no
39// PT_INTERP): enough for sandbox.CheckStatic, never executed.
40func WriteStaticELF(t testing.TB, path string) {
41	t.Helper()
42	var b [64 + 56]byte
43	copy(b[:], "\x7fELF")
44	b[4], b[5], b[6] = 2, 1, 1                // 64-bit, little endian, version
45	binary.LittleEndian.PutUint16(b[16:], 2)  // ET_EXEC
46	binary.LittleEndian.PutUint16(b[18:], 62) // x86-64
47	binary.LittleEndian.PutUint32(b[20:], 1)
48	binary.LittleEndian.PutUint64(b[32:], 64) // e_phoff
49	binary.LittleEndian.PutUint16(b[52:], 64) // e_ehsize
50	binary.LittleEndian.PutUint16(b[54:], 56) // e_phentsize
51	binary.LittleEndian.PutUint16(b[56:], 1)  // e_phnum
52	binary.LittleEndian.PutUint32(b[64:], 1)  // PT_LOAD
53	if err := os.WriteFile(path, b[:], 0o755); err != nil {
54		t.Fatal(err)
55	}
56}
57
58// Plumbing is a netsetup.Plumbing that records what is attached.
59type Plumbing struct {
60	mu       sync.Mutex
61	Attached map[string]netpolicy.Guest
62	Events   []string
63	Resets   int
64	FailNext error
65}
66
67// NewPlumbing returns an empty Plumbing.
68func NewPlumbing() *Plumbing { return &Plumbing{Attached: map[string]netpolicy.Guest{}} }
69
70// EnableForwarding implements netsetup.Plumbing.
71func (p *Plumbing) EnableForwarding() error { return nil }
72
73// Attach implements netsetup.Plumbing.
74func (p *Plumbing) Attach(_ context.Context, g netpolicy.Guest) error {
75	p.mu.Lock()
76	defer p.mu.Unlock()
77	if p.FailNext != nil {
78		err := p.FailNext
79		p.FailNext = nil
80		return err
81	}
82	p.Attached[g.Iface] = g
83	p.Events = append(p.Events, "attach "+g.Iface)
84	return nil
85}
86
87// Detach implements netsetup.Plumbing.
88func (p *Plumbing) Detach(_ context.Context, iface string, _ netip.Addr) error {
89	p.mu.Lock()
90	defer p.mu.Unlock()
91	delete(p.Attached, iface)
92	p.Events = append(p.Events, "detach "+iface)
93	return nil
94}
95
96// Reset implements netsetup.Plumbing.
97func (p *Plumbing) Reset(context.Context) error {
98	p.mu.Lock()
99	defer p.mu.Unlock()
100	p.Resets++
101	p.Attached = map[string]netpolicy.Guest{}
102	return nil
103}
104
105// Close implements netsetup.Plumbing.
106func (p *Plumbing) Close() {}
107
108// Live returns the number of attached guests.
109func (p *Plumbing) Live() int {
110	p.mu.Lock()
111	defer p.mu.Unlock()
112	return len(p.Attached)
113}
114
115// Externals is a fixed, replaceable sandbox.ExternalsSource.
116type Externals struct {
117	mu  sync.Mutex
118	set []sandbox.External
119}
120
121// Set replaces the set the drivers see.
122func (e *Externals) Set(set ...sandbox.External) { e.mu.Lock(); e.set = set; e.mu.Unlock() }
123
124// Externals implements sandbox.ExternalsSource.
125func (e *Externals) Externals() []sandbox.External {
126	e.mu.Lock()
127	defer e.mu.Unlock()
128	return append([]sandbox.External(nil), e.set...)
129}
130
131// NewCache starts a cache service in a temporary directory and closes it with
132// the test.
133func NewCache(t testing.TB) *cachesvc.Service {
134	t.Helper()
135	svc, err := cachesvc.New(cachesvc.Config{Dir: filepath.Join(t.TempDir(), "cache")})
136	if err != nil {
137		t.Fatal(err)
138	}
139	t.Cleanup(func() { _ = svc.CloseAll() })
140	return svc
141}
142
143// CacheLookup sends a cache lookup with token over the Unix socket sock, as the
144// guest relay delivers it, and returns the HTTP status; the error is that of
145// the connection (the socket is closed or gone).
146func CacheLookup(sock, token string) (int, error) {
147	cl := &http.Client{Timeout: 10 * time.Second, Transport: &http.Transport{
148		DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
149			var d net.Dialer
150			return d.DialContext(ctx, "unix", sock)
151		},
152		DisableKeepAlives: true,
153	}}
154	req, err := http.NewRequest("GET", "http://cache"+cachesvc.APIPath+"cache?keys=k&version=v", nil)
155	if err != nil {
156		return 0, err
157	}
158	req.Header.Set("Authorization", "Bearer "+token)
159	resp, err := cl.Do(req)
160	if err != nil {
161		return 0, err
162	}
163	_ = resp.Body.Close()
164	return resp.StatusCode, nil
165}
166
167// CacheToken is a token of the service for a job that may read and write main.
168func CacheToken(svc *cachesvc.Service, job string) string {
169	ref := "refs/heads/main"
170	return svc.Token(job, cachesvc.Scope{Repository: "bnema/app", WriteRef: ref, ReadRefs: []string{ref}}, time.Hour)
171}
172
173// MakeExternal creates a host directory holding bin/node with content and
174// returns it as an external called name.
175func MakeExternal(t testing.TB, name, id, content string) sandbox.External {
176	t.Helper()
177	dir := filepath.Join(t.TempDir(), name)
178	if err := os.MkdirAll(filepath.Join(dir, "bin"), 0o755); err != nil {
179		t.Fatal(err)
180	}
181	if err := os.WriteFile(filepath.Join(dir, "bin", "node"), []byte(content), 0o755); err != nil {
182		t.Fatal(err)
183	}
184	if err := os.Symlink("bin/node", filepath.Join(dir, "nodejs")); err != nil {
185		t.Fatal(err)
186	}
187	return sandbox.External{Name: name, ID: id, Dir: dir}
188}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 4

Tags 11

↑↓ moveenter openesc close