@kapeta/local-cluster-service 0.12.1 → 0.14.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 (56) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/cjs/index.js +4 -0
  3. package/dist/cjs/src/api.d.ts +3 -0
  4. package/dist/cjs/src/api.js +22 -0
  5. package/dist/cjs/src/assetManager.d.ts +3 -1
  6. package/dist/cjs/src/assetManager.js +20 -4
  7. package/dist/cjs/src/assets/routes.js +23 -2
  8. package/dist/cjs/src/containerManager.js +130 -109
  9. package/dist/cjs/src/instanceManager.d.ts +4 -3
  10. package/dist/cjs/src/instanceManager.js +80 -59
  11. package/dist/cjs/src/instances/routes.js +19 -11
  12. package/dist/cjs/src/operatorManager.d.ts +5 -3
  13. package/dist/cjs/src/operatorManager.js +34 -23
  14. package/dist/cjs/src/providerManager.js +1 -1
  15. package/dist/cjs/src/repositoryManager.d.ts +2 -4
  16. package/dist/cjs/src/repositoryManager.js +51 -66
  17. package/dist/cjs/src/socketManager.js +1 -1
  18. package/dist/cjs/src/taskManager.d.ts +64 -0
  19. package/dist/cjs/src/taskManager.js +161 -0
  20. package/dist/cjs/src/tasks/routes.d.ts +3 -0
  21. package/dist/cjs/src/tasks/routes.js +35 -0
  22. package/dist/esm/index.js +4 -0
  23. package/dist/esm/src/api.d.ts +3 -0
  24. package/dist/esm/src/api.js +17 -0
  25. package/dist/esm/src/assetManager.d.ts +3 -1
  26. package/dist/esm/src/assetManager.js +20 -4
  27. package/dist/esm/src/assets/routes.js +23 -2
  28. package/dist/esm/src/containerManager.js +130 -109
  29. package/dist/esm/src/instanceManager.d.ts +4 -3
  30. package/dist/esm/src/instanceManager.js +80 -59
  31. package/dist/esm/src/instances/routes.js +19 -11
  32. package/dist/esm/src/operatorManager.d.ts +5 -3
  33. package/dist/esm/src/operatorManager.js +34 -23
  34. package/dist/esm/src/providerManager.js +1 -1
  35. package/dist/esm/src/repositoryManager.d.ts +2 -4
  36. package/dist/esm/src/repositoryManager.js +51 -66
  37. package/dist/esm/src/socketManager.js +1 -1
  38. package/dist/esm/src/taskManager.d.ts +64 -0
  39. package/dist/esm/src/taskManager.js +157 -0
  40. package/dist/esm/src/tasks/routes.d.ts +3 -0
  41. package/dist/esm/src/tasks/routes.js +30 -0
  42. package/index.ts +9 -0
  43. package/package.json +2 -1
  44. package/src/api.ts +21 -0
  45. package/src/assetManager.ts +28 -4
  46. package/src/assets/routes.ts +24 -2
  47. package/src/containerManager.ts +151 -126
  48. package/src/instanceManager.ts +106 -70
  49. package/src/instances/routes.ts +18 -12
  50. package/src/operatorManager.ts +46 -28
  51. package/src/providerManager.ts +1 -1
  52. package/src/repositoryManager.ts +65 -63
  53. package/src/socketManager.ts +1 -1
  54. package/src/taskManager.ts +223 -0
  55. package/src/tasks/routes.ts +38 -0
  56. package/src/utils/BlockInstanceRunner.ts +0 -2
