@kapeta/local-cluster-service 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/dist/cjs/index.js +2 -0
- package/dist/cjs/src/assetManager.d.ts +3 -1
- package/dist/cjs/src/assetManager.js +20 -4
- package/dist/cjs/src/assets/routes.js +22 -1
- package/dist/cjs/src/containerManager.d.ts +1 -1
- package/dist/cjs/src/containerManager.js +132 -122
- package/dist/cjs/src/instanceManager.d.ts +4 -3
- package/dist/cjs/src/instanceManager.js +87 -60
- package/dist/cjs/src/instances/routes.js +21 -11
- package/dist/cjs/src/operatorManager.d.ts +5 -3
- package/dist/cjs/src/operatorManager.js +34 -22
- package/dist/cjs/src/providerManager.js +1 -1
- package/dist/cjs/src/repositoryManager.d.ts +2 -4
- package/dist/cjs/src/repositoryManager.js +51 -66
- package/dist/cjs/src/socketManager.js +1 -1
- package/dist/cjs/src/taskManager.d.ts +64 -0
- package/dist/cjs/src/taskManager.js +163 -0
- package/dist/cjs/src/tasks/routes.d.ts +3 -0
- package/dist/cjs/src/tasks/routes.js +35 -0
- package/dist/cjs/src/utils/BlockInstanceRunner.js +0 -1
- package/dist/esm/index.js +2 -0
- package/dist/esm/src/assetManager.d.ts +3 -1
- package/dist/esm/src/assetManager.js +20 -4
- package/dist/esm/src/assets/routes.js +22 -1
- package/dist/esm/src/containerManager.d.ts +1 -1
- package/dist/esm/src/containerManager.js +132 -122
- package/dist/esm/src/instanceManager.d.ts +4 -3
- package/dist/esm/src/instanceManager.js +87 -60
- package/dist/esm/src/instances/routes.js +21 -11
- package/dist/esm/src/operatorManager.d.ts +5 -3
- package/dist/esm/src/operatorManager.js +34 -22
- package/dist/esm/src/providerManager.js +1 -1
- package/dist/esm/src/repositoryManager.d.ts +2 -4
- package/dist/esm/src/repositoryManager.js +51 -66
- package/dist/esm/src/socketManager.js +1 -1
- package/dist/esm/src/taskManager.d.ts +64 -0
- package/dist/esm/src/taskManager.js +159 -0
- package/dist/esm/src/tasks/routes.d.ts +3 -0
- package/dist/esm/src/tasks/routes.js +30 -0
- package/dist/esm/src/utils/BlockInstanceRunner.js +0 -1
- package/index.ts +2 -0
- package/package.json +1 -1
- package/src/assetManager.ts +28 -4
- package/src/assets/routes.ts +23 -1
- package/src/containerManager.ts +153 -142
- package/src/instanceManager.ts +116 -70
- package/src/instances/routes.ts +20 -12
- package/src/operatorManager.ts +46 -26
- package/src/providerManager.ts +1 -1
- package/src/repositoryManager.ts +65 -63
- package/src/socketManager.ts +1 -1
- package/src/taskManager.ts +225 -0
- package/src/tasks/routes.ts +38 -0
- package/src/utils/BlockInstanceRunner.ts +0 -4
@@ -0,0 +1,159 @@
|
|
1
|
+
/**
|
2
|
+
* Class that handles processing background tasks.
|
3
|
+
*/
|
4
|
+
import { socketManager } from './socketManager';
|
5
|
+
const EVENT_TASK_UPDATED = 'task-updated';
|
6
|
+
const EVENT_TASK_ADDED = 'task-added';
|
7
|
+
const EVENT_TASK_REMOVED = 'task-removed';
|
8
|
+
export var TaskStatus;
|
9
|
+
(function (TaskStatus) {
|
10
|
+
TaskStatus["PENDING"] = "PENDING";
|
11
|
+
TaskStatus["RUNNING"] = "RUNNING";
|
12
|
+
TaskStatus["COMPLETED"] = "COMPLETED";
|
13
|
+
TaskStatus["FAILED"] = "FAILED";
|
14
|
+
})(TaskStatus || (TaskStatus = {}));
|
15
|
+
export class Task {
|
16
|
+
data;
|
17
|
+
constructor(task) {
|
18
|
+
this.data = task;
|
19
|
+
}
|
20
|
+
get id() {
|
21
|
+
return this.data.id;
|
22
|
+
}
|
23
|
+
get status() {
|
24
|
+
return this.data.status;
|
25
|
+
}
|
26
|
+
get errorMessage() {
|
27
|
+
return this.data.errorMessage;
|
28
|
+
}
|
29
|
+
get metadata() {
|
30
|
+
return this.data.metadata;
|
31
|
+
}
|
32
|
+
get future() {
|
33
|
+
return this.data.future;
|
34
|
+
}
|
35
|
+
get run() {
|
36
|
+
return this.data.run;
|
37
|
+
}
|
38
|
+
set status(status) {
|
39
|
+
this.data.status = status;
|
40
|
+
}
|
41
|
+
set errorMessage(errorMessage) {
|
42
|
+
this.data.errorMessage = errorMessage;
|
43
|
+
}
|
44
|
+
set metadata(metadata) {
|
45
|
+
this.data.metadata = metadata;
|
46
|
+
}
|
47
|
+
emitUpdate() {
|
48
|
+
socketManager.emitGlobal(EVENT_TASK_UPDATED, this.toData());
|
49
|
+
}
|
50
|
+
async wait() {
|
51
|
+
return this.future.promise;
|
52
|
+
}
|
53
|
+
toData() {
|
54
|
+
return { ...this.data };
|
55
|
+
}
|
56
|
+
}
|
57
|
+
function createFuture() {
|
58
|
+
let resolve = () => { };
|
59
|
+
let reject = () => { };
|
60
|
+
const promise = new Promise((res, rej) => {
|
61
|
+
resolve = res;
|
62
|
+
reject = rej;
|
63
|
+
});
|
64
|
+
return {
|
65
|
+
promise,
|
66
|
+
resolve,
|
67
|
+
reject,
|
68
|
+
};
|
69
|
+
}
|
70
|
+
class TaskManager {
|
71
|
+
_tasks = [];
|
72
|
+
add(id, runner, metadata) {
|
73
|
+
const existingTask = this.get(id);
|
74
|
+
if (existingTask) {
|
75
|
+
return existingTask;
|
76
|
+
}
|
77
|
+
const future = createFuture();
|
78
|
+
const task = new Task({
|
79
|
+
id,
|
80
|
+
status: TaskStatus.PENDING,
|
81
|
+
metadata,
|
82
|
+
future,
|
83
|
+
run: runner,
|
84
|
+
});
|
85
|
+
this._tasks.push(task);
|
86
|
+
socketManager.emitGlobal(EVENT_TASK_ADDED, task.toData());
|
87
|
+
this.invokeTask(task).catch(() => { });
|
88
|
+
return task;
|
89
|
+
}
|
90
|
+
async waitFor(filter) {
|
91
|
+
const tasks = this._tasks.filter(filter);
|
92
|
+
while (tasks.length > 0) {
|
93
|
+
const task = tasks.shift();
|
94
|
+
if (!task) {
|
95
|
+
continue;
|
96
|
+
}
|
97
|
+
try {
|
98
|
+
await task.wait();
|
99
|
+
}
|
100
|
+
catch (e) {
|
101
|
+
//Ignore
|
102
|
+
}
|
103
|
+
}
|
104
|
+
}
|
105
|
+
get(taskId) {
|
106
|
+
return this._tasks.find((t) => t.id === taskId);
|
107
|
+
}
|
108
|
+
exists(taskId) {
|
109
|
+
return !!this.get(taskId);
|
110
|
+
}
|
111
|
+
remove(taskId) {
|
112
|
+
const task = this.get(taskId);
|
113
|
+
if (!task) {
|
114
|
+
return;
|
115
|
+
}
|
116
|
+
if (task.status === TaskStatus.RUNNING) {
|
117
|
+
throw new Error('Cannot remove a running task');
|
118
|
+
}
|
119
|
+
this._tasks = this._tasks.filter((t) => t.id !== taskId);
|
120
|
+
socketManager.emitGlobal(EVENT_TASK_REMOVED, task.toData());
|
121
|
+
}
|
122
|
+
list() {
|
123
|
+
return this._tasks.map((t) => t.toData());
|
124
|
+
}
|
125
|
+
async invokeTask(task) {
|
126
|
+
if (task.metadata.group) {
|
127
|
+
const existingTaskInGroup = this._tasks.find((t) => t.id !== task.id && t.metadata.group === task.metadata.group && t.status === TaskStatus.RUNNING);
|
128
|
+
if (existingTaskInGroup) {
|
129
|
+
console.log('existingTaskInGroup', existingTaskInGroup.toData());
|
130
|
+
return;
|
131
|
+
}
|
132
|
+
}
|
133
|
+
try {
|
134
|
+
task.status = TaskStatus.RUNNING;
|
135
|
+
task.emitUpdate();
|
136
|
+
const result = await task.run(task);
|
137
|
+
task.status = TaskStatus.COMPLETED;
|
138
|
+
task.future.resolve(result);
|
139
|
+
task.emitUpdate();
|
140
|
+
}
|
141
|
+
catch (e) {
|
142
|
+
task.errorMessage = e.message;
|
143
|
+
task.status = TaskStatus.FAILED;
|
144
|
+
task.future.reject(e);
|
145
|
+
task.emitUpdate();
|
146
|
+
}
|
147
|
+
finally {
|
148
|
+
this.remove(task.id);
|
149
|
+
}
|
150
|
+
if (task.metadata.group) {
|
151
|
+
const nextTaskInGroup = this._tasks.find((t) => t.id !== task.id && t.metadata.group === task.metadata.group && t.status === TaskStatus.PENDING);
|
152
|
+
if (nextTaskInGroup) {
|
153
|
+
console.log('nextTaskInGroup', nextTaskInGroup.toData());
|
154
|
+
return this.invokeTask(nextTaskInGroup);
|
155
|
+
}
|
156
|
+
}
|
157
|
+
}
|
158
|
+
}
|
159
|
+
export const taskManager = new TaskManager();
|
@@ -0,0 +1,30 @@
|
|
1
|
+
import Router from 'express-promise-router';
|
2
|
+
import { corsHandler } from '../middleware/cors';
|
3
|
+
import { taskManager } from '../taskManager';
|
4
|
+
const router = Router();
|
5
|
+
router.use('/', corsHandler);
|
6
|
+
/**
|
7
|
+
* Get all current tasks
|
8
|
+
*/
|
9
|
+
router.get('/', (req, res) => {
|
10
|
+
res.send(taskManager.list());
|
11
|
+
});
|
12
|
+
router.get('/:taskId', (req, res) => {
|
13
|
+
const task = taskManager.get(req.params.taskId);
|
14
|
+
if (!task) {
|
15
|
+
res.status(404).send({ error: 'Task not found' });
|
16
|
+
return;
|
17
|
+
}
|
18
|
+
res.send(task.toData());
|
19
|
+
});
|
20
|
+
router.delete('/:taskId', (req, res) => {
|
21
|
+
try {
|
22
|
+
taskManager.remove(req.params.taskId);
|
23
|
+
res.send({ ok: true });
|
24
|
+
}
|
25
|
+
catch (e) {
|
26
|
+
res.status(400).send({ error: e.message });
|
27
|
+
return;
|
28
|
+
}
|
29
|
+
});
|
30
|
+
export default router;
|
@@ -318,7 +318,6 @@ export class BlockInstanceRunner {
|
|
318
318
|
}
|
319
319
|
async ensureContainer(opts) {
|
320
320
|
const container = await containerManager.ensureContainer(opts);
|
321
|
-
await containerManager.waitForReady(container);
|
322
321
|
return this._handleContainer(container);
|
323
322
|
}
|
324
323
|
async _handleContainer(container) {
|
package/index.ts
CHANGED
@@ -16,6 +16,7 @@ import FilesystemRoutes from './src/filesystem/routes';
|
|
16
16
|
import AssetsRoutes from './src/assets/routes';
|
17
17
|
import ProviderRoutes from './src/providers/routes';
|
18
18
|
import AttachmentRoutes from './src/attachments/routes';
|
19
|
+
import TaskRoutes from './src/tasks/routes';
|
19
20
|
import { getBindHost } from './src/utils/utils';
|
20
21
|
import request from 'request';
|
21
22
|
|
@@ -36,6 +37,7 @@ function createServer() {
|
|
36
37
|
app.use('/assets', AssetsRoutes);
|
37
38
|
app.use('/providers', ProviderRoutes);
|
38
39
|
app.use('/attachments', AttachmentRoutes);
|
40
|
+
app.use('/tasks', TaskRoutes);
|
39
41
|
app.get('/status', async (req, res) => {
|
40
42
|
res.send({
|
41
43
|
ok: true,
|
package/package.json
CHANGED
package/src/assetManager.ts
CHANGED
@@ -62,6 +62,10 @@ class AssetManager {
|
|
62
62
|
});
|
63
63
|
}
|
64
64
|
|
65
|
+
public clearCache() {
|
66
|
+
this.cache.flushAll();
|
67
|
+
}
|
68
|
+
|
65
69
|
/**
|
66
70
|
*
|
67
71
|
* @param {string[]} [assetKinds]
|
@@ -98,23 +102,32 @@ class AssetManager {
|
|
98
102
|
return asset.data;
|
99
103
|
}
|
100
104
|
|
101
|
-
async getAsset(
|
105
|
+
async getAsset(
|
106
|
+
ref: string,
|
107
|
+
noCache: boolean = false,
|
108
|
+
autoFetch: boolean = true
|
109
|
+
): Promise<EnrichedAsset | undefined> {
|
102
110
|
ref = normalizeKapetaUri(ref);
|
103
111
|
const cacheKey = `getAsset:${ref}`;
|
104
112
|
if (!noCache && this.cache.has(cacheKey)) {
|
105
113
|
return this.cache.get(cacheKey);
|
106
114
|
}
|
107
115
|
const uri = parseKapetaUri(ref);
|
108
|
-
|
116
|
+
if (autoFetch) {
|
117
|
+
await repositoryManager.ensureAsset(uri.handle, uri.name, uri.version, true);
|
118
|
+
}
|
109
119
|
|
110
120
|
let asset = definitionsManager
|
111
121
|
.getDefinitions()
|
112
122
|
.map(enrichAsset)
|
113
123
|
.find((a) => parseKapetaUri(a.ref).equals(uri));
|
114
|
-
if (!asset) {
|
124
|
+
if (autoFetch && !asset) {
|
115
125
|
throw new Error('Asset not found: ' + ref);
|
116
126
|
}
|
117
|
-
|
127
|
+
if (asset) {
|
128
|
+
this.cache.set(cacheKey, asset);
|
129
|
+
}
|
130
|
+
|
118
131
|
return asset;
|
119
132
|
}
|
120
133
|
|
@@ -189,6 +202,17 @@ class AssetManager {
|
|
189
202
|
this.cache.flushAll();
|
190
203
|
await Actions.uninstall(progressListener, [asset.ref]);
|
191
204
|
}
|
205
|
+
|
206
|
+
async installAsset(ref: string) {
|
207
|
+
const asset = await this.getAsset(ref, true, false);
|
208
|
+
if (asset) {
|
209
|
+
throw new Error('Asset already installed: ' + ref);
|
210
|
+
}
|
211
|
+
const uri = parseKapetaUri(ref);
|
212
|
+
console.log('Installing %s', ref);
|
213
|
+
|
214
|
+
return await repositoryManager.ensureAsset(uri.handle, uri.name, uri.version, false);
|
215
|
+
}
|
192
216
|
}
|
193
217
|
|
194
218
|
export const assetManager = new AssetManager();
|
package/src/assets/routes.ts
CHANGED
@@ -44,8 +44,15 @@ router.get('/read', async (req: Request, res: Response) => {
|
|
44
44
|
return;
|
45
45
|
}
|
46
46
|
|
47
|
+
const ensure = req.query.ensure !== 'false';
|
48
|
+
|
47
49
|
try {
|
48
|
-
|
50
|
+
const asset = await assetManager.getAsset(req.query.ref as string, true, ensure);
|
51
|
+
if (asset) {
|
52
|
+
res.send(asset);
|
53
|
+
} else {
|
54
|
+
res.status(404).send({ error: 'Asset not found' });
|
55
|
+
}
|
49
56
|
} catch (err: any) {
|
50
57
|
res.status(400).send({ error: err.message });
|
51
58
|
}
|
@@ -129,4 +136,19 @@ router.put('/import', async (req: Request, res: Response) => {
|
|
129
136
|
}
|
130
137
|
});
|
131
138
|
|
139
|
+
router.put('/install', async (req: Request, res: Response) => {
|
140
|
+
if (!req.query.ref) {
|
141
|
+
res.status(400).send({ error: 'Query parameter "ref" is missing' });
|
142
|
+
return;
|
143
|
+
}
|
144
|
+
|
145
|
+
try {
|
146
|
+
const tasks = await assetManager.installAsset(req.query.ref as string);
|
147
|
+
const taskIds = tasks?.map((t) => t.id) ?? [];
|
148
|
+
res.status(200).send(taskIds);
|
149
|
+
} catch (err: any) {
|
150
|
+
res.status(400).send({ error: err.message });
|
151
|
+
}
|
152
|
+
});
|
153
|
+
|
132
154
|
export default router;
|
package/src/containerManager.ts
CHANGED
@@ -15,6 +15,7 @@ import { socketManager } from './socketManager';
|
|
15
15
|
import { handlers as ArtifactHandlers } from '@kapeta/nodejs-registry-utils';
|
16
16
|
import { progressListener } from './progressListener';
|
17
17
|
import { KapetaAPI } from '@kapeta/nodejs-api-client';
|
18
|
+
import { taskManager, Task } from './taskManager';
|
18
19
|
|
19
20
|
const EVENT_IMAGE_PULL = 'docker-image-pull';
|
20
21
|
|
@@ -66,8 +67,6 @@ export const CONTAINER_LABEL_PORT_PREFIX = 'kapeta_port-';
|
|
66
67
|
const NANO_SECOND = 1000000;
|
67
68
|
const HEALTH_CHECK_INTERVAL = 3000;
|
68
69
|
const HEALTH_CHECK_MAX = 20;
|
69
|
-
const IMAGE_PULL_CACHE_TTL = 30 * 60 * 1000;
|
70
|
-
const IMAGE_PULL_CACHE: { [key: string]: number } = {};
|
71
70
|
|
72
71
|
export const HEALTH_CHECK_TIMEOUT = HEALTH_CHECK_INTERVAL * HEALTH_CHECK_MAX * 2;
|
73
72
|
|
@@ -223,19 +222,12 @@ class ContainerManager {
|
|
223
222
|
return undefined;
|
224
223
|
}
|
225
224
|
|
226
|
-
async pull(image: string
|
225
|
+
async pull(image: string) {
|
227
226
|
let [imageName, tag] = image.split(/:/);
|
228
227
|
if (!tag) {
|
229
228
|
tag = 'latest';
|
230
229
|
}
|
231
230
|
|
232
|
-
if (IMAGE_PULL_CACHE[image]) {
|
233
|
-
const timeSince = Date.now() - IMAGE_PULL_CACHE[image];
|
234
|
-
if (timeSince < cacheForMS) {
|
235
|
-
return false;
|
236
|
-
}
|
237
|
-
}
|
238
|
-
|
239
231
|
const imageTagList = (await this.docker().image.list())
|
240
232
|
.map((image) => image.data as any)
|
241
233
|
.filter((imageData) => !!imageData.RepoTags)
|
@@ -246,153 +238,176 @@ class ContainerManager {
|
|
246
238
|
return false;
|
247
239
|
}
|
248
240
|
|
249
|
-
|
250
|
-
|
241
|
+
let friendlyImageName = image;
|
242
|
+
const imageParts = imageName.split('/');
|
243
|
+
if (imageParts.length > 2) {
|
244
|
+
//Strip the registry to make the name shorter
|
245
|
+
friendlyImageName = `${imageParts.slice(1).join('/')}:${tag}`;
|
246
|
+
}
|
251
247
|
|
252
|
-
const
|
253
|
-
|
248
|
+
const taskName = `Pulling image ${friendlyImageName}`;
|
249
|
+
|
250
|
+
const processor = async (task: Task) => {
|
251
|
+
const timeStarted = Date.now();
|
252
|
+
const api = new KapetaAPI();
|
253
|
+
const accessToken = await api.getAccessToken();
|
254
|
+
|
255
|
+
const auth = image.startsWith('docker.kapeta.com/')
|
256
|
+
? {
|
257
|
+
username: 'kapeta',
|
258
|
+
password: accessToken,
|
259
|
+
serveraddress: 'docker.kapeta.com',
|
260
|
+
}
|
261
|
+
: {};
|
262
|
+
|
263
|
+
const stream = (await this.docker().image.create(auth, {
|
264
|
+
fromImage: imageName,
|
265
|
+
tag: tag,
|
266
|
+
})) as ReadStream;
|
267
|
+
|
268
|
+
const chunks: {
|
269
|
+
[p: string]: {
|
270
|
+
downloading: {
|
271
|
+
total: number;
|
272
|
+
current: number;
|
273
|
+
};
|
274
|
+
extracting: {
|
275
|
+
total: number;
|
276
|
+
current: number;
|
277
|
+
};
|
278
|
+
done: boolean;
|
279
|
+
};
|
280
|
+
} = {};
|
281
|
+
|
282
|
+
let lastEmitted = Date.now();
|
283
|
+
await promisifyStream(stream, (rawData) => {
|
284
|
+
const lines = rawData.toString().trim().split('\n');
|
285
|
+
lines.forEach((line) => {
|
286
|
+
const data = JSON.parse(line);
|
287
|
+
if (
|
288
|
+
![
|
289
|
+
'Waiting',
|
290
|
+
'Downloading',
|
291
|
+
'Extracting',
|
292
|
+
'Download complete',
|
293
|
+
'Pull complete',
|
294
|
+
'Already exists',
|
295
|
+
].includes(data.status)
|
296
|
+
) {
|
297
|
+
return;
|
298
|
+
}
|
254
299
|
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
300
|
+
if (!chunks[data.id]) {
|
301
|
+
chunks[data.id] = {
|
302
|
+
downloading: {
|
303
|
+
total: 0,
|
304
|
+
current: 0,
|
305
|
+
},
|
306
|
+
extracting: {
|
307
|
+
total: 0,
|
308
|
+
current: 0,
|
309
|
+
},
|
310
|
+
done: false,
|
311
|
+
};
|
312
|
+
}
|
262
313
|
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
314
|
+
const chunk = chunks[data.id];
|
315
|
+
|
316
|
+
switch (data.status) {
|
317
|
+
case 'Downloading':
|
318
|
+
chunk.downloading = data.progressDetail;
|
319
|
+
break;
|
320
|
+
case 'Extracting':
|
321
|
+
chunk.extracting = data.progressDetail;
|
322
|
+
break;
|
323
|
+
case 'Download complete':
|
324
|
+
chunk.downloading.current = chunks[data.id].downloading.total;
|
325
|
+
break;
|
326
|
+
case 'Pull complete':
|
327
|
+
chunk.extracting.current = chunks[data.id].extracting.total;
|
328
|
+
chunk.done = true;
|
329
|
+
break;
|
330
|
+
case 'Already exists':
|
331
|
+
// Force layer to be done
|
332
|
+
chunk.downloading.current = 1;
|
333
|
+
chunk.downloading.total = 1;
|
334
|
+
chunk.extracting.current = 1;
|
335
|
+
chunk.extracting.total = 1;
|
336
|
+
chunk.done = true;
|
337
|
+
break;
|
338
|
+
}
|
339
|
+
});
|
267
340
|
|
268
|
-
|
269
|
-
[p: string]: {
|
270
|
-
downloading: {
|
271
|
-
total: number;
|
272
|
-
current: number;
|
273
|
-
};
|
274
|
-
extracting: {
|
275
|
-
total: number;
|
276
|
-
current: number;
|
277
|
-
};
|
278
|
-
done: boolean;
|
279
|
-
};
|
280
|
-
} = {};
|
281
|
-
|
282
|
-
let lastEmitted = Date.now();
|
283
|
-
await promisifyStream(stream, (rawData) => {
|
284
|
-
const lines = rawData.toString().trim().split('\n');
|
285
|
-
lines.forEach((line) => {
|
286
|
-
const data = JSON.parse(line);
|
287
|
-
if (
|
288
|
-
![
|
289
|
-
'Waiting',
|
290
|
-
'Downloading',
|
291
|
-
'Extracting',
|
292
|
-
'Download complete',
|
293
|
-
'Pull complete',
|
294
|
-
'Already exists',
|
295
|
-
].includes(data.status)
|
296
|
-
) {
|
341
|
+
if (Date.now() - lastEmitted < 1000) {
|
297
342
|
return;
|
298
343
|
}
|
299
344
|
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
}
|
313
|
-
|
314
|
-
const chunk = chunks[data.id];
|
315
|
-
|
316
|
-
switch (data.status) {
|
317
|
-
case 'Downloading':
|
318
|
-
chunk.downloading = data.progressDetail;
|
319
|
-
break;
|
320
|
-
case 'Extracting':
|
321
|
-
chunk.extracting = data.progressDetail;
|
322
|
-
break;
|
323
|
-
case 'Download complete':
|
324
|
-
chunk.downloading.current = chunks[data.id].downloading.total;
|
325
|
-
break;
|
326
|
-
case 'Pull complete':
|
327
|
-
chunk.extracting.current = chunks[data.id].extracting.total;
|
328
|
-
chunk.done = true;
|
329
|
-
break;
|
330
|
-
case 'Already exists':
|
331
|
-
// Force layer to be done
|
332
|
-
chunk.downloading.current = 1;
|
333
|
-
chunk.downloading.total = 1;
|
334
|
-
chunk.extracting.current = 1;
|
335
|
-
chunk.extracting.total = 1;
|
336
|
-
chunk.done = true;
|
337
|
-
break;
|
338
|
-
}
|
339
|
-
});
|
345
|
+
const chunkList = Object.values(chunks);
|
346
|
+
let totals = {
|
347
|
+
downloading: {
|
348
|
+
total: 0,
|
349
|
+
current: 0,
|
350
|
+
},
|
351
|
+
extracting: {
|
352
|
+
total: 0,
|
353
|
+
current: 0,
|
354
|
+
},
|
355
|
+
total: chunkList.length,
|
356
|
+
done: 0,
|
357
|
+
};
|
340
358
|
|
341
|
-
|
342
|
-
|
343
|
-
|
359
|
+
chunkList.forEach((chunk) => {
|
360
|
+
if (chunk.downloading.current > 0) {
|
361
|
+
totals.downloading.current += chunk.downloading.current;
|
362
|
+
}
|
344
363
|
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
total: 0,
|
349
|
-
current: 0,
|
350
|
-
},
|
351
|
-
extracting: {
|
352
|
-
total: 0,
|
353
|
-
current: 0,
|
354
|
-
},
|
355
|
-
total: chunkList.length,
|
356
|
-
done: 0,
|
357
|
-
};
|
364
|
+
if (chunk.downloading.total > 0) {
|
365
|
+
totals.downloading.total += chunk.downloading.total;
|
366
|
+
}
|
358
367
|
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
}
|
368
|
+
if (chunk.extracting.current > 0) {
|
369
|
+
totals.extracting.current += chunk.extracting.current;
|
370
|
+
}
|
363
371
|
|
364
|
-
|
365
|
-
|
366
|
-
|
372
|
+
if (chunk.extracting.total > 0) {
|
373
|
+
totals.extracting.total += chunk.extracting.total;
|
374
|
+
}
|
367
375
|
|
368
|
-
|
369
|
-
|
370
|
-
|
376
|
+
if (chunk.done) {
|
377
|
+
totals.done++;
|
378
|
+
}
|
379
|
+
});
|
371
380
|
|
372
|
-
|
373
|
-
totals.extracting.total += chunk.extracting.total;
|
374
|
-
}
|
381
|
+
const progress = totals.total > 0 ? (totals.done / totals.total) * 100 : 0;
|
375
382
|
|
376
|
-
|
377
|
-
|
378
|
-
|
383
|
+
task.metadata = {
|
384
|
+
...task.metadata,
|
385
|
+
image,
|
386
|
+
progress,
|
387
|
+
status: totals,
|
388
|
+
timeTaken: Date.now() - timeStarted,
|
389
|
+
};
|
390
|
+
task.emitUpdate();
|
391
|
+
lastEmitted = Date.now();
|
392
|
+
//console.log('Pulling image %s: %s % [done: %s, total: %s]', image, Math.round(percent), totals.done, totals.total);
|
379
393
|
});
|
380
394
|
|
381
|
-
|
382
|
-
|
383
|
-
socketManager.emitGlobal(EVENT_IMAGE_PULL, {
|
395
|
+
task.metadata = {
|
396
|
+
...task.metadata,
|
384
397
|
image,
|
385
|
-
|
386
|
-
status: totals,
|
398
|
+
progress: 100,
|
387
399
|
timeTaken: Date.now() - timeStarted,
|
388
|
-
}
|
389
|
-
|
390
|
-
|
391
|
-
});
|
400
|
+
};
|
401
|
+
task.emitUpdate();
|
402
|
+
};
|
392
403
|
|
393
|
-
|
404
|
+
const task = taskManager.add(`docker:image:pull:${image}`, processor, {
|
405
|
+
name: taskName,
|
406
|
+
image,
|
407
|
+
progress: -1,
|
408
|
+
});
|
394
409
|
|
395
|
-
|
410
|
+
await task.wait();
|
396
411
|
|
397
412
|
return true;
|
398
413
|
}
|
@@ -435,11 +450,7 @@ class ContainerManager {
|
|
435
450
|
}
|
436
451
|
|
437
452
|
public async ensureContainer(opts: any) {
|
438
|
-
|
439
|
-
|
440
|
-
await this.waitForReady(container);
|
441
|
-
|
442
|
-
return container;
|
453
|
+
return await this.createOrUpdateContainer(opts);
|
443
454
|
}
|
444
455
|
|
445
456
|
private async createOrUpdateContainer(opts: any) {
|