UNPKG

10.9 kBJavaScriptView Raw
1"use strict";
2var __importDefault = (this && this.__importDefault) || function (mod) {
3 return (mod && mod.__esModule) ? mod : { "default": mod };
4};
5Object.defineProperty(exports, "__esModule", { value: true });
6const crypto_1 = __importDefault(require("crypto"));
7const fs_1 = __importDefault(require("fs"));
8const path_1 = __importDefault(require("path"));
9const dockerode_1 = __importDefault(require("dockerode"));
10// @ts-ignore
11const ndjson_1 = __importDefault(require("ndjson"));
12const docker_file_parser_1 = __importDefault(require("docker-file-parser"));
13const tar_fs_1 = __importDefault(require("tar-fs"));
14const zlib_1 = __importDefault(require("zlib"));
15/**
16 * Builds Docker images from Dockerfiles
17 *
18 * Detect's the special `# dockter` comment and,
19 * - sends the instructions prior to the comment to Docker to be built as normal and
20 * - applies all following instructions into a single layer
21 */
22class DockerBuilder {
23 /**
24 * Build a Docker image for a project
25 *
26 * @param dir The project directory
27 * @param name The name to tag the image with
28 * @param dockerfile The name of the Dockerfile within `dir` to use for the build
29 */
30 async build(dir, name, dockerfile = 'Dockerfile') {
31 if (!name) {
32 const hash = crypto_1.default.createHash('md5').update(dir).digest('hex');
33 name = 'dockter-' + hash;
34 }
35 const content = fs_1.default.readFileSync(path_1.default.join(dir, dockerfile), 'utf8');
36 let instructions = docker_file_parser_1.default.parse(content, { includeComments: true });
37 // Collect all instructions prior to any `# dockter` comment into a
38 // new Dockerfile and store remaining instructions for special handling.
39 // Keep track of `WORKDIR` and `USER` instructions for consistent handling of those
40 let workdir = '/';
41 let user = 'root';
42 let dockterize = false;
43 let newContent = '';
44 let index = 0;
45 for (let instruction of instructions) {
46 if (instruction.name === 'WORKDIR') {
47 workdir = path_1.default.join(workdir, instruction.args);
48 }
49 else if (instruction.name === 'USER') {
50 user = instruction.args;
51 }
52 else if (instruction.name === 'COMMENT') {
53 const arg = instruction.args;
54 if (arg.match(/^# *dockter/)) {
55 instructions = instructions.slice(index + 1);
56 dockterize = true;
57 break;
58 }
59 }
60 if (instruction.raw)
61 newContent += instruction.raw + '\n';
62 index += 1;
63 }
64 // If there was no # dockter comment then make sure there are no
65 // 'extra' instructions
66 if (!dockterize)
67 instructions = [];
68 // Pack the directory and replace the Dockerfile with the new one
69 const tar = tar_fs_1.default.pack(dir, {
70 ignore: name => {
71 const relpath = path_1.default.relative(dir, name);
72 // Ignore original Dockerfile
73 // Ignore the special `snapshot` directory which exists when this
74 // is run within a `pkg` binary and dir is `.`
75 return relpath === 'Dockerfile' || relpath[0] === '.' || relpath === 'snapshot';
76 },
77 finalize: false,
78 finish: pack => {
79 // Add new Dockerfile
80 pack.entry({ name: 'Dockerfile' }, newContent);
81 pack.finalize();
82 }
83 });
84 const targz = tar.pipe(zlib_1.default.createGzip());
85 // The following line can be useful in debugging the
86 // above tar stream generation
87 // targz.pipe(fs.createWriteStream('/tmp/dockter-builder-debug-1.tar.gz'))
88 const docker = new dockerode_1.default();
89 const messages = [];
90 const stream = await docker.buildImage(targz, {
91 // Options to Docker ImageBuild operation
92 // See https://docs.docker.com/engine/api/v1.37/#operation/ImageBuild
93 t: name + ':system'
94 });
95 // The following catches errors from abovr and turns them into messages but does
96 // nothing with them. It's commented out for now, so errors get thrown to console,
97 // but will be reinstated when we determine the best place to attach these errors/messages
98 /*
99 .catch(error => {
100 let line
101 let message = error.message
102 const match = message.match(/^\(HTTP code 400\) unexpected - Dockerfile parse error line (\d+): (.*)$/)
103 if (match) {
104 line = parseInt(match[1], 0)
105 message = match[2]
106 }
107 messages.push({
108 level: 'error',
109 line: line,
110 message: message
111 })
112 })
113
114 // If there were any errors then return
115 //if (!stream) return
116 */
117 // Wait for build to finish and record the id of the system layer
118 let currentSystemLayer = await new Promise((resolve, reject) => {
119 let id;
120 stream.pipe(ndjson_1.default.parse()).on('data', (data) => {
121 if (data.error) {
122 messages.push({
123 level: 'error',
124 message: data.error
125 });
126 console.error(data.error);
127 }
128 else if (data.aux && data.aux.ID) {
129 id = data.aux.ID;
130 }
131 else {
132 // We could keep track of data that looks like this
133 // {"stream":"Step 2/2 : RUN foo"}
134 // to match any errors with lines in the Dockerfile content
135 if (data.stream) {
136 process.stderr.write(data.stream);
137 }
138 }
139 });
140 stream.on('end', () => resolve(id));
141 stream.on('error', reject);
142 });
143 // Check for any error message
144 const errors = messages.filter(message => message.level === 'error');
145 if (errors.length)
146 throw new Error(`There was an error when building the image: ${errors.map(error => error.message).join(',')}`);
147 // Get information on the current
148 const image = docker.getImage(name + ':latest');
149 let appLayer;
150 let lastSystemLayer;
151 try {
152 const imageInfo = await image.inspect();
153 appLayer = imageInfo.Id;
154 lastSystemLayer = imageInfo.Config.Labels && imageInfo.Config.Labels.systemLayer;
155 }
156 catch (error) {
157 // No existing image, just continue
158 }
159 // If the foundation image has changed then use the new version,
160 // otherwise use the existing one
161 let layer;
162 if (lastSystemLayer) {
163 if (lastSystemLayer !== currentSystemLayer)
164 layer = currentSystemLayer;
165 else
166 layer = appLayer;
167 }
168 else {
169 layer = currentSystemLayer;
170 }
171 // Create a container from the layer and start it up
172 let container = await docker.createContainer({
173 Image: layer,
174 Tty: true,
175 Cmd: ['/bin/bash']
176 });
177 await container.start();
178 // Handle the remaining instructions
179 let count = 1;
180 let changes = '';
181 for (let instruction of instructions) {
182 const step = `Dockter ${count}/${instructions.length} :`;
183 switch (instruction.name) {
184 case 'USER':
185 user = instruction.args;
186 break;
187 case 'WORKDIR':
188 workdir = path_1.default.join(workdir, instruction.args);
189 break;
190 case 'COPY':
191 case 'ADD':
192 // Add files/subdirs to the container
193 const copy = instruction.args;
194 const to = copy.pop();
195 const pack = tar_fs_1.default.pack(dir, {
196 // Set the destination of each file (last item in `COPY` command)
197 map: function (header) {
198 header.name = to;
199 return header;
200 },
201 // Ignore any files in the directory that are not in the `COPY` list
202 ignore: name => {
203 const relativePath = path_1.default.relative(dir, name);
204 return !copy.includes(relativePath);
205 }
206 });
207 await container.putArchive(pack, { path: workdir });
208 break;
209 case 'RUN':
210 // Execute code in the container
211 const script = instruction.args;
212 const exec = await container.exec({
213 Cmd: ['bash', '-c', `${script}`],
214 AttachStdout: true,
215 AttachStderr: true,
216 User: user,
217 Tty: true
218 });
219 await exec.start();
220 exec.output.pipe(process.stdout);
221 // Wait until the exec has finished running, checking every 100ms
222 while (true) {
223 let status = await exec.inspect();
224 if (status.Running === false)
225 break;
226 await new Promise(resolve => setTimeout(resolve, 100));
227 }
228 break;
229 case 'CMD':
230 // Dockerfile instructions to apply when committing the image
231 changes += instruction.raw + '\n\n';
232 break;
233 case 'COMMENT':
234 // Just ignore it!
235 break;
236 default:
237 throw new Error(`Dockter can not yet handle a ${instruction.name} instruction. Put it before the # dockter comment in your Dockerfile.`);
238 }
239 count += 1;
240 }
241 // Create an image from the modified container
242 const data = await container.commit({
243 // Options to commit
244 // See https://docs.docker.com/engine/api/v1.37/#operation/ImageCommit
245 repo: name,
246 comment: instructions.length > 0 ? 'Updated application layer' : 'No updates requested',
247 changes,
248 User: user,
249 WorkingDir: workdir,
250 Labels: {
251 systemLayer: currentSystemLayer
252 }
253 });
254 await container.stop();
255 }
256}
257exports.default = DockerBuilder;