1 | import Doer from './Doer'
|
2 | import { SoftwareEnvironment, SoftwarePackage } from '@stencila/schema'
|
3 | import { join } from 'path'
|
4 |
|
5 | const VERSION = require('../package').version
|
6 |
|
7 | /**
|
8 | * Generates a Dockerfile for a `SoftwareEnvironment` instance
|
9 | */
|
10 | export default class Generator extends Doer {
|
11 |
|
12 | /**
|
13 | * Generate a Dockerfile for a `SoftwareEnvironment` instance
|
14 | *
|
15 | * @param comments Should a comments be added to the Dockerfile?
|
16 | * @param stencila Should relevant Stencila language packages be installed in the image?
|
17 | */
|
18 | generate (comments: boolean = true, stencila: boolean = false): string {
|
19 | let dockerfile = ''
|
20 |
|
21 | if (comments) {
|
22 | dockerfile += `# Generated by Dockter ${VERSION} at ${new Date().toISOString()}
|
23 | # To stop Dockter generating this file and start editing it yourself,
|
24 | # rename it to "Dockerfile".\n`
|
25 | }
|
26 |
|
27 | if (comments) dockerfile += '\n# This tells Docker which base image to use.\n'
|
28 | const baseIdentifier = this.baseIdentifier()
|
29 | dockerfile += `FROM ${baseIdentifier}\n`
|
30 |
|
31 | if (!this.applies()) return dockerfile
|
32 |
|
33 | const aptRepos = this.aptRepos(baseIdentifier)
|
34 | let aptKeysCommand = this.aptKeysCommand(baseIdentifier)
|
35 |
|
36 | if (aptRepos.length || aptKeysCommand) {
|
37 | if (comments) dockerfile += '\n# This section installs system packages needed to add extra system repositories.'
|
38 | dockerfile += `
|
39 | RUN apt-get update \\
|
40 | && DEBIAN_FRONTEND=noninteractive apt-get install -y \\
|
41 | apt-transport-https \\
|
42 | ca-certificates \\
|
43 | curl \\
|
44 | software-properties-common
|
45 | `
|
46 | }
|
47 |
|
48 | if (comments && (aptKeysCommand || aptRepos.length)) {
|
49 | dockerfile += '\n# This section adds system repositories required to install extra system packages.'
|
50 | }
|
51 | if (aptKeysCommand) dockerfile += `\nRUN ${aptKeysCommand}`
|
52 | if (aptRepos.length) dockerfile += `\nRUN ${aptRepos.map(repo => `apt-add-repository "${repo}"`).join(' \\\n && ')}\n`
|
53 |
|
54 | // Set env vars after previous section to improve caching
|
55 | const envVars = this.envVars(baseIdentifier)
|
56 | if (envVars.length) {
|
57 | if (comments) dockerfile += '\n# This section sets environment variables within the image.'
|
58 | const pairs = envVars.map(([key, value]) => `${key}="${value.replace('"', '\\"')}"`)
|
59 | dockerfile += `\nENV ${pairs.join(' \\\n ')}\n`
|
60 | }
|
61 |
|
62 | let aptPackages: Array<string> = this.aptPackages(baseIdentifier)
|
63 | if (aptPackages.length) {
|
64 | if (comments) {
|
65 | dockerfile += `
|
66 | # This section installs system packages required for your project
|
67 | # If you need extra system packages add them here.`
|
68 | }
|
69 | dockerfile += `
|
70 | RUN apt-get update \\
|
71 | && DEBIAN_FRONTEND=noninteractive apt-get install -y \\
|
72 | ${aptPackages.join(' \\\n ')} \\
|
73 | && apt-get autoremove -y \\
|
74 | && apt-get clean \\
|
75 | && rm -rf /var/lib/apt/lists/*
|
76 | `
|
77 | }
|
78 |
|
79 | if (stencila) {
|
80 | let stencilaInstall = this.stencilaInstall(baseIdentifier)
|
81 | if (stencilaInstall) {
|
82 | if (comments) dockerfile += '\n# This section runs commands to install Stencila execution hosts.'
|
83 | dockerfile += `\nRUN ${stencilaInstall}\n`
|
84 | }
|
85 | }
|
86 |
|
87 | // Once everything that needs root permissions is installed, switch the user to non-root for installing the rest of the packages.
|
88 | if (comments) {
|
89 | dockerfile += `
|
90 | # It's good practice to run Docker images as a non-root user.
|
91 | # This section creates a new user and its home directory as the default working directory.`
|
92 | }
|
93 | dockerfile += `
|
94 | RUN useradd --create-home --uid 1001 -s /bin/bash dockteruser
|
95 | WORKDIR /home/dockteruser
|
96 | `
|
97 |
|
98 | const installFiles = this.installFiles(baseIdentifier)
|
99 | const installCommand = this.installCommand(baseIdentifier)
|
100 | const projectFiles = this.projectFiles(baseIdentifier)
|
101 | const runCommand = this.runCommand(baseIdentifier)
|
102 |
|
103 | // Add Dockter special comment for managed installation of language packages
|
104 | if (installCommand) {
|
105 | if (comments) dockerfile += '\n# This is a special comment to tell Dockter to manage the build from here on'
|
106 | dockerfile += `\n# dockter\n`
|
107 | }
|
108 |
|
109 | // Copy files needed for installation of language packages
|
110 | if (installFiles.length) {
|
111 | if (comments) dockerfile += '\n# This section copies package requirement files into the image'
|
112 | dockerfile += '\n' + installFiles.map(([src, dest]) => `COPY ${src} ${dest}`).join('\n') + '\n'
|
113 | }
|
114 |
|
115 | // Run command to install packages
|
116 | if (installCommand) {
|
117 | if (comments) dockerfile += '\n# This section runs commands to install the packages specified in the requirement file/s'
|
118 | dockerfile += `\nRUN ${installCommand}\n`
|
119 | }
|
120 |
|
121 | // Copy files needed to run project
|
122 | if (projectFiles.length) {
|
123 | if (comments) dockerfile += '\n# This section copies your project\'s files into the image'
|
124 | dockerfile += '\n' + projectFiles.map(([src, dest]) => `COPY ${src} ${dest}`).join('\n') + '\n'
|
125 | }
|
126 |
|
127 | // Now all installation is finished set the user
|
128 | if (comments) dockerfile += '\n# This sets the default user when the container is run'
|
129 | dockerfile += '\nUSER dockteruser\n'
|
130 |
|
131 | // Add any CMD
|
132 | if (runCommand) {
|
133 | if (comments) dockerfile += '\n# This tells Docker the default command to run when the container is started'
|
134 | dockerfile += `\nCMD ${runCommand}\n`
|
135 | }
|
136 |
|
137 | // Write `.Dockerfile` for use by Docker
|
138 | this.write('.Dockerfile', dockerfile)
|
139 |
|
140 | return dockerfile
|
141 | }
|
142 |
|
143 | // Methods that are overridden in derived classes
|
144 |
|
145 | /**
|
146 | * Does this generator apply to the package?
|
147 | */
|
148 | applies (): boolean {
|
149 | return false
|
150 | }
|
151 |
|
152 | /**
|
153 | * Name of the base image
|
154 | */
|
155 | baseName (): string {
|
156 | return 'ubuntu'
|
157 | }
|
158 |
|
159 | /**
|
160 | * Version of the base image
|
161 | */
|
162 | baseVersion (): string {
|
163 | return '18.04'
|
164 | }
|
165 |
|
166 | /**
|
167 | * Get the version name for a base image
|
168 | * @param baseIdentifier The base image name e.g. `ubuntu:18.04`
|
169 | */
|
170 | baseVersionName (baseIdentifier: string): string {
|
171 | let [name, version] = baseIdentifier.split(':')
|
172 | const lookup: { [key: string]: string } = {
|
173 | '14.04': 'trusty',
|
174 | '16.04': 'xenial',
|
175 | '18.04': 'bionic'
|
176 | }
|
177 | return lookup[version]
|
178 | }
|
179 |
|
180 | /**
|
181 | * Generate a base image identifier
|
182 | */
|
183 | baseIdentifier (): string {
|
184 | const joiner = this.baseVersion() === '' ? '' : ':'
|
185 |
|
186 | return `${this.baseName()}${joiner}${this.baseVersion()}`
|
187 | }
|
188 |
|
189 | /**
|
190 | * A list of environment variables to set in the image
|
191 | * as `name`, `value` pairs
|
192 | *
|
193 | * @param sysVersion The Ubuntu system version being used
|
194 | */
|
195 | envVars (sysVersion: string): Array<[string, string]> {
|
196 | return []
|
197 | }
|
198 |
|
199 | /**
|
200 | * A Bash command to run to install required apt keys
|
201 | *
|
202 | * @param sysVersion The Ubuntu system version being used
|
203 | */
|
204 | aptKeysCommand (sysVersion: string): string | undefined {
|
205 | return
|
206 | }
|
207 |
|
208 | /**
|
209 | * A list of any required apt repositories
|
210 | *
|
211 | * @param sysVersion The Ubuntu system version being used
|
212 | */
|
213 | aptRepos (sysVersion: string): Array<string> {
|
214 | return []
|
215 | }
|
216 |
|
217 | /**
|
218 | * A list of any required apt packages
|
219 | *
|
220 | * @param sysVersion The Ubuntu system version being used
|
221 | */
|
222 | aptPackages (sysVersion: string): Array<string> {
|
223 | return []
|
224 | }
|
225 |
|
226 | /**
|
227 | * A Bash command to run to install Stencila execution host package/s
|
228 | *
|
229 | * @param sysVersion The Ubuntu system version being used
|
230 | */
|
231 | stencilaInstall (sysVersion: string): string | undefined {
|
232 | return
|
233 | }
|
234 |
|
235 | /**
|
236 | * A list of files that need to be be copied
|
237 | * into the image before running `installCommand`
|
238 | *
|
239 | * @param sysVersion The Ubuntu system version being used
|
240 | * @returns An array of [src, dest] tuples
|
241 | */
|
242 | installFiles (sysVersion: string): Array<[string, string]> {
|
243 | return []
|
244 | }
|
245 |
|
246 | /**
|
247 | * The Bash command to run to install required language packages
|
248 | *
|
249 | * @param sysVersion The Ubuntu system version being used
|
250 | */
|
251 | installCommand (sysVersion: string): string | undefined {
|
252 | return
|
253 | }
|
254 |
|
255 | /**
|
256 | * The project's files that should be copied across to the image
|
257 | *
|
258 | * @param sysVersion The Ubuntu system version being used
|
259 | * @returns An array of [src, dest] tuples
|
260 | */
|
261 | projectFiles (sysVersion: string): Array<[string, string]> {
|
262 | return []
|
263 | }
|
264 |
|
265 | /**
|
266 | * The default command to run containers created from this image
|
267 | *
|
268 | * @param sysVersion The Ubuntu system version being used
|
269 | */
|
270 | runCommand (sysVersion: string): string | undefined {
|
271 | return
|
272 | }
|
273 | }
|