v0.4.3 bdc41cd

sandbox_test.go RawHistoryFolder

1// SPDX-License-Identifier: MIT
2
3package sandbox
4
5import (
6	"bytes"
7	"context"
8	"errors"
9	"os"
10	"path/filepath"
11	"strings"
12	"testing"
13
14	"github.com/bnema/zerowrap"
15)
16
17func env(m map[string]string) func(string) string { return func(k string) string { return m[k] } }
18
19func TestLoadConfigDefaults(t *testing.T) {
20	c, err := LoadConfig(env(nil))
21	if err != nil {
22		t.Fatal(err)
23	}
24	if c.Isolation != Auto || c.Capacity != 1 || c.UIDBase != 900000 || c.StateDir != DefaultStateDir ||
25		c.JobCPUs != 2 || c.JobMemoryMiB != 4096 || c.JobDiskMiB != 64*1024 || c.Network.String() != "10.200.0.0/16" || c.DNSPort != 53 {
26		t.Fatalf("%+v", c)
27	}
28}
29
30func TestLoadConfigValues(t *testing.T) {
31	c, err := LoadConfig(env(map[string]string{
32		EnvIsolation: "VM", EnvStateDir: "/srv/er", EnvCapacity: "4", EnvImage: "/images/u24",
33		EnvJobCPUs: "8", EnvJobMemory: "16G", EnvJobDisk: "100G", EnvUIDBase: "910000",
34		EnvNetwork: "10.50.0.0/24", EnvJailerMode: "off", EnvFirecracker: "/x/fc", EnvKernel: "/k",
35	}))
36	if err != nil {
37		t.Fatal(err)
38	}
39	if c.Isolation != VM || c.Capacity != 4 || c.JobMemoryMiB != 16384 || c.JobDiskMiB != 100*1024 || c.UIDBase != 910000 ||
40		!c.JailerOff || c.Network.String() != "10.50.0.0/24" || c.ImageDir() != "/images/u24" || c.KernelPath() != "/k" ||
41		c.BaseImagePath() != "/images/u24/base.ext4" || c.OCIImage() != DefaultOCIImage {
42		t.Fatalf("%+v", c)
43	}
44	c, _ = LoadConfig(env(map[string]string{EnvImage: "registry.example/ermite:1"}))
45	if c.OCIImage() != "registry.example/ermite:1" || c.ImageDir() != filepath.Join(DefaultStateDir, "image") {
46		t.Fatalf("OCI ref misread: %q %q", c.OCIImage(), c.ImageDir())
47	}
48}
49
50func TestLoadConfigErrors(t *testing.T) {
51	for name, m := range map[string]map[string]string{
52		"isolation":    {EnvIsolation: "docker"},
53		"relative":     {EnvStateDir: "state"},
54		"capacity 0":   {EnvCapacity: "0"},
55		"capacity abc": {EnvCapacity: "abc"},
56		"cpus":         {EnvJobCPUs: "0"},
57		"memory":       {EnvJobMemory: "10M"},
58		"disk":         {EnvJobDisk: "100M"},
59		"uid base":     {EnvUIDBase: "10"},
60		"network v6":   {EnvNetwork: "fd00::/64"},
61		"network /31":  {EnvNetwork: "10.0.0.0/31"},
62		"network few":  {EnvNetwork: "10.0.0.0/30", EnvCapacity: "2"},
63		"jailer mode":  {EnvJailerMode: "maybe"},
64	} {
65		if _, err := LoadConfig(env(m)); err == nil {
66			t.Errorf("%s: accepted", name)
67		}
68	}
69}
70
71func TestParseSizeMiB(t *testing.T) {
72	for in, want := range map[string]int{"4096": 4096, "512M": 512, "4G": 4096, "64GiB": 65536, "1t": 1048576, " 2g ": 2048, "1024MB": 1024} {
73		if got, err := ParseSizeMiB(in); err != nil || got != want {
74			t.Errorf("%q: %d %v", in, got, err)
75		}
76	}
77	for _, in := range []string{"", "0", "-1", "G", "12K", "1x", "99999999999T"} {
78		if _, err := ParseSizeMiB(in); err == nil {
79			t.Errorf("%q accepted", in)
80		}
81	}
82}
83
84func TestResolve(t *testing.T) {
85	c, _ := LoadConfig(env(nil))
86	s := c.Resolve(JobSpec{JobID: "j", CPUs: 6})
87	if s.CPUs != 6 || s.MemoryMiB != 4096 || s.DiskMiB != 64*1024 || s.JobID != "j" {
88		t.Fatalf("%+v", s)
89	}
90}
91
92func TestSlots(t *testing.T) {
93	s := NewSlots(2)
94	a, _ := s.Acquire()
95	b, _ := s.Acquire()
96	if a != 0 || b != 1 || s.InUse() != 2 {
97		t.Fatal(a, b)
98	}
99	if _, err := s.Acquire(); !errors.Is(err, ErrCapacity) {
100		t.Fatal(err)
101	}
102	s.Release(0)
103	s.Release(0)
104	s.Release(99)
105	if c, _ := s.Acquire(); c != 0 {
106		t.Fatalf("lowest free slot first, got %d", c)
107	}
108}
109
110func TestIDs(t *testing.T) {
111	id, err := NewID()
112	if err != nil || !ValidID(id) {
113		t.Fatal(id, err)
114	}
115	for _, bad := range []string{"", "er", "er12345", "xx12345678", "er1234567g", "er../../etc", "er123456789"} {
116		if ValidID(bad) {
117			t.Errorf("%q valid", bad)
118		}
119	}
120}
121
122// fakeDriver is a Driver that records its construction.
123type fakeDriver struct{ mode Mode }
124
125func (f fakeDriver) Mode() Mode               { return f.mode }
126func (f fakeDriver) Isolation() Isolation     { return Isolation(f.mode) }
127func (fakeDriver) New() (Sandbox, error)      { return nil, nil }
128func (fakeDriver) Reap(context.Context) error { return nil }
129func (fakeDriver) Close() error               { return nil }
130
131func facts(built *[]Mode) Factories {
132	return Factories{
133		VM: func(context.Context, Config) (Driver, error) { *built = append(*built, VM); return fakeDriver{VM}, nil },
134		Container: func(context.Context, Config) (Driver, error) {
135			*built = append(*built, Container)
136			return fakeDriver{Container}, nil
137		},
138	}
139}
140
141// goodProber reports a host where every vm prerequisite exists.
142func goodProber() Prober {
143	return Prober{
144		OpenKVM:  func(string) error { return nil },
145		LookPath: func(n string) (string, error) { return "/usr/bin/" + n, nil },
146		Stat:     func(string) error { return nil },
147	}
148}
149
150func logCtx(buf *bytes.Buffer) context.Context {
151	return zerowrap.WithCtx(context.Background(), zerowrap.New(zerowrap.Config{Level: "debug", Format: "json", Output: buf}))
152}
153
154func TestSelectAutoPicksVMWhenEverythingIsThere(t *testing.T) {
155	var built []Mode
156	var log bytes.Buffer
157	cfg, _ := LoadConfig(env(nil))
158	d, err := Select(logCtx(&log), cfg, facts(&built), goodProber())
159	if err != nil || d.Mode() != VM || len(built) != 1 {
160		t.Fatalf("%v %v %v", d, err, built)
161	}
162	if !strings.Contains(log.String(), `"mode":"vm"`) || !strings.Contains(log.String(), "reason") {
163		t.Fatalf("mode and reason must be logged: %s", log.String())
164	}
165}
166
167// AC4: auto picks container when /dev/kvm is hidden, and says why.
168func TestSelectAutoFallsBackWithoutKVM(t *testing.T) {
169	var built []Mode
170	var log bytes.Buffer
171	cfg, _ := LoadConfig(env(nil))
172	cfg.KVMPath = filepath.Join(t.TempDir(), "kvm") // hidden: does not exist
173	p := goodProber()
174	p.OpenKVM = Prober{}.withDefaults().OpenKVM // the real open, on the hidden path
175	d, err := Select(logCtx(&log), cfg, facts(&built), p)
176	if err != nil || d.Mode() != Container {
177		t.Fatalf("%v %v", d, err)
178	}
179	out := log.String()
180	if !strings.Contains(out, `"mode":"container"`) || !strings.Contains(out, "not usable read-write") {
181		t.Fatalf("fallback reason not logged: %s", out)
182	}
183}
184
185func TestSelectAutoFallbackReasons(t *testing.T) {
186	cfg, _ := LoadConfig(env(nil))
187	cases := map[string]Prober{
188		"firecracker not found": {OpenKVM: func(string) error { return nil }, LookPath: func(n string) (string, error) {
189			if n == "firecracker" {
190				return "", os.ErrNotExist
191			}
192			return "/x", nil
193		}, Stat: func(string) error { return nil }},
194		"jailer not found": {OpenKVM: func(string) error { return nil }, LookPath: func(n string) (string, error) {
195			if n == "jailer" {
196				return "", os.ErrNotExist
197			}
198			return "/x", nil
199		}, Stat: func(string) error { return nil }},
200		"guest kernel": {OpenKVM: func(string) error { return nil }, LookPath: goodProber().LookPath, Stat: func(p string) error {
201			if strings.HasSuffix(p, "vmlinux") {
202				return os.ErrNotExist
203			}
204			return nil
205		}},
206		"base image": {OpenKVM: func(string) error { return nil }, LookPath: goodProber().LookPath, Stat: func(p string) error {
207			if strings.HasSuffix(p, "base.ext4") {
208				return os.ErrNotExist
209			}
210			return nil
211		}},
212	}
213	for want, p := range cases {
214		var built []Mode
215		d, err := Select(context.Background(), cfg, facts(&built), p)
216		if err != nil || d.Mode() != Container {
217			t.Errorf("%s: %v %v", want, d, err)
218		}
219		if why := VMUnavailable(cfg, p); len(why) != 1 || !strings.Contains(why[0], want) {
220			t.Errorf("%s: reasons %v", want, why)
221		}
222	}
223}
224
225func TestSelectExplicitModes(t *testing.T) {
226	cfg, _ := LoadConfig(env(map[string]string{EnvIsolation: "vm"}))
227	var built []Mode
228	bad := Prober{OpenKVM: func(string) error { return os.ErrPermission }, LookPath: goodProber().LookPath, Stat: goodProber().Stat}
229	if _, err := Select(context.Background(), cfg, facts(&built), bad); err == nil || !strings.Contains(err.Error(), "kvm") || len(built) != 0 {
230		t.Fatalf("vm must fail loudly without KVM and build nothing: %v %v", err, built)
231	}
232	cfg.Isolation = Container
233	d, err := Select(context.Background(), cfg, facts(&built), bad)
234	if err != nil || d.Mode() != Container {
235		t.Fatalf("container never probes KVM: %v %v", d, err)
236	}
237	cfg.Isolation = Mode("x")
238	if _, err := Select(context.Background(), cfg, facts(&built), bad); err == nil {
239		t.Fatal("unknown mode accepted")
240	}
241}
242
243func TestSelectDoesNotMaskBrokenVM(t *testing.T) {
244	cfg, _ := LoadConfig(env(nil))
245	boom := errors.New("kernel does not boot")
246	f := Factories{
247		VM:        func(context.Context, Config) (Driver, error) { return nil, boom },
248		Container: func(context.Context, Config) (Driver, error) { t.Fatal("must not fall back silently"); return nil, nil },
249	}
250	if _, err := Select(context.Background(), cfg, f, goodProber()); !errors.Is(err, boom) {
251		t.Fatalf("got %v", err)
252	}
253	if _, err := Select(context.Background(), cfg, Factories{}, goodProber()); err == nil {
254		t.Fatal("missing factory accepted")
255	}
256}
257
258func TestCheckStatic(t *testing.T) {
259	dir := t.TempDir()
260	if err := CheckStatic(filepath.Join(dir, "missing")); err == nil {
261		t.Fatal("missing file accepted")
262	}
263	txt := filepath.Join(dir, "script")
264	_ = os.WriteFile(txt, []byte("#!/bin/sh\n"), 0o755)
265	if err := CheckStatic(txt); err == nil || !strings.Contains(err.Error(), "CGO_ENABLED=0") {
266		t.Fatalf("script accepted: %v", err)
267	}
268	if err := CheckStatic("/bin/sh"); err == nil {
269		t.Skip("/bin/sh is static here")
270	}
271}
272
273func TestNodeSettingAndExternalsKey(t *testing.T) {
274	c, err := LoadConfig(env(nil))
275	if err != nil || !c.NodeExternals {
276		t.Fatalf("on by default: %+v %v", c, err)
277	}
278	for v, want := range map[string]bool{"0": false, "false": false, "NO": false, "1": true, "yes": true, "": true} {
279		c, err := LoadConfig(env(map[string]string{EnvNode: v}))
280		if err != nil || c.NodeExternals != want {
281			t.Errorf("%s=%q: %v %v", EnvNode, v, c.NodeExternals, err)
282		}
283	}
284	if _, err := LoadConfig(env(map[string]string{EnvNode: "maybe"})); err == nil || !strings.Contains(err.Error(), EnvNode) {
285		t.Errorf("got %v", err)
286	}
287
288	a := External{Name: "node24", ID: "x1", Dir: "/a"}
289	b := External{Name: "node20", ID: "y1", Dir: "/b"}
290	k := ExternalsKey([]External{a, b})
291	if k != ExternalsKey([]External{b, a}) {
292		t.Error("the key depends on the order")
293	}
294	if k != ExternalsKey([]External{{Name: "node24", ID: "x1", Dir: "/elsewhere"}, b}) {
295		t.Error("the key depends on the host path, which does not change the content")
296	}
297	if k == ExternalsKey([]External{{Name: "node24", ID: "x2"}, b}) || k == ExternalsKey([]External{a}) {
298		t.Error("a change of content keeps the key")
299	}
300	if (Config{}).ExternalsOf() != nil {
301		t.Error("no source, no externals")
302	}
303}

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