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}