@@ -0,0 +1,17 @@
1
+ import Router from 'express-promise-router';
2
+ import { corsHandler } from "./middleware/cors";
3
+ import { KapetaAPI } from "@kapeta/nodejs-api-client";
4
+ import ClusterConfiguration from '@kapeta/local-cluster-config';
5
+ const { createAPIRoute } = require('@kapeta/web-microfrontend/server');
6
+ const packageJson = require('../package.json');
7
+ const router = Router();
8
+ const remoteServices = ClusterConfiguration.getClusterConfig().remoteServices ?? {};
9
+ router.use('/', corsHandler);
10
+ router.use('/registry', createAPIRoute(remoteServices.registry ?? 'https://registry.kapeta.com', {
11
+ nonce: false,
12
+ userAgent: `KapetaDesktopCluster/${packageJson.version}`,
13
+ tokenFetcher: () => {
14
+ return new KapetaAPI().getAccessToken();
15
+ }
16
+ }));
17
+ export default router;
@@ -13,6 +13,7 @@ export interface EnrichedAsset {
13
13
  declare class AssetManager {
14
14
  private cache;
15
15
  constructor();
16
+ clearCache(): void;
16
17
  /**
17
18
  *
18
19
  * @param {string[]} [assetKinds]
@@ -21,11 +22,12 @@ declare class AssetManager {
21
22
  getAssets(assetKinds?: string[]): EnrichedAsset[];
22
23
  getPlans(): EnrichedAsset[];
23
24
  getPlan(ref: string, noCache?: boolean): Promise<Definition>;
24
- getAsset(ref: string, noCache?: boolean): Promise<EnrichedAsset | undefined>;
25
+ getAsset(ref: string, noCache?: boolean, autoFetch?: boolean): Promise<EnrichedAsset | undefined>;
25
26
  createAsset(path: string, yaml: BlockDefinition): Promise<EnrichedAsset[]>;
26
27
  updateAsset(ref: string, yaml: BlockDefinition): Promise<void>;
27
28
  importFile(filePath: string): Promise<EnrichedAsset[]>;
28
29
  unregisterAsset(ref: string): Promise<void>;
30
+ installAsset(ref: string): Promise<import("./taskManager").Task<void>[] | undefined>;
29
31
  }
30
32
  export declare const assetManager: AssetManager;
31
33
  export {};
@@ -41,6 +41,9 @@ class AssetManager {
41
41
  stdTTL: 60 * 60, // 1 hour
42
42
  });
43
43
  }
44
+ clearCache() {
45
+ this.cache.flushAll();
46
+ }
44
47
  /**
45
48
  *
46
49
  * @param {string[]} [assetKinds]
@@ -70,22 +73,26 @@ class AssetManager {
70
73
  }
71
74
  return asset.data;
72
75
  }
73
- async getAsset(ref, noCache = false) {
76
+ async getAsset(ref, noCache = false, autoFetch = true) {
74
77
  ref = normalizeKapetaUri(ref);
75
78
  const cacheKey = `getAsset:${ref}`;
76
79
  if (!noCache && this.cache.has(cacheKey)) {
77
80
  return this.cache.get(cacheKey);
78
81
  }
79
82
  const uri = parseKapetaUri(ref);
80
- await repositoryManager.ensureAsset(uri.handle, uri.name, uri.version);
83
+ if (autoFetch) {
84
+ await repositoryManager.ensureAsset(uri.handle, uri.name, uri.version, true);
85
+ }
81
86
  let asset = definitionsManager
82
87
  .getDefinitions()
83
88
  .map(enrichAsset)
84
89
  .find((a) => parseKapetaUri(a.ref).equals(uri));
85
- if (!asset) {
90
+ if (autoFetch && !asset) {
86
91
  throw new Error('Asset not found: ' + ref);
87
92
  }
88
- this.cache.set(cacheKey, asset);
93
+ if (asset) {
94
+ this.cache.set(cacheKey, asset);
95
+ }
89
96
  return asset;
90
97
  }
91
98
  async createAsset(path, yaml) {
@@ -146,5 +153,14 @@ class AssetManager {
146
153
  this.cache.flushAll();
147
154
  await Actions.uninstall(progressListener, [asset.ref]);
148
155
  }
156
+ async installAsset(ref) {
157
+ const asset = await this.getAsset(ref, true, false);
158
+ if (asset) {
159
+ throw new Error('Asset already installed: ' + ref);
160
+ }
161
+ const uri = parseKapetaUri(ref);
162
+ console.log('Installing %s', ref);
163
+ return await repositoryManager.ensureAsset(uri.handle, uri.name, uri.version, false);
164
+ }
149
165
  }
150
166
  export const assetManager = new AssetManager();
@@ -24,7 +24,7 @@ router.use('/', stringBody);
24
24
  * Get all local assets available
25
25
  */
26
26
  router.get('/', (req, res) => {
27
- res.send(assetManager.getAssets());
27
+ res.send(assetManager.getAssets([]));
28
28
  });
29
29
  /**
30
30
  * Get single asset
@@ -34,8 +34,15 @@ router.get('/read', async (req, res) => {
34
34
  res.status(400).send({ error: 'Query parameter "ref" is missing' });
35
35
  return;
36
36
  }
37
+ const ensure = req.query.ensure !== 'false';
37
38
  try {
38
- res.send(await assetManager.getAsset(req.query.ref, true));
39
+ const asset = await assetManager.getAsset(req.query.ref, true, ensure);
40
+ if (asset) {
41
+ res.send(asset);
42
+ }
43
+ else {
44
+ res.status(404).send({ error: 'Asset not found' });
45
+ }
39
46
  }
40
47
  catch (err) {
41
48
  res.status(400).send({ error: err.message });
@@ -109,4 +116,18 @@ router.put('/import', async (req, res) => {
109
116
  res.status(400).send({ error: err.message });
110
117
  }
111
118
  });
119
+ router.put('/install', async (req, res) => {
120
+ if (!req.query.ref) {
121
+ res.status(400).send({ error: 'Query parameter "ref" is missing' });
122
+ return;
123
+ }
124
+ try {
125
+ const tasks = await assetManager.installAsset(req.query.ref);
126
+ const taskIds = tasks?.map((t) => t.id) ?? [];
127
+ res.status(200).send(taskIds);
128
+ }
129
+ catch (err) {
130
+ res.status(400).send({ error: err.message });
131
+ }
132
+ });
112
133
  export default router;
@@ -9,8 +9,8 @@ import ClusterConfiguration from '@kapeta/local-cluster-config';
9
9
  import uuid from 'node-uuid';
10
10
  import md5 from 'md5';
11
11
  import { getBlockInstanceContainerName } from './utils/utils';
12
- import { socketManager } from './socketManager';
13
12
  import { KapetaAPI } from '@kapeta/nodejs-api-client';
13
+ import { taskManager } from './taskManager';
14
14
  const EVENT_IMAGE_PULL = 'docker-image-pull';
15
15
  export const CONTAINER_LABEL_PORT_PREFIX = 'kapeta_port-';
16
16
  const NANO_SECOND = 1000000;
@@ -156,120 +156,141 @@ class ContainerManager {
156
156
  console.log('Image found: %s', image);
157
157
  return false;
158
158
  }
159
- const timeStarted = Date.now();
160
- socketManager.emitGlobal(EVENT_IMAGE_PULL, { image, percent: -1 });
161
- const api = new KapetaAPI();
162
- const accessToken = await api.getAccessToken();
163
- const auth = image.startsWith('docker.kapeta.com/')
164
- ? {
165
- username: 'kapeta',
166
- password: accessToken,
167
- serveraddress: 'docker.kapeta.com',
168
- }
169
- : {};
170
- const stream = (await this.docker().image.create(auth, {
171
- fromImage: imageName,
172
- tag: tag,
173
- }));
174
- const chunks = {};
175
- let lastEmitted = Date.now();
176
- await promisifyStream(stream, (rawData) => {
177
- const lines = rawData.toString().trim().split('\n');
178
- lines.forEach((line) => {
179
- const data = JSON.parse(line);
180
- if (![
181
- 'Waiting',
182
- 'Downloading',
183
- 'Extracting',
184
- 'Download complete',
185
- 'Pull complete',
186
- 'Already exists',
187
- ].includes(data.status)) {
188
- return;
189
- }
190
- if (!chunks[data.id]) {
191
- chunks[data.id] = {
192
- downloading: {
193
- total: 0,
194
- current: 0,
195
- },
196
- extracting: {
197
- total: 0,
198
- current: 0,
199
- },
200
- done: false,
201
- };
202
- }
203
- const chunk = chunks[data.id];
204
- switch (data.status) {
205
- case 'Downloading':
206
- chunk.downloading = data.progressDetail;
207
- break;
208
- case 'Extracting':
209
- chunk.extracting = data.progressDetail;
210
- break;
211
- case 'Download complete':
212
- chunk.downloading.current = chunks[data.id].downloading.total;
213
- break;
214
- case 'Pull complete':
215
- chunk.extracting.current = chunks[data.id].extracting.total;
216
- chunk.done = true;
217
- break;
218
- case 'Already exists':
219
- // Force layer to be done
220
- chunk.downloading.current = 1;
221
- chunk.downloading.total = 1;
222
- chunk.extracting.current = 1;
223
- chunk.extracting.total = 1;
224
- chunk.done = true;
225
- break;
226
- }
227
- });
228
- if (Date.now() - lastEmitted < 1000) {
229
- return;
230
- }
231
- const chunkList = Object.values(chunks);
232
- let totals = {
233
- downloading: {
234
- total: 0,
235
- current: 0,
236
- },
237
- extracting: {
238
- total: 0,
239
- current: 0,
240
- },
241
- total: chunkList.length,
242
- done: 0,
243
- };
244
- chunkList.forEach((chunk) => {
245
- if (chunk.downloading.current > 0) {
246
- totals.downloading.current += chunk.downloading.current;
247
- }
248
- if (chunk.downloading.total > 0) {
249
- totals.downloading.total += chunk.downloading.total;
250
- }
251
- if (chunk.extracting.current > 0) {
252
- totals.extracting.current += chunk.extracting.current;
159
+ let friendlyImageName = image;
160
+ const imageParts = imageName.split('/');
161
+ if (imageParts.length > 2) {
162
+ //Strip the registry to make the name shorter
163
+ friendlyImageName = `${imageParts.slice(1).join('/')}:${tag}`;
164
+ }
165
+ const taskName = `Pulling image ${friendlyImageName}`;
166
+ const processor = async (task) => {
167
+ const timeStarted = Date.now();
168
+ const api = new KapetaAPI();
169
+ const accessToken = await api.getAccessToken();
170
+ const auth = image.startsWith('docker.kapeta.com/')
171
+ ? {
172
+ username: 'kapeta',
173
+ password: accessToken,
174
+ serveraddress: 'docker.kapeta.com',
253
175
  }
254
- if (chunk.extracting.total > 0) {
255
- totals.extracting.total += chunk.extracting.total;
256
- }
257
- if (chunk.done) {
258
- totals.done++;
176
+ : {};
177
+ const stream = (await this.docker().image.create(auth, {
178
+ fromImage: imageName,
179
+ tag: tag,
180
+ }));
181
+ const chunks = {};
182
+ let lastEmitted = Date.now();
183
+ await promisifyStream(stream, (rawData) => {
184
+ const lines = rawData.toString().trim().split('\n');
185
+ lines.forEach((line) => {
186
+ const data = JSON.parse(line);
187
+ if (![
188
+ 'Waiting',
189
+ 'Downloading',
190
+ 'Extracting',
191
+ 'Download complete',
192
+ 'Pull complete',
193
+ 'Already exists',
194
+ ].includes(data.status)) {
195
+ return;
196
+ }
197
+ if (!chunks[data.id]) {
198
+ chunks[data.id] = {
199
+ downloading: {
200
+ total: 0,
201
+ current: 0,
202
+ },
203
+ extracting: {
204
+ total: 0,
205
+ current: 0,
206
+ },
207
+ done: false,
208
+ };
209
+ }
210
+ const chunk = chunks[data.id];
211
+ switch (data.status) {
212
+ case 'Downloading':
213
+ chunk.downloading = data.progressDetail;
214
+ break;
215
+ case 'Extracting':
216
+ chunk.extracting = data.progressDetail;
217
+ break;
218
+ case 'Download complete':
219
+ chunk.downloading.current = chunks[data.id].downloading.total;
220
+ break;
221
+ case 'Pull complete':
222
+ chunk.extracting.current = chunks[data.id].extracting.total;
223
+ chunk.done = true;
224
+ break;
225
+ case 'Already exists':
226
+ // Force layer to be done
227
+ chunk.downloading.current = 1;
228
+ chunk.downloading.total = 1;
229
+ chunk.extracting.current = 1;
230
+ chunk.extracting.total = 1;
231
+ chunk.done = true;
232
+ break;
233
+ }
234
+ });
235
+ if (Date.now() - lastEmitted < 1000) {
236
+ return;
259
237
  }
238
+ const chunkList = Object.values(chunks);
239
+ let totals = {
240
+ downloading: {
241
+ total: 0,
242
+ current: 0,
243
+ },
244
+ extracting: {
245
+ total: 0,
246
+ current: 0,
247
+ },
248
+ total: chunkList.length,
249
+ done: 0,
250
+ };
251
+ chunkList.forEach((chunk) => {
252
+ if (chunk.downloading.current > 0) {
253
+ totals.downloading.current += chunk.downloading.current;
254
+ }
255
+ if (chunk.downloading.total > 0) {
256
+ totals.downloading.total += chunk.downloading.total;
257
+ }
258
+ if (chunk.extracting.current > 0) {
259
+ totals.extracting.current += chunk.extracting.current;
260
+ }
261
+ if (chunk.extracting.total > 0) {
262
+ totals.extracting.total += chunk.extracting.total;
263
+ }
264
+ if (chunk.done) {
265
+ totals.done++;
266
+ }
267
+ });
268
+ const progress = totals.total > 0 ? (totals.done / totals.total) * 100 : 0;
269
+ task.metadata = {
270
+ ...task.metadata,
271
+ image,
272
+ progress,
273
+ status: totals,
274
+ timeTaken: Date.now() - timeStarted,
275
+ };
276
+ task.emitUpdate();
277
+ lastEmitted = Date.now();
278
+ //console.log('Pulling image %s: %s % [done: %s, total: %s]', image, Math.round(percent), totals.done, totals.total);
260
279
  });
261
- const percent = totals.total > 0 ? (totals.done / totals.total) * 100 : 0;
262
- //We emit at most every second to not spam the client
263
- socketManager.emitGlobal(EVENT_IMAGE_PULL, {
280
+ task.metadata = {
281
+ ...task.metadata,
264
282
  image,
265
- percent,
266
- status: totals,
283
+ progress: 100,
267
284
  timeTaken: Date.now() - timeStarted,
268
- });
269
- lastEmitted = Date.now();
270
- //console.log('Pulling image %s: %s % [done: %s, total: %s]', image, Math.round(percent), totals.done, totals.total);
285
+ };
286
+ task.emitUpdate();
287
+ };
288
+ const task = taskManager.add(`docker:image:pull:${image}`, processor, {
289
+ name: taskName,
290
+ image,
291
+ progress: -1,
271
292
  });
272
- socketManager.emitGlobal(EVENT_IMAGE_PULL, { image, percent: 100, timeTaken: Date.now() - timeStarted });
293
+ await task.wait();
273
294
  return true;
274
295
  }
275
296
  toDockerMounts(mounts) {
@@ -1,4 +1,5 @@
1
1
  import { InstanceInfo, LogEntry } from './types';
2
+ import { Task } from './taskManager';
2
3
  export declare class InstanceManager {
3
4
  private _interval;
4
5
  private readonly _instances;
@@ -18,11 +19,11 @@ export declare class InstanceManager {
18
19
  registerInstanceFromSDK(systemId: string, instanceId: string, info: Omit<InstanceInfo, 'systemId' | 'instanceId'>): Promise<InstanceInfo | undefined>;
19
20
  private getHealthUrl;
20
21
  markAsStopped(systemId: string, instanceId: string): Promise<void>;
21
- startAllForPlan(systemId: string): Promise<InstanceInfo[]>;
22
+ startAllForPlan(systemId: string): Promise<Task<InstanceInfo[]>>;
22
23
  stop(systemId: string, instanceId: string): Promise<void>;
23
24
  private stopInner;
24
- stopAllForPlan(systemId: string): Promise<void>;
25
- start(systemId: string, instanceId: string): Promise<InstanceInfo>;
25
+ stopAllForPlan(systemId: string): Task<void>;
26
+ start(systemId: string, instanceId: string): Promise<InstanceInfo | Task<InstanceInfo>>;
26
27
  /**
27
28
  * Stops an instance but does not remove it from the list of active instances
28
29
  *
@@ -13,6 +13,7 @@ import { getBlockInstanceContainerName, normalizeKapetaUri } from './utils/utils
13
13
  import { KIND_OPERATOR, operatorManager } from './operatorManager';
14
14
  import { parseKapetaUri } from '@kapeta/nodejs-utils';
15
15
  import { definitionsManager } from './definitionsManager';
16
+ import { Task, taskManager } from './taskManager';
16
17
  const CHECK_INTERVAL = 5000;
17
18
  const DEFAULT_HEALTH_PORT_TYPE = 'rest';
18
19
  const EVENT_STATUS_CHANGED = 'status-changed';
@@ -211,27 +212,37 @@ export class InstanceManager {
211
212
  systemId = normalizeKapetaUri(systemId);
212
213
  const plan = await assetManager.getPlan(systemId, true);
213
214
  if (!plan) {
214
- throw new Error('Plan not found: ' + systemId);
215
+ throw new Error(`Plan not found: ${systemId}`);
215
216
  }
216
217
  if (!plan.spec.blocks) {
217
- console.warn('No blocks found in plan', systemId);
218
- return [];
218
+ throw new Error(`No blocks found in plan: ${systemId}`);
219
219
  }
220
- let promises = [];
221
- let errors = [];
222
- for (let blockInstance of Object.values(plan.spec.blocks)) {
223
- try {
224
- promises.push(this.start(systemId, blockInstance.id));
220
+ return taskManager.add(`plan:start:${systemId}`, async () => {
221
+ let promises = [];
222
+ let errors = [];
223
+ for (let blockInstance of Object.values(plan.spec.blocks)) {
224
+ try {
225
+ promises.push(this.start(systemId, blockInstance.id).then((taskOrInstance) => {
226
+ if (taskOrInstance instanceof Task) {
227
+ return taskOrInstance.wait();
228
+ }
229
+ return taskOrInstance;
230
+ }));
231
+ }
232
+ catch (e) {
233
+ errors.push(e);
234
+ }
225
235
  }
226
- catch (e) {
227
- errors.push(e);
236
+ const settled = await Promise.allSettled(promises);
237
+ if (errors.length > 0) {
238
+ throw errors[0];
228
239
  }
229
- }
230
- const settled = await Promise.allSettled(promises);
231
- if (errors.length > 0) {
232
- throw errors[0];
233
- }
234
- return settled.map((p) => (p.status === 'fulfilled' ? p.value : null)).filter((p) => !!p);
240
+ return settled
241
+ .map((p) => (p.status === 'fulfilled' ? p.value : null))
242
+ .filter((p) => !!p);
243
+ }, {
244
+ name: `Starting plan ${systemId}`,
245
+ });
235
246
  }
236
247
  async stop(systemId, instanceId) {
237
248
  return this.stopInner(systemId, instanceId, true);
@@ -288,10 +299,14 @@ export class InstanceManager {
288
299
  }
289
300
  });
290
301
  }
291
- async stopAllForPlan(systemId) {
302
+ stopAllForPlan(systemId) {
292
303
  systemId = normalizeKapetaUri(systemId);
293
304
  const instancesForPlan = this._instances.filter((instance) => instance.systemId === systemId);
294
- return this.stopInstances(instancesForPlan);
305
+ return taskManager.add(`plan:stop:${systemId}`, async () => {
306
+ return this.stopInstances(instancesForPlan);
307
+ }, {
308
+ name: `Stopping plan ${systemId}`,
309
+ });
295
310
  }
296
311
  async start(systemId, instanceId) {
297
312
  return this.exclusive(systemId, instanceId, async () => {
@@ -367,47 +382,53 @@ export class InstanceManager {
367
382
  }
368
383
  }
369
384
  const instanceConfig = await configManager.getConfigForSection(systemId, instanceId);
370
- const runner = new BlockInstanceRunner(systemId);
371
- const startTime = Date.now();
372
- try {
373
- const processInfo = await runner.start(blockRef, instanceId, instanceConfig);
374
- instance.status = InstanceStatus.READY;
375
- return this.saveInternalInstance({
376
- ...instance,
377
- type: processInfo.type,
378
- pid: processInfo.pid ?? -1,
379
- health: null,
380
- portType: processInfo.portType,
381
- status: InstanceStatus.READY,
382
- });
383
- }
384
- catch (e) {
385
- console.warn('Failed to start instance: ', systemId, instanceId, blockRef, e.message);
386
- const logs = [
387
- {
388
- source: 'stdout',
389
- level: 'ERROR',
390
- message: e.message,
391
- time: Date.now(),
392
- },
393
- ];
394
- const out = await this.saveInternalInstance({
395
- ...instance,
396
- type: InstanceType.UNKNOWN,
397
- pid: null,
398
- health: null,
399
- portType: DEFAULT_HEALTH_PORT_TYPE,
400
- status: InstanceStatus.FAILED,
401
- errorMessage: e.message ?? 'Failed to start - Check logs for details.',
402
- });
403
- this.emitInstanceEvent(systemId, instanceId, EVENT_INSTANCE_LOG, logs[0]);
404
- this.emitInstanceEvent(systemId, blockInstance.id, EVENT_INSTANCE_EXITED, {
405
- error: `Failed to start instance: ${e.message}`,
406
- status: EVENT_INSTANCE_EXITED,
407
- instanceId: blockInstance.id,
408
- });
409
- return out;
410
- }
385
+ const task = taskManager.add(`instance:start:${systemId}:${instanceId}`, async () => {
386
+ const runner = new BlockInstanceRunner(systemId);
387
+ const startTime = Date.now();
388
+ try {
389
+ const processInfo = await runner.start(blockRef, instanceId, instanceConfig);
390
+ instance.status = InstanceStatus.READY;
391
+ return this.saveInternalInstance({
392
+ ...instance,
393
+ type: processInfo.type,
394
+ pid: processInfo.pid ?? -1,
395
+ health: null,
396
+ portType: processInfo.portType,
397
+ status: InstanceStatus.READY,
398
+ });
399
+ }
400
+ catch (e) {
401
+ console.warn('Failed to start instance: ', systemId, instanceId, blockRef, e.message);
402
+ const logs = [
403
+ {
404
+ source: 'stdout',
405
+ level: 'ERROR',
406
+ message: e.message,
407
+ time: Date.now(),
408
+ },
409
+ ];
410
+ const out = await this.saveInternalInstance({
411
+ ...instance,
412
+ type: InstanceType.UNKNOWN,
413
+ pid: null,
414
+ health: null,
415
+ portType: DEFAULT_HEALTH_PORT_TYPE,
416
+ status: InstanceStatus.FAILED,
417
+ errorMessage: e.message ?? 'Failed to start - Check logs for details.',
418
+ });
419
+ this.emitInstanceEvent(systemId, instanceId, EVENT_INSTANCE_LOG, logs[0]);
420
+ this.emitInstanceEvent(systemId, blockInstance.id, EVENT_INSTANCE_EXITED, {
421
+ error: `Failed to start instance: ${e.message}`,
422
+ status: EVENT_INSTANCE_EXITED,
423
+ instanceId: blockInstance.id,
424
+ });
425
+ return out;
426
+ }
427
+ }, {
428
+ name: `Starting instance: ${instance.name}`,
429
+ systemId,
430
+ });
431
+ return task;
411
432
  });
412
433
  }
413
434
  /**
@@ -5,6 +5,7 @@ import { corsHandler } from '../middleware/cors';
5
5
  import { kapetaHeaders } from '../middleware/kapeta';
6
6
  import { stringBody } from '../middleware/stringBody';
7
7
  import { DesiredInstanceStatus, InstanceOwner, InstanceType } from '../types';
8
+ import { Task } from '../taskManager';
8
9
  const router = Router();
9
10
  router.use('/', corsHandler);
10
11
  router.use('/', kapetaHeaders);
@@ -24,33 +25,40 @@ router.get('/:systemId/instances', (req, res) => {
24
25
  * Start all instances in a plan
25
26
  */
26
27
  router.post('/:systemId/start', async (req, res) => {
27
- const instances = await instanceManager.startAllForPlan(req.params.systemId);
28
+ const task = await instanceManager.startAllForPlan(req.params.systemId);
28
29
  res.status(202).send({
29
30
  ok: true,
30
- processes: instances.map((p) => {
31
- return { pid: p.pid, type: p.type };
32
- }),
31
+ taskId: task.id,
33
32
  });
34
33
  });
35
34
  /**
36
35
  * Stop all instances in plan
37
36
  */
38
37
  router.post('/:systemId/stop', async (req, res) => {
39
- await instanceManager.stopAllForPlan(req.params.systemId);
38
+ const task = instanceManager.stopAllForPlan(req.params.systemId);
40
39
  res.status(202).send({
41
40
  ok: true,
41
+ taskId: task.id,
42
42
  });
43
43
  });
44
44
  /**
45
45
  * Start single instance in a plan
46
46
  */
47
47
  router.post('/:systemId/:instanceId/start', async (req, res) => {
48
- const process = await instanceManager.start(req.params.systemId, req.params.instanceId);
49
- res.status(202).send({
50
- ok: true,
51
- pid: process.pid,
52
- type: process.type,
53
- });
48
+ const result = await instanceManager.start(req.params.systemId, req.params.instanceId);
49
+ if (result instanceof Task) {
50
+ res.status(202).send({
51
+ ok: true,
52
+ taskId: result.id,
53
+ });
54
+ }
55
+ else {
56
+ res.status(202).send({
57
+ ok: true,
58
+ pid: result.pid,
59
+ type: result.type,
60
+ });
61
+ }
54
62
  });
55
63
  /**
56
64
  * Stop single instance in a plan