@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.
- package/CHANGELOG.md +14 -0
- package/definitions.d.ts +7 -0
- package/dist/cjs/src/config/routes.js +1 -1
- package/dist/cjs/src/containerManager.d.ts +3 -2
- package/dist/cjs/src/containerManager.js +127 -34
- 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 +56 -21
- package/dist/cjs/src/instances/routes.js +2 -0
- package/dist/cjs/src/operatorManager.d.ts +2 -0
- package/dist/cjs/src/operatorManager.js +70 -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 -3
- package/dist/esm/src/config/routes.js +1 -1
- package/dist/esm/src/containerManager.d.ts +3 -2
- package/dist/esm/src/containerManager.js +128 -35
- 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 +56 -21
- package/dist/esm/src/instances/routes.js +2 -0
- package/dist/esm/src/operatorManager.d.ts +2 -0
- package/dist/esm/src/operatorManager.js +68 -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 -3
- 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 +181 -60
- package/src/definitionsManager.ts +9 -5
- package/src/instanceManager.ts +82 -42
- package/src/instances/routes.ts +3 -1
- package/src/operatorManager.ts +73 -69
- package/src/socketManager.ts +4 -0
- package/src/types.ts +1 -1
- package/src/utils/BlockInstanceRunner.ts +12 -24
- package/src/utils/utils.ts +2 -2
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
|
|
@@ -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
|
-
|
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(
|
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,
|
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
|
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
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
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
|
-
|
252
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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 =
|
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
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
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
|
-
})
|
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
|
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, 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
|
-
|
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
|
-
|
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
|
-
|
103
|
+
return await containerManager.getLogs(instance);
|
90
104
|
|
91
105
|
case InstanceType.UNKNOWN:
|
92
|
-
return [
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
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
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
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 (
|
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.
|
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
|
-
|
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.
|
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
|
}
|
package/src/instances/routes.ts
CHANGED
@@ -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;
|