@kapeta/local-cluster-service 0.11.1 → 0.12.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 (40) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/definitions.d.ts +7 -0
  3. package/dist/cjs/src/config/routes.js +1 -1
  4. package/dist/cjs/src/containerManager.d.ts +2 -1
  5. package/dist/cjs/src/containerManager.js +125 -21
  6. package/dist/cjs/src/definitionsManager.d.ts +1 -0
  7. package/dist/cjs/src/definitionsManager.js +7 -4
  8. package/dist/cjs/src/instanceManager.d.ts +8 -1
  9. package/dist/cjs/src/instanceManager.js +48 -19
  10. package/dist/cjs/src/operatorManager.d.ts +2 -0
  11. package/dist/cjs/src/operatorManager.js +69 -67
  12. package/dist/cjs/src/socketManager.d.ts +1 -0
  13. package/dist/cjs/src/socketManager.js +3 -0
  14. package/dist/cjs/src/types.d.ts +1 -0
  15. package/dist/cjs/src/utils/BlockInstanceRunner.js +2 -2
  16. package/dist/esm/src/config/routes.js +1 -1
  17. package/dist/esm/src/containerManager.d.ts +2 -1
  18. package/dist/esm/src/containerManager.js +126 -22
  19. package/dist/esm/src/definitionsManager.d.ts +1 -0
  20. package/dist/esm/src/definitionsManager.js +8 -5
  21. package/dist/esm/src/instanceManager.d.ts +8 -1
  22. package/dist/esm/src/instanceManager.js +48 -19
  23. package/dist/esm/src/operatorManager.d.ts +2 -0
  24. package/dist/esm/src/operatorManager.js +67 -65
  25. package/dist/esm/src/socketManager.d.ts +1 -0
  26. package/dist/esm/src/socketManager.js +3 -0
  27. package/dist/esm/src/types.d.ts +1 -0
  28. package/dist/esm/src/utils/BlockInstanceRunner.js +2 -2
  29. package/dist/esm/src/utils/utils.js +1 -1
  30. package/package.json +1 -1
  31. package/src/config/routes.ts +1 -1
  32. package/src/containerManager.ts +178 -43
  33. package/src/definitionsManager.ts +9 -5
  34. package/src/instanceManager.ts +70 -40
  35. package/src/instances/routes.ts +1 -1
  36. package/src/operatorManager.ts +72 -70
  37. package/src/socketManager.ts +4 -0
  38. package/src/types.ts +1 -1
  39. package/src/utils/BlockInstanceRunner.ts +12 -22
  40. package/src/utils/utils.ts +2 -2
@@ -194,7 +194,7 @@ export class BlockInstanceRunner {
194
194
  `KAPETA_LOCAL_CLUSTER_PORT=${clusterService.getClusterServicePort()}`,
195
195
  ...Object.entries({
196
196
  ...env,
197
- ...addonEnv
197
+ ...addonEnv,
198
198
  }).map(([key, value]) => `${key}=${value}`),
199
199
  ],
