@kapeta/local-cluster-service 0.10.0 → 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 +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 +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.d.ts +3 -2
- package/dist/cjs/src/utils/BlockInstanceRunner.js +49 -95
- 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.d.ts +3 -2
- package/dist/esm/src/utils/BlockInstanceRunner.js +49 -95
- 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 +74 -109
- 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");
|
@@ -100,7 +99,7 @@ class BlockInstanceRunner {
|
|
100
99
|
processInfo = await this._startLocalProcess(blockInstance, blockUri, env, assetVersion);
|
101
100
|
}
|
102
101
|
else {
|
103
|
-
processInfo = await this._startDockerProcess(blockInstance, blockUri, env);
|
102
|
+
processInfo = await this._startDockerProcess(blockInstance, blockUri, env, assetVersion);
|
104
103
|
}
|
105
104
|
if (portTypes.length > 0) {
|
106
105
|
processInfo.portType = portTypes[0];
|
@@ -133,31 +132,12 @@ 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';
|
140
139
|
const workingDir = localContainer.workingDir ? localContainer.workingDir : '/workspace';
|
141
|
-
const
|
142
|
-
const ExposedPorts = {};
|
143
|
-
const addonEnv = {};
|
144
|
-
const PortBindings = {};
|
145
|
-
const portTypes = getProviderPorts(assetVersion);
|
146
|
-
let port = 80;
|
147
|
-
const promises = portTypes.map(async (portType) => {
|
148
|
-
const publicPort = await serviceManager_1.serviceManager.ensureServicePort(this._systemId, blockInstance.id, portType);
|
149
|
-
const thisPort = port++; //TODO: Not sure how we should handle multiple ports or non-HTTP ports
|
150
|
-
const dockerPort = `${thisPort}/tcp`;
|
151
|
-
ExposedPorts[dockerPort] = {};
|
152
|
-
addonEnv[`KAPETA_LOCAL_SERVER_PORT_${portType.toUpperCase()}`] = '' + thisPort;
|
153
|
-
PortBindings[dockerPort] = [
|
154
|
-
{
|
155
|
-
HostIp: bindHost,
|
156
|
-
HostPort: `${publicPort}`,
|
157
|
-
},
|
158
|
-
];
|
159
|
-
});
|
160
|
-
await Promise.all(promises);
|
140
|
+
const { PortBindings, ExposedPorts, addonEnv } = await this.getDockerPortBindings(blockInstance, assetVersion);
|
161
141
|
let HealthCheck = undefined;
|
162
142
|
if (localContainer.healthcheck) {
|
163
143
|
HealthCheck = containerManager_1.containerManager.toDockerHealth({ cmd: localContainer.healthcheck });
|
@@ -190,73 +170,7 @@ class BlockInstanceRunner {
|
|
190
170
|
...dockerOpts,
|
191
171
|
});
|
192
172
|
}
|
193
|
-
async
|
194
|
-
const logs = new LogData_1.LogData();
|
195
|
-
const container = await containerManager_1.containerManager.ensureContainer(opts);
|
196
|
-
try {
|
197
|
-
if (opts.HealthCheck) {
|
198
|
-
await containerManager_1.containerManager.waitForHealthy(container);
|
199
|
-
}
|
200
|
-
else {
|
201
|
-
await containerManager_1.containerManager.waitForReady(container);
|
202
|
-
}
|
203
|
-
}
|
204
|
-
catch (e) {
|
205
|
-
logs.addLog(e.message, 'ERROR');
|
206
|
-
}
|
207
|
-
return this._handleContainer(container, logs);
|
208
|
-
}
|
209
|
-
async _handleContainer(container, logs, deleteOnExit = false) {
|
210
|
-
let localContainer = container;
|
211
|
-
const logStream = (await container.logs({
|
212
|
-
follow: true,
|
213
|
-
stdout: true,
|
214
|
-
stderr: true,
|
215
|
-
tail: LogData_1.LogData.MAX_LINES,
|
216
|
-
}));
|
217
|
-
const outputEvents = new events_1.default();
|
218
|
-
logStream.on('data', (data) => {
|
219
|
-
logs.addLog(data.toString());
|
220
|
-
outputEvents.emit('data', data);
|
221
|
-
});
|
222
|
-
logStream.on('error', (data) => {
|
223
|
-
logs.addLog(data.toString());
|
224
|
-
outputEvents.emit('data', data);
|
225
|
-
});
|
226
|
-
logStream.on('close', async () => {
|
227
|
-
const status = await container.status();
|
228
|
-
const data = status.data;
|
229
|
-
if (deleteOnExit) {
|
230
|
-
try {
|
231
|
-
await containerManager_1.containerManager.remove(container);
|
232
|
-
}
|
233
|
-
catch (e) { }
|
234
|
-
}
|
235
|
-
outputEvents.emit('exit', data?.State?.ExitCode ?? 0);
|
236
|
-
});
|
237
|
-
return {
|
238
|
-
type: types_1.InstanceType.DOCKER,
|
239
|
-
pid: container.id,
|
240
|
-
output: outputEvents,
|
241
|
-
stop: async () => {
|
242
|
-
if (!localContainer) {
|
243
|
-
return;
|
244
|
-
}
|
245
|
-
try {
|
246
|
-
await localContainer.stop();
|
247
|
-
if (deleteOnExit) {
|
248
|
-
await containerManager_1.containerManager.remove(localContainer);
|
249
|
-
}
|
250
|
-
}
|
251
|
-
catch (e) { }
|
252
|
-
localContainer = null;
|
253
|
-
},
|
254
|
-
logs: () => {
|
255
|
-
return logs.getLogs();
|
256
|
-
},
|
257
|
-
};
|
258
|
-
}
|
259
|
-
async _startDockerProcess(blockInstance, blockInfo, env) {
|
173
|
+
async _startDockerProcess(blockInstance, blockInfo, env, assetVersion) {
|
260
174
|
const { versionFile } = local_cluster_config_1.default.getRepositoryAssetInfoPath(blockInfo.handle, blockInfo.name, blockInfo.version);
|
261
175
|
const versionYml = versionFile;
|
262
176
|
if (!node_fs_1.default.existsSync(versionYml)) {
|
@@ -270,23 +184,28 @@ class BlockInstanceRunner {
|
|
270
184
|
if (!dockerImage) {
|
271
185
|
throw new Error(`Missing docker image information: ${JSON.stringify(versionInfo?.artifact?.details)}`);
|
272
186
|
}
|
273
|
-
const
|
274
|
-
const
|
187
|
+
const { PortBindings, ExposedPorts, addonEnv } = await this.getDockerPortBindings(blockInstance, assetVersion);
|
188
|
+
const containerName = (0, utils_1.getBlockInstanceContainerName)(this._systemId, blockInstance.id);
|
275
189
|
// For windows we need to default to root
|
276
190
|
const innerHome = process.platform === 'win32' ? '/root/.kapeta' : local_cluster_config_1.default.getKapetaBasedir();
|
277
191
|
return this.ensureContainer({
|
278
192
|
Image: dockerImage,
|
279
193
|
name: containerName,
|
194
|
+
ExposedPorts,
|
280
195
|
Labels: {
|
281
196
|
instance: blockInstance.id,
|
282
197
|
},
|
283
198
|
Env: [
|
284
199
|
...DOCKER_ENV_VARS,
|
285
200
|
`KAPETA_LOCAL_CLUSTER_PORT=${clusterService_1.clusterService.getClusterServicePort()}`,
|
286
|
-
...Object.entries(
|
201
|
+
...Object.entries({
|
202
|
+
...env,
|
203
|
+
...addonEnv
|
204
|
+
}).map(([key, value]) => `${key}=${value}`),
|
287
205
|
],
|
288
206
|
HostConfig: {
|
289
207
|
Binds: [`${(0, containerManager_1.toLocalBindVolume)(local_cluster_config_1.default.getKapetaBasedir())}:${innerHome}`],
|
208
|
+
PortBindings,
|
290
209
|
},
|
291
210
|
});
|
292
211
|
}
|
@@ -311,7 +230,8 @@ class BlockInstanceRunner {
|
|
311
230
|
throw new Error(`Provider did not have local image: ${providerRef}`);
|
312
231
|
}
|
313
232
|
const dockerImage = spec?.local?.image;
|
314
|
-
|
233
|
+
//We only want 1 operator per operator type - across all local systems
|
234
|
+
const containerName = (0, utils_1.getBlockInstanceContainerName)(this._systemId, blockInstance.id);
|
315
235
|
const logs = new LogData_1.LogData();
|
316
236
|
const bindHost = (0, utils_1.getBindHost)();
|
317
237
|
const ExposedPorts = {};
|
@@ -338,7 +258,7 @@ class BlockInstanceRunner {
|
|
338
258
|
});
|
339
259
|
}
|
340
260
|
if (spec.local?.mounts) {
|
341
|
-
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);
|
342
262
|
Mounts = containerManager_1.containerManager.toDockerMounts(mounts);
|
343
263
|
}
|
344
264
|
if (spec.local?.health) {
|
@@ -379,5 +299,39 @@ class BlockInstanceRunner {
|
|
379
299
|
}
|
380
300
|
return out;
|
381
301
|
}
|
302
|
+
async getDockerPortBindings(blockInstance, assetVersion) {
|
303
|
+
const bindHost = (0, utils_1.getBindHost)();
|
304
|
+
const ExposedPorts = {};
|
305
|
+
const addonEnv = {};
|
306
|
+
const PortBindings = {};
|
307
|
+
const portTypes = getProviderPorts(assetVersion);
|
308
|
+
let port = 80;
|
309
|
+
const promises = portTypes.map(async (portType) => {
|
310
|
+
const publicPort = await serviceManager_1.serviceManager.ensureServicePort(this._systemId, blockInstance.id, portType);
|
311
|
+
const thisPort = port++; //TODO: Not sure how we should handle multiple ports or non-HTTP ports
|
312
|
+
const dockerPort = `${thisPort}/tcp`;
|
313
|
+
ExposedPorts[dockerPort] = {};
|
314
|
+
addonEnv[`KAPETA_LOCAL_SERVER_PORT_${portType.toUpperCase()}`] = '' + thisPort;
|
315
|
+
PortBindings[dockerPort] = [
|
316
|
+
{
|
317
|
+
HostIp: bindHost,
|
318
|
+
HostPort: `${publicPort}`,
|
319
|
+
},
|
320
|
+
];
|
321
|
+
});
|
322
|
+
await Promise.all(promises);
|
323
|
+
return { PortBindings, ExposedPorts, addonEnv };
|
324
|
+
}
|
325
|
+
async ensureContainer(opts) {
|
326
|
+
const container = await containerManager_1.containerManager.ensureContainer(opts);
|
327
|
+
await containerManager_1.containerManager.waitForReady(container);
|
328
|
+
return this._handleContainer(container);
|
329
|
+
}
|
330
|
+
async _handleContainer(container) {
|
331
|
+
return {
|
332
|
+
type: types_1.InstanceType.DOCKER,
|
333
|
+
pid: container.id
|
334
|
+
};
|
335
|
+
}
|
382
336
|
}
|
383
337
|
exports.BlockInstanceRunner = BlockInstanceRunner;
|
@@ -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
|