v0.1.0 cbb8ad6

jobexec.go RawHistoryFolder

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}

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