1// SPDX-License-Identifier: MIT
2
3package guest
4
5import (
6 "context"
7 "fmt"
8 "os"
9 "os/user"
10 "strconv"
11 "strings"
12 "syscall"
13 "time"
14
15 "github.com/bnema/zerowrap"
16
17 "bnema.dev/ermite-runner/internal/execstream"
18 "bnema.dev/ermite-runner/internal/execstream/execpb"
19 "bnema.dev/ermite-runner/internal/executor"
20)
21
22// frameReporter is the executor.Reporter that writes frames on the stream.
23type frameReporter struct {
24 conn *execstream.Conn
25 // err keeps the first send error; the job goes on without a host.
26 failed chan struct{}
27}
28
29func ts(t time.Time) int64 { return t.UnixNano() }
30
31func conclusionProto(c executor.Conclusion) execpb.StepConclusion {
32 switch c {
33 case executor.ConclusionSuccess:
34 return execpb.StepConclusion_STEP_CONCLUSION_SUCCESS
35 case executor.ConclusionFailure:
36 return execpb.StepConclusion_STEP_CONCLUSION_FAILURE
37 case executor.ConclusionCancelled:
38 return execpb.StepConclusion_STEP_CONCLUSION_CANCELLED
39 case executor.ConclusionSkipped:
40 return execpb.StepConclusion_STEP_CONCLUSION_SKIPPED
41 case executor.ConclusionTimedOut:
42 return execpb.StepConclusion_STEP_CONCLUSION_TIMED_OUT
43 }
44 return execpb.StepConclusion_STEP_CONCLUSION_UNSPECIFIED
45}
46
47func (r *frameReporter) send(f *execpb.Frame) {
48 if err := r.conn.Send(f); err != nil {
49 select {
50 case <-r.failed:
51 default:
52 close(r.failed)
53 }
54 }
55}
56
57func (r *frameReporter) StepStarted(n int, name string, at time.Time) {
58 r.send(&execpb.Frame{Body: &execpb.Frame_StepStarted{StepStarted: &execpb.StepStarted{Number: int32(n), Name: name, UnixNano: ts(at)}}})
59}
60
61func (r *frameReporter) Log(n int, at time.Time, line string) {
62 for i, piece := range execstream.SplitLine(line, execstream.MaxLogLine) {
63 r.send(&execpb.Frame{Body: &execpb.Frame_Log{Log: &execpb.Log{Step: int32(n), UnixNano: ts(at), Text: piece, Continued: i > 0}}})
64 }
65}
66
67func (r *frameReporter) StepCompleted(n int, name string, c executor.Conclusion, at time.Time) {
68 r.send(&execpb.Frame{Body: &execpb.Frame_StepCompleted{StepCompleted: &execpb.StepCompleted{Number: int32(n), Name: name, Conclusion: conclusionProto(c), UnixNano: ts(at)}}})
69}
70
71func (r *frameReporter) JobCompleted(res executor.Result) {
72 r.send(&execpb.Frame{Body: &execpb.Frame_JobResult{JobResult: &execpb.JobResult{
73 Conclusion: conclusionProto(res.Conclusion), Outputs: res.Outputs, Error: res.Error, RunnerShutdown: res.RunnerShutdown,
74 }}})
75}
76
77// StepUser is the account the steps run as: uid, gid and supplementary groups,
78// like the hosted runners' "runner" user. Nil credential means the steps run as
79// the executor itself.
80type StepUser struct {
81 Credential *syscall.Credential
82 Name, Home string
83}
84
85// LookupStepUser finds the user the steps run as. When the executor is not
86// root it cannot switch user: the steps run as the current user (a development
87// or test setup, never the sandbox of a runner, whose init is root). When it is
88// root and the user does not exist, the lookup fails: running a job's code as
89// root of the sandbox, with the docker daemon and the whole image at hand,
90// because an image lacks the account would hide a broken image. A test image
91// opts in with allowRoot.
92func LookupStepUser(name string, allowRoot bool) (StepUser, error) {
93 return lookupStepUser(os.Geteuid(), user.Lookup, name, allowRoot)
94}
95
96func lookupStepUser(euid int, lookup func(string) (*user.User, error), name string, allowRoot bool) (StepUser, error) {
97 if euid != 0 || name == "" {
98 u := StepUser{Home: os.Getenv("HOME")}
99 if cu, err := user.Current(); err == nil {
100 u.Name = cu.Username
101 if u.Home == "" {
102 u.Home = cu.HomeDir
103 }
104 }
105 return u, nil
106 }
107 u, err := lookup(name)
108 if err != nil {
109 if allowRoot {
110 return StepUser{Name: "root", Home: "/root"}, nil
111 }
112 return StepUser{}, fmt.Errorf("the image has no user %q to run the steps as (%v): refusing to run them as root", name, err)
113 }
114 uid, _ := strconv.ParseUint(u.Uid, 10, 32)
115 gid, _ := strconv.ParseUint(u.Gid, 10, 32)
116 cred := &syscall.Credential{Uid: uint32(uid), Gid: uint32(gid)}
117 if ids, err := u.GroupIds(); err == nil {
118 for _, g := range ids {
119 if n, err := strconv.ParseUint(g, 10, 32); err == nil {
120 cred.Groups = append(cred.Groups, uint32(n))
121 }
122 }
123 }
124 return StepUser{Credential: cred, Name: u.Username, Home: u.HomeDir}, nil
125}
126
127// stepEnv is the environment the steps inherit: the executor's, without the
128// variables of the stream, with HOME, USER and LOGNAME of the step user.
129func stepEnv(environ []string, u StepUser) []string {
130 var out []string
131 for _, kv := range environ {
132 k, _, _ := strings.Cut(kv, "=")
133 switch {
134 case strings.HasPrefix(k, "ERMITE_"), k == "RUNNER_TOOL_CACHE", k == "HOME", k == "USER", k == "LOGNAME":
135 continue
136 }
137 out = append(out, kv)
138 }
139 if u.Home != "" {
140 out = append(out, "HOME="+u.Home)
141 }
142 if u.Name != "" {
143 out = append(out, "USER="+u.Name, "LOGNAME="+u.Name)
144 }
145 return out
146}
147
148func cancelKind(k execpb.CancelKind) executor.CancelReason {
149 switch k {
150 case execpb.CancelKind_CANCEL_KIND_TIMEOUT:
151 return executor.CancelTimeout
152 case execpb.CancelKind_CANCEL_KIND_SHUTDOWN:
153 return executor.CancelShutdown
154 }
155 return executor.CancelUser
156}
157
158// runJob runs the job of a Run frame with the executor and reports on conn.
159// It returns the exit code to report.
160func runJob(ctx context.Context, conn *execstream.Conn, cfg ExecConfig, spec *execpb.JobSpec) int {
161 su, err := LookupStepUser(cfg.StepUser, cfg.AllowRootSteps)
162 if err != nil {
163 // Reported like any failure of "Set up job": the job fails, with the reason
164 // in its log, and nothing of the workflow runs.
165 return failSetup(conn, err.Error())
166 }
167 canceller := executor.NewCanceller()
168 rep := &frameReporter{conn: conn, failed: make(chan struct{})}
169
170 // Frames from the host while the job runs: a Cancel stops the job; a lost
171 // host does too, like a runner shutdown. The reader ends with the stream: the
172 // caller closes it after runJob, and the goroutine that reports a failed
173 // send ends with the job (done).
174 done := make(chan struct{})
175 defer close(done)
176 go func() {
177 for {
178 f, err := conn.Recv()
179 if err != nil {
180 canceller.Cancel(executor.CancelShutdown)
181 return
182 }
183 if c := f.GetCancel(); c != nil {
184 canceller.Cancel(cancelKind(c.GetKind()))
185 }
186 }
187 }()
188 // The executor's own context (SIGTERM forwarded by init: the sandbox is
189 // being stopped) is a shutdown.
190 stop := context.AfterFunc(ctx, func() { canceller.Cancel(executor.CancelShutdown) })
191 defer stop()
192 go func() {
193 select {
194 case <-rep.failed:
195 canceller.Cancel(executor.CancelShutdown)
196 case <-canceller.Done():
197 case <-done:
198 }
199 }()
200
201 env := spec.GetRunnerEnvironment()
202 res := executor.Run(canceller, executor.Config{
203 WorkflowPath: spec.GetWorkflowPath(), WorkflowYAML: []byte(spec.GetWorkflowYaml()), JobKey: spec.GetJobKey(),
204 ContextsJSON: spec.GetContexts(), MatrixJSON: spec.GetMatrix(), Repo: spec.GetRepo(),
205 TimeoutMinutes: int(spec.GetTimeoutMinutes()), Deadline: time.Duration(spec.GetDeadlineMs()) * time.Millisecond,
206 Runner: executor.RunnerInfo{Name: spec.GetRunnerName(), Environment: env},
207 RunnerVersion: spec.GetRunnerVersion(),
208 Home: su.Home, ToolCache: os.Getenv("RUNNER_TOOL_CACHE"), BaseEnv: stepEnv(os.Environ(), su), Credential: su.Credential,
209 Debug: spec.GetDebug(), Reporter: rep, Log: zerowrap.FromCtx(ctx),
210 TermAfter: cfg.TermAfter, KillAfter: cfg.KillAfter, WaitDelay: cfg.WaitDelay,
211 })
212 if res.Conclusion == executor.ConclusionSuccess {
213 return 0
214 }
215 return 1
216}
217
218// failSetup ends a job that cannot start, like a failed "Set up job": step 1
219// starts, logs the reason as an error, and fails; the job result is a failure.
220func failSetup(conn *execstream.Conn, msg string) int {
221 rep := &frameReporter{conn: conn, failed: make(chan struct{})}
222 now := time.Now()
223 rep.StepStarted(1, executor.SetUpJobName, now)
224 rep.Log(1, now, "##[error]"+msg)
225 rep.StepCompleted(1, executor.SetUpJobName, executor.ConclusionFailure, time.Now())
226 rep.JobCompleted(executor.Result{Conclusion: executor.ConclusionFailure, Error: msg})
227 return 1
228}