@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
package/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ # [0.13.0](https://github.com/kapetacom/local-cluster-service/compare/v0.12.1...v0.13.0) (2023-08-03)
2
+
3
+
4
+ ### Features
5
+
6
+ * Adds background task concept ([#55](https://github.com/kapetacom/local-cluster-service/issues/55)) ([71cc63c](https://github.com/kapetacom/local-cluster-service/commit/71cc63c9c3eb8bec1dec8b31d3340694e93bd3e5))
7
+
1
8
  ## [0.12.1](https://github.com/kapetacom/local-cluster-service/compare/v0.12.0...v0.12.1) (2023-08-02)
2
9
 
3
10
 
package/dist/cjs/index.js CHANGED
@@ -20,6 +20,7 @@ const routes_6 = __importDefault(require("./src/filesystem/routes"));
20
20
  const routes_7 = __importDefault(require("./src/assets/routes"));
21
21
  const routes_8 = __importDefault(require("./src/providers/routes"));
22
22
  const routes_9 = __importDefault(require("./src/attachments/routes"));
23
+ const routes_10 = __importDefault(require("./src/tasks/routes"));
23
24
  const utils_1 = require("./src/utils/utils");
24
25
  const request_1 = __importDefault(require("request"));
25
26
  let currentServer = null;
@@ -34,6 +35,7 @@ function createServer() {
34
35
  app.use('/assets', routes_7.default);
35
36
  app.use('/providers', routes_8.default);
36
37
  app.use('/attachments', routes_9.default);
38
+ app.use('/tasks', routes_10.default);
37
39
  app.get('/status', async (req, res) => {
38
40
  res.send({
39
41
  ok: true,
@@ -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 {};
@@ -47,6 +47,9 @@ class AssetManager {
47
47
  stdTTL: 60 * 60, // 1 hour
48
48
  });
49
49
  }
50
+ clearCache() {
51
+ this.cache.flushAll();
52
+ }
50
53
  /**
51
54
  *
52
55
  * @param {string[]} [assetKinds]
@@ -76,22 +79,26 @@ class AssetManager {
76
79
  }
77
80
  return asset.data;
78
81
  }
79
- async getAsset(ref, noCache = false) {
82
+ async getAsset(ref, noCache = false, autoFetch = true) {
80
83
  ref = (0, utils_1.normalizeKapetaUri)(ref);
81
84
  const cacheKey = `getAsset:${ref}`;
82
85
  if (!noCache && this.cache.has(cacheKey)) {
83
86
  return this.cache.get(cacheKey);
84
87
  }
85
88
  const uri = (0, nodejs_utils_1.parseKapetaUri)(ref);
86
- await repositoryManager_1.repositoryManager.ensureAsset(uri.handle, uri.name, uri.version);
89
+ if (autoFetch) {
90
+ await repositoryManager_1.repositoryManager.ensureAsset(uri.handle, uri.name, uri.version, true);
91
+ }
87
92
  let asset = definitionsManager_1.definitionsManager
88
93
  .getDefinitions()
89
94
  .map(enrichAsset)
90
95
  .find((a) => (0, nodejs_utils_1.parseKapetaUri)(a.ref).equals(uri));
91
- if (!asset) {
96
+ if (autoFetch && !asset) {
92
97
  throw new Error('Asset not found: ' + ref);
93
98
  }
94
- this.cache.set(cacheKey, asset);
99
+ if (asset) {
100
+ this.cache.set(cacheKey, asset);
101
+ }
95
102
  return asset;
96
103
  }
97
104
  async createAsset(path, yaml) {
@@ -152,5 +159,14 @@ class AssetManager {
152
159
  this.cache.flushAll();
153
160
  await nodejs_registry_utils_1.Actions.uninstall(progressListener_1.progressListener, [asset.ref]);
154
161
  }
162
+ async installAsset(ref) {
163
+ const asset = await this.getAsset(ref, true, false);
164
+ if (asset) {
165
+ throw new Error('Asset already installed: ' + ref);
166
+ }
167
+ const uri = (0, nodejs_utils_1.parseKapetaUri)(ref);
168
+ console.log('Installing %s', ref);
169
+ return await repositoryManager_1.repositoryManager.ensureAsset(uri.handle, uri.name, uri.version, false);
170
+ }
155
171
  }
156
172
  exports.assetManager = new AssetManager();
@@ -39,8 +39,15 @@ router.get('/read', async (req, res) => {
39
39
  res.status(400).send({ error: 'Query parameter "ref" is missing' });
40
40
  return;
41
41
  }
42
+ const ensure = req.query.ensure !== 'false';
42
43
  try {
43
- res.send(await assetManager_1.assetManager.getAsset(req.query.ref, true));
44
+ const asset = await assetManager_1.assetManager.getAsset(req.query.ref, true, ensure);
45
+ if (asset) {
46
+ res.send(asset);
47
+ }
48
+ else {
49
+ res.status(404).send({ error: 'Asset not found' });
50
+ }
44
51
  }
45
52
  catch (err) {
46
53
  res.status(400).send({ error: err.message });
@@ -114,4 +121,18 @@ router.put('/import', async (req, res) => {
114
121
  res.status(400).send({ error: err.message });
115
122
  }
116
123
  });
124
+ router.put('/install', async (req, res) => {
125
+ if (!req.query.ref) {
126
+ res.status(400).send({ error: 'Query parameter "ref" is missing' });
127
+ return;
128
+ }
129
+ try {
130
+ const tasks = await assetManager_1.assetManager.installAsset(req.query.ref);
131
+ const taskIds = tasks?.map((t) => t.id) ?? [];
132
+ res.status(200).send(taskIds);
133
+ }
134
+ catch (err) {
135
+ res.status(400).send({ error: err.message });
136
+ }
137
+ });
117
138
  exports.default = router;
@@ -15,8 +15,8 @@ const local_cluster_config_1 = __importDefault(require("@kapeta/local-cluster-co
15
15
  const node_uuid_1 = __importDefault(require("node-uuid"));
16
16
  const md5_1 = __importDefault(require("md5"));
17
17
  const utils_1 = require("./utils/utils");
18
- const socketManager_1 = require("./socketManager");
19
18
  const nodejs_api_client_1 = require("@kapeta/nodejs-api-client");
19
+ const taskManager_1 = require("./taskManager");
20
20
  const EVENT_IMAGE_PULL = 'docker-image-pull';
21
21
  exports.CONTAINER_LABEL_PORT_PREFIX = 'kapeta_port-';
22
22
  const NANO_SECOND = 1000000;
@@ -162,120 +162,141 @@ class ContainerManager {
162
162
  console.log('Image found: %s', image);
163
163
  return false;
164
164
  }
165
- const timeStarted = Date.now();
166
- socketManager_1.socketManager.emitGlobal(EVENT_IMAGE_PULL, { image, percent: -1 });
167
- const api = new nodejs_api_client_1.KapetaAPI();
168
- const accessToken = await api.getAccessToken();
169
- const auth = image.startsWith('docker.kapeta.com/')
170
- ? {
171
- username: 'kapeta',
172
- password: accessToken,
173
- serveraddress: 'docker.kapeta.com',
174
- }
175
- : {};
176
- const stream = (await this.docker().image.create(auth, {
177
- fromImage: imageName,
178
- tag: tag,
179
- }));
180
- const chunks = {};
181
- let lastEmitted = Date.now();
182
- await promisifyStream(stream, (rawData) => {
183
- const lines = rawData.toString().trim().split('\n');
184
- lines.forEach((line) => {
185
- const data = JSON.parse(line);
186
- if (![
187
- 'Waiting',
188
- 'Downloading',
189
- 'Extracting',
190
- 'Download complete',
191
- 'Pull complete',
192
- 'Already exists',
193
- ].includes(data.status)) {
194
- return;
195
- }
196
- if (!chunks[data.id]) {
197
- chunks[data.id] = {
198
- downloading: {
199
- total: 0,
200
- current: 0,
201
- },
202
- extracting: {
203
- total: 0,
204
- current: 0,
205
- },
206
- done: false,
207
- };
208
- }
209
- const chunk = chunks[data.id];
210
- switch (data.status) {
211
- case 'Downloading':
212
- chunk.downloading = data.progressDetail;
213
- break;
214
- case 'Extracting':
215
- chunk.extracting = data.progressDetail;
216
- break;
217
- case 'Download complete':
218
- chunk.downloading.current = chunks[data.id].downloading.total;
219
- break;
220
- case 'Pull complete':
221
- chunk.extracting.current = chunks[data.id].extracting.total;
222
- chunk.done = true;
223
- break;
224
- case 'Already exists':
225
- // Force layer to be done
226
- chunk.downloading.current = 1;
227
- chunk.downloading.total = 1;
228
- chunk.extracting.current = 1;
229
- chunk.extracting.total = 1;
230
- chunk.done = true;
231
- break;
232
- }
233
- });
234
- if (Date.now() - lastEmitted < 1000) {
235
- return;
236
- }
237
- const chunkList = Object.values(chunks);
238
- let totals = {
239
- downloading: {
240
- total: 0,
241
- current: 0,
242
- },
243
- extracting: {
244
- total: 0,
245
- current: 0,
246
- },
247
- total: chunkList.length,
248
- done: 0,
249
- };
250
- chunkList.forEach((chunk) => {
251
- if (chunk.downloading.current > 0) {
252
- totals.downloading.current += chunk.downloading.current;
253
- }
254
- if (chunk.downloading.total > 0) {
255
- totals.downloading.total += chunk.downloading.total;
256
- }
257
- if (chunk.extracting.current > 0) {
258
- totals.extracting.current += chunk.extracting.current;
165
+ let friendlyImageName = image;
166
+ const imageParts = imageName.split('/');
167
+ if (imageParts.length > 2) {
168
+ //Strip the registry to make the name shorter
169
+ friendlyImageName = `${imageParts.slice(1).join('/')}:${tag}`;
170
+ }
171
+ const taskName = `Pulling image ${friendlyImageName}`;
172
+ const processor = async (task) => {
173
+ const timeStarted = Date.now();
174
+ const api = new nodejs_api_client_1.KapetaAPI();
175
+ const accessToken = await api.getAccessToken();
176
+ const auth = image.startsWith('docker.kapeta.com/')
177
+ ? {
178
+ username: 'kapeta',
179
+ password: accessToken,
180
+ serveraddress: 'docker.kapeta.com',
259
181
  }
260
- if (chunk.extracting.total > 0) {
261
- totals.extracting.total += chunk.extracting.total;
262
- }
263
- if (chunk.done) {
264
- totals.done++;
182
+ : {};
183
+ const stream = (await this.docker().image.create(auth, {
184
+ fromImage: imageName,
185
+ tag: tag,
186
+ }));
187
+ const chunks = {};
188
+ let lastEmitted = Date.now();
189
+ await promisifyStream(stream, (rawData) => {
190
+ const lines = rawData.toString().trim().split('\n');
191
+ lines.forEach((line) => {
192
+ const data = JSON.parse(line);
193
+ if (![
194
+ 'Waiting',
195
+ 'Downloading',
196
+ 'Extracting',
197
+ 'Download complete',
198
+ 'Pull complete',
199
+ 'Already exists',
200
+ ].includes(data.status)) {
201
+ return;
202
+ }
203
+ if (!chunks[data.id]) {
204
+ chunks[data.id] = {
205
+ downloading: {
206
+ total: 0,
207
+ current: 0,
208
+ },
209
+ extracting: {
210
+ total: 0,
211
+ current: 0,
212
+ },
213
+ done: false,
214
+ };
215
+ }
216
+ const chunk = chunks[data.id];
217
+ switch (data.status) {
218
+ case 'Downloading':
219
+ chunk.downloading = data.progressDetail;
220
+ break;
221
+ case 'Extracting':
222
+ chunk.extracting = data.progressDetail;
223
+ break;
224
+ case 'Download complete':
225
+ chunk.downloading.current = chunks[data.id].downloading.total;
226
+ break;
227
+ case 'Pull complete':
228
+ chunk.extracting.current = chunks[data.id].extracting.total;
229
+ chunk.done = true;
230
+ break;
231
+ case 'Already exists':
232
+ // Force layer to be done
233
+ chunk.downloading.current = 1;
234
+ chunk.downloading.total = 1;
235
+ chunk.extracting.current = 1;
236
+ chunk.extracting.total = 1;
237
+ chunk.done = true;
238
+ break;
239
+ }
240
+ });
241
+ if (Date.now() - lastEmitted < 1000) {
242
+ return;
265
243
  }
244
+ const chunkList = Object.values(chunks);
245
+ let totals = {
246
+ downloading: {
247
+ total: 0,
248
+ current: 0,
249
+ },
250
+ extracting: {
251
+ total: 0,
252
+ current: 0,
253
+ },
254
+ total: chunkList.length,
255
+ done: 0,
256
+ };
257
+ chunkList.forEach((chunk) => {
258
+ if (chunk.downloading.current > 0) {
259
+ totals.downloading.current += chunk.downloading.current;
260
+ }
261
+ if (chunk.downloading.total > 0) {
262
+ totals.downloading.total += chunk.downloading.total;
263
+ }
264
+ if (chunk.extracting.current > 0) {
265
+ totals.extracting.current += chunk.extracting.current;
266
+ }
267
+ if (chunk.extracting.total > 0) {
268
+ totals.extracting.total += chunk.extracting.total;
269
+ }
270
+ if (chunk.done) {
271
+ totals.done++;
272
+ }
273
+ });
274
+ const progress = totals.total > 0 ? (totals.done / totals.total) * 100 : 0;
275
+ task.metadata = {
276
+ ...task.metadata,
277
+ image,
278
+ progress,
279
+ status: totals,
280
+ timeTaken: Date.now() - timeStarted,
281
+ };
282
+ task.emitUpdate();
283
+ lastEmitted = Date.now();
284
+ //console.log('Pulling image %s: %s % [done: %s, total: %s]', image, Math.round(percent), totals.done, totals.total);
266
285
  });
267
- const percent = totals.total > 0 ? (totals.done / totals.total) * 100 : 0;
268
- //We emit at most every second to not spam the client
269
- socketManager_1.socketManager.emitGlobal(EVENT_IMAGE_PULL, {
286
+ task.metadata = {
287
+ ...task.metadata,
270
288
  image,
271
- percent,
272
- status: totals,
289
+ progress: 100,
273
290
  timeTaken: Date.now() - timeStarted,
274
- });
275
- lastEmitted = Date.now();
276
- //console.log('Pulling image %s: %s % [done: %s, total: %s]', image, Math.round(percent), totals.done, totals.total);
291
+ };
292
+ task.emitUpdate();
293
+ };
294
+ const task = taskManager_1.taskManager.add(`docker:image:pull:${image}`, processor, {
295
+ name: taskName,
296
+ image,
297
+ progress: -1,
277
298
  });
278
- socketManager_1.socketManager.emitGlobal(EVENT_IMAGE_PULL, { image, percent: 100, timeTaken: Date.now() - timeStarted });
299
+ await task.wait();
279
300
  return true;
280
301
  }
281
302
  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
  *
@@ -19,6 +19,7 @@ const utils_1 = require("./utils/utils");
19
19
  const operatorManager_1 = require("./operatorManager");
20
20
  const nodejs_utils_1 = require("@kapeta/nodejs-utils");
21
21
  const definitionsManager_1 = require("./definitionsManager");
22
+ const taskManager_1 = require("./taskManager");
22
23
  const CHECK_INTERVAL = 5000;
23
24
  const DEFAULT_HEALTH_PORT_TYPE = 'rest';
24
25
  const EVENT_STATUS_CHANGED = 'status-changed';
@@ -217,27 +218,37 @@ class InstanceManager {
217
218
  systemId = (0, utils_1.normalizeKapetaUri)(systemId);
218
219
  const plan = await assetManager_1.assetManager.getPlan(systemId, true);
219
220
  if (!plan) {
220
- throw new Error('Plan not found: ' + systemId);
221
+ throw new Error(`Plan not found: ${systemId}`);
221
222
  }
222
223
  if (!plan.spec.blocks) {
223
- console.warn('No blocks found in plan', systemId);
224
- return [];
224
+ throw new Error(`No blocks found in plan: ${systemId}`);
225
225
  }
226
- let promises = [];
227
- let errors = [];
228
- for (let blockInstance of Object.values(plan.spec.blocks)) {
229
- try {
230
- promises.push(this.start(systemId, blockInstance.id));
226
+ return taskManager_1.taskManager.add(`plan:start:${systemId}`, async () => {
227
+ let promises = [];
228
+ let errors = [];
229
+ for (let blockInstance of Object.values(plan.spec.blocks)) {
230
+ try {
231
+ promises.push(this.start(systemId, blockInstance.id).then((taskOrInstance) => {
232
+ if (taskOrInstance instanceof taskManager_1.Task) {
233
+ return taskOrInstance.wait();
234
+ }
235
+ return taskOrInstance;
236
+ }));
237
+ }
238
+ catch (e) {
239
+ errors.push(e);
240
+ }
231
241
  }
232
- catch (e) {
233
- errors.push(e);
242
+ const settled = await Promise.allSettled(promises);
243
+ if (errors.length > 0) {
244
+ throw errors[0];
234
245
  }
235
- }
236
- const settled = await Promise.allSettled(promises);
237
- if (errors.length > 0) {
238
- throw errors[0];
239
- }
240
- return settled.map((p) => (p.status === 'fulfilled' ? p.value : null)).filter((p) => !!p);
246
+ return settled
247
+ .map((p) => (p.status === 'fulfilled' ? p.value : null))
248
+ .filter((p) => !!p);
249
+ }, {
250
+ name: `Starting plan ${systemId}`,
251
+ });
241
252
  }
242
253
  async stop(systemId, instanceId) {
243
254
  return this.stopInner(systemId, instanceId, true);
@@ -294,10 +305,14 @@ class InstanceManager {
294
305
  }
295
306
  });
296
307
  }
297
- async stopAllForPlan(systemId) {
308
+ stopAllForPlan(systemId) {
298
309
  systemId = (0, utils_1.normalizeKapetaUri)(systemId);
299
310
  const instancesForPlan = this._instances.filter((instance) => instance.systemId === systemId);
300
- return this.stopInstances(instancesForPlan);
311
+ return taskManager_1.taskManager.add(`plan:stop:${systemId}`, async () => {
312
+ return this.stopInstances(instancesForPlan);
313
+ }, {
314
+ name: `Stopping plan ${systemId}`,
315
+ });
301
316
  }
302
317
  async start(systemId, instanceId) {
303
318
  return this.exclusive(systemId, instanceId, async () => {
@@ -373,47 +388,53 @@ class InstanceManager {
373
388
  }
374
389
  }
375
390
  const instanceConfig = await configManager_1.configManager.getConfigForSection(systemId, instanceId);
376
- const runner = new BlockInstanceRunner_1.BlockInstanceRunner(systemId);
377
- const startTime = Date.now();
378
- try {
379
- const processInfo = await runner.start(blockRef, instanceId, instanceConfig);
380
- instance.status = types_1.InstanceStatus.READY;
381
- return this.saveInternalInstance({
382
- ...instance,
383
- type: processInfo.type,
384
- pid: processInfo.pid ?? -1,
385
- health: null,
386
- portType: processInfo.portType,
387
- status: types_1.InstanceStatus.READY,
388
- });
389
- }
390
- catch (e) {
391
- console.warn('Failed to start instance: ', systemId, instanceId, blockRef, e.message);
392
- const logs = [
393
- {
394
- source: 'stdout',
395
- level: 'ERROR',
396
- message: e.message,
397
- time: Date.now(),
398
- },
399
- ];
400
- const out = await this.saveInternalInstance({
401
- ...instance,
402
- type: types_1.InstanceType.UNKNOWN,
403
- pid: null,
404
- health: null,
405
- portType: DEFAULT_HEALTH_PORT_TYPE,
406
- status: types_1.InstanceStatus.FAILED,
407
- errorMessage: e.message ?? 'Failed to start - Check logs for details.',
408
- });
409
- this.emitInstanceEvent(systemId, instanceId, EVENT_INSTANCE_LOG, logs[0]);
410
- this.emitInstanceEvent(systemId, blockInstance.id, EVENT_INSTANCE_EXITED, {
411
- error: `Failed to start instance: ${e.message}`,
412
- status: EVENT_INSTANCE_EXITED,
413
- instanceId: blockInstance.id,
414
- });
415
- return out;
416
- }
391
+ const task = taskManager_1.taskManager.add(`instance:start:${systemId}:${instanceId}`, async () => {
392
+ const runner = new BlockInstanceRunner_1.BlockInstanceRunner(systemId);
393
+ const startTime = Date.now();
394
+ try {
395
+ const processInfo = await runner.start(blockRef, instanceId, instanceConfig);
396
+ instance.status = types_1.InstanceStatus.READY;
397
+ return this.saveInternalInstance({
398
+ ...instance,
399
+ type: processInfo.type,
400
+ pid: processInfo.pid ?? -1,
401
+ health: null,
402
+ portType: processInfo.portType,
403
+ status: types_1.InstanceStatus.READY,
404
+ });
405
+ }
406
+ catch (e) {
407
+ console.warn('Failed to start instance: ', systemId, instanceId, blockRef, e.message);
408
+ const logs = [
409
+ {
410
+ source: 'stdout',
411
+ level: 'ERROR',
412
+ message: e.message,
413
+ time: Date.now(),
414
+ },
415
+ ];
416
+ const out = await this.saveInternalInstance({
417
+ ...instance,
418
+ type: types_1.InstanceType.UNKNOWN,
419
+ pid: null,
420
+ health: null,
421
+ portType: DEFAULT_HEALTH_PORT_TYPE,
422
+ status: types_1.InstanceStatus.FAILED,
423
+ errorMessage: e.message ?? 'Failed to start - Check logs for details.',
424
+ });
425
+ this.emitInstanceEvent(systemId, instanceId, EVENT_INSTANCE_LOG, logs[0]);
426
+ this.emitInstanceEvent(systemId, blockInstance.id, EVENT_INSTANCE_EXITED, {
427
+ error: `Failed to start instance: ${e.message}`,
428
+ status: EVENT_INSTANCE_EXITED,
429
+ instanceId: blockInstance.id,
430
+ });
431
+ return out;
432
+ }
433
+ }, {
434
+ name: `Starting instance: ${instance.name}`,
435
+ systemId,
436
+ });
437
+ return task;
417
438
  });
418
439
  }
419
440
  /**
@@ -10,6 +10,7 @@ const cors_1 = require("../middleware/cors");
10
10
  const kapeta_1 = require("../middleware/kapeta");
11
11
  const stringBody_1 = require("../middleware/stringBody");
12
12
  const types_1 = require("../types");
13
+ const taskManager_1 = require("../taskManager");
13
14
  const router = (0, express_promise_router_1.default)();
14
15
  router.use('/', cors_1.corsHandler);
15
16
  router.use('/', kapeta_1.kapetaHeaders);
@@ -29,33 +30,40 @@ router.get('/:systemId/instances', (req, res) => {
29
30
  * Start all instances in a plan
30
31
  */
31
32
  router.post('/:systemId/start', async (req, res) => {
32
- const instances = await instanceManager_1.instanceManager.startAllForPlan(req.params.systemId);
33
+ const task = await instanceManager_1.instanceManager.startAllForPlan(req.params.systemId);
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
  /**
41
40
  * Stop all instances in plan
42
41
  */
43
42
  router.post('/:systemId/stop', async (req, res) => {
44
- await instanceManager_1.instanceManager.stopAllForPlan(req.params.systemId);
43
+ const task = instanceManager_1.instanceManager.stopAllForPlan(req.params.systemId);
45
44
  res.status(202).send({
46
45
  ok: true,
46
+ taskId: task.id,
47
47
  });
48
48
  });
49
49
  /**
50
50
  * Start single instance in a plan
51
51
  */
52
52
  router.post('/:systemId/:instanceId/start', async (req, res) => {
53
- const process = await instanceManager_1.instanceManager.start(req.params.systemId, req.params.instanceId);
54
- res.status(202).send({
55
- ok: true,
56
- pid: process.pid,
57
- type: process.type,
58
- });
53
+ const result = await instanceManager_1.instanceManager.start(req.params.systemId, req.params.instanceId);
54
+ if (result instanceof taskManager_1.Task) {
55
+ res.status(202).send({
56
+ ok: true,
57
+ taskId: result.id,
58
+ });
59
+ }
60
+ else {
61
+ res.status(202).send({
62
+ ok: true,
63
+ pid: result.pid,
64
+ type: result.type,
65
+ });
66
+ }
59
67
  });
60
68
  /**
61
69
  * Stop single instance in a plan