@kapeta/local-cluster-service 0.12.1 → 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 (51) hide show
  1. package/CHANGELOG.md +7 -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.js +130 -109
  7. package/dist/cjs/src/instanceManager.d.ts +4 -3
  8. package/dist/cjs/src/instanceManager.js +80 -59
  9. package/dist/cjs/src/instances/routes.js +19 -11
  10. package/dist/cjs/src/operatorManager.d.ts +5 -3
  11. package/dist/cjs/src/operatorManager.js +34 -23
  12. package/dist/cjs/src/providerManager.js +1 -1
  13. package/dist/cjs/src/repositoryManager.d.ts +2 -4
  14. package/dist/cjs/src/repositoryManager.js +51 -66
  15. package/dist/cjs/src/socketManager.js +1 -1
  16. package/dist/cjs/src/taskManager.d.ts +64 -0
  17. package/dist/cjs/src/taskManager.js +163 -0
  18. package/dist/cjs/src/tasks/routes.d.ts +3 -0
  19. package/dist/cjs/src/tasks/routes.js +35 -0
  20. package/dist/esm/index.js +2 -0
  21. package/dist/esm/src/assetManager.d.ts +3 -1
  22. package/dist/esm/src/assetManager.js +20 -4
  23. package/dist/esm/src/assets/routes.js +22 -1
  24. package/dist/esm/src/containerManager.js +130 -109
  25. package/dist/esm/src/instanceManager.d.ts +4 -3
  26. package/dist/esm/src/instanceManager.js +80 -59
  27. package/dist/esm/src/instances/routes.js +19 -11
  28. package/dist/esm/src/operatorManager.d.ts +5 -3
  29. package/dist/esm/src/operatorManager.js +34 -23
  30. package/dist/esm/src/providerManager.js +1 -1
  31. package/dist/esm/src/repositoryManager.d.ts +2 -4
  32. package/dist/esm/src/repositoryManager.js +51 -66
  33. package/dist/esm/src/socketManager.js +1 -1
  34. package/dist/esm/src/taskManager.d.ts +64 -0
  35. package/dist/esm/src/taskManager.js +159 -0
  36. package/dist/esm/src/tasks/routes.d.ts +3 -0
  37. package/dist/esm/src/tasks/routes.js +30 -0
  38. package/index.ts +2 -0
  39. package/package.json +1 -1
  40. package/src/assetManager.ts +28 -4
  41. package/src/assets/routes.ts +23 -1
  42. package/src/containerManager.ts +151 -126
  43. package/src/instanceManager.ts +106 -70
  44. package/src/instances/routes.ts +18 -12
  45. package/src/operatorManager.ts +46 -28
  46. package/src/providerManager.ts +1 -1
  47. package/src/repositoryManager.ts +65 -63
  48. package/src/socketManager.ts +1 -1
  49. package/src/taskManager.ts +225 -0
  50. package/src/tasks/routes.ts +38 -0
  51. package/src/utils/BlockInstanceRunner.ts +0 -2
@@ -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
 
@@ -67,7 +68,6 @@ const NANO_SECOND = 1000000;
67
68
  const HEALTH_CHECK_INTERVAL = 3000;
68
69
  const HEALTH_CHECK_MAX = 20;
69
70
 
70
-
71
71
  export const HEALTH_CHECK_TIMEOUT = HEALTH_CHECK_INTERVAL * HEALTH_CHECK_MAX * 2;
72
72
 
73
73
  const promisifyStream = (stream: ReadStream, handler: (d: string | Buffer) => void) =>
@@ -238,151 +238,176 @@ class ContainerManager {
238
238
  return false;
239
239
  }
240
240
 
241
- const timeStarted = Date.now();
242
- socketManager.emitGlobal(EVENT_IMAGE_PULL, { image, percent: -1 });
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
+ }
243
247
 
244
- const api = new KapetaAPI();
245
- 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
+ }
246
299
 
247
- const auth = image.startsWith('docker.kapeta.com/')
248
- ? {
249
- username: 'kapeta',
250
- password: accessToken,
251
- serveraddress: 'docker.kapeta.com',
252
- }
253
- : {};
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
+ }
254
313
 
255
- const stream = (await this.docker().image.create(auth, {
256
- fromImage: imageName,
257
- tag: tag,
258
- })) 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
+ });
259
340
 
