1// SPDX-License-Identifier: MIT
2
3package main
4
5import (
6 "context"
7 "errors"
8 "flag"
9 "fmt"
10 "net"
11 "os"
12 "time"
13
14 "github.com/bnema/zerowrap"
15
16 "bnema.dev/ermite-api/gen/ermite/ermiteconnect"
17
18 "bnema.dev/ermite-runner/internal/externals"
19 "bnema.dev/ermite-runner/internal/host"
20 "bnema.dev/ermite-runner/internal/sandbox"
21 "bnema.dev/ermite-runner/internal/sandbox/drivers"
22)
23
24// loadHostConfig reads the sandbox settings and the host settings together, so
25// that the state directory and the capacity are the same for both.
26func loadHostConfig() (sandbox.Config, host.Config, error) {
27 scfg, err := sandbox.LoadConfig(os.Getenv)
28 if err != nil {
29 return scfg, host.Config{}, err
30 }
31 name, _ := os.Hostname()
32 hcfg, err := host.LoadConfig(os.Getenv, name, scfg.StateDir, scfg.Capacity, version)
33 return scfg, hcfg, err
34}
35
36// externalsWait is how long the start of the runner waits for the Node.js
37// downloads. A runner with a working network is then ready for its first job; one
38// without goes on after the first failure (nothing waits for a dead network), and
39// the downloads that are still running when the time is up finish in the
40// background.
41const externalsWait = 2 * time.Minute
42
43// setupExternals downloads the Node.js externals (README, "Externals") and hands
44// them to the sandbox drivers through scfg. It never fails the start: whatever
45// cannot be provided is reported to the jobs that need it.
46func setupExternals(ctx context.Context, scfg *sandbox.Config) {
47 log := zerowrap.FromCtx(ctx)
48 if !scfg.NodeExternals {
49 log.Info().Msg("Node.js externals are off (" + sandbox.EnvNode + "=0)")
50 return
51 }
52 m, ok := externals.New(scfg.StateDir)
53 if !ok {
54 log.Warn().Msg("no Node.js builds for this architecture: jobs get no externals")
55 return
56 }
57 m.Start(ctx, externalsWait)
58 scfg.Externals = m
59}
60
61func buildDriver(ctx context.Context, scfg sandbox.Config) (sandbox.Driver, error) {
62 return sandbox.Select(ctx, scfg, drivers.Factories(), sandbox.Prober{})
63}
64
65// registerCmd is `ermite-runner register [--token-stdin] [--token T] [--replace]`:
66// it chooses the isolation mode like `run` will, registers the runner with it,
67// and stores the runner token (mode 0600) in the state directory. The register
68// token is read from standard input (--token-stdin) or from
69// ERMITE_RUNNER_REGISTER_TOKEN; --token is accepted but the arguments of a
70// process are visible to every user of the host.
71func registerCmd(ctx context.Context, args []string) int {
72 fs := flag.NewFlagSet("register", flag.ContinueOnError)
73 token := fs.String("token", "", "token with the runner:register scope (visible in the process list: prefer $"+host.EnvRegisterToken+" or --token-stdin)")
74 fromStdin := fs.Bool("token-stdin", false, "read the token from standard input")
75 replace := fs.Bool("replace", false, "replace a runner of the same name")
76 if err := fs.Parse(args); err != nil {
77 return 2
78 }
79 tok, err := host.ResolveRegisterToken(*token, *fromStdin, os.Stdin, os.Getenv)
80 if err != nil {
81 fmt.Fprintln(os.Stderr, "ermite-runner:", err)
82 return 2
83 }
84 *token = tok
85 scfg, hcfg, err := loadHostConfig()
86 if err != nil {
87 fmt.Fprintln(os.Stderr, "ermite-runner:", err)
88 return 2
89 }
90 if hcfg.URL == "" {
91 fmt.Fprintf(os.Stderr, "ermite-runner: %s is not set\n", host.EnvURL)
92 return 2
93 }
94 drv, err := buildDriver(ctx, scfg)
95 if err != nil {
96 fmt.Fprintln(os.Stderr, "ermite-runner:", err)
97 return 1
98 }
99 iso := drv.Isolation()
100 _ = drv.Close()
101
102 client := hcfg.Client(hcfg.URL, *token)
103 st, err := host.Register(ctx, hcfg, client, iso, *replace)
104 if err != nil {
105 fmt.Fprintln(os.Stderr, "ermite-runner:", err)
106 return 1
107 }
108 fmt.Printf("registered runner %q (%s) with isolation %s; token stored in %s\n", st.Name, st.RunnerID, iso, host.StateFile(hcfg.StateDir))
109 return 0
110}
111
112// runCmd is `ermite-runner run`: poll ermite for jobs and run each one in its
113// own sandbox until SIGINT or SIGTERM, then cancel the running jobs with
114// runner_shutdown and report them.
115func runCmd(ctx context.Context) int {
116 log := zerowrap.FromCtx(ctx)
117 scfg, hcfg, err := loadHostConfig()
118 if err != nil {
119 fmt.Fprintln(os.Stderr, "ermite-runner:", err)
120 return 2
121 }
122 st, err := host.LoadState(hcfg.StateDir)
123 if err != nil {
124 fmt.Fprintln(os.Stderr, "ermite-runner:", err)
125 return 2
126 }
127 url := hcfg.URL
128 if url == "" {
129 url = st.URL
130 }
131 if url == "" {
132 fmt.Fprintf(os.Stderr, "ermite-runner: %s is not set\n", host.EnvURL)
133 return 2
134 }
135 if scfg.GuestForgeEnabled {
136 // The sandboxes reach the forge the way the runner does: by the host name
137 // and port of the URL the runner itself uses.
138 gf, err := sandbox.ResolveGuestForge(ctx, url, hcfg.InsecureHTTP, net.DefaultResolver.LookupHost)
139 if err != nil {
140 fmt.Fprintln(os.Stderr, "ermite-runner:", err)
141 return 2
142 }
143 scfg.GuestForge = gf
144 hcfg.GuestForgeOrigin = gf.Origin
145 }
146 setupExternals(ctx, &scfg)
147 drv, err := buildDriver(ctx, scfg)
148 if err != nil {
149 fmt.Fprintln(os.Stderr, "ermite-runner:", err)
150 return 1
151 }
152 defer func() { _ = drv.Close() }()
153 log.Info().Str("runner", st.Name).Str("isolation", string(drv.Isolation())).Int("capacity", scfg.Capacity).Msg("runner started")
154
155 r := &host.Runner{
156 Cfg: hcfg,
157 Driver: drv,
158 Poll: hcfg.Client(url, st.RunnerToken),
159 Job: func(tok string) ermiteconnect.RunnerServiceClient {
160 return hcfg.Client(url, tok)
161 },
162 Features: hcfg.ServerFeaturesFunc(url),
163 }
164 r.Cfg.URL = url
165 if err := r.Run(ctx); err != nil {
166 if errors.Is(err, host.ErrRemoved) {
167 fmt.Fprintln(os.Stderr, "ermite-runner:", err)
168 return 3
169 }
170 fmt.Fprintln(os.Stderr, "ermite-runner:", err)
171 return 1
172 }
173 log.Info().Msg("runner stopped")
174 return 0
175}