@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.
Files changed (55) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/cjs/index.js +2 -0
  3. package/dist/cjs/src/assetManager.d.ts +3 -1
  4. package/dist/cjs/src/assetManager.js +20 -4
  5. package/dist/cjs/src/assets/routes.js +22 -1
  6. package/dist/cjs/src/containerManager.d.ts +1 -1
  7. package/dist/cjs/src/containerManager.js +132 -122
  8. package/dist/cjs/src/instanceManager.d.ts +4 -3
  9. package/dist/cjs/src/instanceManager.js +87 -60
  10. package/dist/cjs/src/instances/routes.js +21 -11
  11. package/dist/cjs/src/operatorManager.d.ts +5 -3
  12. package/dist/cjs/src/operatorManager.js +34 -22
  13. package/dist/cjs/src/providerManager.js +1 -1
  14. package/dist/cjs/src/repositoryManager.d.ts +2 -4
  15. package/dist/cjs/src/repositoryManager.js +51 -66
  16. package/dist/cjs/src/socketManager.js +1 -1
  17. package/dist/cjs/src/taskManager.d.ts +64 -0
  18. package/dist/cjs/src/taskManager.js +163 -0
  19. package/dist/cjs/src/tasks/routes.d.ts +3 -0
  20. package/dist/cjs/src/tasks/routes.js +35 -0
  21. package/dist/cjs/src/utils/BlockInstanceRunner.js +0 -1
  22. package/dist/esm/index.js +2 -0
  23. package/dist/esm/src/assetManager.d.ts +3 -1
  24. package/dist/esm/src/assetManager.js +20 -4
  25. package/dist/esm/src/assets/routes.js +22 -1
  26. package/dist/esm/src/containerManager.d.ts +1 -1
  27. package/dist/esm/src/containerManager.js +132 -122
  28. package/dist/esm/src/instanceManager.d.ts +4 -3
  29. package/dist/esm/src/instanceManager.js +87 -60
  30. package/dist/esm/src/instances/routes.js +21 -11
  31. package/dist/esm/src/operatorManager.d.ts +5 -3
  32. package/dist/esm/src/operatorManager.js +34 -22
  33. package/dist/esm/src/providerManager.js +1 -1
  34. package/dist/esm/src/repositoryManager.d.ts +2 -4
  35. package/dist/esm/src/repositoryManager.js +51 -66
  36. package/dist/esm/src/socketManager.js +1 -1
  37. package/dist/esm/src/taskManager.d.ts +64 -0
  38. package/dist/esm/src/taskManager.js +159 -0
  39. package/dist/esm/src/tasks/routes.d.ts +3 -0
  40. package/dist/esm/src/tasks/routes.js +30 -0
  41. package/dist/esm/src/utils/BlockInstanceRunner.js +0 -1
  42. package/index.ts +2 -0
  43. package/package.json +1 -1
  44. package/src/assetManager.ts +28 -4
  45. package/src/assets/routes.ts +23 -1
  46. package/src/containerManager.ts +153 -142
  47. package/src/instanceManager.ts +116 -70
  48. package/src/instances/routes.ts +20 -12
  49. package/src/operatorManager.ts +46 -26
  50. package/src/providerManager.ts +1 -1
  51. package/src/repositoryManager.ts +65 -63
  52. package/src/socketManager.ts +1 -1
  53. package/src/taskManager.ts +225 -0
  54. package/src/tasks/routes.ts +38 -0
  55. 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,3 @@
1
+ /// <reference types="express" />
2
+ declare const router: import("express").Router;
3
+ export default router;
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kapeta/local-cluster-service",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Manages configuration, ports and service discovery for locally running Kapeta systems",
5
5
  "type": "commonjs",
6
6
  "exports": {
@@ -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(ref: string, noCache: boolean = false): Promise<EnrichedAsset | undefined> {
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
- await repositoryManager.ensureAsset(uri.handle, uri.name, uri.version);
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
- this.cache.set(cacheKey, asset);
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();
@@ -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
- res.send(await assetManager.getAsset(req.query.ref as string, true));
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;
@@ -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, cacheForMS: number = IMAGE_PULL_CACHE_TTL) {
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
- const timeStarted = Date.now();
250
- socketManager.emitGlobal(EVENT_IMAGE_PULL, { image, percent: 0 });
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 api = new KapetaAPI();
253
- const accessToken = await api.getAccessToken();
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
- const auth = image.startsWith('docker.kapeta.com/')
256
- ? {
257
- username: 'kapeta',
258
- password: accessToken,
259
- serveraddress: 'docker.kapeta.com',
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
- const stream = (await this.docker().image.create(auth, {
264
- fromImage: imageName,
265
- tag: tag,
266
- })) as ReadStream;
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
- 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
- ) {
341
+ if (Date.now() - lastEmitted < 1000) {
297
342
  return;
298
343
  }
299
344
 
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
- }
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
- if (Date.now() - lastEmitted < 1000) {
342
- return;
343
- }
359
+ chunkList.forEach((chunk) => {
360
+ if (chunk.downloading.current > 0) {
361
+ totals.downloading.current += chunk.downloading.current;
362
+ }
344
363
 
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
- };
364
+ if (chunk.downloading.total > 0) {
365
+ totals.downloading.total += chunk.downloading.total;
366
+ }
358
367
 
359
- chunkList.forEach((chunk) => {
360
- if (chunk.downloading.current > 0) {
361
- totals.downloading.current += chunk.downloading.current;
362
- }
368
+ if (chunk.extracting.current > 0) {
369
+ totals.extracting.current += chunk.extracting.current;
370
+ }
363
371
 
364
- if (chunk.downloading.total > 0) {
365
- totals.downloading.total += chunk.downloading.total;
366
- }
372
+ if (chunk.extracting.total > 0) {
373
+ totals.extracting.total += chunk.extracting.total;
374
+ }
367
375
 
368
- if (chunk.extracting.current > 0) {
369
- totals.extracting.current += chunk.extracting.current;
370
- }
376
+ if (chunk.done) {
377
+ totals.done++;
378
+ }
379
+ });
371
380
 
372
- if (chunk.extracting.total > 0) {
373
- totals.extracting.total += chunk.extracting.total;
374
- }
381
+ const progress = totals.total > 0 ? (totals.done / totals.total) * 100 : 0;
375
382
 
376
- if (chunk.done) {
377
- totals.done++;
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
- const percent = totals.total > 0 ? (totals.done / totals.total) * 100 : 0;
382
- //We emit at most every second to not spam the client
383
- socketManager.emitGlobal(EVENT_IMAGE_PULL, {
395
+ task.metadata = {
396
+ ...task.metadata,
384
397
  image,
385
- percent,
386
- status: totals,
398
+ progress: 100,
387
399
  timeTaken: Date.now() - timeStarted,
388
- });
389
- lastEmitted = Date.now();
390
- //console.log('Pulling image %s: %s % [done: %s, total: %s]', image, Math.round(percent), totals.done, totals.total);
391
- });
400
+ };
401
+ task.emitUpdate();
402
+ };
392
403
 
393
- IMAGE_PULL_CACHE[image] = Date.now();
404
+ const task = taskManager.add(`docker:image:pull:${image}`, processor, {
405
+ name: taskName,
406
+ image,
407
+ progress: -1,
408
+ });
394
409
 
395
- socketManager.emitGlobal(EVENT_IMAGE_PULL, { image, percent: 100, timeTaken: Date.now() - timeStarted });
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
- const container = await this.createOrUpdateContainer(opts);
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) {