260
- const chunks: {
261
- [p: string]: {
262
- downloading: {
263
- total: number;
264
- current: number;
265
- };
266
- extracting: {
267
- total: number;
268
- current: number;
269
- };
270
- done: boolean;
271
- };
272
- } = {};
273
-
274
- let lastEmitted = Date.now();
275
- await promisifyStream(stream, (rawData) => {
276
- const lines = rawData.toString().trim().split('\n');
277
- lines.forEach((line) => {
278
- const data = JSON.parse(line);
279
- if (
280
- ![
281
- 'Waiting',
282
- 'Downloading',
283
- 'Extracting',
284
- 'Download complete',
285
- 'Pull complete',
286
- 'Already exists',
287
- ].includes(data.status)
288
- ) {
341
+ if (Date.now() - lastEmitted < 1000) {
289
342
  return;
290
343
  }
291
344
 
292
- if (!chunks[data.id]) {
293
- chunks[data.id] = {
294
- downloading: {
295
- total: 0,
296
- current: 0,
297
- },
298
- extracting: {
299
- total: 0,
300
- current: 0,
301
- },
302
- done: false,
303
- };
304
- }
305
-
306
- const chunk = chunks[data.id];
307
-
308
- switch (data.status) {
309
- case 'Downloading':
310
- chunk.downloading = data.progressDetail;
311
- break;
312
- case 'Extracting':
313
- chunk.extracting = data.progressDetail;
314
- break;
315
- case 'Download complete':
316
- chunk.downloading.current = chunks[data.id].downloading.total;
317
- break;
318
- case 'Pull complete':
319
- chunk.extracting.current = chunks[data.id].extracting.total;
320
- chunk.done = true;
321
- break;
322
- case 'Already exists':
323
- // Force layer to be done
324
- chunk.downloading.current = 1;
325
- chunk.downloading.total = 1;
326
- chunk.extracting.current = 1;
327
- chunk.extracting.total = 1;
328
- chunk.done = true;
329
- break;
330
- }
331
- });
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
+ };
332
358
 
333
- if (Date.now() - lastEmitted < 1000) {
334
- return;
335
- }
359
+ chunkList.forEach((chunk) => {
360
+ if (chunk.downloading.current > 0) {
361
+ totals.downloading.current += chunk.downloading.current;
362
+ }
336
363
 
337
- const chunkList = Object.values(chunks);
338
- let totals = {
339
- downloading: {
340
- total: 0,
341
- current: 0,
342
- },
343
- extracting: {
344
- total: 0,
345
- current: 0,
346
- },
347
- total: chunkList.length,
348
- done: 0,
349
- };
364
+ if (chunk.downloading.total > 0) {
365
+ totals.downloading.total += chunk.downloading.total;
366
+ }
350
367
 
351
- chunkList.forEach((chunk) => {
352
- if (chunk.downloading.current > 0) {
353
- totals.downloading.current += chunk.downloading.current;
354
- }
368
+ if (chunk.extracting.current > 0) {
369
+ totals.extracting.current += chunk.extracting.current;
370
+ }
355
371
 
356
- if (chunk.downloading.total > 0) {
357
- totals.downloading.total += chunk.downloading.total;
358
- }
372
+ if (chunk.extracting.total > 0) {
373
+ totals.extracting.total += chunk.extracting.total;
374
+ }
359
375
 
360
- if (chunk.extracting.current > 0) {
361
- totals.extracting.current += chunk.extracting.current;
362
- }
376
+ if (chunk.done) {
377
+ totals.done++;
378
+ }
379
+ });
363
380
 
364
- if (chunk.extracting.total > 0) {
365
- totals.extracting.total += chunk.extracting.total;
366
- }
381
+ const progress = totals.total > 0 ? (totals.done / totals.total) * 100 : 0;
367
382
 
368
- if (chunk.done) {
369
- totals.done++;
370
- }
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);
371
393
  });
372
394
 
373
- const percent = totals.total > 0 ? (totals.done / totals.total) * 100 : 0;
374
- //We emit at most every second to not spam the client
375
- socketManager.emitGlobal(EVENT_IMAGE_PULL, {
395
+ task.metadata = {
396
+ ...task.metadata,
376
397
  image,
377
- percent,
378
- status: totals,
398
+ progress: 100,
379
399
  timeTaken: Date.now() - timeStarted,
380
- });
381
- lastEmitted = Date.now();
382
- //console.log('Pulling image %s: %s % [done: %s, total: %s]', image, Math.round(percent), totals.done, totals.total);
400
+ };
401
+ task.emitUpdate();
402
+ };
403
+
404
+ const task = taskManager.add(`docker:image:pull:${image}`, processor, {
405
+ name: taskName,
406
+ image,
407
+ progress: -1,
383
408
  });
