@kapeta/local-cluster-service 0.9.1 → 0.10.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 +4 -8
- package/dist/cjs/src/containerManager.js +69 -64
- package/dist/cjs/src/instanceManager.js +4 -2
- package/dist/cjs/src/operatorManager.js +40 -25
- package/dist/cjs/src/utils/BlockInstanceRunner.d.ts +3 -1
- package/dist/cjs/src/utils/BlockInstanceRunner.js +159 -242
- package/dist/esm/src/containerManager.d.ts +4 -8
- package/dist/esm/src/containerManager.js +69 -64
- package/dist/esm/src/instanceManager.js +4 -2
- package/dist/esm/src/operatorManager.js +42 -27
- package/dist/esm/src/utils/BlockInstanceRunner.d.ts +3 -1
- package/dist/esm/src/utils/BlockInstanceRunner.js +159 -242
- package/package.json +1 -1
- package/src/containerManager.ts +69 -73
- package/src/instanceManager.ts +8 -2
- package/src/operatorManager.ts +52 -26
- package/src/utils/BlockInstanceRunner.ts +203 -272
@@ -100,7 +100,7 @@ class BlockInstanceRunner {
|
|
100
100
|
processInfo = await this._startLocalProcess(blockInstance, blockUri, env, assetVersion);
|
101
101
|
}
|
102
102
|
else {
|
103
|
-
processInfo = await this._startDockerProcess(blockInstance, blockUri, env);
|
103
|
+
processInfo = await this._startDockerProcess(blockInstance, blockUri, env, assetVersion);
|
104
104
|
}
|
105
105
|
if (portTypes.length > 0) {
|
106
106
|
processInfo.portType = portTypes[0];
|
@@ -134,57 +134,16 @@ class BlockInstanceRunner {
|
|
134
134
|
throw new Error(`Missing docker image information: ${JSON.stringify(localContainer)}`);
|
135
135
|
}
|
136
136
|
const containerName = (0, utils_1.getBlockInstanceContainerName)(blockInstance.id);
|
137
|
-
const logs = new LogData_1.LogData();
|
138
|
-
logs.addLog(`Starting block ${blockInstance.ref}`);
|
139
|
-
let containerInfo = await containerManager_1.containerManager.getContainerByName(containerName);
|
140
|
-
let container = containerInfo?.native;
|
141
|
-
console.log('Starting dev container', containerName);
|
142
|
-
if (containerInfo) {
|
143
|
-
console.log(`Dev container already exists. Deleting...`);
|
144
|
-
try {
|
145
|
-
await containerInfo.remove({
|
146
|
-
force: true,
|
147
|
-
});
|
148
|
-
}
|
149
|
-
catch (e) {
|
150
|
-
throw new Error('Failed to delete existing container: ' + e.message);
|
151
|
-
}
|
152
|
-
container = undefined;
|
153
|
-
containerInfo = undefined;
|
154
|
-
}
|
155
|
-
logs.addLog(`Creating new container for block: ${containerName}`);
|
156
|
-
console.log('Creating new dev container', containerName, dockerImage);
|
157
|
-
await containerManager_1.containerManager.pull(dockerImage);
|
158
137
|
const startCmd = localContainer.handlers?.onCreate ? localContainer.handlers.onCreate : '';
|
159
138
|
const dockerOpts = localContainer.options ?? {};
|
160
139
|
const homeDir = localContainer.userHome ? localContainer.userHome : '/root';
|
161
140
|
const workingDir = localContainer.workingDir ? localContainer.workingDir : '/workspace';
|
162
|
-
const
|
163
|
-
const ExposedPorts = {};
|
164
|
-
const addonEnv = {};
|
165
|
-
const PortBindings = {};
|
166
|
-
const portTypes = getProviderPorts(assetVersion);
|
167
|
-
let port = 80;
|
168
|
-
const promises = portTypes.map(async (portType) => {
|
169
|
-
const publicPort = await serviceManager_1.serviceManager.ensureServicePort(this._systemId, blockInstance.id, portType);
|
170
|
-
const thisPort = port++; //TODO: Not sure how we should handle multiple ports or non-HTTP ports
|
171
|
-
const dockerPort = `${thisPort}/tcp`;
|
172
|
-
ExposedPorts[dockerPort] = {};
|
173
|
-
addonEnv[`KAPETA_LOCAL_SERVER_PORT_${portType.toUpperCase()}`] = '' + thisPort;
|
174
|
-
PortBindings[dockerPort] = [
|
175
|
-
{
|
176
|
-
HostIp: bindHost,
|
177
|
-
HostPort: `${publicPort}`,
|
178
|
-
},
|
179
|
-
];
|
180
|
-
});
|
181
|
-
await Promise.all(promises);
|
141
|
+
const { PortBindings, ExposedPorts, addonEnv } = await this.getDockerPortBindings(blockInstance, assetVersion);
|
182
142
|
let HealthCheck = undefined;
|
183
143
|
if (localContainer.healthcheck) {
|
184
144
|
HealthCheck = containerManager_1.containerManager.toDockerHealth({ cmd: localContainer.healthcheck });
|
185
145
|
}
|
186
|
-
|
187
|
-
container = await containerManager_1.containerManager.startContainer({
|
146
|
+
return this.ensureContainer({
|
188
147
|
Image: dockerImage,
|
189
148
|
name: containerName,
|
190
149
|
WorkingDir: workingDir,
|
@@ -211,8 +170,163 @@ class BlockInstanceRunner {
|
|
211
170
|
},
|
212
171
|
...dockerOpts,
|
213
172
|
});
|
173
|
+
}
|
174
|
+
async _startDockerProcess(blockInstance, blockInfo, env, assetVersion) {
|
175
|
+
const { versionFile } = local_cluster_config_1.default.getRepositoryAssetInfoPath(blockInfo.handle, blockInfo.name, blockInfo.version);
|
176
|
+
const versionYml = versionFile;
|
177
|
+
if (!node_fs_1.default.existsSync(versionYml)) {
|
178
|
+
throw new Error(`Did not find version info at the expected path: ${versionYml}`);
|
179
|
+
}
|
180
|
+
const versionInfo = (0, utils_1.readYML)(versionYml);
|
181
|
+
if (versionInfo?.artifact?.type !== 'docker') {
|
182
|
+
throw new Error(`Unsupported artifact type: ${versionInfo?.artifact?.type}`);
|
183
|
+
}
|
184
|
+
const dockerImage = versionInfo?.artifact?.details?.primary;
|
185
|
+
if (!dockerImage) {
|
186
|
+
throw new Error(`Missing docker image information: ${JSON.stringify(versionInfo?.artifact?.details)}`);
|
187
|
+
}
|
188
|
+
const { PortBindings, ExposedPorts, addonEnv } = await this.getDockerPortBindings(blockInstance, assetVersion);
|
189
|
+
const containerName = (0, utils_1.getBlockInstanceContainerName)(blockInstance.id);
|
190
|
+
// For windows we need to default to root
|
191
|
+
const innerHome = process.platform === 'win32' ? '/root/.kapeta' : local_cluster_config_1.default.getKapetaBasedir();
|
192
|
+
return this.ensureContainer({
|
193
|
+
Image: dockerImage,
|
194
|
+
name: containerName,
|
195
|
+
ExposedPorts,
|
196
|
+
Labels: {
|
197
|
+
instance: blockInstance.id,
|
198
|
+
},
|
199
|
+
Env: [
|
200
|
+
...DOCKER_ENV_VARS,
|
201
|
+
`KAPETA_LOCAL_CLUSTER_PORT=${clusterService_1.clusterService.getClusterServicePort()}`,
|
202
|
+
...Object.entries({
|
203
|
+
...env,
|
204
|
+
...addonEnv
|
205
|
+
}).map(([key, value]) => `${key}=${value}`),
|
206
|
+
],
|
207
|
+
HostConfig: {
|
208
|
+
Binds: [`${(0, containerManager_1.toLocalBindVolume)(local_cluster_config_1.default.getKapetaBasedir())}:${innerHome}`],
|
209
|
+
PortBindings,
|
210
|
+
},
|
211
|
+
});
|
212
|
+
}
|
213
|
+
/**
|
214
|
+
*
|
215
|
+
* @param blockInstance
|
216
|
+
* @param blockUri
|
217
|
+
* @param providerDefinition
|
218
|
+
* @param {{[key:string]:string}} env
|
219
|
+
* @return {Promise<ProcessDetails>}
|
220
|
+
* @private
|
221
|
+
*/
|
222
|
+
async _startOperatorProcess(blockInstance, blockUri, providerDefinition, env) {
|
223
|
+
const { assetFile } = local_cluster_config_1.default.getRepositoryAssetInfoPath(blockUri.handle, blockUri.name, blockUri.version);
|
224
|
+
const kapetaYmlPath = assetFile;
|
225
|
+
if (!node_fs_1.default.existsSync(kapetaYmlPath)) {
|
226
|
+
throw new Error(`Did not find kapeta.yml at the expected path: ${kapetaYmlPath}`);
|
227
|
+
}
|
228
|
+
const spec = providerDefinition.definition.spec;
|
229
|
+
const providerRef = `${providerDefinition.definition.metadata.name}:${providerDefinition.version}`;
|
230
|
+
if (!spec?.local?.image) {
|
231
|
+
throw new Error(`Provider did not have local image: ${providerRef}`);
|
232
|
+
}
|
233
|
+
const dockerImage = spec?.local?.image;
|
234
|
+
const containerName = (0, utils_1.getBlockInstanceContainerName)(blockInstance.id);
|
235
|
+
const logs = new LogData_1.LogData();
|
236
|
+
const bindHost = (0, utils_1.getBindHost)();
|
237
|
+
const ExposedPorts = {};
|
238
|
+
const addonEnv = {};
|
239
|
+
const PortBindings = {};
|
240
|
+
let HealthCheck = undefined;
|
241
|
+
let Mounts = [];
|
242
|
+
const promises = Object.entries(spec.local.ports).map(async ([portType, value]) => {
|
243
|
+
const dockerPort = `${value.port}/${value.type}`;
|
244
|
+
ExposedPorts[dockerPort] = {};
|
245
|
+
addonEnv[`KAPETA_LOCAL_SERVER_PORT_${portType.toUpperCase()}`] = value.port;
|
246
|
+
const publicPort = await serviceManager_1.serviceManager.ensureServicePort(this._systemId, blockInstance.id, portType);
|
247
|
+
PortBindings[dockerPort] = [
|
248
|
+
{
|
249
|
+
HostIp: bindHost,
|
250
|
+
HostPort: `${publicPort}`,
|
251
|
+
},
|
252
|
+
];
|
253
|
+
});
|
254
|
+
await Promise.all(promises);
|
255
|
+
if (spec.local?.env) {
|
256
|
+
Object.entries(spec.local.env).forEach(([key, value]) => {
|
257
|
+
addonEnv[key] = value;
|
258
|
+
});
|
259
|
+
}
|
260
|
+
if (spec.local?.mounts) {
|
261
|
+
const mounts = containerManager_1.containerManager.createMounts(blockUri.id, spec.local.mounts);
|
262
|
+
Mounts = containerManager_1.containerManager.toDockerMounts(mounts);
|
263
|
+
}
|
264
|
+
if (spec.local?.health) {
|
265
|
+
HealthCheck = containerManager_1.containerManager.toDockerHealth(spec.local?.health);
|
266
|
+
}
|
267
|
+
// For windows we need to default to root
|
268
|
+
const innerHome = process.platform === 'win32' ? '/root/.kapeta' : local_cluster_config_1.default.getKapetaBasedir();
|
269
|
+
logs.addLog(`Creating new container for block: ${containerName}`);
|
270
|
+
const out = await this.ensureContainer({
|
271
|
+
Image: dockerImage,
|
272
|
+
name: containerName,
|
273
|
+
ExposedPorts,
|
274
|
+
HealthCheck,
|
275
|
+
HostConfig: {
|
276
|
+
Binds: [
|
277
|
+
`${(0, containerManager_1.toLocalBindVolume)(kapetaYmlPath)}:/kapeta.yml:ro`,
|
278
|
+
`${(0, containerManager_1.toLocalBindVolume)(local_cluster_config_1.default.getKapetaBasedir())}:${innerHome}`,
|
279
|
+
],
|
280
|
+
PortBindings,
|
281
|
+
Mounts,
|
282
|
+
},
|
283
|
+
Labels: {
|
284
|
+
instance: blockInstance.id,
|
285
|
+
},
|
286
|
+
Env: [
|
287
|
+
`KAPETA_INSTANCE_NAME=${blockInstance.ref}`,
|
288
|
+
`KAPETA_LOCAL_CLUSTER_PORT=${clusterService_1.clusterService.getClusterServicePort()}`,
|
289
|
+
...DOCKER_ENV_VARS,
|
290
|
+
...Object.entries({
|
291
|
+
...env,
|
292
|
+
...addonEnv,
|
293
|
+
}).map(([key, value]) => `${key}=${value}`),
|
294
|
+
],
|
295
|
+
});
|
296
|
+
const portTypes = spec.local.ports ? Object.keys(spec.local.ports) : [];
|
297
|
+
if (portTypes.length > 0) {
|
298
|
+
out.portType = portTypes[0];
|
299
|
+
}
|
300
|
+
return out;
|
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 logs = new LogData_1.LogData();
|
327
|
+
const container = await containerManager_1.containerManager.ensureContainer(opts);
|
214
328
|
try {
|
215
|
-
if (HealthCheck) {
|
329
|
+
if (opts.HealthCheck) {
|
216
330
|
await containerManager_1.containerManager.waitForHealthy(container);
|
217
331
|
}
|
218
332
|
else {
|
@@ -274,202 +388,5 @@ class BlockInstanceRunner {
|
|
274
388
|
},
|
275
389
|
};
|
276
390
|
}
|
277
|
-
async _startDockerProcess(blockInstance, blockInfo, env) {
|
278
|
-
const { versionFile } = local_cluster_config_1.default.getRepositoryAssetInfoPath(blockInfo.handle, blockInfo.name, blockInfo.version);
|
279
|
-
const versionYml = versionFile;
|
280
|
-
if (!node_fs_1.default.existsSync(versionYml)) {
|
281
|
-
throw new Error(`Did not find version info at the expected path: ${versionYml}`);
|
282
|
-
}
|
283
|
-
const versionInfo = (0, utils_1.readYML)(versionYml);
|
284
|
-
if (versionInfo?.artifact?.type !== 'docker') {
|
285
|
-
throw new Error(`Unsupported artifact type: ${versionInfo?.artifact?.type}`);
|
286
|
-
}
|
287
|
-
const dockerImage = versionInfo?.artifact?.details?.primary;
|
288
|
-
if (!dockerImage) {
|
289
|
-
throw new Error(`Missing docker image information: ${JSON.stringify(versionInfo?.artifact?.details)}`);
|
290
|
-
}
|
291
|
-
const containerName = (0, utils_1.getBlockInstanceContainerName)(blockInstance.id);
|
292
|
-
const logs = new LogData_1.LogData();
|
293
|
-
const containerInfo = await containerManager_1.containerManager.getContainerByName(containerName);
|
294
|
-
let container = containerInfo?.native;
|
295
|
-
// For windows we need to default to root
|
296
|
-
const innerHome = process.platform === 'win32' ? '/root/.kapeta' : local_cluster_config_1.default.getKapetaBasedir();
|
297
|
-
if (container) {
|
298
|
-
const containerData = container.data;
|
299
|
-
if (containerData.State === 'running') {
|
300
|
-
logs.addLog(`Found existing running container for block: ${containerName}`);
|
301
|
-
}
|
302
|
-
else {
|
303
|
-
logs.addLog(`Found existing container for block: ${containerName}. Starting now`);
|
304
|
-
await container.start();
|
305
|
-
}
|
306
|
-
}
|
307
|
-
else {
|
308
|
-
logs.addLog(`Creating new container for block: ${containerName}`);
|
309
|
-
container = await containerManager_1.containerManager.startContainer({
|
310
|
-
Image: dockerImage,
|
311
|
-
name: containerName,
|
312
|
-
Labels: {
|
313
|
-
instance: blockInstance.id,
|
314
|
-
},
|
315
|
-
Env: [
|
316
|
-
...DOCKER_ENV_VARS,
|
317
|
-
`KAPETA_LOCAL_CLUSTER_PORT=${clusterService_1.clusterService.getClusterServicePort()}`,
|
318
|
-
...Object.entries(env).map(([key, value]) => `${key}=${value}`),
|
319
|
-
],
|
320
|
-
HostConfig: {
|
321
|
-
Binds: [`${(0, containerManager_1.toLocalBindVolume)(local_cluster_config_1.default.getKapetaBasedir())}:${innerHome}`],
|
322
|
-
},
|
323
|
-
});
|
324
|
-
try {
|
325
|
-
await containerManager_1.containerManager.waitForReady(container);
|
326
|
-
}
|
327
|
-
catch (e) {
|
328
|
-
logs.addLog(e.message, 'ERROR');
|
329
|
-
}
|
330
|
-
}
|
331
|
-
return this._handleContainer(container, logs);
|
332
|
-
}
|
333
|
-
/**
|
334
|
-
*
|
335
|
-
* @param blockInstance
|
336
|
-
* @param blockUri
|
337
|
-
* @param providerDefinition
|
338
|
-
* @param {{[key:string]:string}} env
|
339
|
-
* @return {Promise<ProcessDetails>}
|
340
|
-
* @private
|
341
|
-
*/
|
342
|
-
async _startOperatorProcess(blockInstance, blockUri, providerDefinition, env) {
|
343
|
-
const { assetFile } = local_cluster_config_1.default.getRepositoryAssetInfoPath(blockUri.handle, blockUri.name, blockUri.version);
|
344
|
-
const kapetaYmlPath = assetFile;
|
345
|
-
if (!node_fs_1.default.existsSync(kapetaYmlPath)) {
|
346
|
-
throw new Error(`Did not find kapeta.yml at the expected path: ${kapetaYmlPath}`);
|
347
|
-
}
|
348
|
-
const spec = providerDefinition.definition.spec;
|
349
|
-
const providerRef = `${providerDefinition.definition.metadata.name}:${providerDefinition.version}`;
|
350
|
-
if (!spec?.local?.image) {
|
351
|
-
throw new Error(`Provider did not have local image: ${providerRef}`);
|
352
|
-
}
|
353
|
-
const dockerImage = spec?.local?.image;
|
354
|
-
try {
|
355
|
-
await containerManager_1.containerManager.pull(dockerImage);
|
356
|
-
}
|
357
|
-
catch (e) {
|
358
|
-
console.warn('Failed to pull image. Continuing...', e);
|
359
|
-
}
|
360
|
-
const containerName = (0, utils_1.getBlockInstanceContainerName)(blockInstance.id);
|
361
|
-
const logs = new LogData_1.LogData();
|
362
|
-
const containerInfo = await containerManager_1.containerManager.getContainerByName(containerName);
|
363
|
-
let container = containerInfo?.native;
|
364
|
-
if (container) {
|
365
|
-
const containerData = container.data;
|
366
|
-
if (containerData.State === 'running') {
|
367
|
-
logs.addLog(`Found existing running container for block: ${containerName}`);
|
368
|
-
}
|
369
|
-
else {
|
370
|
-
if (containerData.State?.ExitCode > 0) {
|
371
|
-
logs.addLog(`Container exited with code: ${containerData.State.ExitCode}. Deleting...`);
|
372
|
-
try {
|
373
|
-
await containerManager_1.containerManager.remove(container);
|
374
|
-
}
|
375
|
-
catch (e) { }
|
376
|
-
container = undefined;
|
377
|
-
}
|
378
|
-
else {
|
379
|
-
logs.addLog(`Found existing container for block: ${containerName}. Starting now`);
|
380
|
-
try {
|
381
|
-
await container.start();
|
382
|
-
}
|
383
|
-
catch (e) {
|
384
|
-
console.warn('Failed to start container. Deleting...', e);
|
385
|
-
try {
|
386
|
-
await containerManager_1.containerManager.remove(container);
|
387
|
-
}
|
388
|
-
catch (e) { }
|
389
|
-
container = undefined;
|
390
|
-
}
|
391
|
-
}
|
392
|
-
}
|
393
|
-
}
|
394
|
-
const bindHost = (0, utils_1.getBindHost)();
|
395
|
-
if (!container) {
|
396
|
-
const ExposedPorts = {};
|
397
|
-
const addonEnv = {};
|
398
|
-
const PortBindings = {};
|
399
|
-
let HealthCheck = undefined;
|
400
|
-
let Mounts = [];
|
401
|
-
const promises = Object.entries(spec.local.ports).map(async ([portType, value]) => {
|
402
|
-
const dockerPort = `${value.port}/${value.type}`;
|
403
|
-
ExposedPorts[dockerPort] = {};
|
404
|
-
addonEnv[`KAPETA_LOCAL_SERVER_PORT_${portType.toUpperCase()}`] = value.port;
|
405
|
-
const publicPort = await serviceManager_1.serviceManager.ensureServicePort(this._systemId, blockInstance.id, portType);
|
406
|
-
PortBindings[dockerPort] = [
|
407
|
-
{
|
408
|
-
HostIp: bindHost,
|
409
|
-
HostPort: `${publicPort}`,
|
410
|
-
},
|
411
|
-
];
|
412
|
-
});
|
413
|
-
await Promise.all(promises);
|
414
|
-
if (spec.local?.env) {
|
415
|
-
Object.entries(spec.local.env).forEach(([key, value]) => {
|
416
|
-
addonEnv[key] = value;
|
417
|
-
});
|
418
|
-
}
|
419
|
-
if (spec.local?.mounts) {
|
420
|
-
const mounts = containerManager_1.containerManager.createMounts(blockUri.id, spec.local.mounts);
|
421
|
-
Mounts = containerManager_1.containerManager.toDockerMounts(mounts);
|
422
|
-
}
|
423
|
-
if (spec.local?.health) {
|
424
|
-
HealthCheck = containerManager_1.containerManager.toDockerHealth(spec.local?.health);
|
425
|
-
}
|
426
|
-
// For windows we need to default to root
|
427
|
-
const innerHome = process.platform === 'win32' ? '/root/.kapeta' : local_cluster_config_1.default.getKapetaBasedir();
|
428
|
-
logs.addLog(`Creating new container for block: ${containerName}`);
|
429
|
-
container = await containerManager_1.containerManager.startContainer({
|
430
|
-
Image: dockerImage,
|
431
|
-
name: containerName,
|
432
|
-
ExposedPorts,
|
433
|
-
HealthCheck,
|
434
|
-
HostConfig: {
|
435
|
-
Binds: [
|
436
|
-
`${(0, containerManager_1.toLocalBindVolume)(kapetaYmlPath)}:/kapeta.yml:ro`,
|
437
|
-
`${(0, containerManager_1.toLocalBindVolume)(local_cluster_config_1.default.getKapetaBasedir())}:${innerHome}`,
|
438
|
-
],
|
439
|
-
PortBindings,
|
440
|
-
Mounts,
|
441
|
-
},
|
442
|
-
Labels: {
|
443
|
-
instance: blockInstance.id,
|
444
|
-
},
|
445
|
-
Env: [
|
446
|
-
`KAPETA_INSTANCE_NAME=${blockInstance.ref}`,
|
447
|
-
`KAPETA_LOCAL_CLUSTER_PORT=${clusterService_1.clusterService.getClusterServicePort()}`,
|
448
|
-
...DOCKER_ENV_VARS,
|
449
|
-
...Object.entries({
|
450
|
-
...env,
|
451
|
-
...addonEnv,
|
452
|
-
}).map(([key, value]) => `${key}=${value}`),
|
453
|
-
],
|
454
|
-
});
|
455
|
-
try {
|
456
|
-
if (HealthCheck) {
|
457
|
-
await containerManager_1.containerManager.waitForHealthy(container);
|
458
|
-
}
|
459
|
-
else {
|
460
|
-
await containerManager_1.containerManager.waitForReady(container);
|
461
|
-
}
|
462
|
-
}
|
463
|
-
catch (e) {
|
464
|
-
logs.addLog(e.message, 'ERROR');
|
465
|
-
}
|
466
|
-
}
|
467
|
-
const out = await this._handleContainer(container, logs, true);
|
468
|
-
const portTypes = spec.local.ports ? Object.keys(spec.local.ports) : [];
|
469
|
-
if (portTypes.length > 0) {
|
470
|
-
out.portType = portTypes[0];
|
471
|
-
}
|
472
|
-
return out;
|
473
|
-
}
|
474
391
|
}
|
475
392
|
exports.BlockInstanceRunner = BlockInstanceRunner;
|
@@ -41,6 +41,7 @@ interface Health {
|
|
41
41
|
timeout?: number;
|
42
42
|
retries?: number;
|
43
43
|
}
|
44
|
+
export declare const CONTAINER_LABEL_PORT_PREFIX = "kapeta_port-";
|
44
45
|
export declare const HEALTH_CHECK_TIMEOUT: number;
|
45
46
|
declare class ContainerManager {
|
46
47
|
private _docker;
|
@@ -56,7 +57,7 @@ declare class ContainerManager {
|
|
56
57
|
ping(): Promise<void>;
|
57
58
|
docker(): Docker;
|
58
59
|
getContainerByName(containerName: string): Promise<ContainerInfo | undefined>;
|
59
|
-
pull(image: string, cacheForMS?: number): Promise<
|
60
|
+
pull(image: string, cacheForMS?: number): Promise<boolean>;
|
60
61
|
toDockerMounts(mounts: StringMap): DockerMounts[];
|
61
62
|
toDockerHealth(health: Health): {
|
62
63
|
Test: string[];
|
@@ -64,13 +65,8 @@ declare class ContainerManager {
|
|
64
65
|
Timeout: number;
|
65
66
|
Retries: number;
|
66
67
|
};
|
67
|
-
|
68
|
-
|
69
|
-
mounts: {};
|
70
|
-
env: {};
|
71
|
-
cmd: string;
|
72
|
-
health: Health;
|
73
|
-
}): Promise<ContainerInfo>;
|
68
|
+
private applyHash;
|
69
|
+
ensureContainer(opts: any): Promise<Container>;
|
74
70
|
startContainer(opts: any): Promise<Container>;
|
75
71
|
waitForReady(container: Container, attempt?: number): Promise<void>;
|
76
72
|
waitForHealthy(container: Container, attempt?: number): Promise<void>;
|
@@ -6,9 +6,9 @@ import FSExtra from 'fs-extra';
|
|
6
6
|
import { Docker } from 'node-docker-api';
|
7
7
|
import { parseKapetaUri } from '@kapeta/nodejs-utils';
|
8
8
|
import ClusterConfiguration from '@kapeta/local-cluster-config';
|
9
|
-
import
|
10
|
-
import
|
11
|
-
const
|
9
|
+
import uuid from 'node-uuid';
|
10
|
+
import md5 from 'md5';
|
11
|
+
export const CONTAINER_LABEL_PORT_PREFIX = 'kapeta_port-';
|
12
12
|
const NANO_SECOND = 1000000;
|
13
13
|
const HEALTH_CHECK_INTERVAL = 3000;
|
14
14
|
const HEALTH_CHECK_MAX = 20;
|
@@ -141,22 +141,19 @@ class ContainerManager {
|
|
141
141
|
if (!tag) {
|
142
142
|
tag = 'latest';
|
143
143
|
}
|
144
|
-
if (
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
return;
|
149
|
-
}
|
150
|
-
}
|
151
|
-
const imageTagList = (await this.docker().image.list())
|
152
|
-
.map((image) => image.data)
|
153
|
-
.filter((imageData) => !!imageData.RepoTags)
|
154
|
-
.map((imageData) => imageData.RepoTags);
|
155
|
-
if (imageTagList.some((imageTags) => imageTags.indexOf(image) > -1)) {
|
156
|
-
console.log('Image found: %s', image);
|
157
|
-
return;
|
144
|
+
if (IMAGE_PULL_CACHE[image]) {
|
145
|
+
const timeSince = Date.now() - IMAGE_PULL_CACHE[image];
|
146
|
+
if (timeSince < cacheForMS) {
|
147
|
+
return false;
|
158
148
|
}
|
159
|
-
|
149
|
+
}
|
150
|
+
const imageTagList = (await this.docker().image.list())
|
151
|
+
.map((image) => image.data)
|
152
|
+
.filter((imageData) => !!imageData.RepoTags)
|
153
|
+
.map((imageData) => imageData.RepoTags);
|
154
|
+
if (imageTagList.some((imageTags) => imageTags.indexOf(image) > -1)) {
|
155
|
+
console.log('Image found: %s', image);
|
156
|
+
return false;
|
160
157
|
}
|
161
158
|
console.log('Pulling image: %s', image);
|
162
159
|
await this.docker()
|
@@ -167,6 +164,7 @@ class ContainerManager {
|
|
167
164
|
.then((stream) => promisifyStream(stream));
|
168
165
|
IMAGE_PULL_CACHE[image] = Date.now();
|
169
166
|
console.log('Image pulled: %s', image);
|
167
|
+
return true;
|
170
168
|
}
|
171
169
|
toDockerMounts(mounts) {
|
172
170
|
const Mounts = [];
|
@@ -189,51 +187,58 @@ class ContainerManager {
|
|
189
187
|
Retries: health.retries || 10,
|
190
188
|
};
|
191
189
|
}
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
const Labels = {
|
196
|
-
kapeta: 'true',
|
197
|
-
};
|
198
|
-
await this.pull(image);
|
199
|
-
const bindHost = getBindHost();
|
200
|
-
const ExposedPorts = {};
|
201
|
-
_.forEach(opts.ports, (portInfo, containerPort) => {
|
202
|
-
ExposedPorts['' + containerPort] = {};
|
203
|
-
PortBindings['' + containerPort] = [
|
204
|
-
{
|
205
|
-
HostPort: '' + portInfo.hostPort,
|
206
|
-
HostIp: bindHost,
|
207
|
-
},
|
208
|
-
];
|
209
|
-
Labels[LABEL_PORT_PREFIX + portInfo.hostPort] = portInfo.type;
|
210
|
-
});
|
211
|
-
const Mounts = this.toDockerMounts(opts.mounts);
|
212
|
-
_.forEach(opts.env, (value, name) => {
|
213
|
-
Env.push(name + '=' + value);
|
214
|
-
});
|
215
|
-
let HealthCheck = undefined;
|
216
|
-
if (opts.health) {
|
217
|
-
HealthCheck = this.toDockerHealth(opts.health);
|
218
|
-
}
|
219
|
-
const dockerContainer = await this.startContainer({
|
220
|
-
name: name,
|
221
|
-
Image: image,
|
222
|
-
Hostname: name + '.kapeta',
|
223
|
-
Labels,
|
224
|
-
Cmd: opts.cmd,
|
225
|
-
ExposedPorts,
|
226
|
-
Env,
|
227
|
-
HealthCheck,
|
228
|
-
HostConfig: {
|
229
|
-
PortBindings,
|
230
|
-
Mounts,
|
231
|
-
},
|
232
|
-
});
|
233
|
-
if (opts.health) {
|
234
|
-
await this.waitForHealthy(dockerContainer);
|
190
|
+
applyHash(dockerOpts) {
|
191
|
+
if (dockerOpts?.Labels?.HASH) {
|
192
|
+
delete dockerOpts.Labels.HASH;
|
235
193
|
}
|
236
|
-
|
194
|
+
const hash = md5(JSON.stringify(dockerOpts));
|
195
|
+
if (!dockerOpts.Labels) {
|
196
|
+
dockerOpts.Labels = {};
|
197
|
+
}
|
198
|
+
dockerOpts.Labels.HASH = hash;
|
199
|
+
}
|
200
|
+
async ensureContainer(opts) {
|
201
|
+
let imagePulled = false;
|
202
|
+
try {
|
203
|
+
imagePulled = await this.pull(opts.Image);
|
204
|
+
}
|
205
|
+
catch (e) {
|
206
|
+
console.warn('Failed to pull image. Continuing...', e);
|
207
|
+
}
|
208
|
+
this.applyHash(opts);
|
209
|
+
if (!opts.name) {
|
210
|
+
console.log('Starting unnamed container: %s', opts.Image);
|
211
|
+
return this.startContainer(opts);
|
212
|
+
}
|
213
|
+
const containerInfo = await this.getContainerByName(opts.name);
|
214
|
+
if (imagePulled) {
|
215
|
+
console.log('New version of image was pulled: %s', opts.Image);
|
216
|
+
}
|
217
|
+
else {
|
218
|
+
// If image was pulled always recreate
|
219
|
+
if (!containerInfo) {
|
220
|
+
console.log('Starting new container: %s', opts.name);
|
221
|
+
return this.startContainer(opts);
|
222
|
+
}
|
223
|
+
const containerData = containerInfo.native.data;
|
224
|
+
if (containerData?.Labels?.HASH === opts.Labels.HASH) {
|
225
|
+
if (!(await containerInfo.isRunning())) {
|
226
|
+
console.log('Starting previously created container: %s', opts.name);
|
227
|
+
await containerInfo.start();
|
228
|
+
}
|
229
|
+
else {
|
230
|
+
console.log('Previously created container already running: %s', opts.name);
|
231
|
+
}
|
232
|
+
return containerInfo.native;
|
233
|
+
}
|
234
|
+
}
|
235
|
+
if (containerInfo) {
|
236
|
+
// Remove the container and start a new one
|
237
|
+
console.log('Replacing previously created container: %s', opts.name);
|
238
|
+
await containerInfo.remove({ force: true });
|
239
|
+
}
|
240
|
+
console.log('Starting new container: %s', opts.name);
|
241
|
+
return this.startContainer(opts);
|
237
242
|
}
|
238
243
|
async startContainer(opts) {
|
239
244
|
const extraHosts = getExtraHosts(this._version);
|
@@ -411,10 +416,10 @@ export class ContainerInfo {
|
|
411
416
|
const portTypes = {};
|
412
417
|
const ports = {};
|
413
418
|
_.forEach(inspectResult.Config.Labels, (portType, name) => {
|
414
|
-
if (!name.startsWith(
|
419
|
+
if (!name.startsWith(CONTAINER_LABEL_PORT_PREFIX)) {
|
415
420
|
return;
|
416
421
|
}
|
417
|
-
const hostPort = name.substr(
|
422
|
+
const hostPort = name.substr(CONTAINER_LABEL_PORT_PREFIX.length);
|
418
423
|
portTypes[hostPort] = portType;
|
419
424
|
});
|
420
425
|
_.forEach(inspectResult.HostConfig.PortBindings, (portBindings, containerPortSpec) => {
|
@@ -447,7 +447,8 @@ export class InstanceManager {
|
|
447
447
|
changed = true;
|
448
448
|
}
|
449
449
|
}
|
450
|
-
if (instance.desiredStatus === DesiredInstanceStatus.RUN &&
|
450
|
+
if (instance.desiredStatus === DesiredInstanceStatus.RUN &&
|
451
|
+
[InstanceStatus.STOPPED, InstanceStatus.FAILED, InstanceStatus.STOPPING].includes(newStatus)) {
|
451
452
|
//If the instance is stopped but we want it to run, start it
|
452
453
|
try {
|
453
454
|
await this.start(instance.systemId, instance.instanceId);
|
@@ -457,7 +458,8 @@ export class InstanceManager {
|
|
457
458
|
}
|
458
459
|
return;
|
459
460
|
}
|
460
|
-
if (instance.desiredStatus === DesiredInstanceStatus.STOP &&
|
461
|
+
if (instance.desiredStatus === DesiredInstanceStatus.STOP &&
|
462
|
+
[InstanceStatus.READY, InstanceStatus.STARTING, InstanceStatus.UNHEALTHY].includes(newStatus)) {
|
461
463
|
//If the instance is running but we want it to stop, stop it
|
462
464
|
try {
|
463
465
|
await this.stop(instance.systemId, instance.instanceId);
|