@kapeta/local-cluster-service 0.11.1 → 0.12.1

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 (42) hide show
  1. package/CHANGELOG.md +14 -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 +3 -2
  5. package/dist/cjs/src/containerManager.js +127 -34
  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 +56 -21
  10. package/dist/cjs/src/instances/routes.js +2 -0
  11. package/dist/cjs/src/operatorManager.d.ts +2 -0
  12. package/dist/cjs/src/operatorManager.js +70 -67
  13. package/dist/cjs/src/socketManager.d.ts +1 -0
  14. package/dist/cjs/src/socketManager.js +3 -0
  15. package/dist/cjs/src/types.d.ts +1 -0
  16. package/dist/cjs/src/utils/BlockInstanceRunner.js +2 -3
  17. package/dist/esm/src/config/routes.js +1 -1
  18. package/dist/esm/src/containerManager.d.ts +3 -2
  19. package/dist/esm/src/containerManager.js +128 -35
  20. package/dist/esm/src/definitionsManager.d.ts +1 -0
  21. package/dist/esm/src/definitionsManager.js +8 -5
  22. package/dist/esm/src/instanceManager.d.ts +8 -1
  23. package/dist/esm/src/instanceManager.js +56 -21
  24. package/dist/esm/src/instances/routes.js +2 -0
  25. package/dist/esm/src/operatorManager.d.ts +2 -0
  26. package/dist/esm/src/operatorManager.js +68 -65
  27. package/dist/esm/src/socketManager.d.ts +1 -0
  28. package/dist/esm/src/socketManager.js +3 -0
  29. package/dist/esm/src/types.d.ts +1 -0
  30. package/dist/esm/src/utils/BlockInstanceRunner.js +2 -3
  31. package/dist/esm/src/utils/utils.js +1 -1
  32. package/package.json +1 -1
  33. package/src/config/routes.ts +1 -1
  34. package/src/containerManager.ts +181 -60
  35. package/src/definitionsManager.ts +9 -5
  36. package/src/instanceManager.ts +82 -42
  37. package/src/instances/routes.ts +3 -1
  38. package/src/operatorManager.ts +73 -69
  39. package/src/socketManager.ts +4 -0
  40. package/src/types.ts +1 -1
  41. package/src/utils/BlockInstanceRunner.ts +12 -24
  42. package/src/utils/utils.ts +2 -2
@@ -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
 
@@ -62,12 +66,11 @@ export const CONTAINER_LABEL_PORT_PREFIX = 'kapeta_port-';
62
66
  const NANO_SECOND = 1000000;
63
67
  const HEALTH_CHECK_INTERVAL = 3000;
64
68
  const HEALTH_CHECK_MAX = 20;
65
- const IMAGE_PULL_CACHE_TTL = 30 * 60 * 1000;
66
- const IMAGE_PULL_CACHE: { [key: string]: number } = {};
69
+
67
70
 
68
71
  export const HEALTH_CHECK_TIMEOUT = HEALTH_CHECK_INTERVAL * HEALTH_CHECK_MAX * 2;
69
72
 