384
409
 
385
- socketManager.emitGlobal(EVENT_IMAGE_PULL, { image, percent: 100, timeTaken: Date.now() - timeStarted });
410
+ await task.wait();
386
411
 
387
412
  return true;
388
413
  }
@@ -9,11 +9,12 @@ import { assetManager } from './assetManager';
9
9
  import { containerManager, HEALTH_CHECK_TIMEOUT } from './containerManager';
10
10
  import { configManager } from './configManager';
11
11
  import { DesiredInstanceStatus, InstanceInfo, InstanceOwner, InstanceStatus, InstanceType, LogEntry } from './types';
12
- import {BlockDefinitionSpec, BlockInstance, Plan} from '@kapeta/schemas';
12
+ import { BlockDefinitionSpec, BlockInstance, Plan } from '@kapeta/schemas';
13
13
  import { getBlockInstanceContainerName, normalizeKapetaUri } from './utils/utils';
14
14
  import { KIND_OPERATOR, operatorManager } from './operatorManager';
15
15
  import { parseKapetaUri } from '@kapeta/nodejs-utils';
16
16
  import { definitionsManager } from './definitionsManager';
17
+ import { Task, taskManager } from './taskManager';
17
18
 
18
19
  const CHECK_INTERVAL = 5000;
19
20
  const DEFAULT_HEALTH_PORT_TYPE = 'rest';
@@ -74,7 +75,9 @@ export class InstanceManager {
74
75
 
75
76
  const instanceIds = plan.spec.blocks.map((block) => block.id);
76
77
 
77
- return this._instances.filter((instance) => instance.systemId === systemId && instanceIds.includes(instance.instanceId));
78
+ return this._instances.filter(
79
+ (instance) => instance.systemId === systemId && instanceIds.includes(instance.instanceId)
80
+ );
78
81
  }
79
82
 
80
83
  public getInstance(systemId: string, instanceId: string) {
@@ -271,35 +274,51 @@ export class InstanceManager {
271
274
  });
272
275
  }
273
276
 
274
- public async startAllForPlan(systemId: string): Promise<InstanceInfo[]> {
277
+ public async startAllForPlan(systemId: string): Promise<Task<InstanceInfo[]>> {
275
278
  systemId = normalizeKapetaUri(systemId);
276
279
  const plan = await assetManager.getPlan(systemId, true);
277
280
  if (!plan) {
278
- throw new Error('Plan not found: ' + systemId);
281
+ throw new Error(`Plan not found: ${systemId}`);
279
282
  }
280
283
 
281
284
  if (!plan.spec.blocks) {
282
- console.warn('No blocks found in plan', systemId);
283
- return [];
285
+ throw new Error(`No blocks found in plan: ${systemId}`);
284
286
  }
285
287
 
286
- let promises: Promise<InstanceInfo>[] = [];
287
- let errors = [];
288
- for (let blockInstance of Object.values(plan.spec.blocks as BlockInstance[])) {
289
- try {
290
- promises.push(this.start(systemId, blockInstance.id));
291
- } catch (e) {
292
- errors.push(e);
293
- }
294
- }
288
+ return taskManager.add(
289
+ `plan:start:${systemId}`,
290
+ async () => {
291
+ let promises: Promise<InstanceInfo>[] = [];
292
+ let errors = [];
293
+ for (let blockInstance of Object.values(plan.spec.blocks as BlockInstance[])) {
294
+ try {
295
+ promises.push(
296
+ this.start(systemId, blockInstance.id).then((taskOrInstance) => {
297
+ if (taskOrInstance instanceof Task) {
298
+ return taskOrInstance.wait();
299
+ }
300
+ return taskOrInstance;
301
+ })
302
+ );
303
+ } catch (e) {
304
+ errors.push(e);
305
+ }
306
+ }
295
307
 
296
- const settled = await Promise.allSettled(promises);
308
+ const settled = await Promise.allSettled(promises);
297
309
 
298
- if (errors.length > 0) {
299
- throw errors[0];
300
- }
310
+ if (errors.length > 0) {
311
+ throw errors[0];
312
+ }
301
313
 
302
- return settled.map((p) => (p.status === 'fulfilled' ? p.value : null)).filter((p) => !!p) as InstanceInfo[];
314
+ return settled
315
+ .map((p) => (p.status === 'fulfilled' ? p.value : null))
316
+ .filter((p) => !!p) as InstanceInfo[];
317
+ },
318
+ {
319
+ name: `Starting plan ${systemId}`,
320
+ }
321
+ );
303
322
  }
304
323
 
305
324
  public async stop(systemId: string, instanceId: string) {
@@ -363,14 +382,21 @@ export class InstanceManager {
363
382
  });
364
383
  }
