UNPKG

8.38 kBPlain TextView Raw
1import Doer from './Doer'
2import { SoftwareEnvironment, SoftwarePackage } from '@stencila/schema'
3import { join } from 'path'
4
5const VERSION = require('../package').version
6
7/**
8 * Generates a Dockerfile for a `SoftwareEnvironment` instance
9 */
10export 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 += `
39RUN 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 += `
70RUN 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 += `
94RUN useradd --create-home --uid 1001 -s /bin/bash dockteruser
95WORKDIR /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}