@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.
- package/CHANGELOG.md +7 -0
- package/definitions.d.ts +7 -0
- package/dist/cjs/src/config/routes.js +1 -1
- package/dist/cjs/src/containerManager.d.ts +2 -1
- package/dist/cjs/src/containerManager.js +125 -21
- package/dist/cjs/src/definitionsManager.d.ts +1 -0
- package/dist/cjs/src/definitionsManager.js +7 -4
- package/dist/cjs/src/instanceManager.d.ts +8 -1
- package/dist/cjs/src/instanceManager.js +48 -19
- package/dist/cjs/src/operatorManager.d.ts +2 -0
- package/dist/cjs/src/operatorManager.js +69 -67
- package/dist/cjs/src/socketManager.d.ts +1 -0
- package/dist/cjs/src/socketManager.js +3 -0
- package/dist/cjs/src/types.d.ts +1 -0
- package/dist/cjs/src/utils/BlockInstanceRunner.js +2 -2
- package/dist/esm/src/config/routes.js +1 -1
- package/dist/esm/src/containerManager.d.ts +2 -1
- package/dist/esm/src/containerManager.js +126 -22
- package/dist/esm/src/definitionsManager.d.ts +1 -0
- package/dist/esm/src/definitionsManager.js +8 -5
- package/dist/esm/src/instanceManager.d.ts +8 -1
- package/dist/esm/src/instanceManager.js +48 -19
- package/dist/esm/src/operatorManager.d.ts +2 -0
- package/dist/esm/src/operatorManager.js +67 -65
- package/dist/esm/src/socketManager.d.ts +1 -0
- package/dist/esm/src/socketManager.js +3 -0
- package/dist/esm/src/types.d.ts +1 -0
- package/dist/esm/src/utils/BlockInstanceRunner.js +2 -2
- package/dist/esm/src/utils/utils.js +1 -1
- package/package.json +1 -1
- package/src/config/routes.ts +1 -1
- package/src/containerManager.ts +178 -43
- package/src/definitionsManager.ts +9 -5
- package/src/instanceManager.ts +70 -40
- package/src/instances/routes.ts +1 -1
- package/src/operatorManager.ts +72 -70
- package/src/socketManager.ts +4 -0
- package/src/types.ts +1 -1
- package/src/utils/BlockInstanceRunner.ts +12 -22
- 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
|
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
package/src/config/routes.ts
CHANGED
@@ -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.
|
44
|
+
await instanceManager.prepareForRestart(req.kapeta!.systemId, req.kapeta!.instanceId);
|
45
45
|
} else {
|
46
46
|
configManager.setConfigForSystem(req.kapeta!.systemId, config);
|
47
47
|
}
|
package/src/containerManager.ts
CHANGED
@@ -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
|
13
|
-
import {InstanceInfo, LogEntry, LogSource} from
|
14
|
-
import
|
15
|
-
import {
|
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(
|
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,
|
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
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
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
|
-
)
|
379
|
+
});
|
250
380
|
|
251
|
-
|
252
|
-
|
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
|
-
|
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 =
|
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
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
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
|
-
})
|
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
|
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
|
-
|
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();
|
package/src/instanceManager.ts
CHANGED
@@ -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
|
-
|
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
|
-
|
93
|
+
return await containerManager.getLogs(instance);
|
90
94
|
|
91
95
|
case InstanceType.UNKNOWN:
|
92
|
-
return [
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
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
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
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 (
|
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
|
-
|
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.
|
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
|
}
|
package/src/instances/routes.ts
CHANGED