365
384
 
366
- public async stopAllForPlan(systemId: string) {
385
+ public stopAllForPlan(systemId: string) {
367
386
  systemId = normalizeKapetaUri(systemId);
368
387
  const instancesForPlan = this._instances.filter((instance) => instance.systemId === systemId);
369
-
370
- return this.stopInstances(instancesForPlan);
388
+ return taskManager.add(
389
+ `plan:stop:${systemId}`,
390
+ async () => {
391
+ return this.stopInstances(instancesForPlan);
392
+ },
393
+ {
394
+ name: `Stopping plan ${systemId}`,
395
+ }
396
+ );
371
397
  }
372
398
 
373
- public async start(systemId: string, instanceId: string): Promise<InstanceInfo> {
399
+ public async start(systemId: string, instanceId: string): Promise<InstanceInfo | Task<InstanceInfo>> {
374
400
  return this.exclusive(systemId, instanceId, async () => {
375
401
  systemId = normalizeKapetaUri(systemId);
376
402
  const plan = await assetManager.getPlan(systemId, true);
@@ -463,53 +489,63 @@ export class InstanceManager {
463
489
  }
464
490
 
465
491
  const instanceConfig = await configManager.getConfigForSection(systemId, instanceId);
466
- const runner = new BlockInstanceRunner(systemId);
467
-
468
- const startTime = Date.now();
469
- try {
470
- const processInfo = await runner.start(blockRef, instanceId, instanceConfig);
471
-
472
- instance.status = InstanceStatus.READY;
473
-
474
- return this.saveInternalInstance({
475
- ...instance,
476
- type: processInfo.type,
477
- pid: processInfo.pid ?? -1,
478
- health: null,
479
- portType: processInfo.portType,
480
- status: InstanceStatus.READY,
481
- });
482
- } catch (e: any) {
483
- console.warn('Failed to start instance: ', systemId, instanceId, blockRef, e.message);
484
- const logs: LogEntry[] = [
485
- {
486
- source: 'stdout',
487
- level: 'ERROR',
488
- message: e.message,
489
- time: Date.now(),
490
- },
491
- ];
492
-
493
- const out = await this.saveInternalInstance({
494
- ...instance,
495
- type: InstanceType.UNKNOWN,
496
- pid: null,
497
- health: null,
498
- portType: DEFAULT_HEALTH_PORT_TYPE,
499
- status: InstanceStatus.FAILED,
500
- errorMessage: e.message ?? 'Failed to start - Check logs for details.',
501
- });
502
-
503
- this.emitInstanceEvent(systemId, instanceId, EVENT_INSTANCE_LOG, logs[0]);
504
-
505
- this.emitInstanceEvent(systemId, blockInstance.id, EVENT_INSTANCE_EXITED, {
506
- error: `Failed to start instance: ${e.message}`,
507
- status: EVENT_INSTANCE_EXITED,
508
- instanceId: blockInstance.id,
509
- });
492
+ const task = taskManager.add(
493
+ `instance:start:${systemId}:${instanceId}`,
494
+ async () => {
495
+ const runner = new BlockInstanceRunner(systemId);
496
+ const startTime = Date.now();
497
+ try {
498
+ const processInfo = await runner.start(blockRef, instanceId, instanceConfig);
499
+
500
+ instance.status = InstanceStatus.READY;
501
+
502
+ return this.saveInternalInstance({
503
+ ...instance,
504
+ type: processInfo.type,
505
+ pid: processInfo.pid ?? -1,
506
+ health: null,
507
+ portType: processInfo.portType,
508
+ status: InstanceStatus.READY,
509
+ });
510
+ } catch (e: any) {
511
+ console.warn('Failed to start instance: ', systemId, instanceId, blockRef, e.message);
512
+ const logs: LogEntry[] = [
513
+ {
514
+ source: 'stdout',
515
+ level: 'ERROR',
516
+ message: e.message,
517
+ time: Date.now(),
518
+ },
519
+ ];
520
+
521
+ const out = await this.saveInternalInstance({
522
+ ...instance,
523
+ type: InstanceType.UNKNOWN,
524
+ pid: null,
525
+ health: null,
526
+ portType: DEFAULT_HEALTH_PORT_TYPE,
527
+ status: InstanceStatus.FAILED,
528
+ errorMessage: e.message ?? 'Failed to start - Check logs for details.',
529
+ });
530
+
531
+ this.emitInstanceEvent(systemId, instanceId, EVENT_INSTANCE_LOG, logs[0]);
532
+
533
+ this.emitInstanceEvent(systemId, blockInstance.id, EVENT_INSTANCE_EXITED, {
534
+ error: `Failed to start instance: ${e.message}`,
535
+ status: EVENT_INSTANCE_EXITED,
536
+ instanceId: blockInstance.id,
537
+ });
538
+
539
+ return out;
540
+ }
541
+ },
542
+ {
543
+ name: `Starting instance: ${instance.name}`,
544
+ systemId,
545
+ }
546
+ );
510
547
 
511
- return out;
512
- }
548
+ return task;
513
549
  });
514
550
  }
515
551
 
@@ -6,6 +6,7 @@ import { NextFunction, Request, Response } from 'express';
6
6
  import { kapetaHeaders, KapetaRequest } from '../middleware/kapeta';
7
7
  import { stringBody } from '../middleware/stringBody';
8
8
  import { DesiredInstanceStatus, InstanceInfo, InstanceOwner, InstanceType, KapetaBodyRequest } from '../types';
9
+ import { Task } from '../taskManager';
9
10
 
10
11
  const router = Router();
11
12
  router.use('/', corsHandler);
@@ -28,13 +29,11 @@ router.get('/:systemId/instances', (req: Request, res: Response) => {
28
29
  * Start all instances in a plan
29
30
  */
30
31
  router.post('/:systemId/start', async (req: Request, res: Response) => {
31
- const instances = await instanceManager.startAllForPlan(req.params.systemId);
32
+ const task = await instanceManager.startAllForPlan(req.params.systemId);
32
33
 
33
34
  res.status(202).send({
34
35
  ok: true,
35
- processes: instances.map((p) => {
36
- return { pid: p.pid, type: p.type };
37
- }),
36
+ taskId: task.id,
38
37
  });
39
38
  });
40
39
 
@@ -42,10 +41,11 @@ router.post('/:systemId/start', async (req: Request, res: Response) => {
42
41
  * Stop all instances in plan
43
42
  */
44
43
  router.post('/:systemId/stop', async (req: Request, res: Response) => {
45
- await instanceManager.stopAllForPlan(req.params.systemId);
44
+ const task = instanceManager.stopAllForPlan(req.params.systemId);
46
45
 
47
46
  res.status(202).send({
48
47
  ok: true,
48
+ taskId: task.id,
49
49
  });
50
50
  });
51
51
 
@@ -53,13 +53,19 @@ router.post('/:systemId/stop', async (req: Request, res: Response) => {
53
53
  * Start single instance in a plan
54
54
  */
55
55
  router.post('/:systemId/:instanceId/start', async (req: Request, res: Response) => {
56
- const process = await instanceManager.start(req.params.systemId, req.params.instanceId);
57
-
58
- res.status(202).send({
59
- ok: true,
60
- pid: process.pid,
61
- type: process.type,
62
- });
56
+ const result = await instanceManager.start(req.params.systemId, req.params.instanceId);
57
+ if (result instanceof Task) {
58
+ res.status(202).send({
59
+ ok: true,
60
+ taskId: result.id,
61
+ });
62
+ } else {
63
+ res.status(202).send({
64
+ ok: true,
65
+ pid: result.pid,
66
+ type: result.type,
67
+ });
68
+ }
63
69
  });
64
70
 
65
71
  /**