v0.4.0 76a5504

select.go RawHistoryFolder

1// SPDX-License-Identifier: MIT
2
3package sandbox
4
5import (
6	"context"
7	"debug/elf"
8	"errors"
9	"fmt"
10	"os"
11	"os/exec"
12	"strings"
13
14	"github.com/bnema/zerowrap"
15)
16
17// Factory builds the driver of one mode. It is injected so that this package
18// does not import the drivers (which import it).
19type Factory func(ctx context.Context, cfg Config) (Driver, error)
20
21// Factories are the available drivers.
22type Factories struct {
23	VM        Factory
24	Container Factory
25}
26
27// Prober checks the host. The zero value probes the real host; tests replace
28// fields.
29type Prober struct {
30	// OpenKVM opens the KVM device read-write and closes it.
31	OpenKVM func(path string) error
32	// LookPath finds a binary in PATH.
33	LookPath func(name string) (string, error)
34	// Stat reports whether a path exists.
35	Stat func(path string) error
36}
37
38func (p Prober) withDefaults() Prober {
39	if p.OpenKVM == nil {
40		p.OpenKVM = func(path string) error {
41			f, err := os.OpenFile(path, os.O_RDWR, 0)
42			if err != nil {
43				return err
44			}
45			return f.Close()
46		}
47	}
48	if p.LookPath == nil {
49		p.LookPath = exec.LookPath
50	}
51	if p.Stat == nil {
52		p.Stat = func(path string) error { _, err := os.Stat(path); return err }
53	}
54	return p
55}
56
57// VMUnavailable lists why the vm mode cannot run on this host: an empty result
58// means every prerequisite (readable-writable KVM, Firecracker, jailer unless
59// disabled, guest kernel, base image) is there.
60func VMUnavailable(cfg Config, p Prober) []string {
61	p = p.withDefaults()
62	var why []string
63	if err := p.OpenKVM(cfg.KVMPath); err != nil {
64		why = append(why, fmt.Sprintf("%s not usable read-write: %v", cfg.KVMPath, err))
65	}
66	if cfg.Firecracker != "" {
67		if err := p.Stat(cfg.Firecracker); err != nil {
68			why = append(why, fmt.Sprintf("firecracker %s: %v", cfg.Firecracker, err))
69		}
70	} else if _, err := p.LookPath("firecracker"); err != nil {
71		why = append(why, "firecracker not found in PATH")
72	}
73	if !cfg.JailerOff {
74		if cfg.Jailer != "" {
75			if err := p.Stat(cfg.Jailer); err != nil {
76				why = append(why, fmt.Sprintf("jailer %s: %v", cfg.Jailer, err))
77			}
78		} else if _, err := p.LookPath("jailer"); err != nil {
79			why = append(why, "jailer not found in PATH")
80		}
81	}
82	if err := p.Stat(cfg.KernelPath()); err != nil {
83		why = append(why, fmt.Sprintf("guest kernel %s: %v", cfg.KernelPath(), err))
84	}
85	if err := p.Stat(cfg.BaseImagePath()); err != nil {
86		why = append(why, fmt.Sprintf("base image %s: %v", cfg.BaseImagePath(), err))
87	}
88	return why
89}
90
91// Select picks the isolation mode and builds its driver. For auto it chooses vm
92// when VMUnavailable is empty, container otherwise. For vm it fails when a
93// prerequisite is missing; for container it never probes the VM side. The
94// chosen mode and the reasons are logged with the logger of ctx. A driver
95// factory error is returned as is (auto does not mask a broken vm setup by
96// silently running jobs with weaker isolation).
97func Select(ctx context.Context, cfg Config, f Factories, p Prober) (Driver, error) {
98	log := zerowrap.FromCtx(ctx)
99	mode := cfg.Isolation
100	var reasons []string
101	switch mode {
102	case VM, Auto:
103		reasons = VMUnavailable(cfg, p)
104		if mode == VM && len(reasons) > 0 {
105			return nil, fmt.Errorf("isolation vm is not available: %s", strings.Join(reasons, "; "))
106		}
107		if len(reasons) == 0 {
108			mode = VM
109		} else {
110			mode = Container
111		}
112	case Container:
113	default:
114		return nil, fmt.Errorf("unknown isolation %q", mode)
115	}
116
117	build := f.VM
118	if mode == Container {
119		build = f.Container
120	}
121	if build == nil {
122		return nil, fmt.Errorf("isolation %s: no driver compiled in", mode)
123	}
124	ev := log.Info().Str("configured", string(cfg.Isolation)).Str("mode", string(mode))
125	switch {
126	case cfg.Isolation == Auto && mode == VM:
127		ev = ev.Str("reason", "KVM, firecracker, kernel and base image are available")
128	case cfg.Isolation == Auto:
129		ev = ev.Strs("reasons", reasons)
130	default:
131		ev = ev.Str("reason", "set by "+EnvIsolation)
132	}
133	ev.Msg("isolation mode chosen")
134	d, err := build(ctx, cfg)
135	if err != nil {
136		return nil, fmt.Errorf("isolation %s: %w", mode, err)
137	}
138	return d, nil
139}
140
141// CheckStatic fails unless the executable at path is a statically linked ELF
142// binary: it runs as PID 1 on a root disk that holds nothing else, and in an
143// image of another distribution.
144func CheckStatic(path string) error {
145	f, err := elf.Open(path)
146	if err != nil {
147		return fmt.Errorf("%s is not an ELF executable (%w); build the runner with CGO_ENABLED=0", path, err)
148	}
149	defer func() { _ = f.Close() }()
150	for _, p := range f.Progs {
151		if p.Type == elf.PT_INTERP {
152			return errors.New(path + " is dynamically linked; build the runner with CGO_ENABLED=0")
153		}
154	}
155	return nil
156}

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