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}