70
- const promisifyStream = (stream: ReadStream, handler:(d:string|Buffer) => void) =>
73
+ const promisifyStream = (stream: ReadStream, handler: (d: string | Buffer) => void) =>
71
74
  new Promise((resolve, reject) => {
72
75
  stream.on('data', handler);
73
76
  stream.on('end', resolve);
@@ -79,6 +82,7 @@ class ContainerManager {
79
82
  private _alive: boolean;
80
83
  private _mountDir: string;
81
84
  private _version: string;
85
+ private _lastDockerAccessCheck: number = 0;
82
86
 
83
87
  constructor() {
84
88
  this._docker = null;
@@ -155,24 +159,27 @@ class ContainerManager {
155
159
  return this._alive;
156
160
  }
157
161
 
158
- getMountPoint(systemId:string, ref: string, mountName: string) {
162
+ getMountPoint(systemId: string, ref: string, mountName: string) {
159
163
  const kindUri = parseKapetaUri(ref);
160
- const systemUri = parseKapetaUri(systemId)
161
- return Path.join(this._mountDir,
164
+ const systemUri = parseKapetaUri(systemId);
165
+ return Path.join(
166
+ this._mountDir,
162
167
  systemUri.handle,
163
168
  systemUri.name,
164
169
  systemUri.version,
165
170
  kindUri.handle,
166
171
  kindUri.name,
167
- kindUri.version, mountName);
172
+ kindUri.version,
173
+ mountName
174
+ );
168
175
  }
169
176
 
170
- async createMounts(systemId:string, kind: string, mountOpts: StringMap|null|undefined): Promise<StringMap> {
177
+ async createMounts(systemId: string, kind: string, mountOpts: StringMap | null | undefined): Promise<StringMap> {
171
178
  const mounts: StringMap = {};
172
179
 
173
180
  if (mountOpts) {
174
181
  const mountOptList = Object.entries(mountOpts);
175
- for(const [mountName, containerPath] of mountOptList) {
182
+ for (const [mountName, containerPath] of mountOptList) {
176
183
  const hostPath = this.getMountPoint(systemId, kind, mountName);
177
184
  await FSExtra.mkdirp(hostPath);
178
185
  mounts[containerPath] = hostPath;
@@ -215,19 +222,12 @@ class ContainerManager {
215
222
  return undefined;
216
223
  }
217
224
 
218
- async pull(image: string, cacheForMS: number = IMAGE_PULL_CACHE_TTL) {
225
+ async pull(image: string) {
219
226
  let [imageName, tag] = image.split(/:/);
220
227
  if (!tag) {
221
228
  tag = 'latest';
222
229
  }
223
230
 
224
- if (IMAGE_PULL_CACHE[image]) {
225
- const timeSince = Date.now() - IMAGE_PULL_CACHE[image];
226
- if (timeSince < cacheForMS) {
227
- return false;
228
- }
229
- }
230
-
231
231
  const imageTagList = (await this.docker().image.list())
232
232
  .map((image) => image.data as any)
233
233
  .filter((imageData) => !!imageData.RepoTags)
@@ -238,23 +238,151 @@ class ContainerManager {
238
238
  return false;
239
239
  }
240
240
 
241
- console.log('Pulling image: %s', image);
242
- const stream = await this.docker()
243
- .image.create(
244
- {},
245
- {
246
- fromImage: imageName,
247
- tag: tag,
241
+ const timeStarted = Date.now();
242
+ socketManager.emitGlobal(EVENT_IMAGE_PULL, { image, percent: -1 });
243
+
244
+ const api = new KapetaAPI();
245
+ const accessToken = await api.getAccessToken();
246
+
247
+ const auth = image.startsWith('docker.kapeta.com/')
248
+ ? {
249
+ username: 'kapeta',
250
+ password: accessToken,
251
+ serveraddress: 'docker.kapeta.com',
252
+ }
253
+ : {};
254
+
255
+ const stream = (await this.docker().image.create(auth, {
256
+ fromImage: imageName,
257
+ tag: tag,
258
+ })) as ReadStream;
259
+
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
+ ) {
289
+ return;
248
290
  }
249
- ) as ReadStream;
250
291
 
251
- await promisifyStream(stream, (chunk) => {
252
- console.log('Data from docker: "%s"', chunk.toString());
253
- });
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
+ });
332
+
333
+ if (Date.now() - lastEmitted < 1000) {
334
+ return;
335
+ }
254
336
 
255
- IMAGE_PULL_CACHE[image] = Date.now();
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
+ };
256
350
 
257
- console.log('Image pulled: %s', image);
351
+ chunkList.forEach((chunk) => {
352
+ if (chunk.downloading.current > 0) {
353
+ totals.downloading.current += chunk.downloading.current;
354
+ }
355
+
356
+ if (chunk.downloading.total > 0) {
357
+ totals.downloading.total += chunk.downloading.total;
358
+ }
359
+
360
+ if (chunk.extracting.current > 0) {
361
+ totals.extracting.current += chunk.extracting.current;
362
+ }
363
+
364
+ if (chunk.extracting.total > 0) {
365
+ totals.extracting.total += chunk.extracting.total;
366
+ }
367
+
368
+ if (chunk.done) {
369
+ totals.done++;
370
+ }
371
+ });
372
+
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, {
376
+ image,
377
+ percent,
378
+ status: totals,
379
+ 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);
383
+ });
384
+
385
+ socketManager.emitGlobal(EVENT_IMAGE_PULL, { image, percent: 100, timeTaken: Date.now() - timeStarted });
258
386
 
259
387
  return true;
260
388
  }
@@ -297,20 +425,11 @@ class ContainerManager {
297
425
  }
298
426
 
299
427
  public async ensureContainer(opts: any) {
300
- const container = await this.createOrUpdateContainer(opts);
301
-
302
- await this.waitForReady(container);
303
-
304
- return container;
428
+ return await this.createOrUpdateContainer(opts);
305
429
  }
306
430
 
307
431
  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
- }
432
+ let imagePulled = await this.pull(opts.Image);
314
433
 
315
434
  this.applyHash(opts);
316
435
  if (!opts.name) {
@@ -448,24 +567,27 @@ class ContainerManager {
448
567
  return new ContainerInfo(dockerContainer);
449
568
  }
450
569
 
451
- async getLogs(instance: InstanceInfo):Promise<LogEntry[]> {
570
+ async getLogs(instance: InstanceInfo): Promise<LogEntry[]> {
452
571
  const containerName = getBlockInstanceContainerName(instance.systemId, instance.instanceId);
453
572
  const containerInfo = await this.getContainerByName(containerName);
454
573
  if (!containerInfo) {
455
- return [{
456
- source: "stdout",
457
- level: "ERROR",
458
- time: Date.now(),
459
- message: "Container not found"
460
- }];
574
+ return [
575
+ {
576
+ source: 'stdout',
577
+ level: 'ERROR',
578
+ time: Date.now(),
579
+ message: 'Container not found',
580
+ },
581
+ ];
461
582
  }
462
583
 
463
- return containerInfo.getLogs()
584
+ return containerInfo.getLogs();
464
585
  }
465
586
  }
466
587
 
467
588
  export class ContainerInfo {
468
589
  private readonly _container: Container;
590
+
469
591
  /**
470
592
  *
471
593
  * @param {Container} dockerContainer
@@ -572,21 +694,20 @@ export class ContainerInfo {
572
694
  return ports;
573
695
  }
574
696
 
575
- async getLogs():Promise<LogEntry[]> {
576
-
577
- const logStream = await this.native.logs({
697
+ async getLogs(): Promise<LogEntry[]> {
698
+ const logStream = (await this.native.logs({
578
699
  stdout: true,
579
700
  stderr: true,
580
701
  follow: false,
581
702
  tail: 100,
582
703
  timestamps: true,
583
- }) as ReadStream;
704
+ })) as ReadStream;
584
705
 
585
706
  const out = [] as LogEntry[];
586
707
  await promisifyStream(logStream, (data) => {
587
708
  const buf = data as Buffer;
588
709
  let offset = 0;
589
- while(offset < buf.length) {
710
+ while (offset < buf.length) {
590
711
  try {
591
712
  // Read the docker log format - explained here:
592
713
  // https://docs.docker.com/engine/api/v1.41/#operation/ContainerAttach
@@ -594,7 +715,7 @@ export class ContainerInfo {
594
715
 
595
716
  // First byte is stream type
596
717
  const streamTypeInt = buf.readInt8(offset);
597
- const streamType:LogSource = streamTypeInt === 1 ? 'stdout' : 'stderr';
718
+ const streamType: LogSource = streamTypeInt === 1 ? 'stdout' : 'stderr';
598
719
 
599
720
  // Bytes 4-8 is frame size
600
721
  const messageLength = buf.readInt32BE(offset + 4);
@@ -619,7 +740,7 @@ export class ContainerInfo {
619
740
  });
620
741
  } catch (err) {
621
742
  console.error('Error parsing log entry', err);
622
- offset = buf.length
743
+ offset = buf.length;
623
744
  }
624
745
  }
625
746
  });
@@ -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, Plan} 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', []);
@@ -61,7 +64,17 @@ export class InstanceManager {
61
64
 
62
65
  systemId = normalizeKapetaUri(systemId);
63
66
 
64
- return this._instances.filter((instance) => instance.systemId === systemId);
67
+ const planInfo = definitionsManager.getDefinition(systemId);
68
+
69
+ if (!planInfo) {
70
+ return [];
71
+ }
72
+
73
+ const plan = planInfo.definition as Plan;
74
+
75
+ const instanceIds = plan.spec.blocks.map((block) => block.id);
76
+
77
+ return this._instances.filter((instance) => instance.systemId === systemId && instanceIds.includes(instance.instanceId));
65
78
  }
66
79
 
67
80
  public getInstance(systemId: string, instanceId: string) {
@@ -70,15 +83,16 @@ export class InstanceManager {
70
83
  return this._instances.find((i) => i.systemId === systemId && i.instanceId === instanceId);
71
84
  }
72
85
 
73
-
74
86
  private async exclusive<T = any>(systemId: string, instanceId: string, fn: () => Promise<T>) {
75
87
  systemId = normalizeKapetaUri(systemId);
76
88
  const key = `${systemId}/${instanceId}`;
77
- return this.instanceLocks.acquire(key, fn);
89
+ //console.log(`Acquiring lock for ${key}`, this.instanceLocks.isBusy(key));
90
+ const result = await this.instanceLocks.acquire(key, fn);
91
+ //console.log(`Releasing lock for ${key}`, this.instanceLocks.isBusy(key));
92
+ return result;
78
93
  }
79
94
 
80
-
81
- public async getLogs(systemId: string, instanceId: string):Promise<LogEntry[]> {
95
+ public async getLogs(systemId: string, instanceId: string): Promise<LogEntry[]> {
82
96
  const instance = this.getInstance(systemId, instanceId);
83
97
  if (!instance) {
84
98
  throw new Error(`Instance ${systemId}/${instanceId} not found`);
@@ -86,23 +100,27 @@ export class InstanceManager {
86
100
 
87
101
  switch (instance.type) {
88
102
  case InstanceType.DOCKER:
89
- return await containerManager.getLogs(instance);
103
+ return await containerManager.getLogs(instance);
90
104
 
91
105
  case InstanceType.UNKNOWN:
92
- return [{
93
- level: 'INFO',
94
- message: 'Instance is starting...',
95
- time: Date.now(),
96
- source: 'stdout',
97
- }];
106
+ return [
107
+ {
108
+ level: 'INFO',
109
+ message: 'Instance is starting...',
110
+ time: Date.now(),
111
+ source: 'stdout',
112
+ },
113
+ ];
98
114
 
99
115
  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
- }];
116
+ return [
117
+ {
118
+ level: 'INFO',
119
+ message: 'Instance started outside Kapeta - logs not available...',
120
+ time: Date.now(),
121
+ source: 'stdout',
122
+ },
123
+ ];
106
124
  }
107
125
 
108
126
  return [];
@@ -167,7 +185,10 @@ export class InstanceManager {
167
185
  const healthUrl = this.getHealthUrl(info, address);
168
186
 
169
187
  if (instance) {
170
- if (instance.status === InstanceStatus.STOPPING && instance.desiredStatus === DesiredInstanceStatus.STOP) {
188
+ if (
189
+ instance.status === InstanceStatus.STOPPING &&
190
+ instance.desiredStatus === DesiredInstanceStatus.STOP
191
+ ) {
171
192
  //If instance is stopping do not interfere
172
193
  return;
173
194
  }
@@ -239,7 +260,7 @@ export class InstanceManager {
239
260
  public markAsStopped(systemId: string, instanceId: string) {
240
261
  return this.exclusive(systemId, instanceId, async () => {
241
262
  systemId = normalizeKapetaUri(systemId);
242
- const instance = _.find(this._instances, {systemId, instanceId});
263
+ const instance = _.find(this._instances, { systemId, instanceId });
243
264
  if (instance && instance.owner === InstanceOwner.EXTERNAL && instance.status !== InstanceStatus.STOPPED) {
244
265
  instance.status = InstanceStatus.STOPPED;
245
266
  instance.pid = null;
@@ -247,7 +268,7 @@ export class InstanceManager {
247
268
  this.emitSystemEvent(systemId, EVENT_STATUS_CHANGED, instance);
248
269
  this.save();
249
270
  }
250
- })
271
+ });
251
272
  }
252
273
 
253
274
  public async startAllForPlan(systemId: string): Promise<InstanceInfo[]> {
@@ -281,12 +302,11 @@ export class InstanceManager {
281
302
  return settled.map((p) => (p.status === 'fulfilled' ? p.value : null)).filter((p) => !!p) as InstanceInfo[];
282
303
  }
283
304
 
284
-
285
305
  public async stop(systemId: string, instanceId: string) {
286
306
  return this.stopInner(systemId, instanceId, true);
287
307
  }
288
308
 
289
- private async stopInner(systemId: string, instanceId: string, changeDesired:boolean = false) {
309
+ private async stopInner(systemId: string, instanceId: string, changeDesired: boolean = false) {
290
310
  return this.exclusive(systemId, instanceId, async () => {
291
311
  systemId = normalizeKapetaUri(systemId);
292
312
  const instance = this.getInstance(systemId, instanceId);
@@ -298,8 +318,7 @@ export class InstanceManager {
298
318
  return;
299
319
  }
300
320
 
301
- if (changeDesired &&
302
- instance.desiredStatus !== DesiredInstanceStatus.EXTERNAL) {
321
+ if (changeDesired && instance.desiredStatus !== DesiredInstanceStatus.EXTERNAL) {
303
322
  instance.desiredStatus = DesiredInstanceStatus.STOP;
304
323
  }
305
324
 
@@ -359,7 +378,7 @@ export class InstanceManager {
359
378
  throw new Error('Plan not found: ' + systemId);
360
379
  }
361
380
 
362
- const blockInstance = plan.spec && plan.spec.blocks ? _.find(plan.spec.blocks, {id: instanceId}) : null;
381
+ const blockInstance = plan.spec && plan.spec.blocks ? _.find(plan.spec.blocks, { id: instanceId }) : null;
363
382
  if (!blockInstance) {
364
383
  throw new Error('Block instance not found: ' + instanceId);
365
384
  }
@@ -412,6 +431,28 @@ export class InstanceManager {
412
431
  // Save the instance before starting it, so that we can track the status
413
432
  await this.saveInternalInstance(instance);
414
433
 
434
+ const blockSpec = blockAsset.data.spec as BlockDefinitionSpec;
435
+ if (blockSpec.consumers) {
436
+ const promises = blockSpec.consumers.map((consumer) => {
437
+ const consumerUri = parseKapetaUri(consumer.kind);
438
+ const asset = definitionsManager.getDefinition(consumer.kind);
439
+ if (!asset) {
440
+ // Definition not found
441
+ return Promise.resolve();
442
+ }
443
+
444
+ if (KIND_OPERATOR.toLowerCase() !== asset.definition.kind.toLowerCase()) {
445
+ // Not an operator
446
+ return Promise.resolve();
447
+ }
448
+
449
+ console.log('Ensuring resource: %s in %s', consumerUri.id, systemId);
450
+ return operatorManager.ensureResource(systemId, consumerUri.fullName, consumerUri.version);
451
+ });
452
+
453
+ await Promise.all(promises);
454
+ }
455
+
415
456
  if (existingInstance) {
416
457
  // Check if the instance is already running - but after we've commmuicated the desired status
417
458
  const currentStatus = await this.requestInstanceStatus(existingInstance);
@@ -439,7 +480,7 @@ export class InstanceManager {
439
480
  status: InstanceStatus.READY,
440
481
  });
441
482
  } catch (e: any) {
442
- console.warn('Failed to start instance', e);
483
+ console.warn('Failed to start instance: ', systemId, instanceId, blockRef, e.message);
443
484
  const logs: LogEntry[] = [
444
485
  {
445
486
  source: 'stdout',
@@ -451,11 +492,12 @@ export class InstanceManager {
451
492
 
452
493
  const out = await this.saveInternalInstance({
453
494
  ...instance,
454
- type: InstanceType.LOCAL,
495
+ type: InstanceType.UNKNOWN,
455
496
  pid: null,
456
497
  health: null,
457
498
  portType: DEFAULT_HEALTH_PORT_TYPE,
458
499
  status: InstanceStatus.FAILED,
500
+ errorMessage: e.message ?? 'Failed to start - Check logs for details.',
459
501
  });
460
502
 
461
503
  this.emitInstanceEvent(systemId, instanceId, EVENT_INSTANCE_LOG, logs[0]);
@@ -471,17 +513,16 @@ export class InstanceManager {
471
513
  });
472
514
  }
473
515
 
474
- public async restart(systemId: string, instanceId: string) {
516
+ /**
517
+ * Stops an instance but does not remove it from the list of active instances
518
+ *
519
+ * It will be started again next time the system checks the status of the instance
520
+ *
521
+ * We do it this way to not cause the user to wait for the instance to start again
522
+ */
523
+ public async prepareForRestart(systemId: string, instanceId: string) {
475
524
  systemId = normalizeKapetaUri(systemId);
476
525
  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
526
  }
486
527
 
487
528
  public async stopAll() {
@@ -553,8 +594,7 @@ export class InstanceManager {
553
594
  const skipUpdate =
554
595
  (newStatus === InstanceStatus.STOPPED && instance.status === InstanceStatus.FAILED) ||
555
596
  ([InstanceStatus.READY, InstanceStatus.UNHEALTHY].includes(newStatus) &&
556
- instance.status === InstanceStatus.STOPPING &&
557
- instance.desiredStatus === DesiredInstanceStatus.STOP) ||
597
+ instance.status === InstanceStatus.STOPPING) ||
558
598
  (newStatus === InstanceStatus.STOPPED &&
559
599
  instance.status === InstanceStatus.STARTING &&
560
600
  instance.desiredStatus === DesiredInstanceStatus.RUN);
@@ -608,7 +648,7 @@ export class InstanceManager {
608
648
  //If the instance is unhealthy, try to restart it
609
649
  console.log('Restarting unhealthy instance', instance);
610
650
  try {
611
- await this.restart(instance.systemId, instance.instanceId);
651
+ await this.prepareForRestart(instance.systemId, instance.instanceId);
612
652
  } catch (e) {
613
653
  console.warn('Failed to restart instance', instance.systemId, instance.instanceId, e);
614
654
  }
@@ -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
  });
@@ -146,8 +146,10 @@ router.put('/', async (req: KapetaBodyRequest, res: Response) => {
146
146
  const oldInstance = instanceManager.getInstance(req.kapeta!.systemId, req.kapeta!.instanceId);
147
147
  if (oldInstance) {
148
148
  instance.pid = oldInstance.pid;
149
+ instance.desiredStatus = oldInstance.desiredStatus;
149
150
  }
150
151
  instance.type = InstanceType.DOCKER;
152
+ instance.owner = InstanceOwner.INTERNAL;
151
153
  } else {
152
154
  // Coming from user starting the instance outside of kapeta
153
155
  instance.type = InstanceType.LOCAL;