fix/step-umask a16908b

host_cmd.go RawHistoryFolder

1// SPDX-License-Identifier: MIT
2
3package main
4
5import (
6	"context"
7	"errors"
8	"flag"
9	"fmt"
10	"net"
11	"os"
12	"path/filepath"
13	"strings"
14	"time"
15
16	"github.com/bnema/zerowrap"
17
18	"bnema.dev/ermite-api/gen/ermite/ermiteconnect"
19
20	"bnema.dev/ermite-runner/internal/actionscache"
21	"bnema.dev/ermite-runner/internal/cachesvc"
22	"bnema.dev/ermite-runner/internal/externals"
23	"bnema.dev/ermite-runner/internal/host"
24	"bnema.dev/ermite-runner/internal/resultssvc"
25	"bnema.dev/ermite-runner/internal/sandbox"
26	"bnema.dev/ermite-runner/internal/sandbox/drivers"
27)
28
29// loadHostConfig reads the sandbox settings and the host settings together, so
30// that the state directory and the capacity are the same for both.
31func loadHostConfig() (sandbox.Config, host.Config, error) {
32	scfg, err := sandbox.LoadConfig(os.Getenv)
33	if err != nil {
34		return scfg, host.Config{}, err
35	}
36	name, _ := os.Hostname()
37	hcfg, err := host.LoadConfig(os.Getenv, name, scfg.StateDir, scfg.Capacity, version)
38	return scfg, hcfg, err
39}
40
41// setupCache starts the cache service of the runner (README, "Cache") and hands
42// it to the sandbox drivers through scfg. ERMITE_RUNNER_CACHE=0 turns it off;
43// ERMITE_RUNNER_CACHE_MAX is the size cap (default 10 GiB).
44func setupCache(ctx context.Context, scfg *sandbox.Config) (*cachesvc.Service, *actionscache.Cache, error) {
45	log := zerowrap.FromCtx(ctx)
46	switch strings.ToLower(strings.TrimSpace(os.Getenv(envCache))) {
47	case "", "1", "true", "yes":
48	case "0", "false", "no":
49		log.Info().Msg("the cache service is off (" + envCache + "=0)")
50		return nil, nil, nil
51	default:
52		return nil, nil, fmt.Errorf("%s: %q is not 0 or 1", envCache, os.Getenv(envCache))
53	}
54	max, err := cachesvc.ParseMax(os.Getenv(cachesvc.EnvMax))
55	if err != nil {
56		return nil, nil, err
57	}
58	archives, err := setupArchives(ctx, scfg.StateDir)
59	if err != nil {
60		return nil, nil, err
61	}
62	svc, err := cachesvc.New(cachesvc.Config{Dir: filepath.Join(scfg.StateDir, "cache"), Max: max, Log: log, Archives: archives})
63	if err != nil {
64		return nil, nil, err
65	}
66	scfg.Cache = svc
67	log.Info().Int64("max_bytes", max).Msg("cache service on (guest loopback only, no host port)")
68	return svc, archives, nil
69}
70
71// setupArchives opens the cache of action archives (ERMITE_RUNNER_ACTIONS_CACHE_MAX,
72// default 2 GiB, 0 turns it off). It is served on the channel of the cache
73// service, so it is only used when that is on.
74func setupArchives(ctx context.Context, stateDir string) (*actionscache.Cache, error) {
75	log := zerowrap.FromCtx(ctx)
76	max, err := actionscache.ParseMax(os.Getenv(actionscache.EnvMax))
77	if err != nil {
78		return nil, err
79	}
80	if max == 0 {
81		log.Info().Msg("the action archive cache is off (" + actionscache.EnvMax + "=0)")
82		return nil, nil
83	}
84	c, err := actionscache.New(actionscache.Config{Dir: filepath.Join(stateDir, "actions-cache"), Max: max, Log: log})
85	if err != nil {
86		return nil, err
87	}
88	log.Info().Int64("max_bytes", max).Msg("action archive cache on")
89	return c, nil
90}
91
92// envCache=0 turns the cache service off.
93const envCache = "ERMITE_RUNNER_CACHE"
94
95// envArtifacts=0 turns the results service (artifacts v4) off.
96const envArtifacts = "ERMITE_RUNNER_ARTIFACTS"
97
98// setupResults makes the results service of the runner (README, "Artifacts"):
99// on by default, off with ERMITE_RUNNER_ARTIFACTS=0. It is served on the cache
100// channel of each sandbox and checks the cache's runtime tokens, so it needs
101// the cache service; a job gets it only when the server has ci.artifacts.
102func setupResults(ctx context.Context, cache *cachesvc.Service) (*resultssvc.Service, error) {
103	log := zerowrap.FromCtx(ctx)
104	switch strings.ToLower(strings.TrimSpace(os.Getenv(envArtifacts))) {
105	case "", "1", "true", "yes":
106	case "0", "false", "no":
107		log.Info().Msg("the results service is off (" + envArtifacts + "=0): artifact actions fail")
108		return nil, nil
109	default:
110		return nil, fmt.Errorf("%s: %q is not 0 or 1", envArtifacts, os.Getenv(envArtifacts))
111	}
112	if cache == nil {
113		log.Warn().Msg("the results service needs the cache service (" + envCache + "=0): artifact actions fail")
114		return nil, nil
115	}
116	log.Info().Msg("results service on (artifacts v4, on the cache channel; stored by ermite)")
117	return resultssvc.New(resultssvc.Config{Auth: cache, Log: log}), nil
118}
119
120// externalsWait is how long the start of the runner waits for the Node.js
121// downloads. A runner with a working network is then ready for its first job; one
122// without goes on after the first failure (nothing waits for a dead network), and
123// the downloads that are still running when the time is up finish in the
124// background.
125const externalsWait = 2 * time.Minute
126
127// setupExternals downloads the Node.js externals (README, "Externals") and hands
128// them to the sandbox drivers through scfg. It never fails the start: whatever
129// cannot be provided is reported to the jobs that need it.
130func setupExternals(ctx context.Context, scfg *sandbox.Config) {
131	log := zerowrap.FromCtx(ctx)
132	if !scfg.NodeExternals {
133		log.Info().Msg("Node.js externals are off (" + sandbox.EnvNode + "=0)")
134		return
135	}
136	m, ok := externals.New(scfg.StateDir)
137	if !ok {
138		log.Warn().Msg("no Node.js builds for this architecture: jobs get no externals")
139		return
140	}
141	m.Start(ctx, externalsWait)
142	scfg.Externals = m
143}
144
145func buildDriver(ctx context.Context, scfg sandbox.Config) (sandbox.Driver, error) {
146	return sandbox.Select(ctx, scfg, drivers.Factories(), sandbox.Prober{})
147}
148
149// registerCmd is `ermite-runner register [--token-stdin] [--token T] [--replace]`:
150// it chooses the isolation mode like `run` will, registers the runner with it,
151// and stores the runner token (mode 0600) in the state directory. The register
152// token is read from standard input (--token-stdin) or from
153// ERMITE_RUNNER_REGISTER_TOKEN; --token is accepted but the arguments of a
154// process are visible to every user of the host.
155func registerCmd(ctx context.Context, args []string) int {
156	fs := flag.NewFlagSet("register", flag.ContinueOnError)
157	token := fs.String("token", "", "token with the runner:register scope (visible in the process list: prefer $"+host.EnvRegisterToken+" or --token-stdin)")
158	fromStdin := fs.Bool("token-stdin", false, "read the token from standard input")
159	replace := fs.Bool("replace", false, "replace a runner of the same name")
160	if err := fs.Parse(args); err != nil {
161		return 2
162	}
163	tok, err := host.ResolveRegisterToken(*token, *fromStdin, os.Stdin, os.Getenv)
164	if err != nil {
165		fmt.Fprintln(os.Stderr, "ermite-runner:", err)
166		return 2
167	}
168	*token = tok
169	scfg, hcfg, err := loadHostConfig()
170	if err != nil {
171		fmt.Fprintln(os.Stderr, "ermite-runner:", err)
172		return 2
173	}
174	if hcfg.URL == "" {
175		fmt.Fprintf(os.Stderr, "ermite-runner: %s is not set\n", host.EnvURL)
176		return 2
177	}
178	drv, err := buildDriver(ctx, scfg)
179	if err != nil {
180		fmt.Fprintln(os.Stderr, "ermite-runner:", err)
181		return 1
182	}
183	iso := drv.Isolation()
184	_ = drv.Close()
185
186	client := hcfg.Client(hcfg.URL, *token)
187	st, err := host.Register(ctx, hcfg, client, iso, *replace)
188	if err != nil {
189		fmt.Fprintln(os.Stderr, "ermite-runner:", err)
190		return 1
191	}
192	fmt.Printf("registered runner %q (%s) with isolation %s; token stored in %s\n", st.Name, st.RunnerID, iso, host.StateFile(hcfg.StateDir))
193	return 0
194}
195
196// runCmd is `ermite-runner run`: poll ermite for jobs and run each one in its
197// own sandbox until SIGINT or SIGTERM, then cancel the running jobs with
198// runner_shutdown and report them.
199func runCmd(ctx context.Context) int {
200	log := zerowrap.FromCtx(ctx)
201	scfg, hcfg, err := loadHostConfig()
202	if err != nil {
203		fmt.Fprintln(os.Stderr, "ermite-runner:", err)
204		return 2
205	}
206	st, err := host.LoadState(hcfg.StateDir)
207	if err != nil {
208		fmt.Fprintln(os.Stderr, "ermite-runner:", err)
209		return 2
210	}
211	url := hcfg.URL
212	if url == "" {
213		url = st.URL
214	}
215	if url == "" {
216		fmt.Fprintf(os.Stderr, "ermite-runner: %s is not set\n", host.EnvURL)
217		return 2
218	}
219	if scfg.GuestForgeEnabled {
220		// The sandboxes reach the forge the way the runner does: by the host name
221		// and port of the URL the runner itself uses.
222		gf, err := sandbox.ResolveGuestForge(ctx, url, hcfg.InsecureHTTP, net.DefaultResolver.LookupHost)
223		if err != nil {
224			fmt.Fprintln(os.Stderr, "ermite-runner:", err)
225			return 2
226		}
227		scfg.GuestForge = gf
228		hcfg.GuestForgeOrigin = gf.Origin
229	}
230	setupExternals(ctx, &scfg)
231	cache, archives, err := setupCache(ctx, &scfg)
232	if err != nil {
233		fmt.Fprintln(os.Stderr, "ermite-runner:", err)
234		return 2
235	}
236	if cache != nil {
237		defer func() { _ = cache.CloseAll() }()
238	}
239	results, err := setupResults(ctx, cache)
240	if err != nil {
241		fmt.Fprintln(os.Stderr, "ermite-runner:", err)
242		return 2
243	}
244	drv, err := buildDriver(ctx, scfg)
245	if err != nil {
246		fmt.Fprintln(os.Stderr, "ermite-runner:", err)
247		return 1
248	}
249	defer func() { _ = drv.Close() }()
250	log.Info().Str("runner", st.Name).Str("isolation", string(drv.Isolation())).Int("capacity", scfg.Capacity).Msg("runner started")
251
252	r := &host.Runner{
253		Cfg:    hcfg,
254		Driver: drv,
255		Poll:   hcfg.Client(url, st.RunnerToken),
256		Job: func(tok string) ermiteconnect.RunnerServiceClient {
257			return hcfg.Client(url, tok)
258		},
259		Features: hcfg.ServerFeaturesFunc(url),
260		Cache:    cache,
261		Archives: archives,
262		Results:  results,
263		Artifacts: func(tok string) ermiteconnect.JobArtifactServiceClient {
264			return hcfg.ArtifactClient(url, tok)
265		},
266	}
267	r.Cfg.URL = url
268	if err := r.Run(ctx); err != nil {
269		if errors.Is(err, host.ErrRemoved) {
270			fmt.Fprintln(os.Stderr, "ermite-runner:", err)
271			return 3
272		}
273		fmt.Fprintln(os.Stderr, "ermite-runner:", err)
274		return 1
275	}
276	log.Info().Msg("runner stopped")
277	return 0
278}

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