@kapeta/local-cluster-service 0.10.1 → 0.11.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/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 +6 -2
- package/dist/cjs/src/instanceManager.js +240 -233
- 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 +6 -2
- package/dist/esm/src/instanceManager.js +240 -233
- 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 +3 -1
- package/src/containerManager.ts +126 -49
- package/src/definitionsManager.ts +8 -0
- package/src/instanceManager.ts +270 -255
- 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
package/CHANGELOG.md
CHANGED
@@ -1,3 +1,17 @@
|
|
1
|
+
## [0.11.1](https://github.com/kapetacom/local-cluster-service/compare/v0.11.0...v0.11.1) (2023-07-31)
|
2
|
+
|
3
|
+
|
4
|
+
### Bug Fixes
|
5
|
+
|
6
|
+
* Ensure we do not attempt to start / stop the same instance at the ([493e077](https://github.com/kapetacom/local-cluster-service/commit/493e077d0c6acdbcc371dae2ef8b6fbf2478c950))
|
7
|
+
|
8
|
+
# [0.11.0](https://github.com/kapetacom/local-cluster-service/compare/v0.10.1...v0.11.0) (2023-07-31)
|
9
|
+
|
10
|
+
|
11
|
+
### Features
|
12
|
+
|
13
|
+
* Always get logs from docker ([#53](https://github.com/kapetacom/local-cluster-service/issues/53)) ([5cab8cb](https://github.com/kapetacom/local-cluster-service/commit/5cab8cbf18b38edf99d538e1819e135f0a5bd7e3))
|
14
|
+
|
1
15
|
## [0.10.1](https://github.com/kapetacom/local-cluster-service/compare/v0.10.0...v0.10.1) (2023-07-27)
|
2
16
|
|
3
17
|
|
@@ -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
|
/**
|
@@ -14,6 +14,7 @@ const nodejs_utils_1 = require("@kapeta/nodejs-utils");
|
|
14
14
|
const local_cluster_config_1 = __importDefault(require("@kapeta/local-cluster-config"));
|
15
15
|
const node_uuid_1 = __importDefault(require("node-uuid"));
|
16
16
|
const md5_1 = __importDefault(require("md5"));
|
17
|
+
const utils_1 = require("./utils/utils");
|
17
18
|
exports.CONTAINER_LABEL_PORT_PREFIX = 'kapeta_port-';
|
18
19
|
const NANO_SECOND = 1000000;
|
19
20
|
const HEALTH_CHECK_INTERVAL = 3000;
|
@@ -21,8 +22,8 @@ const HEALTH_CHECK_MAX = 20;
|
|
21
22
|
const IMAGE_PULL_CACHE_TTL = 30 * 60 * 1000;
|
22
23
|
const IMAGE_PULL_CACHE = {};
|
23
24
|
exports.HEALTH_CHECK_TIMEOUT = HEALTH_CHECK_INTERVAL * HEALTH_CHECK_MAX * 2;
|
24
|
-
const promisifyStream = (stream) => new Promise((resolve, reject) => {
|
25
|
-
stream.on('data',
|
25
|
+
const promisifyStream = (stream, handler) => new Promise((resolve, reject) => {
|
26
|
+
stream.on('data', handler);
|
26
27
|
stream.on('end', resolve);
|
27
28
|
stream.on('error', reject);
|
28
29
|
});
|
@@ -101,17 +102,21 @@ class ContainerManager {
|
|
101
102
|
isAlive() {
|
102
103
|
return this._alive;
|
103
104
|
}
|
104
|
-
getMountPoint(
|
105
|
-
const kindUri = (0, nodejs_utils_1.parseKapetaUri)(
|
106
|
-
|
105
|
+
getMountPoint(systemId, ref, mountName) {
|
106
|
+
const kindUri = (0, nodejs_utils_1.parseKapetaUri)(ref);
|
107
|
+
const systemUri = (0, nodejs_utils_1.parseKapetaUri)(systemId);
|
108
|
+
return path_1.default.join(this._mountDir, systemUri.handle, systemUri.name, systemUri.version, kindUri.handle, kindUri.name, kindUri.version, mountName);
|
107
109
|
}
|
108
|
-
createMounts(kind, mountOpts) {
|
110
|
+
async createMounts(systemId, kind, mountOpts) {
|
109
111
|
const mounts = {};
|
110
|
-
|
111
|
-
const
|
112
|
-
|
113
|
-
|
114
|
-
|
112
|
+
if (mountOpts) {
|
113
|
+
const mountOptList = Object.entries(mountOpts);
|
114
|
+
for (const [mountName, containerPath] of mountOptList) {
|
115
|
+
const hostPath = this.getMountPoint(systemId, kind, mountName);
|
116
|
+
await fs_extra_1.default.mkdirp(hostPath);
|
117
|
+
mounts[containerPath] = hostPath;
|
118
|
+
}
|
119
|
+
}
|
115
120
|
return mounts;
|
116
121
|
}
|
117
122
|
async ping() {
|
@@ -162,12 +167,14 @@ class ContainerManager {
|
|
162
167
|
return false;
|
163
168
|
}
|
164
169
|
console.log('Pulling image: %s', image);
|
165
|
-
await this.docker()
|
170
|
+
const stream = await this.docker()
|
166
171
|
.image.create({}, {
|
167
172
|
fromImage: imageName,
|
168
173
|
tag: tag,
|
169
|
-
})
|
170
|
-
|
174
|
+
});
|
175
|
+
await promisifyStream(stream, (chunk) => {
|
176
|
+
console.log('Data from docker: "%s"', chunk.toString());
|
177
|
+
});
|
171
178
|
IMAGE_PULL_CACHE[image] = Date.now();
|
172
179
|
console.log('Image pulled: %s', image);
|
173
180
|
return true;
|
@@ -204,6 +211,11 @@ class ContainerManager {
|
|
204
211
|
dockerOpts.Labels.HASH = hash;
|
205
212
|
}
|
206
213
|
async ensureContainer(opts) {
|
214
|
+
const container = await this.createOrUpdateContainer(opts);
|
215
|
+
await this.waitForReady(container);
|
216
|
+
return container;
|
217
|
+
}
|
218
|
+
async createOrUpdateContainer(opts) {
|
207
219
|
let imagePulled = false;
|
208
220
|
try {
|
209
221
|
imagePulled = await this.pull(opts.Image);
|
@@ -283,28 +295,6 @@ class ContainerManager {
|
|
283
295
|
}, HEALTH_CHECK_INTERVAL);
|
284
296
|
});
|
285
297
|
}
|
286
|
-
async waitForHealthy(container, attempt) {
|
287
|
-
if (!attempt) {
|
288
|
-
attempt = 0;
|
289
|
-
}
|
290
|
-
if (attempt >= HEALTH_CHECK_MAX) {
|
291
|
-
throw new Error('Container did not become healthy within the timeout');
|
292
|
-
}
|
293
|
-
if (await this._isHealthy(container)) {
|
294
|
-
return;
|
295
|
-
}
|
296
|
-
return new Promise((resolve, reject) => {
|
297
|
-
setTimeout(async () => {
|
298
|
-
try {
|
299
|
-
await this.waitForHealthy(container, (attempt ?? 0) + 1);
|
300
|
-
resolve();
|
301
|
-
}
|
302
|
-
catch (err) {
|
303
|
-
reject(err);
|
304
|
-
}
|
305
|
-
}, HEALTH_CHECK_INTERVAL);
|
306
|
-
});
|
307
|
-
}
|
308
298
|
async _isReady(container) {
|
309
299
|
let info;
|
310
300
|
try {
|
@@ -318,16 +308,12 @@ class ContainerManager {
|
|
318
308
|
if (state?.Status === 'exited' || state?.Status === 'removing' || state?.Status === 'dead') {
|
319
309
|
throw new Error('Container exited unexpectedly');
|
320
310
|
}
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
try {
|
325
|
-
const info = await container.status();
|
326
|
-
const infoData = info?.data;
|
327
|
-
return infoData?.State?.Health?.Status === 'healthy';
|
311
|
+
if (infoData?.State?.Health) {
|
312
|
+
// If container has health info - wait for it to become healthy
|
313
|
+
return infoData.State.Health.Status === 'healthy';
|
328
314
|
}
|
329
|
-
|
330
|
-
return false;
|
315
|
+
else {
|
316
|
+
return infoData?.State?.Running ?? false;
|
331
317
|
}
|
332
318
|
}
|
333
319
|
async remove(container, opts) {
|
@@ -357,6 +343,19 @@ class ContainerManager {
|
|
357
343
|
}
|
358
344
|
return new ContainerInfo(dockerContainer);
|
359
345
|
}
|
346
|
+
async getLogs(instance) {
|
347
|
+
const containerName = (0, utils_1.getBlockInstanceContainerName)(instance.systemId, instance.instanceId);
|
348
|
+
const containerInfo = await this.getContainerByName(containerName);
|
349
|
+
if (!containerInfo) {
|
350
|
+
return [{
|
351
|
+
source: "stdout",
|
352
|
+
level: "ERROR",
|
353
|
+
time: Date.now(),
|
354
|
+
message: "Container not found"
|
355
|
+
}];
|
356
|
+
}
|
357
|
+
return containerInfo.getLogs();
|
358
|
+
}
|
360
359
|
}
|
361
360
|
class ContainerInfo {
|
362
361
|
_container;
|
@@ -440,6 +439,62 @@ class ContainerInfo {
|
|
440
439
|
});
|
441
440
|
return ports;
|
442
441
|
}
|
442
|
+
async getLogs() {
|
443
|
+
const logStream = await this.native.logs({
|
444
|
+
stdout: true,
|
445
|
+
stderr: true,
|
446
|
+
follow: false,
|
447
|
+
tail: 100,
|
448
|
+
timestamps: true,
|
449
|
+
});
|
450
|
+
const out = [];
|
451
|
+
await promisifyStream(logStream, (data) => {
|
452
|
+
const buf = data;
|
453
|
+
let offset = 0;
|
454
|
+
while (offset < buf.length) {
|
455
|
+
try {
|
456
|
+
// Read the docker log format - explained here:
|
457
|
+
// https://docs.docker.com/engine/api/v1.41/#operation/ContainerAttach
|
458
|
+
// or here : https://ahmet.im/blog/docker-logs-api-binary-format-explained/
|
459
|
+
// First byte is stream type
|
460
|
+
const streamTypeInt = buf.readInt8(offset);
|
461
|
+
const streamType = streamTypeInt === 1 ? 'stdout' : 'stderr';
|
462
|
+
// Bytes 4-8 is frame size
|
463
|
+
const messageLength = buf.readInt32BE(offset + 4);
|
464
|
+
// After that is the message - with the message length
|
465
|
+
const dataWithoutStreamType = buf.subarray(offset + 8, offset + 8 + messageLength);
|
466
|
+
const raw = dataWithoutStreamType.toString();
|
467
|
+
// Split the message into date and message
|
468
|
+
const firstSpaceIx = raw.indexOf(' ');
|
469
|
+
const dateString = raw.substring(0, firstSpaceIx);
|
470
|
+
const line = raw.substring(firstSpaceIx + 1);
|
471
|
+
offset = offset + messageLength + 8;
|
472
|
+
if (!dateString) {
|
473
|
+
continue;
|
474
|
+
}
|
475
|
+
out.push({
|
476
|
+
time: new Date(dateString).getTime(),
|
477
|
+
message: line,
|
478
|
+
level: 'INFO',
|
479
|
+
source: streamType,
|
480
|
+
});
|
481
|
+
}
|
482
|
+
catch (err) {
|
483
|
+
console.error('Error parsing log entry', err);
|
484
|
+
offset = buf.length;
|
485
|
+
}
|
486
|
+
}
|
487
|
+
});
|
488
|
+
if (out.length === 0) {
|
489
|
+
out.push({
|
490
|
+
time: Date.now(),
|
491
|
+
message: 'No logs found for container',
|
492
|
+
level: 'INFO',
|
493
|
+
source: 'stdout',
|
494
|
+
});
|
495
|
+
}
|
496
|
+
return out;
|
497
|
+
}
|
443
498
|
}
|
444
499
|
exports.ContainerInfo = ContainerInfo;
|
445
500
|
function getExtraHosts(dockerVersion) {
|
@@ -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;
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
6
6
|
exports.definitionsManager = void 0;
|
7
7
|
const local_cluster_config_1 = __importDefault(require("@kapeta/local-cluster-config"));
|
8
|
+
const nodejs_utils_1 = require("@kapeta/nodejs-utils");
|
8
9
|
const CACHE_TTL = 60 * 1000; // 1 min
|
9
10
|
class DefinitionsManager {
|
10
11
|
cache = {};
|
@@ -37,6 +38,12 @@ class DefinitionsManager {
|
|
37
38
|
const key = this.getKey(kindFilter);
|
38
39
|
return this.doCached(key, () => local_cluster_config_1.default.getDefinitions(kindFilter));
|
39
40
|
}
|
41
|
+
exists(ref) {
|
42
|
+
const uri = (0, nodejs_utils_1.parseKapetaUri)(ref);
|
43
|
+
return !!this.getDefinitions().find((d) => {
|
44
|
+
return (0, nodejs_utils_1.parseKapetaUri)(`${d.definition.metadata.name}:${d.version}`).id === uri.id;
|
45
|
+
});
|
46
|
+
}
|
40
47
|
getProviderDefinitions() {
|
41
48
|
return this.doCached('providers', () => local_cluster_config_1.default.getProviderDefinitions());
|
42
49
|
}
|
@@ -1,12 +1,15 @@
|
|
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;
|
5
|
+
private readonly instanceLocks;
|
5
6
|
constructor();
|
6
7
|
private checkInstancesLater;
|
7
8
|
getInstances(): InstanceInfo[];
|
8
9
|
getInstancesForPlan(systemId: string): InstanceInfo[];
|
9
10
|
getInstance(systemId: string, instanceId: string): InstanceInfo | undefined;
|
11
|
+
private exclusive;
|
12
|
+
getLogs(systemId: string, instanceId: string): Promise<LogEntry[]>;
|
10
13
|
saveInternalInstance(instance: InstanceInfo): Promise<InstanceInfo>;
|
11
14
|
/**
|
12
15
|
* Method is called when instance is started from the Kapeta SDKs (e.g. NodeJS SDK)
|
@@ -14,9 +17,10 @@ export declare class InstanceManager {
|
|
14
17
|
*/
|
15
18
|
registerInstanceFromSDK(systemId: string, instanceId: string, info: Omit<InstanceInfo, 'systemId' | 'instanceId'>): Promise<InstanceInfo | undefined>;
|
16
19
|
private getHealthUrl;
|
17
|
-
markAsStopped(systemId: string, instanceId: string): void
|
20
|
+
markAsStopped(systemId: string, instanceId: string): Promise<void>;
|
18
21
|
startAllForPlan(systemId: string): Promise<InstanceInfo[]>;
|
19
22
|
stop(systemId: string, instanceId: string): Promise<void>;
|
23
|
+
private stopInner;
|
20
24
|
stopAllForPlan(systemId: string): Promise<void>;
|
21
25
|
start(systemId: string, instanceId: string): Promise<InstanceInfo>;
|
22
26
|
restart(systemId: string, instanceId: string): Promise<InstanceInfo>;
|