200
200
  HostConfig: {
@@ -324,7 +324,7 @@ export class BlockInstanceRunner {
324
324
  async _handleContainer(container) {
325
325
  return {
326
326
  type: InstanceType.DOCKER,
327
- pid: container.id
327
+ pid: container.id,
328
328
  };
329
329
  }
330
330
  }
@@ -1,7 +1,7 @@
1
1
  import FS from 'node:fs';
2
2
  import YAML from 'yaml';
3
3
  import { parseKapetaUri } from '@kapeta/nodejs-utils';
4
- import md5 from "md5";
4
+ import md5 from 'md5';
5
5
  export function getBlockInstanceContainerName(systemId, instanceId) {
6
6
  return `kapeta-block-instance-${md5(systemId + instanceId)}`;
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kapeta/local-cluster-service",
3
- "version": "0.11.1",
3
+ "version": "0.12.0",
4
4
  "description": "Manages configuration, ports and service discovery for locally running Kapeta systems",
5
5
  "type": "commonjs",
6
6
  "exports": {
@@ -41,7 +41,7 @@ router.put('/instance', async (req: KapetaBodyRequest, res) => {
41
41
  if (req.kapeta!.instanceId) {
42
42
  configManager.setConfigForSection(req.kapeta!.systemId, req.kapeta!.instanceId, config);
43
43
  //Restart the instance if it is running after config change
44
- await instanceManager.restart(req.kapeta!.systemId, req.kapeta!.instanceId);
44
+ await instanceManager.prepareForRestart(req.kapeta!.systemId, req.kapeta!.instanceId);
45
45
  } else {
46
46
  configManager.setConfigForSystem(req.kapeta!.systemId, config);
47
47
  }
@@ -9,10 +9,14 @@ import ClusterConfiguration from '@kapeta/local-cluster-config';
9
9
  import { Container } from 'node-docker-api/lib/container';
10
10
  import uuid from 'node-uuid';
11
11
  import md5 from 'md5';
12
- import {getBlockInstanceContainerName} from "./utils/utils";
13
- import {InstanceInfo, LogEntry, LogSource} from "./types";
14
- import EventEmitter from "events";
15
- import {LogData} from "./utils/LogData";
12
+ import { getBlockInstanceContainerName } from './utils/utils';
13
+ import { InstanceInfo, LogEntry, LogSource } from './types';
14
+ import { socketManager } from './socketManager';
15
+ import { handlers as ArtifactHandlers } from '@kapeta/nodejs-registry-utils';
16
+ import { progressListener } from './progressListener';
17
+ import { KapetaAPI } from '@kapeta/nodejs-api-client';
18
+
19
+ const EVENT_IMAGE_PULL = 'docker-image-pull';
16
20
 
17
21
  type StringMap = { [key: string]: string };
18
22
 
@@ -67,7 +71,7 @@ const IMAGE_PULL_CACHE: { [key: string]: number } = {};
67
71
 
68
72
  export const HEALTH_CHECK_TIMEOUT = HEALTH_CHECK_INTERVAL * HEALTH_CHECK_MAX * 2;
69
73
 
70
- const promisifyStream = (stream: ReadStream, handler:(d:string|Buffer) => void) =>
74
+ const promisifyStream = (stream: ReadStream, handler: (d: string | Buffer) => void) =>
71
75
  new Promise((resolve, reject) => {
72
76
  stream.on('data', handler);
73
77
  stream.on('end', resolve);
@@ -79,6 +83,7 @@ class ContainerManager {
79
83
  private _alive: boolean;
80
84
  private _mountDir: string;
81
85
  private _version: string;
86
+ private _lastDockerAccessCheck: number = 0;
82
87
 
83
88
  constructor() {
84
89
  this._docker = null;
@@ -155,24 +160,27 @@ class ContainerManager {
155
160
  return this._alive;
156
161
  }
157
162
 
158
- getMountPoint(systemId:string, ref: string, mountName: string) {
163
+ getMountPoint(systemId: string, ref: string, mountName: string) {
159
164
  const kindUri = parseKapetaUri(ref);
160
- const systemUri = parseKapetaUri(systemId)
161
- return Path.join(this._mountDir,
165
+ const systemUri = parseKapetaUri(systemId);
166
+ return Path.join(
167
+ this._mountDir,
162
168
  systemUri.handle,
163
169
  systemUri.name,
164
170
  systemUri.version,
165
171
  kindUri.handle,
166
172
  kindUri.name,
167
- kindUri.version, mountName);
173
+ kindUri.version,
174
+ mountName
175
+ );
168
176
  }
169
177
 
170
- async createMounts(systemId:string, kind: string, mountOpts: StringMap|null|undefined): Promise<StringMap> {
178
+ async createMounts(systemId: string, kind: string, mountOpts: StringMap | null | undefined): Promise<StringMap> {
171
179
  const mounts: StringMap = {};
172
180
 
173
181
  if (mountOpts) {
174
182
  const mountOptList = Object.entries(mountOpts);
175
- for(const [mountName, containerPath] of mountOptList) {
183
+ for (const [mountName, containerPath] of mountOptList) {
176
184
  const hostPath = this.getMountPoint(systemId, kind, mountName);
177
185
  await FSExtra.mkdirp(hostPath);
178
186
  mounts[containerPath] = hostPath;
@@ -238,23 +246,153 @@ class ContainerManager {
238
246
  return false;
239
247
  }
240
248
 
241
- console.log('Pulling image: %s', image);
242
- const stream = await this.docker()
243
- .image.create(
244
- {},
245
- {
246
- fromImage: imageName,
247
- tag: tag,
249
+ const timeStarted = Date.now();
250
+ socketManager.emitGlobal(EVENT_IMAGE_PULL, { image, percent: 0 });
251
+
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
+ }
299
+
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
+ });
340
+
341
+ if (Date.now() - lastEmitted < 1000) {
342
+ return;
343
+ }
344
+
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
+ };
358
+
359
+ chunkList.forEach((chunk) => {
360
+ if (chunk.downloading.current > 0) {
361
+ totals.downloading.current += chunk.downloading.current;
362
+ }
363
+
364
+ if (chunk.downloading.total > 0) {
365
+ totals.downloading.total += chunk.downloading.total;
366
+ }
367
+
368
+ if (chunk.extracting.current > 0) {
369
+ totals.extracting.current += chunk.extracting.current;
370
+ }
371
+
372
+ if (chunk.extracting.total > 0) {
373
+ totals.extracting.total += chunk.extracting.total;
374
+ }
375
+
376
+ if (chunk.done) {
377
+ totals.done++;
248
378
  }
249
- ) as ReadStream;
379
+ });
250
380
 
251
- await promisifyStream(stream, (chunk) => {
252
- console.log('Data from docker: "%s"', chunk.toString());
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, {
384
+ image,
385
+ percent,
386
+ status: totals,
387
+ 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);
253
391
  });
254
392
 
255
393
  IMAGE_PULL_CACHE[image] = Date.now();
256
394
 
257
- console.log('Image pulled: %s', image);
395
+ socketManager.emitGlobal(EVENT_IMAGE_PULL, { image, percent: 100, timeTaken: Date.now() - timeStarted });
258
396
 
259
397
  return true;
260
398
  }
@@ -305,12 +443,7 @@ class ContainerManager {
305
443
  }
306
444
 
307
445
  private async createOrUpdateContainer(opts: any) {
308
- let imagePulled = false;
309
- try {
310
- imagePulled = await this.pull(opts.Image);
311
- } catch (e) {
312
- console.warn('Failed to pull image. Continuing...', e);
313
- }
446
+ let imagePulled = await this.pull(opts.Image);
314
447
 
315
448
  this.applyHash(opts);
316
449
  if (!opts.name) {
@@ -448,24 +581,27 @@ class ContainerManager {
448
581
  return new ContainerInfo(dockerContainer);
449
582
  }
450
583
 
451
- async getLogs(instance: InstanceInfo):Promise<LogEntry[]> {
584
+ async getLogs(instance: InstanceInfo): Promise<LogEntry[]> {
452
585
  const containerName = getBlockInstanceContainerName(instance.systemId, instance.instanceId);
453
586
  const containerInfo = await this.getContainerByName(containerName);
454
587
  if (!containerInfo) {
455
- return [{
456
- source: "stdout",
457
- level: "ERROR",
458
- time: Date.now(),
459
- message: "Container not found"
460
- }];
588
+ return [
589
+ {
590
+ source: 'stdout',
591
+ level: 'ERROR',
592
+ time: Date.now(),
593
+ message: 'Container not found',
594
+ },
595
+ ];
461
596
  }
462
597
 
463
- return containerInfo.getLogs()
598
+ return containerInfo.getLogs();
464
599
  }
465
600
  }
466
601
 
467
602
  export class ContainerInfo {
468
603
  private readonly _container: Container;
604
+
469
605
  /**
470
606
  *
471
607
  * @param {Container} dockerContainer
@@ -572,21 +708,20 @@ export class ContainerInfo {
572
708
  return ports;
573
709
  }
574
710
 
575
- async getLogs():Promise<LogEntry[]> {
576
-
577
- const logStream = await this.native.logs({
711
+ async getLogs(): Promise<LogEntry[]> {
712
+ const logStream = (await this.native.logs({
578
713
  stdout: true,
579
714
  stderr: true,
580
715
  follow: false,
581
716
  tail: 100,
582
717
  timestamps: true,
583
- }) as ReadStream;
718
+ })) as ReadStream;
584
719
 
585
720
  const out = [] as LogEntry[];
586
721
  await promisifyStream(logStream, (data) => {
587
722
  const buf = data as Buffer;
588
723
  let offset = 0;
589
- while(offset < buf.length) {
724
+ while (offset < buf.length) {
590
725
  try {
591
726
  // Read the docker log format - explained here:
592
727
  // https://docs.docker.com/engine/api/v1.41/#operation/ContainerAttach
@@ -594,7 +729,7 @@ export class ContainerInfo {
594
729
 
595
730
  // First byte is stream type
596
731
  const streamTypeInt = buf.readInt8(offset);
597
- const streamType:LogSource = streamTypeInt === 1 ? 'stdout' : 'stderr';
732
+ const streamType: LogSource = streamTypeInt === 1 ? 'stdout' : 'stderr';
598
733
 
599
734
  // Bytes 4-8 is frame size
600
735
  const messageLength = buf.readInt32BE(offset + 4);
@@ -619,7 +754,7 @@ export class ContainerInfo {
619
754
  });
620
755
  } catch (err) {
621
756
  console.error('Error parsing log entry', err);
622
- offset = buf.length
757
+ offset = buf.length;
623
758
  }
624
759
  }
625
760
  });
@@ -1,5 +1,5 @@
1
1
  import ClusterConfiguration, { DefinitionInfo } from '@kapeta/local-cluster-config';
2
- import {parseKapetaUri} from "@kapeta/nodejs-utils";
2
+ import { parseKapetaUri } from '@kapeta/nodejs-utils';
3
3
 
4
4
  const CACHE_TTL = 60 * 1000; // 1 min
5
5
 
@@ -48,15 +48,19 @@ class DefinitionsManager {
48
48
  }
49
49
 
50
50
  public exists(ref: string) {
51
- const uri = parseKapetaUri(ref);
52
- return !!this.getDefinitions().find((d) => {
53
- return parseKapetaUri(`${d.definition.metadata.name}:${d.version}`).id === uri.id;
54
- });
51
+ return !!this.getDefinition(ref);
55
52
  }
56
53
 
57
54
  public getProviderDefinitions() {
58
55
  return this.doCached('providers', () => ClusterConfiguration.getProviderDefinitions());
59
56
  }
57
+
58
+ public getDefinition(ref: string) {
59
+ const uri = parseKapetaUri(ref);
60
+ return this.getDefinitions().find((d) => {
61
+ return parseKapetaUri(`${d.definition.metadata.name}:${d.version}`).id === uri.id;
62
+ });
63
+ }
60
64
  }
61
65
 
62
66
  export const definitionsManager = new DefinitionsManager();
@@ -9,8 +9,11 @@ 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 { BlockInstance } from '@kapeta/schemas';
12
+ import { BlockDefinitionSpec, BlockInstance } from '@kapeta/schemas';
13
13
  import { getBlockInstanceContainerName, normalizeKapetaUri } from './utils/utils';
14
+ import { KIND_OPERATOR, operatorManager } from './operatorManager';
15
+ import { parseKapetaUri } from '@kapeta/nodejs-utils';
16
+ import { definitionsManager } from './definitionsManager';
14
17
 
15
18
  const CHECK_INTERVAL = 5000;
16
19
  const DEFAULT_HEALTH_PORT_TYPE = 'rest';
@@ -26,7 +29,7 @@ export class InstanceManager {
26
29
 
27
30
  private readonly _instances: InstanceInfo[] = [];
28
31
 
29
- private readonly instanceLocks:AsyncLock = new AsyncLock();
32
+ private readonly instanceLocks: AsyncLock = new AsyncLock();
30
33
 
31
34
  constructor() {
32
35
  this._instances = storageService.section('instances', []);
@@ -70,15 +73,16 @@ export class InstanceManager {
70
73
  return this._instances.find((i) => i.systemId === systemId && i.instanceId === instanceId);
71
74
  }
72
75
 
73
-
74
76
  private async exclusive<T = any>(systemId: string, instanceId: string, fn: () => Promise<T>) {
75
77
  systemId = normalizeKapetaUri(systemId);
76
78
  const key = `${systemId}/${instanceId}`;
77
- return this.instanceLocks.acquire(key, fn);
79
+ //console.log(`Acquiring lock for ${key}`, this.instanceLocks.isBusy(key));
80
+ const result = await this.instanceLocks.acquire(key, fn);
81
+ //console.log(`Releasing lock for ${key}`, this.instanceLocks.isBusy(key));
82
+ return result;
78
83
  }
79
84
 
80
-
81
- public async getLogs(systemId: string, instanceId: string):Promise<LogEntry[]> {
85
+ public async getLogs(systemId: string, instanceId: string): Promise<LogEntry[]> {
82
86
  const instance = this.getInstance(systemId, instanceId);
83
87
  if (!instance) {
84
88
  throw new Error(`Instance ${systemId}/${instanceId} not found`);
@@ -86,23 +90,27 @@ export class InstanceManager {
86
90
 
87
91
  switch (instance.type) {
88
92
  case InstanceType.DOCKER:
89
- return await containerManager.getLogs(instance);
93
+ return await containerManager.getLogs(instance);
90
94
 
91
95
  case InstanceType.UNKNOWN:
92
- return [{
93
- level: 'INFO',
94
- message: 'Instance is starting...',
95
- time: Date.now(),
96
- source: 'stdout',
97
- }];
96
+ return [
97
+ {
98
+ level: 'INFO',
99
+ message: 'Instance is starting...',
100
+ time: Date.now(),
101
+ source: 'stdout',
102
+ },
103
+ ];
98
104
 
99
105
  case InstanceType.LOCAL:
100
- return [{
101
- level: 'INFO',
102
- message: 'Instance started outside Kapeta - logs not available...',
103
- time: Date.now(),
104
- source: 'stdout',
105
- }];
106
+ return [
107
+ {
108
+ level: 'INFO',
109
+ message: 'Instance started outside Kapeta - logs not available...',
110
+ time: Date.now(),
111
+ source: 'stdout',
112
+ },
113
+ ];
106
114
  }
107
115
 
108
116
  return [];
@@ -167,7 +175,10 @@ export class InstanceManager {
167
175
  const healthUrl = this.getHealthUrl(info, address);
168
176
 
169
177
  if (instance) {
170
- if (instance.status === InstanceStatus.STOPPING && instance.desiredStatus === DesiredInstanceStatus.STOP) {
178
+ if (
179
+ instance.status === InstanceStatus.STOPPING &&
180
+ instance.desiredStatus === DesiredInstanceStatus.STOP
181
+ ) {
171
182
  //If instance is stopping do not interfere
172
183
  return;
173
184
  }
@@ -239,7 +250,7 @@ export class InstanceManager {
239
250
  public markAsStopped(systemId: string, instanceId: string) {
240
251
  return this.exclusive(systemId, instanceId, async () => {
241
252
  systemId = normalizeKapetaUri(systemId);
242
- const instance = _.find(this._instances, {systemId, instanceId});
253
+ const instance = _.find(this._instances, { systemId, instanceId });
243
254
  if (instance && instance.owner === InstanceOwner.EXTERNAL && instance.status !== InstanceStatus.STOPPED) {
244
255
  instance.status = InstanceStatus.STOPPED;
245
256
  instance.pid = null;
@@ -247,7 +258,7 @@ export class InstanceManager {
247
258
  this.emitSystemEvent(systemId, EVENT_STATUS_CHANGED, instance);
248
259
  this.save();
249
260
  }
250
- })
261
+ });
251
262
  }
252
263
 
253
264
  public async startAllForPlan(systemId: string): Promise<InstanceInfo[]> {
@@ -281,12 +292,11 @@ export class InstanceManager {
281
292
  return settled.map((p) => (p.status === 'fulfilled' ? p.value : null)).filter((p) => !!p) as InstanceInfo[];
282
293
  }
283
294
 
284
-
285
295
  public async stop(systemId: string, instanceId: string) {
286
296
  return this.stopInner(systemId, instanceId, true);
287
297
  }
288
298
 
289
- private async stopInner(systemId: string, instanceId: string, changeDesired:boolean = false) {
299
+ private async stopInner(systemId: string, instanceId: string, changeDesired: boolean = false) {
290
300
  return this.exclusive(systemId, instanceId, async () => {
291
301
  systemId = normalizeKapetaUri(systemId);
292
302
  const instance = this.getInstance(systemId, instanceId);
@@ -298,8 +308,7 @@ export class InstanceManager {
298
308
  return;
299
309
  }
300
310
 
301
- if (changeDesired &&
302
- instance.desiredStatus !== DesiredInstanceStatus.EXTERNAL) {
311
+ if (changeDesired && instance.desiredStatus !== DesiredInstanceStatus.EXTERNAL) {
303
312
  instance.desiredStatus = DesiredInstanceStatus.STOP;
304
313
  }
305
314
 
@@ -359,7 +368,7 @@ export class InstanceManager {
359
368
  throw new Error('Plan not found: ' + systemId);
360
369
  }
361
370
 
362
- const blockInstance = plan.spec && plan.spec.blocks ? _.find(plan.spec.blocks, {id: instanceId}) : null;
371
+ const blockInstance = plan.spec && plan.spec.blocks ? _.find(plan.spec.blocks, { id: instanceId }) : null;
363
372
  if (!blockInstance) {
364
373
  throw new Error('Block instance not found: ' + instanceId);
365
374
  }
@@ -412,6 +421,28 @@ export class InstanceManager {
412
421
  // Save the instance before starting it, so that we can track the status
413
422
  await this.saveInternalInstance(instance);
414
423
 
424
+ const blockSpec = blockAsset.data.spec as BlockDefinitionSpec;
425
+ if (blockSpec.consumers) {
426
+ const promises = blockSpec.consumers.map((consumer) => {
427
+ const consumerUri = parseKapetaUri(consumer.kind);
428
+ const asset = definitionsManager.getDefinition(consumer.kind);
429
+ if (!asset) {
430
+ // Definition not found
431
+ return Promise.resolve();
432
+ }
433
+
434
+ if (KIND_OPERATOR.toLowerCase() !== asset.definition.kind.toLowerCase()) {
435
+ // Not an operator
436
+ return Promise.resolve();
437
+ }
438
+
439
+ console.log('Ensuring resource: %s in %s', consumerUri.id, systemId);
440
+ return operatorManager.ensureResource(systemId, consumerUri.fullName, consumerUri.version);
441
+ });
442
+
443
+ await Promise.all(promises);
444
+ }
445
+
415
446
  if (existingInstance) {
416
447
  // Check if the instance is already running - but after we've commmuicated the desired status
417
448
  const currentStatus = await this.requestInstanceStatus(existingInstance);
@@ -439,7 +470,7 @@ export class InstanceManager {
439
470
  status: InstanceStatus.READY,
440
471
  });
441
472
  } catch (e: any) {
442
- console.warn('Failed to start instance', e);
473
+ console.warn('Failed to start instance: ', systemId, instanceId, blockRef, e.message);
443
474
  const logs: LogEntry[] = [
444
475
  {
445
476
  source: 'stdout',
@@ -456,6 +487,7 @@ export class InstanceManager {
456
487
  health: null,
457
488
  portType: DEFAULT_HEALTH_PORT_TYPE,
458
489
  status: InstanceStatus.FAILED,
490
+ errorMessage: e.message ?? 'Failed to start - Check logs for details.',
459
491
  });
460
492
 
461
493
  this.emitInstanceEvent(systemId, instanceId, EVENT_INSTANCE_LOG, logs[0]);
@@ -471,17 +503,16 @@ export class InstanceManager {
471
503
  });
472
504
  }
473
505
 
474
- public async restart(systemId: string, instanceId: string) {
506
+ /**
507
+ * Stops an instance but does not remove it from the list of active instances
508
+ *
509
+ * It will be started again next time the system checks the status of the instance
510
+ *
511
+ * We do it this way to not cause the user to wait for the instance to start again
512
+ */
513
+ public async prepareForRestart(systemId: string, instanceId: string) {
475
514
  systemId = normalizeKapetaUri(systemId);
476
515
  await this.stopInner(systemId, instanceId);
477
-
478
- const existingInstance = this.getInstance(systemId, instanceId);
479
- if (existingInstance?.desiredStatus === DesiredInstanceStatus.STOP) {
480
- // Internal instance was marked as stopped - abort restart
481
- return existingInstance;
482
- }
483
-
484
- return this.start(systemId, instanceId);
485
516
  }
486
517
 
487
518
  public async stopAll() {
@@ -553,8 +584,7 @@ export class InstanceManager {
553
584
  const skipUpdate =
554
585
  (newStatus === InstanceStatus.STOPPED && instance.status === InstanceStatus.FAILED) ||
555
586
  ([InstanceStatus.READY, InstanceStatus.UNHEALTHY].includes(newStatus) &&
556
- instance.status === InstanceStatus.STOPPING &&
557
- instance.desiredStatus === DesiredInstanceStatus.STOP) ||
587
+ instance.status === InstanceStatus.STOPPING) ||
558
588
  (newStatus === InstanceStatus.STOPPED &&
559
589
  instance.status === InstanceStatus.STARTING &&
560
590
  instance.desiredStatus === DesiredInstanceStatus.RUN);
@@ -608,7 +638,7 @@ export class InstanceManager {
608
638
  //If the instance is unhealthy, try to restart it
609
639
  console.log('Restarting unhealthy instance', instance);
610
640
  try {
611
- await this.restart(instance.systemId, instance.instanceId);
641
+ await this.prepareForRestart(instance.systemId, instance.instanceId);
612
642
  } catch (e) {
613
643
  console.warn('Failed to restart instance', instance.systemId, instance.instanceId, e);
614
644
  }
@@ -86,7 +86,7 @@ router.get('/:systemId/:instanceId/logs', async (req: Request, res: Response) =>
86
86
  res.status(200).send({
87
87
  logs,
88
88
  });
89
- } catch (e:any) {
89
+ } catch (e: any) {
90
90
  res.status(500).send({ ok: false, error: e.message });
91
91
  }
92
92
  });