@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
@@ -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
|
}
|
package/dist/esm/src/types.d.ts
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
/// <reference types="node" />
|
2
|
-
import EventEmitter from 'events';
|
3
1
|
import express from 'express';
|
4
2
|
import { Resource } from '@kapeta/schemas';
|
5
3
|
import { StringBodyRequest } from './middleware/stringBody';
|
@@ -50,10 +48,7 @@ export declare enum DesiredInstanceStatus {
|
|
50
48
|
export type ProcessInfo = {
|
51
49
|
type: InstanceType;
|
52
50
|
pid?: number | string | null;
|
53
|
-
output: EventEmitter;
|
54
51
|
portType?: string;
|
55
|
-
logs: () => LogEntry[];
|
56
|
-
stop: () => Promise<void> | void;
|
57
52
|
};
|
58
53
|
export type InstanceInfo = {
|
59
54
|
systemId: string;
|
@@ -69,10 +64,6 @@ export type InstanceInfo = {
|
|
69
64
|
health?: string | null;
|
70
65
|
pid?: number | string | null;
|
71
66
|
portType?: string;
|
72
|
-
internal?: {
|
73
|
-
output: EventEmitter;
|
74
|
-
logs: () => LogEntry[];
|
75
|
-
};
|
76
67
|
};
|
77
68
|
interface ResourceRef {
|
78
69
|
blockId: string;
|
@@ -14,8 +14,6 @@ export declare class BlockInstanceRunner {
|
|
14
14
|
* Starts local process
|
15
15
|
*/
|
16
16
|
private _startLocalProcess;
|
17
|
-
private ensureContainer;
|
18
|
-
private _handleContainer;
|
19
17
|
private _startDockerProcess;
|
20
18
|
/**
|
21
19
|
*
|
@@ -27,4 +25,7 @@ export declare class BlockInstanceRunner {
|
|
27
25
|
* @private
|
28
26
|
*/
|
29
27
|
_startOperatorProcess(blockInstance: BlockProcessParams, blockUri: KapetaURI, providerDefinition: DefinitionInfo, env: StringMap): Promise<ProcessInfo>;
|
28
|
+
private getDockerPortBindings;
|
29
|
+
private ensureContainer;
|
30
|
+
private _handleContainer;
|
30
31
|
}
|
@@ -5,7 +5,6 @@ import { parseKapetaUri } from '@kapeta/nodejs-utils';
|
|
5
5
|
import { serviceManager } from '../serviceManager';
|
6
6
|
import { containerManager, toLocalBindVolume } from '../containerManager';
|
7
7
|
import { LogData } from './LogData';
|
8
|
-
import EventEmitter from 'events';
|
9
8
|
import { clusterService } from '../clusterService';
|
10
9
|
import { InstanceType } from '../types';
|
11
10
|
import { definitionsManager } from '../definitionsManager';
|
@@ -94,7 +93,7 @@ export class BlockInstanceRunner {
|
|
94
93
|
processInfo = await this._startLocalProcess(blockInstance, blockUri, env, assetVersion);
|
95
94
|
}
|
96
95
|
else {
|
97
|
-
processInfo = await this._startDockerProcess(blockInstance, blockUri, env);
|
96
|
+
processInfo = await this._startDockerProcess(blockInstance, blockUri, env, assetVersion);
|
98
97
|
}
|
99
98
|
if (portTypes.length > 0) {
|
100
99
|
processInfo.portType = portTypes[0];
|
@@ -127,31 +126,12 @@ export class BlockInstanceRunner {
|
|
127
126
|
if (!dockerImage) {
|
128
127
|
throw new Error(`Missing docker image information: ${JSON.stringify(localContainer)}`);
|
129
128
|
}
|
130
|
-
const containerName = getBlockInstanceContainerName(blockInstance.id);
|
129
|
+
const containerName = getBlockInstanceContainerName(this._systemId, blockInstance.id);
|
131
130
|
const startCmd = localContainer.handlers?.onCreate ? localContainer.handlers.onCreate : '';
|
132
131
|
const dockerOpts = localContainer.options ?? {};
|
133
132
|
const homeDir = localContainer.userHome ? localContainer.userHome : '/root';
|
134
133
|
const workingDir = localContainer.workingDir ? localContainer.workingDir : '/workspace';
|
135
|
-
const
|
136
|
-
const ExposedPorts = {};
|
137
|
-
const addonEnv = {};
|
138
|
-
const PortBindings = {};
|
139
|
-
const portTypes = getProviderPorts(assetVersion);
|
140
|
-
let port = 80;
|
141
|
-
const promises = portTypes.map(async (portType) => {
|
142
|
-
const publicPort = await serviceManager.ensureServicePort(this._systemId, blockInstance.id, portType);
|
143
|
-
const thisPort = port++; //TODO: Not sure how we should handle multiple ports or non-HTTP ports
|
144
|
-
const dockerPort = `${thisPort}/tcp`;
|
145
|
-
ExposedPorts[dockerPort] = {};
|
146
|
-
addonEnv[`KAPETA_LOCAL_SERVER_PORT_${portType.toUpperCase()}`] = '' + thisPort;
|
147
|
-
PortBindings[dockerPort] = [
|
148
|
-
{
|
149
|
-
HostIp: bindHost,
|
150
|
-
HostPort: `${publicPort}`,
|
151
|
-
},
|
152
|
-
];
|
153
|
-
});
|
154
|
-
await Promise.all(promises);
|
134
|
+
const { PortBindings, ExposedPorts, addonEnv } = await this.getDockerPortBindings(blockInstance, assetVersion);
|
155
135
|
let HealthCheck = undefined;
|
156
136
|
if (localContainer.healthcheck) {
|
157
137
|
HealthCheck = containerManager.toDockerHealth({ cmd: localContainer.healthcheck });
|
@@ -184,73 +164,7 @@ export class BlockInstanceRunner {
|
|
184
164
|
...dockerOpts,
|
185
165
|
});
|
186
166
|
}
|
187
|
-
async
|
188
|
-
const logs = new LogData();
|
189
|
-
const container = await containerManager.ensureContainer(opts);
|
190
|
-
try {
|
191
|
-
if (opts.HealthCheck) {
|
192
|
-
await containerManager.waitForHealthy(container);
|
193
|
-
}
|
194
|
-
else {
|
195
|
-
await containerManager.waitForReady(container);
|
196
|
-
}
|
197
|
-
}
|
198
|
-
catch (e) {
|
199
|
-
logs.addLog(e.message, 'ERROR');
|
200
|
-
}
|
201
|
-
return this._handleContainer(container, logs);
|
202
|
-
}
|
203
|
-
async _handleContainer(container, logs, deleteOnExit = false) {
|
204
|
-
let localContainer = container;
|
205
|
-
const logStream = (await container.logs({
|
206
|
-
follow: true,
|
207
|
-
stdout: true,
|
208
|
-
stderr: true,
|
209
|
-
tail: LogData.MAX_LINES,
|
210
|
-
}));
|
211
|
-
const outputEvents = new EventEmitter();
|
212
|
-
logStream.on('data', (data) => {
|
213
|
-
logs.addLog(data.toString());
|
214
|
-
outputEvents.emit('data', data);
|
215
|
-
});
|
216
|
-
logStream.on('error', (data) => {
|
217
|
-
logs.addLog(data.toString());
|
218
|
-
outputEvents.emit('data', data);
|
219
|
-
});
|
220
|
-
logStream.on('close', async () => {
|
221
|
-
const status = await container.status();
|
222
|
-
const data = status.data;
|
223
|
-
if (deleteOnExit) {
|
224
|
-
try {
|
225
|
-
await containerManager.remove(container);
|
226
|
-
}
|
227
|
-
catch (e) { }
|
228
|
-
}
|
229
|
-
outputEvents.emit('exit', data?.State?.ExitCode ?? 0);
|
230
|
-
});
|
231
|
-
return {
|
232
|
-
type: InstanceType.DOCKER,
|
233
|
-
pid: container.id,
|
234
|
-
output: outputEvents,
|
235
|
-
stop: async () => {
|
236
|
-
if (!localContainer) {
|
237
|
-
return;
|
238
|
-
}
|
239
|
-
try {
|
240
|
-
await localContainer.stop();
|
241
|
-
if (deleteOnExit) {
|
242
|
-
await containerManager.remove(localContainer);
|
243
|
-
}
|
244
|
-
}
|
245
|
-
catch (e) { }
|
246
|
-
localContainer = null;
|
247
|
-
},
|
248
|
-
logs: () => {
|
249
|
-
return logs.getLogs();
|
250
|
-
},
|
251
|
-
};
|
252
|
-
}
|
253
|
-
async _startDockerProcess(blockInstance, blockInfo, env) {
|
167
|
+
async _startDockerProcess(blockInstance, blockInfo, env, assetVersion) {
|
254
168
|
const { versionFile } = ClusterConfig.getRepositoryAssetInfoPath(blockInfo.handle, blockInfo.name, blockInfo.version);
|
255
169
|
const versionYml = versionFile;
|
256
170
|
if (!FS.existsSync(versionYml)) {
|
@@ -264,23 +178,28 @@ export class BlockInstanceRunner {
|
|
264
178
|
if (!dockerImage) {
|
265
179
|
throw new Error(`Missing docker image information: ${JSON.stringify(versionInfo?.artifact?.details)}`);
|
266
180
|
}
|
267
|
-
const
|
268
|
-
const
|
181
|
+
const { PortBindings, ExposedPorts, addonEnv } = await this.getDockerPortBindings(blockInstance, assetVersion);
|
182
|
+
const containerName = getBlockInstanceContainerName(this._systemId, blockInstance.id);
|
269
183
|
// For windows we need to default to root
|
270
184
|
const innerHome = process.platform === 'win32' ? '/root/.kapeta' : ClusterConfig.getKapetaBasedir();
|
271
185
|
return this.ensureContainer({
|
272
186
|
Image: dockerImage,
|
273
187
|
name: containerName,
|
188
|
+
ExposedPorts,
|
274
189
|
Labels: {
|
275
190
|
instance: blockInstance.id,
|
276
191
|
},
|
277
192
|
Env: [
|
278
193
|
...DOCKER_ENV_VARS,
|
279
194
|
`KAPETA_LOCAL_CLUSTER_PORT=${clusterService.getClusterServicePort()}`,
|
280
|
-
...Object.entries(
|
195
|
+
...Object.entries({
|
196
|
+
...env,
|
197
|
+
...addonEnv
|
198
|
+
}).map(([key, value]) => `${key}=${value}`),
|
281
199
|
],
|
282
200
|
HostConfig: {
|
283
201
|
Binds: [`${toLocalBindVolume(ClusterConfig.getKapetaBasedir())}:${innerHome}`],
|
202
|
+
PortBindings,
|
284
203
|
},
|
285
204
|
});
|
286
205
|
}
|
@@ -305,7 +224,8 @@ export class BlockInstanceRunner {
|
|
305
224
|
throw new Error(`Provider did not have local image: ${providerRef}`);
|
306
225
|
}
|
307
226
|
const dockerImage = spec?.local?.image;
|
308
|
-
|
227
|
+
//We only want 1 operator per operator type - across all local systems
|
228
|
+
const containerName = getBlockInstanceContainerName(this._systemId, blockInstance.id);
|
309
229
|
const logs = new LogData();
|
310
230
|
const bindHost = getBindHost();
|
311
231
|
const ExposedPorts = {};
|
@@ -332,7 +252,7 @@ export class BlockInstanceRunner {
|
|
332
252
|
});
|
333
253
|
}
|
334
254
|
if (spec.local?.mounts) {
|
335
|
-
const mounts = containerManager.createMounts(blockUri.id, spec.local.mounts);
|
255
|
+
const mounts = await containerManager.createMounts(this._systemId, blockUri.id, spec.local.mounts);
|
336
256
|
Mounts = containerManager.toDockerMounts(mounts);
|
337
257
|
}
|
338
258
|
if (spec.local?.health) {
|
@@ -373,4 +293,38 @@ export class BlockInstanceRunner {
|
|
373
293
|
}
|
374
294
|
return out;
|
375
295
|
}
|
296
|
+
async getDockerPortBindings(blockInstance, assetVersion) {
|
297
|
+
const bindHost = getBindHost();
|
298
|
+
const ExposedPorts = {};
|
299
|
+
const addonEnv = {};
|
300
|
+
const PortBindings = {};
|
301
|
+
const portTypes = getProviderPorts(assetVersion);
|
302
|
+
let port = 80;
|
303
|
+
const promises = portTypes.map(async (portType) => {
|
304
|
+
const publicPort = await serviceManager.ensureServicePort(this._systemId, blockInstance.id, portType);
|
305
|
+
const thisPort = port++; //TODO: Not sure how we should handle multiple ports or non-HTTP ports
|
306
|
+
const dockerPort = `${thisPort}/tcp`;
|
307
|
+
ExposedPorts[dockerPort] = {};
|
308
|
+
addonEnv[`KAPETA_LOCAL_SERVER_PORT_${portType.toUpperCase()}`] = '' + thisPort;
|
309
|
+
PortBindings[dockerPort] = [
|
310
|
+
{
|
311
|
+
HostIp: bindHost,
|
312
|
+
HostPort: `${publicPort}`,
|
313
|
+
},
|
314
|
+
];
|
315
|
+
});
|
316
|
+
await Promise.all(promises);
|
317
|
+
return { PortBindings, ExposedPorts, addonEnv };
|
318
|
+
}
|
319
|
+
async ensureContainer(opts) {
|
320
|
+
const container = await containerManager.ensureContainer(opts);
|
321
|
+
await containerManager.waitForReady(container);
|
322
|
+
return this._handleContainer(container);
|
323
|
+
}
|
324
|
+
async _handleContainer(container) {
|
325
|
+
return {
|
326
|
+
type: InstanceType.DOCKER,
|
327
|
+
pid: container.id
|
328
|
+
};
|
329
|
+
}
|
376
330
|
}
|
@@ -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;
|
@@ -1,8 +1,9 @@
|
|
1
1
|
import FS from 'node:fs';
|
2
2
|
import YAML from 'yaml';
|
3
3
|
import { parseKapetaUri } from '@kapeta/nodejs-utils';
|
4
|
-
|
5
|
-
|
4
|
+
import md5 from "md5";
|
5
|
+
export function getBlockInstanceContainerName(systemId, instanceId) {
|
6
|
+
return `kapeta-block-instance-${md5(systemId + instanceId)}`;
|
6
7
|
}
|
7
8
|
export function normalizeKapetaUri(uri) {
|
8
9
|
if (!uri) {
|
package/package.json
CHANGED
package/src/containerManager.ts
CHANGED
@@ -9,6 +9,10 @@ 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 "./utils/utils";
|
13
|
+
import {InstanceInfo, LogEntry, LogSource} from "./types";
|
14
|
+
import EventEmitter from "events";
|
15
|
+
import {LogData} from "./utils/LogData";
|
12
16
|
|
13
17
|
type StringMap = { [key: string]: string };
|
14
18
|
|
@@ -63,9 +67,9 @@ const IMAGE_PULL_CACHE: { [key: string]: number } = {};
|
|
63
67
|
|
64
68
|
export const HEALTH_CHECK_TIMEOUT = HEALTH_CHECK_INTERVAL * HEALTH_CHECK_MAX * 2;
|
65
69
|
|
66
|
-
const promisifyStream = (stream: ReadStream) =>
|
70
|
+
const promisifyStream = (stream: ReadStream, handler:(d:string|Buffer) => void) =>
|
67
71
|
new Promise((resolve, reject) => {
|
68
|
-
stream.on('data',
|
72
|
+
stream.on('data', handler);
|
69
73
|
stream.on('end', resolve);
|
70
74
|
stream.on('error', reject);
|
71
75
|
});
|
@@ -151,19 +155,30 @@ class ContainerManager {
|
|
151
155
|
return this._alive;
|
152
156
|
}
|
153
157
|
|
154
|
-
getMountPoint(
|
155
|
-
const kindUri = parseKapetaUri(
|
156
|
-
|
158
|
+
getMountPoint(systemId:string, ref: string, mountName: string) {
|
159
|
+
const kindUri = parseKapetaUri(ref);
|
160
|
+
const systemUri = parseKapetaUri(systemId)
|
161
|
+
return Path.join(this._mountDir,
|
162
|
+
systemUri.handle,
|
163
|
+
systemUri.name,
|
164
|
+
systemUri.version,
|
165
|
+
kindUri.handle,
|
166
|
+
kindUri.name,
|
167
|
+
kindUri.version, mountName);
|
157
168
|
}
|
158
169
|
|
159
|
-
createMounts(kind: string, mountOpts: StringMap): StringMap {
|
170
|
+
async createMounts(systemId:string, kind: string, mountOpts: StringMap|null|undefined): Promise<StringMap> {
|
160
171
|
const mounts: StringMap = {};
|
161
172
|
|
162
|
-
|
163
|
-
const
|
164
|
-
|
165
|
-
|
166
|
-
|
173
|
+
if (mountOpts) {
|
174
|
+
const mountOptList = Object.entries(mountOpts);
|
175
|
+
for(const [mountName, containerPath] of mountOptList) {
|
176
|
+
const hostPath = this.getMountPoint(systemId, kind, mountName);
|
177
|
+
await FSExtra.mkdirp(hostPath);
|
178
|
+
mounts[containerPath] = hostPath;
|
179
|
+
}
|
180
|
+
}
|
181
|
+
|
167
182
|
return mounts;
|
168
183
|
}
|
169
184
|
|
@@ -224,15 +239,18 @@ class ContainerManager {
|
|
224
239
|
}
|
225
240
|
|
226
241
|
console.log('Pulling image: %s', image);
|
227
|
-
await this.docker()
|
242
|
+
const stream = await this.docker()
|
228
243
|
.image.create(
|
229
244
|
{},
|
230
245
|
{
|
231
246
|
fromImage: imageName,
|
232
247
|
tag: tag,
|
233
248
|
}
|
234
|
-
)
|
235
|
-
|
249
|
+
) as ReadStream;
|
250
|
+
|
251
|
+
await promisifyStream(stream, (chunk) => {
|
252
|
+
console.log('Data from docker: "%s"', chunk.toString());
|
253
|
+
});
|
236
254
|
|
237
255
|
IMAGE_PULL_CACHE[image] = Date.now();
|
238
256
|
|
@@ -278,7 +296,15 @@ class ContainerManager {
|
|
278
296
|
dockerOpts.Labels.HASH = hash;
|
279
297
|
}
|
280
298
|
|
281
|
-
async ensureContainer(opts: any) {
|
299
|
+
public async ensureContainer(opts: any) {
|
300
|
+
const container = await this.createOrUpdateContainer(opts);
|
301
|
+
|
302
|
+
await this.waitForReady(container);
|
303
|
+
|
304
|
+
return container;
|
305
|
+
}
|
306
|
+
|
307
|
+
private async createOrUpdateContainer(opts: any) {
|
282
308
|
let imagePulled = false;
|
283
309
|
try {
|
284
310
|
imagePulled = await this.pull(opts.Image);
|
@@ -369,31 +395,6 @@ class ContainerManager {
|
|
369
395
|
});
|
370
396
|
}
|
371
397
|
|
372
|
-
async waitForHealthy(container: Container, attempt?: number): Promise<void> {
|
373
|
-
if (!attempt) {
|
374
|
-
attempt = 0;
|
375
|
-
}
|
376
|
-
|
377
|
-
if (attempt >= HEALTH_CHECK_MAX) {
|
378
|
-
throw new Error('Container did not become healthy within the timeout');
|
379
|
-
}
|
380
|
-
|
381
|
-
if (await this._isHealthy(container)) {
|
382
|
-
return;
|
383
|
-
}
|
384
|
-
|
385
|
-
return new Promise((resolve, reject) => {
|
386
|
-
setTimeout(async () => {
|
387
|
-
try {
|
388
|
-
await this.waitForHealthy(container, (attempt ?? 0) + 1);
|
389
|
-
resolve();
|
390
|
-
} catch (err) {
|
391
|
-
reject(err);
|
392
|
-
}
|
393
|
-
}, HEALTH_CHECK_INTERVAL);
|
394
|
-
});
|
395
|
-
}
|
396
|
-
|
397
398
|
async _isReady(container: Container) {
|
398
399
|
let info: Container;
|
399
400
|
try {
|
@@ -403,19 +404,16 @@ class ContainerManager {
|
|
403
404
|
}
|
404
405
|
const infoData: any = info?.data;
|
405
406
|
const state = infoData?.State as DockerState;
|
407
|
+
|
406
408
|
if (state?.Status === 'exited' || state?.Status === 'removing' || state?.Status === 'dead') {
|
407
409
|
throw new Error('Container exited unexpectedly');
|
408
410
|
}
|
409
|
-
return infoData?.State?.Running ?? false;
|
410
|
-
}
|
411
411
|
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
return infoData?.State?.
|
417
|
-
} catch (err) {
|
418
|
-
return false;
|
412
|
+
if (infoData?.State?.Health) {
|
413
|
+
// If container has health info - wait for it to become healthy
|
414
|
+
return infoData.State.Health.Status === 'healthy';
|
415
|
+
} else {
|
416
|
+
return infoData?.State?.Running ?? false;
|
419
417
|
}
|
420
418
|
}
|
421
419
|
|
@@ -449,6 +447,21 @@ class ContainerManager {
|
|
449
447
|
|
450
448
|
return new ContainerInfo(dockerContainer);
|
451
449
|
}
|
450
|
+
|
451
|
+
async getLogs(instance: InstanceInfo):Promise<LogEntry[]> {
|
452
|
+
const containerName = getBlockInstanceContainerName(instance.systemId, instance.instanceId);
|
453
|
+
const containerInfo = await this.getContainerByName(containerName);
|
454
|
+
if (!containerInfo) {
|
455
|
+
return [{
|
456
|
+
source: "stdout",
|
457
|
+
level: "ERROR",
|
458
|
+
time: Date.now(),
|
459
|
+
message: "Container not found"
|
460
|
+
}];
|
461
|
+
}
|
462
|
+
|
463
|
+
return containerInfo.getLogs()
|
464
|
+
}
|
452
465
|
}
|
453
466
|
|
454
467
|
export class ContainerInfo {
|
@@ -558,6 +571,70 @@ export class ContainerInfo {
|
|
558
571
|
|
559
572
|
return ports;
|
560
573
|
}
|
574
|
+
|
575
|
+
async getLogs():Promise<LogEntry[]> {
|
576
|
+
|
577
|
+
const logStream = await this.native.logs({
|
578
|
+
stdout: true,
|
579
|
+
stderr: true,
|
580
|
+
follow: false,
|
581
|
+
tail: 100,
|
582
|
+
timestamps: true,
|
583
|
+
}) as ReadStream;
|
584
|
+
|
585
|
+
const out = [] as LogEntry[];
|
586
|
+
await promisifyStream(logStream, (data) => {
|
587
|
+
const buf = data as Buffer;
|
588
|
+
let offset = 0;
|
589
|
+
while(offset < buf.length) {
|
590
|
+
try {
|
591
|
+
// Read the docker log format - explained here:
|
592
|
+
// https://docs.docker.com/engine/api/v1.41/#operation/ContainerAttach
|
593
|
+
// or here : https://ahmet.im/blog/docker-logs-api-binary-format-explained/
|
594
|
+
|
595
|
+
// First byte is stream type
|
596
|
+
const streamTypeInt = buf.readInt8(offset);
|
597
|
+
const streamType:LogSource = streamTypeInt === 1 ? 'stdout' : 'stderr';
|
598
|
+
|
599
|
+
// Bytes 4-8 is frame size
|
600
|
+
const messageLength = buf.readInt32BE(offset + 4);
|
601
|
+
|
602
|
+
// After that is the message - with the message length
|
603
|
+
const dataWithoutStreamType = buf.subarray(offset + 8, offset + 8 + messageLength);
|
604
|
+
const raw = dataWithoutStreamType.toString();
|
605
|
+
|
606
|
+
// Split the message into date and message
|
607
|
+
const firstSpaceIx = raw.indexOf(' ');
|
608
|
+
const dateString = raw.substring(0, firstSpaceIx);
|
609
|
+
const line = raw.substring(firstSpaceIx + 1);
|
610
|
+
offset = offset + messageLength + 8;
|
611
|
+
if (!dateString) {
|
612
|
+
continue;
|
613
|
+
}
|
614
|
+
out.push({
|
615
|
+
time: new Date(dateString).getTime(),
|
616
|
+
message: line,
|
617
|
+
level: 'INFO',
|
618
|
+
source: streamType,
|
619
|
+
});
|
620
|
+
} catch (err) {
|
621
|
+
console.error('Error parsing log entry', err);
|
622
|
+
offset = buf.length
|
623
|
+
}
|
624
|
+
}
|
625
|
+
});
|
626
|
+
|
627
|
+
if (out.length === 0) {
|
628
|
+
out.push({
|
629
|
+
time: Date.now(),
|
630
|
+
message: 'No logs found for container',
|
631
|
+
level: 'INFO',
|
632
|
+
source: 'stdout',
|
633
|
+
});
|
634
|
+
}
|
635
|
+
|
636
|
+
return out;
|
637
|
+
}
|
561
638
|
}
|
562
639
|
|
563
640
|
export function getExtraHosts(dockerVersion: string): string[] | undefined {
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import ClusterConfiguration, { DefinitionInfo } from '@kapeta/local-cluster-config';
|
2
|
+
import {parseKapetaUri} from "@kapeta/nodejs-utils";
|
2
3
|
|
3
4
|
const CACHE_TTL = 60 * 1000; // 1 min
|
4
5
|
|
@@ -46,6 +47,13 @@ class DefinitionsManager {
|
|
46
47
|
return this.doCached(key, () => ClusterConfiguration.getDefinitions(kindFilter));
|
47
48
|
}
|
48
49
|
|
50
|
+
public exists(ref: string) {
|
51
|
+
const uri = parseKapetaUri(ref);
|
52
|
+
return !!this.getDefinitions().find((d) => {
|
53
|
+
return parseKapetaUri(`${d.definition.metadata.name}:${d.version}`).id === uri.id;
|
54
|
+
});
|
55
|
+
}
|
56
|
+
|
49
57
|
public getProviderDefinitions() {
|
50
58
|
return this.doCached('providers', () => ClusterConfiguration.getProviderDefinitions());
|
51
59
|
}
|