-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathdocker.ts
More file actions
250 lines (226 loc) · 7.08 KB
/
docker.ts
File metadata and controls
250 lines (226 loc) · 7.08 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
import Docker from 'dockerode';
import os from 'os';
import { readdir } from 'fs/promises';
import { createSigintAbortSignal } from '../utils/abortController.js';
import { CONFIG_FILE } from '../config/config.js';
type ProgressEvent = { stream?: string };
type FinishOutputRow = { error?: string };
type FinishBuildOutputRow = FinishOutputRow & { aux?: { ID?: string } };
const docker = new Docker();
export async function checkDockerDaemon() {
try {
await docker.ping();
} catch {
throw Error(
'Docker daemon is not accessible, make sure docker is installed and running'
);
}
}
export async function dockerBuild({
tag = undefined,
isForTest = false,
progressCallback = () => {},
}: {
tag?: string;
isForTest?: boolean;
progressCallback?: (msg: string) => void;
}): Promise<string> {
const osType = os.type();
const contextPath = process.cwd(); // Use current working directory
const contextFiles = await readdir(contextPath);
const buildArgs = {
context: contextPath,
src: contextFiles.filter((fileName) => fileName !== CONFIG_FILE), // exclude config file from build context even if not in dockerignore
};
// by default force to build amd64 image which is architecture used in iExec workers
// this require buildx builder to support 'linux/amd64' (some devices may need QEMU for amd64 architecture emulation)
let platform = 'linux/amd64';
// for MacOS local testing only build arm64 variant
if (osType === 'Darwin' && isForTest) {
platform = 'linux/arm64';
}
const { signal, clear } = createSigintAbortSignal();
// Perform the Docker build operation
const buildImageStream = await docker.buildImage(buildArgs, {
t: tag,
platform,
pull: true, // docker store does not support multi platform image, this can cause issues when switching build target platform, pulling ensures the right image is used
abortSignal: signal,
});
return new Promise((resolve, reject) => {
docker.modem.followProgress(buildImageStream, onFinished, onProgress);
function onFinished(err: Error | null, output: FinishBuildOutputRow[]) {
clear();
/**
* expected output format for image id
* ```
* {
* aux: {
* ID: 'sha256:e994101ce877e9b42f31f1508e11bbeb8fa5096a1fb2d0c650a6a26797b1906b'
* }
* },
* ```
*/
const builtImageId = output?.find((row) => row?.aux?.ID)?.aux?.ID;
/**
* 3 kind of error possible, we want to catch each of them:
* - stream error
* - build error
* - no image id (should not happen)
*
* expected output format for build error
* ```
* {
* errorDetail: {
* code: 1,
* message: "The command '/bin/sh -c npm ci' returned a non-zero code: 1"
* },
* error: "The command '/bin/sh -c npm ci' returned a non-zero code: 1"
* }
* ```
*/
const errorOrErrorMessage =
err || // stream error
output.find((row) => row?.error)?.error || // build error message
(!builtImageId && 'Failed to retrieve generated image ID'); // no image id -> error message
if (errorOrErrorMessage) {
const error =
errorOrErrorMessage instanceof Error
? errorOrErrorMessage
: Error(errorOrErrorMessage);
reject(error);
} else {
resolve(builtImageId!);
}
}
function onProgress(event: ProgressEvent) {
if (event?.stream) {
progressCallback(event.stream);
}
}
});
}
// Function to push a Docker image
export async function pushDockerImage({
tag,
dockerhubUsername,
dockerhubAccessToken,
progressCallback = () => {},
}: {
tag: string;
dockerhubUsername?: string;
dockerhubAccessToken: string;
progressCallback?: (msg: string) => void;
}) {
if (!dockerhubUsername || !dockerhubAccessToken) {
throw new Error('Missing DockerHub credentials.');
}
const dockerImage = docker.getImage(tag);
const sigint = createSigintAbortSignal();
const imagePushStream = await dockerImage.push({
authconfig: {
username: dockerhubUsername,
password: dockerhubAccessToken,
},
abortSignal: sigint.signal,
});
await new Promise((resolve, reject) => {
docker.modem.followProgress(imagePushStream, onFinished, onProgress);
function onFinished(err: Error | null, output: FinishOutputRow[]) {
sigint.clear();
/**
* 2 kind of error possible, we want to catch each of them:
* - stream error
* - push error
*
* expected output format for push error
* ```
* {
* errorDetail: {
* message: 'Get "https://registry-1.docker.io/v2/": dial tcp: lookup registry-1.docker.io: Temporary failure in name resolution'
* },
* error: 'Get "https://registry-1.docker.io/v2/": dial tcp: lookup registry-1.docker.io: Temporary failure in name resolution'
* }
* ```
*/
const errorOrErrorMessage =
err || // stream error
output.find((row) => row?.error)?.error; // push error message
if (errorOrErrorMessage) {
const error =
errorOrErrorMessage instanceof Error
? errorOrErrorMessage
: Error(errorOrErrorMessage);
return reject(error);
}
resolve(tag);
}
function onProgress(event: ProgressEvent) {
if (event?.stream) {
progressCallback(event.stream);
}
}
});
}
export async function runDockerContainer({
image,
cmd,
volumes = [],
env = [],
memory = undefined,
logsCallback = () => {},
}: {
image: string;
cmd: string[];
volumes?: string[];
env?: string[];
memory?: number;
logsCallback?: (msg: string) => void;
}) {
const sigint = createSigintAbortSignal();
const container = await docker.createContainer({
Image: image,
Cmd: cmd,
HostConfig: {
Binds: volumes,
AutoRemove: false, // do not auto remove, we want to inspect after the container is exited
Memory: memory,
},
Env: env,
abortSignal: sigint.signal,
});
// Handle abort signal
if (sigint.signal) {
sigint.signal.addEventListener('abort', async () => {
await container.kill();
logsCallback('Container execution aborted');
});
}
// Start the container
await container.start();
// get the logs stream
const logsStream = await container.logs({
follow: true,
stdout: true,
stderr: true,
});
logsStream.on('data', (chunk) => {
// const streamType = chunk[0]; // 1 = stdout, 2 = stderr
const logData = chunk.slice(8).toString('utf-8'); // strip multiplexed stream header
logsCallback(logData);
});
logsStream.on('error', (err) => {
logsCallback(`Error streaming logs: ${err.message}`);
});
// Wait for the container to finish
await container.wait();
sigint.clear();
// Check container status after waiting
const { State } = await container.inspect();
// Done with the container, remove the container
await container.remove();
return {
exitCode: State.ExitCode,
outOfMemory: State.OOMKilled,
};
}