v0.3.0 8f98fd3

containers_start.go RawHistoryFolder

1// SPDX-License-Identifier: MIT
2
3package executor
4
5import (
6	"errors"
7	"fmt"
8	"os"
9	"path/filepath"
10	"strings"
11)
12
13// createCmd is DockerCommandManager.DockerCreate (DockerCommandManager.cs:
14// 119-215), in the order of the runner's options.
15func (j *job) createCmd(c *container) dockerCmd {
16	parts := []dockerPart{raw("--name", c.name), raw("--label", j.dockerLabel())}
17	if c.workDir != "" {
18		parts = append(parts, raw("--workdir", c.workDir))
19	}
20	if c.network != "" {
21		parts = append(parts, raw("--network", c.network))
22	}
23	if c.alias != "" {
24		parts = append(parts, raw("--network-alias", c.alias))
25	}
26	for _, p := range c.ports {
27		parts = append(parts, raw("-p", p))
28	}
29	parts = append(parts, optionsParts(c.options))
30	for _, k := range c.env.Keys() {
31		v, _ := c.env.Get(k)
32		parts = append(parts, j.envKV(k, v))
33	}
34	parts = append(parts, raw("-e", "GITHUB_ACTIONS=true"))
35	if _, ok := c.env.Get("CI"); !ok {
36		parts = append(parts, raw("-e", "CI=true"))
37	}
38	for _, m := range c.mounts {
39		parts = append(parts, mountOpt(m))
40	}
41	if c.entrypoint != "" {
42		parts = append(parts, entrypointOpt(c.entrypoint))
43	}
44	parts = append(parts, raw(c.image), dockerPart{line: c.entryArgs, args: ParseArguments(c.entryArgs)})
45	return j.newDockerCmd("create", parts...)
46}
47
48// mountWellKnownDirectories is ContainerOperationProvider.MountWellKnownDirectories
49// (:242-283): the job container sees the work directory, the externals, the
50// temp, actions and tool cache directories, and a home and a workflow
51// directory of its own.
52func (j *job) mountWellKnownDirectories(c *container) error {
53	lay := j.state.Layout()
54	workspace := j.githubValue("workspace")
55	tr := c.translateToContainer
56	c.mounts = append(c.mounts,
57		mount{source: lay.WorkRoot, target: tr(lay.WorkRoot)},
58		mount{source: j.externalsDir(), target: tr(j.externalsDir()), readOnly: true},
59		mount{source: lay.Temp, target: tr(lay.Temp)},
60		mount{source: lay.Actions, target: tr(lay.Actions)},
61		mount{source: lay.ToolCache, target: tr(lay.ToolCache)})
62	for _, d := range []struct{ dir, target string }{
63		{"_github_home", "/github/home"}, {"_github_workflow", "/github/workflow"},
64	} {
65		host := filepath.Join(lay.Temp, d.dir)
66		if err := os.MkdirAll(host, 0o755); err != nil {
67			return err
68		}
69		j.own(host)
70		c.mounts = append(c.mounts, mount{source: host, target: d.target})
71		c.maps = append([]pathMap{{host, d.target}}, c.maps...) // AddPathTranslateMapping inserts first
72		if d.dir == "_github_home" {
73			c.env.Set("HOME", tr(host))
74		}
75	}
76	c.workDir = tr(workspace)
77	c.entrypoint = "tail"
78	c.entryArgs = `"-f" "/dev/null"`
79	return nil
80}
81
82// startContainer is StartContainerAsync (ContainerOperationProvider.cs:
83// 123-240).
84func (j *job) startContainer(s *stepRun, c *container, cancel <-chan struct{}) error {
85	num := s.num
86	group := "Starting job container"
87	if !c.isJob {
88		group = "Starting " + c.alias + " service container"
89	}
90	j.out(num, "##[group]"+group)
91	for _, m := range c.userMounts() {
92		if m.source == "/" {
93			j.tagged(num, "##[warning]", fmt.Sprintf("Volume mount %s is going to mount '/' into the container which may cause file ownership change in the entire file system and cause Actions Runner to lose permission to access the disk.", m.user))
94		}
95	}
96	cfg, err := j.registryLogin(s, c, cancel)
97	if err != nil {
98		if rerr := j.removeConfigDir(cfg); rerr != nil {
99			j.errorf(num, "%s", rerr.Error())
100		}
101		return err
102	}
103	defer func() { _ = j.removeConfigDir(cfg) }() // also on a panic; idempotent
104	code, err := j.pullWithRetry(s, c.image, cfg, cancel)
105	// Credentials are removed after the pull, whatever it did (:171-173).
106	if rerr := j.removeConfigDir(cfg); rerr != nil {
107		if err == nil {
108			err = rerr
109		} else {
110			j.errorf(num, "%s", rerr.Error())
111		}
112	}
113	if err != nil {
114		return err
115	}
116	if code != 0 {
117		return ghErrorf("Docker pull failed with exit code %d", code)
118	}
119	if c.isJob {
120		if err := j.mountWellKnownDirectories(c); err != nil {
121			return err
122		}
123	}
124	if err := checkEnvNames(c.env.Keys()); err != nil {
125		return err
126	}
127	c.createTried = true // the container may exist from here on, named c.name
128	out, err := j.dockerList(s, j.createCmd(c), cancel)
129	if err != nil {
130		var ee *exitError
131		if errors.As(err, &ee) {
132			c.createTried = false // docker said no: there is nothing to remove
133		}
134		return err
135	}
136	if len(out) == 0 || out[0] == "" {
137		return ghErrorf("Value cannot be null. (Parameter 'container.ContainerId')")
138	}
139	// docker create prints the id and nothing else; anything else (the help
140	// text of an option in options:, say) is not an id to use or to remove.
141	if !containerIDRe.MatchString(out[0]) {
142		return ghErrorf("Docker create did not return a container id: '%s'", out[0])
143	}
144	c.id = out[0]
145	code, err = j.dockerCode(s, j.newDockerCmd("start", raw(c.id)), cancel)
146	if err != nil {
147		return err
148	}
149	if code != 0 {
150		return ghErrorf("Docker start fail with exit code %d", code)
151	}
152	j.checkRunning(s, c, cancel)
153	if !c.isJob {
154		ports, err := j.dockerList(s, j.newDockerCmd("port", raw(c.id)), cancel)
155		if err != nil {
156			return err
157		}
158		c.portMappings = parseDockerPort(ports)
159		j.mu.Lock()
160		j.state.SetJobService(c.alias, c.id, c.network, c.portMappings)
161		j.mu.Unlock()
162	} else {
163		env, err := j.dockerList(s, j.newDockerCmd("inspect", quotedOpt("--format", "{{range .Config.Env}}{{println .}}{{end}}"), raw(c.id)), cancel)
164		if err != nil {
165			return err
166		}
167		c.runtimePath = parsePathFromConfigEnv(env)
168		j.mu.Lock()
169		j.state.SetJobContainer("id", c.id)
170		j.mu.Unlock()
171	}
172	j.out(num, "##[endgroup]")
173	return nil
174}
175
176// pullWithRetry is the pull loop of StartContainerAsync (:158-178): three
177// tries, a random back off of 1 to 10 s between them.
178func (j *job) pullWithRetry(s *stepRun, image, cfg string, cancel <-chan struct{}) (int, error) {
179	code := 0
180	for try := 1; try <= 3; try++ {
181		var err error
182		_, code, err = j.dockerDo(j.pullCmd(image, cfg), dockerOpts{s: s, cancel: cancel, all: true, timeout: j.timeouts().Pull})
183		if err != nil {
184			return code, err
185		}
186		if code == 0 {
187			return 0, nil
188		}
189		if try < 3 {
190			d := j.containerBackoff()
191			j.tagged(s.num, "##[warning]", fmt.Sprintf("Docker pull failed with exit code %d, back off %s seconds before retry.", code, seconds(d)))
192			if !j.sleepOrCancel(d, cancel) {
193				return code, errCancelled
194			}
195		}
196	}
197	return code, nil
198}
199
200// checkRunning is the best-effort check of :193-213.
201func (j *job) checkRunning(s *stepRun, c *container, cancel <-chan struct{}) {
202	fmtOpt := quotedOpt("--format", "{{.ID}} {{.Status}}")
203	out, err := j.dockerList(s, j.newDockerCmd("ps", raw("--all"), raw("--filter", "id="+c.id), raw("--filter", "status=running"), raw("--no-trunc"), fmtOpt), cancel)
204	if err != nil {
205		return
206	}
207	for _, l := range out {
208		if l != "" {
209			if strings.HasPrefix(l, c.id) {
210				return
211			}
212			break
213		}
214	}
215	_, _ = j.dockerList(s, j.newDockerCmd("ps", raw("--all"), raw("--filter", "id="+c.id), raw("--no-trunc"), fmtOpt), cancel)
216	if code, err := j.dockerCode(s, j.newDockerCmd("logs", raw("--details", c.id)), cancel); err == nil && code != 0 {
217		j.tagged(s.num, "##[warning]", fmt.Sprintf("Docker logs fail with exit code %d", code))
218	}
219	j.tagged(s.num, "##[warning]", fmt.Sprintf("Docker container %s is not in running state.", c.id))
220}
221
222// parseDockerPort is DockerUtil.ParseDockerPort (DockerUtil.cs:14-37).
223func parseDockerPort(lines []string) []portMapping {
224	var out []portMapping
225	for _, l := range lines {
226		if m := portLineRe.FindStringSubmatch(l); m != nil {
227			out = append(out, portMapping{host: m[4], container: m[1], proto: m[2]})
228		}
229	}
230	return out
231}
232
233// parsePathFromConfigEnv is DockerUtil.ParsePathFromConfigEnv (:39-52).
234func parsePathFromConfigEnv(lines []string) string {
235	for _, l := range lines {
236		if k, v, ok := strings.Cut(l, "="); ok && k == "PATH" {
237			return v
238		}
239	}
240	return ""
241}

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