1 | import os from 'os'
|
2 | import path from 'path'
|
3 | import stream from 'stream'
|
4 |
|
5 | import Docker from 'dockerode'
|
6 |
|
7 | /**
|
8 | * Executes a Docker environment.
|
9 | *
|
10 | * This class has a single method, `execute`, which starts a container from an
|
11 | * image and runs the command specified in the Dockerfile `CMD` instruction.
|
12 | *
|
13 | * It mounts the project's directory into the container a `/work` and uses it
|
14 | * as the working directory.
|
15 | *
|
16 | * It also sets the current user and group as the
|
17 | * user and group in the container. This means that within the container the
|
18 | * command that runs has the same permissions as the current user does in the
|
19 | * `/work` directory.
|
20 | *
|
21 | * Finally, it removes the container (but not the image).
|
22 | *
|
23 | * This then is the equivalent of running the container with Docker from within
|
24 | * the project directory using,
|
25 | *
|
26 | * docker run --rm --volume $(pwd):/work --workdir=/work --user=$(id -u):$(id -g) <image>
|
27 | */
|
28 | export default class DockerExecutor {
|
29 |
|
30 | /**
|
31 | * Run a Docker container
|
32 | *
|
33 | * @param name Name of the Docker image to use
|
34 | * @param folder Path of the project folder which will be mounted into the image
|
35 | */
|
36 | async execute (name: string, folder: string, command: string = '') {
|
37 | // Capture stdout so we can attempt to parse it
|
38 | // to JSON
|
39 | let out = ''
|
40 | let stdout = new stream.Writable({
|
41 | write (chunk, encoding, callback) {
|
42 | out += chunk.toString()
|
43 | callback()
|
44 | }
|
45 | })
|
46 |
|
47 | // Just write errors through to local console error
|
48 | let stderr = new stream.Writable({
|
49 | write (chunk, encoding, callback) {
|
50 | console.error(chunk.toString())
|
51 | callback()
|
52 | }
|
53 | })
|
54 |
|
55 | // Get and set user:group
|
56 | const userInfo = os.userInfo()
|
57 | const user = `${userInfo.uid}:${userInfo.gid}`
|
58 |
|
59 | // Run the container!
|
60 | // Options from https://docs.docker.com/engine/api/v1.37/#operation/ContainerCreate
|
61 | const docker = new Docker()
|
62 | // If the user has specified a command thaen use that, otherwise fallback to the
|
63 | // CMD in the Dockerfile
|
64 | let cmd
|
65 | if (command) cmd = command.split(' ')
|
66 | const container = await docker.run(name, [], [stdout, stderr], {
|
67 | Cmd: cmd,
|
68 | HostConfig: {
|
69 | Binds: [
|
70 | `${path.resolve(folder)}:/work`
|
71 | ]
|
72 | },
|
73 | Tty: false,
|
74 | User: user,
|
75 | WorkingDir: '/work'
|
76 | })
|
77 | container.remove()
|
78 |
|
79 | // Attempt to parse output as JSON
|
80 | try {
|
81 | return JSON.parse(out)
|
82 | } catch {
|
83 | return out.trim()
|
84 | }
|
85 | }
|
86 | }
|