@kapeta/local-cluster-service 0.10.1 → 0.11.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/dist/cjs/src/containerManager.d.ts +6 -4
- package/dist/cjs/src/containerManager.js +100 -45
- package/dist/cjs/src/definitionsManager.d.ts +1 -0
- package/dist/cjs/src/definitionsManager.js +7 -0
- package/dist/cjs/src/instanceManager.d.ts +2 -1
- package/dist/cjs/src/instanceManager.js +29 -46
- package/dist/cjs/src/instances/routes.js +10 -4
- package/dist/cjs/src/operatorManager.js +8 -6
- package/dist/cjs/src/repositoryManager.js +4 -4
- package/dist/cjs/src/types.d.ts +0 -9
- package/dist/cjs/src/utils/BlockInstanceRunner.js +9 -64
- package/dist/cjs/src/utils/utils.d.ts +1 -1
- package/dist/cjs/src/utils/utils.js +3 -2
- package/dist/esm/src/containerManager.d.ts +6 -4
- package/dist/esm/src/containerManager.js +100 -45
- package/dist/esm/src/definitionsManager.d.ts +1 -0
- package/dist/esm/src/definitionsManager.js +7 -0
- package/dist/esm/src/instanceManager.d.ts +2 -1
- package/dist/esm/src/instanceManager.js +29 -46
- package/dist/esm/src/instances/routes.js +10 -4
- package/dist/esm/src/operatorManager.js +8 -6
- package/dist/esm/src/repositoryManager.js +4 -4
- package/dist/esm/src/types.d.ts +0 -9
- package/dist/esm/src/utils/BlockInstanceRunner.js +9 -64
- package/dist/esm/src/utils/utils.d.ts +1 -1
- package/dist/esm/src/utils/utils.js +3 -2
- package/package.json +1 -1
- package/src/containerManager.ts +126 -49
- package/src/definitionsManager.ts +8 -0
- package/src/instanceManager.ts +35 -50
- package/src/instances/routes.ts +9 -4
- package/src/operatorManager.ts +9 -8
- package/src/repositoryManager.ts +5 -5
- package/src/types.ts +0 -7
- package/src/utils/BlockInstanceRunner.ts +10 -66
- package/src/utils/LogData.ts +1 -0
- package/src/utils/utils.ts +3 -2
@@ -11,7 +11,6 @@ const nodejs_utils_1 = require("@kapeta/nodejs-utils");
|
|
11
11
|
const serviceManager_1 = require("../serviceManager");
|
12
12
|
const containerManager_1 = require("../containerManager");
|
13
13
|
const LogData_1 = require("./LogData");
|
14
|
-
const events_1 = __importDefault(require("events"));
|
15
14
|
const clusterService_1 = require("../clusterService");
|
16
15
|
const types_1 = require("../types");
|
17
16
|
const definitionsManager_1 = require("../definitionsManager");
|
@@ -133,7 +132,7 @@ class BlockInstanceRunner {
|
|
133
132
|
if (!dockerImage) {
|
134
133
|
throw new Error(`Missing docker image information: ${JSON.stringify(localContainer)}`);
|
135
134
|
}
|
136
|
-
const containerName = (0, utils_1.getBlockInstanceContainerName)(blockInstance.id);
|
135
|
+
const containerName = (0, utils_1.getBlockInstanceContainerName)(this._systemId, blockInstance.id);
|
137
136
|
const startCmd = localContainer.handlers?.onCreate ? localContainer.handlers.onCreate : '';
|
138
137
|
const dockerOpts = localContainer.options ?? {};
|
139
138
|
const homeDir = localContainer.userHome ? localContainer.userHome : '/root';
|
@@ -186,7 +185,7 @@ class BlockInstanceRunner {
|
|
186
185
|
throw new Error(`Missing docker image information: ${JSON.stringify(versionInfo?.artifact?.details)}`);
|
187
186
|
}
|
188
187
|
const { PortBindings, ExposedPorts, addonEnv } = await this.getDockerPortBindings(blockInstance, assetVersion);
|
189
|
-
const containerName = (0, utils_1.getBlockInstanceContainerName)(blockInstance.id);
|
188
|
+
const containerName = (0, utils_1.getBlockInstanceContainerName)(this._systemId, blockInstance.id);
|
190
189
|
// For windows we need to default to root
|
191
190
|
const innerHome = process.platform === 'win32' ? '/root/.kapeta' : local_cluster_config_1.default.getKapetaBasedir();
|
192
191
|
return this.ensureContainer({
|
@@ -231,7 +230,8 @@ class BlockInstanceRunner {
|
|
231
230
|
throw new Error(`Provider did not have local image: ${providerRef}`);
|
232
231
|
}
|
233
232
|
const dockerImage = spec?.local?.image;
|
234
|
-
|
233
|
+
//We only want 1 operator per operator type - across all local systems
|
234
|
+
const containerName = (0, utils_1.getBlockInstanceContainerName)(this._systemId, blockInstance.id);
|
235
235
|
const logs = new LogData_1.LogData();
|
236
236
|
const bindHost = (0, utils_1.getBindHost)();
|
237
237
|
const ExposedPorts = {};
|
@@ -258,7 +258,7 @@ class BlockInstanceRunner {
|
|
258
258
|
});
|
259
259
|
}
|
260
260
|
if (spec.local?.mounts) {
|
261
|
-
const mounts = containerManager_1.containerManager.createMounts(blockUri.id, spec.local.mounts);
|
261
|
+
const mounts = await containerManager_1.containerManager.createMounts(this._systemId, blockUri.id, spec.local.mounts);
|
262
262
|
Mounts = containerManager_1.containerManager.toDockerMounts(mounts);
|
263
263
|
}
|
264
264
|
if (spec.local?.health) {
|
@@ -323,69 +323,14 @@ class BlockInstanceRunner {
|
|
323
323
|
return { PortBindings, ExposedPorts, addonEnv };
|
324
324
|
}
|
325
325
|
async ensureContainer(opts) {
|
326
|
-
const logs = new LogData_1.LogData();
|
327
326
|
const container = await containerManager_1.containerManager.ensureContainer(opts);
|
328
|
-
|
329
|
-
|
330
|
-
await containerManager_1.containerManager.waitForHealthy(container);
|
331
|
-
}
|
332
|
-
else {
|
333
|
-
await containerManager_1.containerManager.waitForReady(container);
|
334
|
-
}
|
335
|
-
}
|
336
|
-
catch (e) {
|
337
|
-
logs.addLog(e.message, 'ERROR');
|
338
|
-
}
|
339
|
-
return this._handleContainer(container, logs);
|
327
|
+
await containerManager_1.containerManager.waitForReady(container);
|
328
|
+
return this._handleContainer(container);
|
340
329
|
}
|
341
|
-
async _handleContainer(container
|
342
|
-
let localContainer = container;
|
343
|
-
const logStream = (await container.logs({
|
344
|
-
follow: true,
|
345
|
-
stdout: true,
|
346
|
-
stderr: true,
|
347
|
-
tail: LogData_1.LogData.MAX_LINES,
|
348
|
-
}));
|
349
|
-
const outputEvents = new events_1.default();
|
350
|
-
logStream.on('data', (data) => {
|
351
|
-
logs.addLog(data.toString());
|
352
|
-
outputEvents.emit('data', data);
|
353
|
-
});
|
354
|
-
logStream.on('error', (data) => {
|
355
|
-
logs.addLog(data.toString());
|
356
|
-
outputEvents.emit('data', data);
|
357
|
-
});
|
358
|
-
logStream.on('close', async () => {
|
359
|
-
const status = await container.status();
|
360
|
-
const data = status.data;
|
361
|
-
if (deleteOnExit) {
|
362
|
-
try {
|
363
|
-
await containerManager_1.containerManager.remove(container);
|
364
|
-
}
|
365
|
-
catch (e) { }
|
366
|
-
}
|
367
|
-
outputEvents.emit('exit', data?.State?.ExitCode ?? 0);
|
368
|
-
});
|
330
|
+
async _handleContainer(container) {
|
369
331
|
return {
|
370
332
|
type: types_1.InstanceType.DOCKER,
|
371
|
-
pid: container.id
|
372
|
-
output: outputEvents,
|
373
|
-
stop: async () => {
|
374
|
-
if (!localContainer) {
|
375
|
-
return;
|
376
|
-
}
|
377
|
-
try {
|
378
|
-
await localContainer.stop();
|
379
|
-
if (deleteOnExit) {
|
380
|
-
await containerManager_1.containerManager.remove(localContainer);
|
381
|
-
}
|
382
|
-
}
|
383
|
-
catch (e) { }
|
384
|
-
localContainer = null;
|
385
|
-
},
|
386
|
-
logs: () => {
|
387
|
-
return logs.getLogs();
|
388
|
-
},
|
333
|
+
pid: container.id
|
389
334
|
};
|
390
335
|
}
|
391
336
|
}
|
@@ -1,4 +1,4 @@
|
|
1
|
-
export declare function getBlockInstanceContainerName(instanceId: string): string;
|
1
|
+
export declare function getBlockInstanceContainerName(systemId: string, instanceId: string): string;
|
2
2
|
export declare function normalizeKapetaUri(uri: string): string;
|
3
3
|
export declare function readYML(path: string): any;
|
4
4
|
export declare function isWindows(): boolean;
|
@@ -7,8 +7,9 @@ exports.getBindHost = exports.isLinux = exports.isMac = exports.isWindows = expo
|
|
7
7
|
const node_fs_1 = __importDefault(require("node:fs"));
|
8
8
|
const yaml_1 = __importDefault(require("yaml"));
|
9
9
|
const nodejs_utils_1 = require("@kapeta/nodejs-utils");
|
10
|
-
|
11
|
-
|
10
|
+
const md5_1 = __importDefault(require("md5"));
|
11
|
+
function getBlockInstanceContainerName(systemId, instanceId) {
|
12
|
+
return `kapeta-block-instance-${(0, md5_1.default)(systemId + instanceId)}`;
|
12
13
|
}
|
13
14
|
exports.getBlockInstanceContainerName = getBlockInstanceContainerName;
|
14
15
|
function normalizeKapetaUri(uri) {
|
@@ -1,5 +1,6 @@
|
|
1
1
|
import { Docker } from 'node-docker-api';
|
2
2
|
import { Container } from 'node-docker-api/lib/container';
|
3
|
+
import { InstanceInfo, LogEntry } from "./types";
|
3
4
|
type StringMap = {
|
4
5
|
[key: string]: string;
|
5
6
|
};
|
@@ -52,8 +53,8 @@ declare class ContainerManager {
|
|
52
53
|
initialize(): Promise<void>;
|
53
54
|
checkAlive(): Promise<boolean>;
|
54
55
|
isAlive(): boolean;
|
55
|
-
getMountPoint(
|
56
|
-
createMounts(kind: string, mountOpts: StringMap): StringMap
|
56
|
+
getMountPoint(systemId: string, ref: string, mountName: string): string;
|
57
|
+
createMounts(systemId: string, kind: string, mountOpts: StringMap | null | undefined): Promise<StringMap>;
|
57
58
|
ping(): Promise<void>;
|
58
59
|
docker(): Docker;
|
59
60
|
getContainerByName(containerName: string): Promise<ContainerInfo | undefined>;
|
@@ -67,11 +68,10 @@ declare class ContainerManager {
|
|
67
68
|
};
|
68
69
|
private applyHash;
|
69
70
|
ensureContainer(opts: any): Promise<Container>;
|
71
|
+
private createOrUpdateContainer;
|
70
72
|
startContainer(opts: any): Promise<Container>;
|
71
73
|
waitForReady(container: Container, attempt?: number): Promise<void>;
|
72
|
-
waitForHealthy(container: Container, attempt?: number): Promise<void>;
|
73
74
|
_isReady(container: Container): Promise<any>;
|
74
|
-
_isHealthy(container: Container): Promise<boolean>;
|
75
75
|
remove(container: Container, opts?: {
|
76
76
|
force?: boolean;
|
77
77
|
}): Promise<void>;
|
@@ -81,6 +81,7 @@ declare class ContainerManager {
|
|
81
81
|
* @return {Promise<ContainerInfo>}
|
82
82
|
*/
|
83
83
|
get(name: string): Promise<ContainerInfo | null>;
|
84
|
+
getLogs(instance: InstanceInfo): Promise<LogEntry[]>;
|
84
85
|
}
|
85
86
|
export declare class ContainerInfo {
|
86
87
|
private readonly _container;
|
@@ -105,6 +106,7 @@ export declare class ContainerInfo {
|
|
105
106
|
inspect(): Promise<any>;
|
106
107
|
status(): Promise<DockerState>;
|
107
108
|
getPorts(): Promise<PortMap | false>;
|
109
|
+
getLogs(): Promise<LogEntry[]>;
|
108
110
|
}
|
109
111
|
export declare function getExtraHosts(dockerVersion: string): string[] | undefined;
|
110
112
|
/**
|
@@ -8,6 +8,7 @@ import { parseKapetaUri } from '@kapeta/nodejs-utils';
|
|
8
8
|
import ClusterConfiguration from '@kapeta/local-cluster-config';
|
9
9
|
import uuid from 'node-uuid';
|
10
10
|
import md5 from 'md5';
|
11
|
+
import { getBlockInstanceContainerName } from "./utils/utils";
|
11
12
|
export const CONTAINER_LABEL_PORT_PREFIX = 'kapeta_port-';
|
12
13
|
const NANO_SECOND = 1000000;
|
13
14
|
const HEALTH_CHECK_INTERVAL = 3000;
|
@@ -15,8 +16,8 @@ const HEALTH_CHECK_MAX = 20;
|
|
15
16
|
const IMAGE_PULL_CACHE_TTL = 30 * 60 * 1000;
|
16
17
|
const IMAGE_PULL_CACHE = {};
|
17
18
|
export const HEALTH_CHECK_TIMEOUT = HEALTH_CHECK_INTERVAL * HEALTH_CHECK_MAX * 2;
|
18
|
-
const promisifyStream = (stream) => new Promise((resolve, reject) => {
|
19
|
-
stream.on('data',
|
19
|
+
const promisifyStream = (stream, handler) => new Promise((resolve, reject) => {
|
20
|
+
stream.on('data', handler);
|
20
21
|
stream.on('end', resolve);
|
21
22
|
stream.on('error', reject);
|
22
23
|
});
|
@@ -95,17 +96,21 @@ class ContainerManager {
|
|
95
96
|
isAlive() {
|
96
97
|
return this._alive;
|
97
98
|
}
|
98
|
-
getMountPoint(
|
99
|
-
const kindUri = parseKapetaUri(
|
100
|
-
|
99
|
+
getMountPoint(systemId, ref, mountName) {
|
100
|
+
const kindUri = parseKapetaUri(ref);
|
101
|
+
const systemUri = parseKapetaUri(systemId);
|
102
|
+
return Path.join(this._mountDir, systemUri.handle, systemUri.name, systemUri.version, kindUri.handle, kindUri.name, kindUri.version, mountName);
|
101
103
|
}
|
102
|
-
createMounts(kind, mountOpts) {
|
104
|
+
async createMounts(systemId, kind, mountOpts) {
|
103
105
|
const mounts = {};
|
104
|
-
|
105
|
-
const
|
106
|
-
|
107
|
-
|
108
|
-
|
106
|
+
if (mountOpts) {
|
107
|
+
const mountOptList = Object.entries(mountOpts);
|
108
|
+
for (const [mountName, containerPath] of mountOptList) {
|
109
|
+
const hostPath = this.getMountPoint(systemId, kind, mountName);
|
110
|
+
await FSExtra.mkdirp(hostPath);
|
111
|
+
mounts[containerPath] = hostPath;
|
112
|
+
}
|
113
|
+
}
|
109
114
|
return mounts;
|
110
115
|
}
|
111
116
|
async ping() {
|
@@ -156,12 +161,14 @@ class ContainerManager {
|
|
156
161
|
return false;
|
157
162
|
}
|
158
163
|
console.log('Pulling image: %s', image);
|
159
|
-
await this.docker()
|
164
|
+
const stream = await this.docker()
|
160
165
|
.image.create({}, {
|
161
166
|
fromImage: imageName,
|
162
167
|
tag: tag,
|
163
|
-
})
|
164
|
-
|
168
|
+
});
|
169
|
+
await promisifyStream(stream, (chunk) => {
|
170
|
+
console.log('Data from docker: "%s"', chunk.toString());
|
171
|
+
});
|
165
172
|
IMAGE_PULL_CACHE[image] = Date.now();
|
166
173
|
console.log('Image pulled: %s', image);
|
167
174
|
return true;
|
@@ -198,6 +205,11 @@ class ContainerManager {
|
|
198
205
|
dockerOpts.Labels.HASH = hash;
|
199
206
|
}
|
200
207
|
async ensureContainer(opts) {
|
208
|
+
const container = await this.createOrUpdateContainer(opts);
|
209
|
+
await this.waitForReady(container);
|
210
|
+
return container;
|
211
|
+
}
|
212
|
+
async createOrUpdateContainer(opts) {
|
201
213
|
let imagePulled = false;
|
202
214
|
try {
|
203
215
|
imagePulled = await this.pull(opts.Image);
|
@@ -277,28 +289,6 @@ class ContainerManager {
|
|
277
289
|
}, HEALTH_CHECK_INTERVAL);
|
278
290
|
});
|
279
291
|
}
|
280
|
-
async waitForHealthy(container, attempt) {
|
281
|
-
if (!attempt) {
|
282
|
-
attempt = 0;
|
283
|
-
}
|
284
|
-
if (attempt >= HEALTH_CHECK_MAX) {
|
285
|
-
throw new Error('Container did not become healthy within the timeout');
|
286
|
-
}
|
287
|
-
if (await this._isHealthy(container)) {
|
288
|
-
return;
|
289
|
-
}
|
290
|
-
return new Promise((resolve, reject) => {
|
291
|
-
setTimeout(async () => {
|
292
|
-
try {
|
293
|
-
await this.waitForHealthy(container, (attempt ?? 0) + 1);
|
294
|
-
resolve();
|
295
|
-
}
|
296
|
-
catch (err) {
|
297
|
-
reject(err);
|
298
|
-
}
|
299
|
-
}, HEALTH_CHECK_INTERVAL);
|
300
|
-
});
|
301
|
-
}
|
302
292
|
async _isReady(container) {
|
303
293
|
let info;
|
304
294
|
try {
|
@@ -312,16 +302,12 @@ class ContainerManager {
|
|
312
302
|
if (state?.Status === 'exited' || state?.Status === 'removing' || state?.Status === 'dead') {
|
313
303
|
throw new Error('Container exited unexpectedly');
|
314
304
|
}
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
try {
|
319
|
-
const info = await container.status();
|
320
|
-
const infoData = info?.data;
|
321
|
-
return infoData?.State?.Health?.Status === 'healthy';
|
305
|
+
if (infoData?.State?.Health) {
|
306
|
+
// If container has health info - wait for it to become healthy
|
307
|
+
return infoData.State.Health.Status === 'healthy';
|
322
308
|
}
|
323
|
-
|
324
|
-
return false;
|
309
|
+
else {
|
310
|
+
return infoData?.State?.Running ?? false;
|
325
311
|
}
|
326
312
|
}
|
327
313
|
async remove(container, opts) {
|
@@ -351,6 +337,19 @@ class ContainerManager {
|
|
351
337
|
}
|
352
338
|
return new ContainerInfo(dockerContainer);
|
353
339
|
}
|
340
|
+
async getLogs(instance) {
|
341
|
+
const containerName = getBlockInstanceContainerName(instance.systemId, instance.instanceId);
|
342
|
+
const containerInfo = await this.getContainerByName(containerName);
|
343
|
+
if (!containerInfo) {
|
344
|
+
return [{
|
345
|
+
source: "stdout",
|
346
|
+
level: "ERROR",
|
347
|
+
time: Date.now(),
|
348
|
+
message: "Container not found"
|
349
|
+
}];
|
350
|
+
}
|
351
|
+
return containerInfo.getLogs();
|
352
|
+
}
|
354
353
|
}
|
355
354
|
export class ContainerInfo {
|
356
355
|
_container;
|
@@ -434,6 +433,62 @@ export class ContainerInfo {
|
|
434
433
|
});
|
435
434
|
return ports;
|
436
435
|
}
|
436
|
+
async getLogs() {
|
437
|
+
const logStream = await this.native.logs({
|
438
|
+
stdout: true,
|
439
|
+
stderr: true,
|
440
|
+
follow: false,
|
441
|
+
tail: 100,
|
442
|
+
timestamps: true,
|
443
|
+
});
|
444
|
+
const out = [];
|
445
|
+
await promisifyStream(logStream, (data) => {
|
446
|
+
const buf = data;
|
447
|
+
let offset = 0;
|
448
|
+
while (offset < buf.length) {
|
449
|
+
try {
|
450
|
+
// Read the docker log format - explained here:
|
451
|
+
// https://docs.docker.com/engine/api/v1.41/#operation/ContainerAttach
|
452
|
+
// or here : https://ahmet.im/blog/docker-logs-api-binary-format-explained/
|
453
|
+
// First byte is stream type
|
454
|
+
const streamTypeInt = buf.readInt8(offset);
|
455
|
+
const streamType = streamTypeInt === 1 ? 'stdout' : 'stderr';
|
456
|
+
// Bytes 4-8 is frame size
|
457
|
+
const messageLength = buf.readInt32BE(offset + 4);
|
458
|
+
// After that is the message - with the message length
|
459
|
+
const dataWithoutStreamType = buf.subarray(offset + 8, offset + 8 + messageLength);
|
460
|
+
const raw = dataWithoutStreamType.toString();
|
461
|
+
// Split the message into date and message
|
462
|
+
const firstSpaceIx = raw.indexOf(' ');
|
463
|
+
const dateString = raw.substring(0, firstSpaceIx);
|
464
|
+
const line = raw.substring(firstSpaceIx + 1);
|
465
|
+
offset = offset + messageLength + 8;
|
466
|
+
if (!dateString) {
|
467
|
+
continue;
|
468
|
+
}
|
469
|
+
out.push({
|
470
|
+
time: new Date(dateString).getTime(),
|
471
|
+
message: line,
|
472
|
+
level: 'INFO',
|
473
|
+
source: streamType,
|
474
|
+
});
|
475
|
+
}
|
476
|
+
catch (err) {
|
477
|
+
console.error('Error parsing log entry', err);
|
478
|
+
offset = buf.length;
|
479
|
+
}
|
480
|
+
}
|
481
|
+
});
|
482
|
+
if (out.length === 0) {
|
483
|
+
out.push({
|
484
|
+
time: Date.now(),
|
485
|
+
message: 'No logs found for container',
|
486
|
+
level: 'INFO',
|
487
|
+
source: 'stdout',
|
488
|
+
});
|
489
|
+
}
|
490
|
+
return out;
|
491
|
+
}
|
437
492
|
}
|
438
493
|
export function getExtraHosts(dockerVersion) {
|
439
494
|
if (process.platform !== 'darwin' && process.platform !== 'win32') {
|
@@ -5,6 +5,7 @@ declare class DefinitionsManager {
|
|
5
5
|
clearCache(): void;
|
6
6
|
private doCached;
|
7
7
|
getDefinitions(kindFilter?: string | string[]): DefinitionInfo[];
|
8
|
+
exists(ref: string): boolean;
|
8
9
|
getProviderDefinitions(): DefinitionInfo[];
|
9
10
|
}
|
10
11
|
export declare const definitionsManager: DefinitionsManager;
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import ClusterConfiguration from '@kapeta/local-cluster-config';
|
2
|
+
import { parseKapetaUri } from "@kapeta/nodejs-utils";
|
2
3
|
const CACHE_TTL = 60 * 1000; // 1 min
|
3
4
|
class DefinitionsManager {
|
4
5
|
cache = {};
|
@@ -31,6 +32,12 @@ class DefinitionsManager {
|
|
31
32
|
const key = this.getKey(kindFilter);
|
32
33
|
return this.doCached(key, () => ClusterConfiguration.getDefinitions(kindFilter));
|
33
34
|
}
|
35
|
+
exists(ref) {
|
36
|
+
const uri = parseKapetaUri(ref);
|
37
|
+
return !!this.getDefinitions().find((d) => {
|
38
|
+
return parseKapetaUri(`${d.definition.metadata.name}:${d.version}`).id === uri.id;
|
39
|
+
});
|
40
|
+
}
|
34
41
|
getProviderDefinitions() {
|
35
42
|
return this.doCached('providers', () => ClusterConfiguration.getProviderDefinitions());
|
36
43
|
}
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { InstanceInfo } from './types';
|
1
|
+
import { InstanceInfo, LogEntry } from './types';
|
2
2
|
export declare class InstanceManager {
|
3
3
|
private _interval;
|
4
4
|
private readonly _instances;
|
@@ -7,6 +7,7 @@ export declare class InstanceManager {
|
|
7
7
|
getInstances(): InstanceInfo[];
|
8
8
|
getInstancesForPlan(systemId: string): InstanceInfo[];
|
9
9
|
getInstance(systemId: string, instanceId: string): InstanceInfo | undefined;
|
10
|
+
getLogs(systemId: string, instanceId: string): Promise<LogEntry[]>;
|
10
11
|
saveInternalInstance(instance: InstanceInfo): Promise<InstanceInfo>;
|
11
12
|
/**
|
12
13
|
* Method is called when instance is started from the Kapeta SDKs (e.g. NodeJS SDK)
|
@@ -50,6 +50,31 @@ export class InstanceManager {
|
|
50
50
|
systemId = normalizeKapetaUri(systemId);
|
51
51
|
return this._instances.find((i) => i.systemId === systemId && i.instanceId === instanceId);
|
52
52
|
}
|
53
|
+
async getLogs(systemId, instanceId) {
|
54
|
+
const instance = this.getInstance(systemId, instanceId);
|
55
|
+
if (!instance) {
|
56
|
+
throw new Error(`Instance ${systemId}/${instanceId} not found`);
|
57
|
+
}
|
58
|
+
switch (instance.type) {
|
59
|
+
case InstanceType.DOCKER:
|
60
|
+
return await containerManager.getLogs(instance);
|
61
|
+
case InstanceType.UNKNOWN:
|
62
|
+
return [{
|
63
|
+
level: 'INFO',
|
64
|
+
message: 'Instance is starting...',
|
65
|
+
time: Date.now(),
|
66
|
+
source: 'stdout',
|
67
|
+
}];
|
68
|
+
case InstanceType.LOCAL:
|
69
|
+
return [{
|
70
|
+
level: 'INFO',
|
71
|
+
message: 'Instance started outside Kapeta - logs not available...',
|
72
|
+
time: Date.now(),
|
73
|
+
source: 'stdout',
|
74
|
+
}];
|
75
|
+
}
|
76
|
+
return [];
|
77
|
+
}
|
53
78
|
async saveInternalInstance(instance) {
|
54
79
|
instance.systemId = normalizeKapetaUri(instance.systemId);
|
55
80
|
if (instance.ref) {
|
@@ -100,7 +125,6 @@ export class InstanceManager {
|
|
100
125
|
}
|
101
126
|
instance.desiredStatus = info.desiredStatus;
|
102
127
|
instance.owner = info.owner;
|
103
|
-
instance.internal = undefined;
|
104
128
|
instance.status = InstanceStatus.STARTING;
|
105
129
|
instance.startedAt = Date.now();
|
106
130
|
}
|
@@ -199,7 +223,7 @@ export class InstanceManager {
|
|
199
223
|
this.save();
|
200
224
|
try {
|
201
225
|
if (instance.type === 'docker') {
|
202
|
-
const containerName = getBlockInstanceContainerName(instance.instanceId);
|
226
|
+
const containerName = getBlockInstanceContainerName(instance.systemId, instance.instanceId);
|
203
227
|
const container = await containerManager.getContainerByName(containerName);
|
204
228
|
if (container) {
|
205
229
|
try {
|
@@ -275,7 +299,7 @@ export class InstanceManager {
|
|
275
299
|
name: blockAsset.data.metadata.name,
|
276
300
|
desiredStatus: DesiredInstanceStatus.RUN,
|
277
301
|
owner: InstanceOwner.INTERNAL,
|
278
|
-
type: InstanceType.UNKNOWN,
|
302
|
+
type: existingInstance?.type ?? InstanceType.UNKNOWN,
|
279
303
|
status: InstanceStatus.STARTING,
|
280
304
|
startedAt: Date.now(),
|
281
305
|
};
|
@@ -295,41 +319,6 @@ export class InstanceManager {
|
|
295
319
|
const startTime = Date.now();
|
296
320
|
try {
|
297
321
|
const processInfo = await runner.start(blockRef, instanceId, instanceConfig);
|
298
|
-
//emit stdout/stderr via sockets
|
299
|
-
processInfo.output.on('data', (data) => {
|
300
|
-
const payload = {
|
301
|
-
source: 'stdout',
|
302
|
-
level: 'INFO',
|
303
|
-
message: data.toString(),
|
304
|
-
time: Date.now(),
|
305
|
-
};
|
306
|
-
this.emitInstanceEvent(systemId, instanceId, EVENT_INSTANCE_LOG, payload);
|
307
|
-
});
|
308
|
-
processInfo.output.on('exit', (exitCode) => {
|
309
|
-
const timeRunning = Date.now() - startTime;
|
310
|
-
const instance = this.getInstance(systemId, instanceId);
|
311
|
-
if (instance?.status === InstanceStatus.READY) {
|
312
|
-
//It's already been running
|
313
|
-
return;
|
314
|
-
}
|
315
|
-
if (exitCode === 143 || exitCode === 137) {
|
316
|
-
//Process got SIGTERM (143) or SIGKILL (137)
|
317
|
-
//TODO: Windows?
|
318
|
-
return;
|
319
|
-
}
|
320
|
-
if (exitCode !== 0 || timeRunning < MIN_TIME_RUNNING) {
|
321
|
-
const instance = this.getInstance(systemId, instanceId);
|
322
|
-
if (instance) {
|
323
|
-
instance.status = InstanceStatus.FAILED;
|
324
|
-
this.save();
|
325
|
-
}
|
326
|
-
this.emitSystemEvent(systemId, EVENT_INSTANCE_EXITED, {
|
327
|
-
error: 'Failed to start instance',
|
328
|
-
status: EVENT_INSTANCE_EXITED,
|
329
|
-
instanceId: blockInstance.id,
|
330
|
-
});
|
331
|
-
}
|
332
|
-
});
|
333
322
|
instance.status = InstanceStatus.READY;
|
334
323
|
return this.saveInternalInstance({
|
335
324
|
...instance,
|
@@ -338,10 +327,6 @@ export class InstanceManager {
|
|
338
327
|
health: null,
|
339
328
|
portType: processInfo.portType,
|
340
329
|
status: InstanceStatus.READY,
|
341
|
-
internal: {
|
342
|
-
logs: processInfo.logs,
|
343
|
-
output: processInfo.output,
|
344
|
-
},
|
345
330
|
});
|
346
331
|
}
|
347
332
|
catch (e) {
|
@@ -387,9 +372,7 @@ export class InstanceManager {
|
|
387
372
|
save() {
|
388
373
|
try {
|
389
374
|
storageService.put('instances', this._instances.map((instance) => {
|
390
|
-
|
391
|
-
delete copy.internal;
|
392
|
-
return copy;
|
375
|
+
return { ...instance };
|
393
376
|
}));
|
394
377
|
}
|
395
378
|
catch (e) {
|
@@ -491,7 +474,7 @@ export class InstanceManager {
|
|
491
474
|
}
|
492
475
|
async getExternalStatus(instance) {
|
493
476
|
if (instance.type === InstanceType.DOCKER) {
|
494
|
-
const containerName = getBlockInstanceContainerName(instance.instanceId);
|
477
|
+
const containerName = getBlockInstanceContainerName(instance.systemId, instance.instanceId);
|
495
478
|
const container = await containerManager.getContainerByName(containerName);
|
496
479
|
if (!container) {
|
497
480
|
// If the container doesn't exist, we consider the instance stopped
|
@@ -62,15 +62,21 @@ router.post('/:systemId/:instanceId/stop', async (req, res) => {
|
|
62
62
|
/**
|
63
63
|
* Get logs for instance in a plan
|
64
64
|
*/
|
65
|
-
router.get('/:systemId/:instanceId/logs', (req, res) => {
|
65
|
+
router.get('/:systemId/:instanceId/logs', async (req, res) => {
|
66
66
|
const instanceInfo = instanceManager.getInstance(req.params.systemId, req.params.instanceId);
|
67
67
|
if (!instanceInfo) {
|
68
68
|
res.status(404).send({ ok: false });
|
69
69
|
return;
|
70
70
|
}
|
71
|
-
|
72
|
-
logs
|
73
|
-
|
71
|
+
try {
|
72
|
+
const logs = await instanceManager.getLogs(req.params.systemId, req.params.instanceId);
|
73
|
+
res.status(200).send({
|
74
|
+
logs,
|
75
|
+
});
|
76
|
+
}
|
77
|
+
catch (e) {
|
78
|
+
res.status(500).send({ ok: false, error: e.message });
|
79
|
+
}
|
74
80
|
});
|
75
81
|
/**
|
76
82
|
* Get public address for instance in a plan if available
|
@@ -111,13 +111,11 @@ class OperatorManager {
|
|
111
111
|
const operatorData = operator.getData();
|
112
112
|
const portTypes = Object.keys(operatorData.ports);
|
113
113
|
portTypes.sort();
|
114
|
-
const containerBaseName = 'kapeta-resource';
|
115
|
-
const nameParts = [resourceType.toLowerCase()];
|
116
114
|
const ports = {};
|
117
115
|
for (let i = 0; i < portTypes.length; i++) {
|
118
116
|
const portType = portTypes[i];
|
119
117
|
let containerPortInfo = operatorData.ports[portType];
|
120
|
-
const hostPort = await serviceManager.ensureServicePort(resourceType, portType);
|
118
|
+
const hostPort = await serviceManager.ensureServicePort(systemId, resourceType, portType);
|
121
119
|
if (typeof containerPortInfo === 'number' || typeof containerPortInfo === 'string') {
|
122
120
|
containerPortInfo = { port: containerPortInfo, type: 'tcp' };
|
123
121
|
}
|
@@ -125,14 +123,18 @@ class OperatorManager {
|
|
125
123
|
containerPortInfo.type = 'tcp';
|
126
124
|
}
|
127
125
|
const portId = containerPortInfo.port + '/' + containerPortInfo.type;
|
128
|
-
nameParts.push(portType + '-' + portId + '-' + hostPort);
|
129
126
|
ports[portId] = {
|
130
127
|
type: portType,
|
131
128
|
hostPort,
|
132
129
|
};
|
133
130
|
}
|
134
|
-
const mounts = containerManager.createMounts(resourceType, operatorData.mounts);
|
135
|
-
const
|
131
|
+
const mounts = await containerManager.createMounts(systemId, resourceType, operatorData.mounts);
|
132
|
+
const nameParts = [
|
133
|
+
systemId,
|
134
|
+
resourceType.toLowerCase(),
|
135
|
+
version
|
136
|
+
];
|
137
|
+
const containerName = `kapeta-resource-${md5(nameParts.join('_'))}`;
|
136
138
|
const PortBindings = {};
|
137
139
|
const Env = [];
|
138
140
|
const Labels = {
|
@@ -107,9 +107,11 @@ class RepositoryManager {
|
|
107
107
|
this._installQueue.push(async () => {
|
108
108
|
try {
|
109
109
|
const normalizedRefs = refs.map((ref) => parseKapetaUri(ref).id);
|
110
|
-
const filteredRefs = normalizedRefs
|
111
|
-
|
110
|
+
const filteredRefs = normalizedRefs
|
111
|
+
.filter((ref) => !INSTALL_ATTEMPTED[ref])
|
112
|
+
.filter((ref) => !definitionsManager.exists(ref));
|
112
113
|
if (filteredRefs.length > 0) {
|
114
|
+
console.log(`Auto-installing dependencies: ${filteredRefs.join(', ')}`);
|
113
115
|
filteredRefs.forEach((ref) => (INSTALL_ATTEMPTED[ref] = true));
|
114
116
|
//Auto-install missing asset
|
115
117
|
try {
|
@@ -200,14 +202,12 @@ class RepositoryManager {
|
|
200
202
|
}
|
201
203
|
this._cache[ref] = true;
|
202
204
|
if (!installedAsset) {
|
203
|
-
console.log(`Auto-installing missing asset: ${ref}`);
|
204
205
|
await this._install([ref]);
|
205
206
|
}
|
206
207
|
else {
|
207
208
|
//Ensure dependencies are installed
|
208
209
|
const refs = assetVersion.dependencies.map((dep) => dep.name);
|
209
210
|
if (refs.length > 0) {
|
210
|
-
console.log(`Auto-installing dependencies: ${refs.join(', ')}`);
|
211
211
|
await this._install(refs);
|
212
212
|
}
|
213
213
|
}
|