@kapeta/local-cluster-service 0.34.2 → 0.36.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.
Files changed (39) hide show
  1. package/.github/workflows/check-license.yml +0 -1
  2. package/CHANGELOG.md +14 -0
  3. package/dist/cjs/src/config/routes.js +9 -0
  4. package/dist/cjs/src/containerManager.d.ts +2 -8
  5. package/dist/cjs/src/containerManager.js +7 -7
  6. package/dist/cjs/src/instanceManager.d.ts +8 -3
  7. package/dist/cjs/src/instanceManager.js +147 -25
  8. package/dist/cjs/src/operatorManager.d.ts +9 -9
  9. package/dist/cjs/src/operatorManager.js +21 -26
  10. package/dist/cjs/src/serviceManager.d.ts +1 -0
  11. package/dist/cjs/src/serviceManager.js +9 -9
  12. package/dist/cjs/src/types.d.ts +40 -0
  13. package/dist/cjs/src/utils/BlockInstanceRunner.d.ts +2 -13
  14. package/dist/cjs/src/utils/BlockInstanceRunner.js +91 -79
  15. package/dist/cjs/src/utils/utils.d.ts +6 -2
  16. package/dist/cjs/src/utils/utils.js +35 -2
  17. package/dist/esm/src/config/routes.js +9 -0
  18. package/dist/esm/src/containerManager.d.ts +2 -8
  19. package/dist/esm/src/containerManager.js +7 -7
  20. package/dist/esm/src/instanceManager.d.ts +8 -3
  21. package/dist/esm/src/instanceManager.js +147 -25
  22. package/dist/esm/src/operatorManager.d.ts +9 -9
  23. package/dist/esm/src/operatorManager.js +21 -26
  24. package/dist/esm/src/serviceManager.d.ts +1 -0
  25. package/dist/esm/src/serviceManager.js +9 -9
  26. package/dist/esm/src/types.d.ts +40 -0
  27. package/dist/esm/src/utils/BlockInstanceRunner.d.ts +2 -13
  28. package/dist/esm/src/utils/BlockInstanceRunner.js +91 -79
  29. package/dist/esm/src/utils/utils.d.ts +6 -2
  30. package/dist/esm/src/utils/utils.js +35 -2
  31. package/package.json +1 -1
  32. package/src/config/routes.ts +15 -0
  33. package/src/containerManager.ts +8 -15
  34. package/src/instanceManager.ts +218 -34
  35. package/src/operatorManager.ts +26 -31
  36. package/src/serviceManager.ts +11 -8
  37. package/src/types.ts +36 -0
  38. package/src/utils/BlockInstanceRunner.ts +119 -92
  39. package/src/utils/utils.ts +40 -3
@@ -5,22 +5,24 @@
5
5
 
6
6
  import FSExtra from 'fs-extra';
7
7
  import ClusterConfig, { DefinitionInfo } from '@kapeta/local-cluster-config';
8
- import { getBindHost, getBlockInstanceContainerName, readYML } from './utils';
8
+ import { getBindHost, getBlockInstanceContainerName, readYML, toPortInfo } from './utils';
9
9
  import { KapetaURI, parseKapetaUri, normalizeKapetaUri } from '@kapeta/nodejs-utils';
10
10
  import { DEFAULT_PORT_TYPE, HTTP_PORT_TYPE, HTTP_PORTS, serviceManager } from '../serviceManager';
11
11
  import {
12
12
  COMPOSE_LABEL_PROJECT,
13
13
  COMPOSE_LABEL_SERVICE,
14
+ CONTAINER_LABEL_PORT_PREFIX,
14
15
  containerManager,
15
16
  DockerMounts,
16
17
  toLocalBindVolume,
17
18
  } from '../containerManager';
18
19
  import { LogData } from './LogData';
19
20
  import { clusterService } from '../clusterService';
20
- import { AnyMap, BlockProcessParams, InstanceType, ProcessInfo, StringMap } from '../types';
21
+ import { AnyMap, BlockProcessParams, InstanceType, LocalImageOptions, ProcessInfo, StringMap } from '../types';
21
22
  import { definitionsManager } from '../definitionsManager';
22
23
  import Docker from 'dockerode';
23
24
  import OS from 'node:os';
25
+ import { taskManager } from '../taskManager';
24
26
 
25
27
  const KIND_BLOCK_TYPE_OPERATOR = 'core/block-type-operator';
26
28
  const KAPETA_SYSTEM_ID = 'KAPETA_SYSTEM_ID';
@@ -200,7 +202,7 @@ export class BlockInstanceRunner {
200
202
  throw new Error(`Missing docker image information: ${JSON.stringify(localContainer)}`);
201
203
  }
202
204
 
203
- const containerName = getBlockInstanceContainerName(this._systemId, blockInstance.id);
205
+ const containerName = await getBlockInstanceContainerName(this._systemId, blockInstance.id, targetKindUri.id);
204
206
  const startCmd = localContainer.handlers?.onCreate ? localContainer.handlers.onCreate : '';
205
207
  const dockerOpts = localContainer.options ?? {};
206
208
  const homeDir = localContainer.userHome ? localContainer.userHome : '/root';
@@ -321,7 +323,7 @@ export class BlockInstanceRunner {
321
323
  providerVersion
322
324
  );
323
325
 
324
- const containerName = getBlockInstanceContainerName(this._systemId, blockInstance.id);
326
+ const containerName = await getBlockInstanceContainerName(this._systemId, blockInstance.id, kindUri.id);
325
327
 
326
328
  // For windows we need to default to root
327
329
  const innerHome = process.platform === 'win32' ? '/root/.kapeta' : ClusterConfig.getKapetaBasedir();
@@ -351,16 +353,7 @@ export class BlockInstanceRunner {
351
353
  });
352
354
  }
353
355
 
354
- /**
355
- *
356
- * @param blockInstance
357
- * @param blockUri
358
- * @param providerDefinition
359
- * @param {{[key:string]:string}} env
360
- * @return {Promise<ProcessDetails>}
361
- * @private
362
- */
363
- async _startOperatorProcess(
356
+ private async _startOperatorProcess(
364
357
  blockInstance: BlockProcessParams,
365
358
  blockUri: KapetaURI,
366
359
  providerDefinition: DefinitionInfo,
@@ -384,90 +377,124 @@ export class BlockInstanceRunner {
384
377
  throw new Error(`Provider did not have local image: ${providerRef}`);
385
378
  }
386
379
 
387
- const dockerImage = spec?.local?.image;
388
-
389
- //We only want 1 operator per operator type - across all local systems
390
- const containerName = getBlockInstanceContainerName(this._systemId, blockInstance.id);
391
- const logs = new LogData();
392
-
393
- const bindHost = getBindHost();
394
-
395
- const ExposedPorts: AnyMap = {};
396
- const addonEnv: StringMap = {};
397
- const PortBindings: AnyMap = {};
398
- let HealthCheck = undefined;
399
- let Mounts: DockerMounts[] = [];
400
- const localPorts = spec.local.ports as { [p: string]: { port: string; type: string } };
401
- const promises = Object.entries(localPorts).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.ensureServicePort(this._systemId, blockInstance.id, portType);
406
- PortBindings[dockerPort] = [
407
- {
408
- HostIp: bindHost,
409
- HostPort: `${publicPort}`,
410
- },
411
- ];
412
- });
413
-
414
- await Promise.all(promises);
415
-
416
- if (spec.local?.env) {
417
- Object.entries(spec.local.env).forEach(([key, value]) => {
418
- addonEnv[key] = value as string;
419
- });
420
- }
380
+ const local = spec.local as LocalImageOptions;
421
381
 
422
- if (spec.local?.mounts) {
423
- const mounts = await containerManager.createMounts(this._systemId, blockUri.id, spec.local.mounts);
424
- Mounts = containerManager.toDockerMounts(mounts);
425
- }
382
+ const dockerImage = local.image;
383
+ const operatorUri = local.singleton ? parseKapetaUri(providerRef) : blockUri;
384
+ const operatorId = local.singleton ? providerRef : blockInstance.id;
385
+ const operatorRef = local.singleton ? providerRef : blockInstance.ref;
426
386
 
427
- if (spec.local?.health) {
428
- HealthCheck = containerManager.toDockerHealth(spec.local?.health);
387
+ if (local.singleton && env) {
388
+ env[KAPETA_BLOCK_REF] = operatorRef;
389
+ env[KAPETA_INSTANCE_ID] = operatorId;
429
390
  }
430
391
 
431
- // For windows we need to default to root
432
- const innerHome = process.platform === 'win32' ? '/root/.kapeta' : ClusterConfig.getKapetaBasedir();
433
-
434
- const systemUri = parseKapetaUri(this._systemId);
435
- logs.addLog(`Creating new container for block: ${containerName}`);
436
- const out = await this.ensureContainer({
437
- Image: dockerImage,
438
- name: containerName,
439
- ExposedPorts,
440
- HealthCheck,
441
- HostConfig: {
442
- Binds: [
443
- `${toLocalBindVolume(kapetaYmlPath)}:/kapeta.yml:ro`,
444
- `${toLocalBindVolume(ClusterConfig.getKapetaBasedir())}:${innerHome}`,
445
- ],
446
- PortBindings,
447
- Mounts,
448
- },
449
- Labels: {
450
- instance: blockInstance.id,
451
- [COMPOSE_LABEL_PROJECT]: systemUri.id.replace(/[^a-z0-9]/gi, '_'),
452
- [COMPOSE_LABEL_SERVICE]: blockUri.id.replace(/[^a-z0-9]/gi, '_'),
392
+ const containerName = await getBlockInstanceContainerName(this._systemId, blockInstance.id, providerRef);
393
+
394
+ const task = taskManager.add(
395
+ `container:start:${containerName}`,
396
+ async () => {
397
+ const logs = new LogData();
398
+
399
+ const bindHost = getBindHost();
400
+
401
+ const ExposedPorts: AnyMap = {};
402
+ const addonEnv: StringMap = {};
403
+ const PortBindings: AnyMap = {};
404
+ let HealthCheck = undefined;
405
+ let Mounts: DockerMounts[] = [];
406
+ const localPorts = local.ports ?? {};
407
+ const labels: { [key: string]: string } = {};
408
+ const promises = Object.entries(localPorts).map(async ([portType, value]) => {
409
+ const portInfo = toPortInfo(value);
410
+ const dockerPort = `${portInfo.port}/${portInfo.type}`;
411
+ ExposedPorts[dockerPort] = {};
412
+ addonEnv[`KAPETA_LOCAL_SERVER_PORT_${portType.toUpperCase()}`] = `${portInfo.port}`;
413
+ const publicPort = await serviceManager.ensureServicePort(this._systemId, operatorId, portType);
414
+ PortBindings[dockerPort] = [
415
+ {
416
+ HostIp: bindHost,
417
+ HostPort: `${publicPort}`,
418
+ },
419
+ ];
420
+
421
+ labels[CONTAINER_LABEL_PORT_PREFIX + publicPort] = portType;
422
+ });
423
+
424
+ await Promise.all(promises);
425
+
426
+ if (local.env) {
427
+ Object.entries(local.env).forEach(([key, value]) => {
428
+ addonEnv[key] = value as string;
429
+ });
430
+ }
431
+
432
+ if (local.mounts) {
433
+ Mounts = await containerManager.createVolumes(this._systemId, operatorUri.id, local.mounts);
434
+ }
435
+
436
+ if (local.health) {
437
+ HealthCheck = containerManager.toDockerHealth(local.health);
438
+ }
439
+
440
+ // For windows we need to default to root
441
+ const innerHome = process.platform === 'win32' ? '/root/.kapeta' : ClusterConfig.getKapetaBasedir();
442
+
443
+ const Binds = local.singleton
444
+ ? [`${toLocalBindVolume(ClusterConfig.getKapetaBasedir())}:${innerHome}`]
445
+ : [
446
+ `${toLocalBindVolume(kapetaYmlPath)}:/kapeta.yml:ro`,
447
+ `${toLocalBindVolume(ClusterConfig.getKapetaBasedir())}:${innerHome}`,
448
+ ];
449
+
450
+ const systemUri = parseKapetaUri(this._systemId);
451
+
452
+ console.log(
453
+ `Ensuring container for operator block: ${containerName} [singleton: ${!!local.singleton}]`
454
+ );
455
+
456
+ logs.addLog(`Ensuring container for operator block: ${containerName}`);
457
+ const out = await this.ensureContainer({
458
+ Image: dockerImage,
459
+ name: containerName,
460
+ ExposedPorts,
461
+ HealthCheck,
462
+ HostConfig: {
463
+ Binds,
464
+ PortBindings,
465
+ Mounts,
466
+ },
467
+ Labels: {
468
+ ...labels,
469
+ instance: operatorId,
470
+ [COMPOSE_LABEL_PROJECT]: systemUri.id.replace(/[^a-z0-9]/gi, '_'),
471
+ [COMPOSE_LABEL_SERVICE]: operatorUri.id.replace(/[^a-z0-9]/gi, '_'),
472
+ },
473
+ Env: [
474
+ `KAPETA_INSTANCE_NAME=${operatorRef}`,
475
+ `KAPETA_LOCAL_CLUSTER_PORT=${clusterService.getClusterServicePort()}`,
476
+ ...DOCKER_ENV_VARS,
477
+ ...Object.entries({
478
+ ...env,
479
+ ...addonEnv,
480
+ }).map(([key, value]) => `${key}=${value}`),
481
+ ],
482
+ });
483
+
484
+ const portTypes = local.ports ? Object.keys(local.ports) : [];
485
+ if (portTypes.length > 0) {
486
+ out.portType = portTypes[0];
487
+ }
488
+
489
+ return out;
453
490
  },
454
- Env: [
455
- `KAPETA_INSTANCE_NAME=${blockInstance.ref}`,
456
- `KAPETA_LOCAL_CLUSTER_PORT=${clusterService.getClusterServicePort()}`,
457
- ...DOCKER_ENV_VARS,
458
- ...Object.entries({
459
- ...env,
460
- ...addonEnv,
461
- }).map(([key, value]) => `${key}=${value}`),
462
- ],
463
- });
464
-
465
- const portTypes = spec.local.ports ? Object.keys(spec.local.ports) : [];
466
- if (portTypes.length > 0) {
467
- out.portType = portTypes[0];
468
- }
491
+ {
492
+ name: `Starting container for ${providerRef}`,
493
+ systemId: this._systemId,
494
+ }
495
+ );
469
496
 
470
- return out;
497
+ return task.wait();
471
498
  }
472
499
 
473
500
  private async getDockerPortBindings(
@@ -5,17 +5,54 @@
5
5
 
6
6
  import FS from 'node:fs';
7
7
  import YAML from 'yaml';
8
- import { parseKapetaUri } from '@kapeta/nodejs-utils';
9
8
  import md5 from 'md5';
10
9
  import { EntityList } from '@kapeta/schemas';
11
10
  import _ from 'lodash';
12
- import { AnyMap } from '../types';
11
+ import { AnyMap, PortInfo } from '../types';
13
12
  import ClusterConfiguration from '@kapeta/local-cluster-config';
13
+ import { definitionsManager } from '../definitionsManager';
14
+ import { parseKapetaUri } from '@kapeta/nodejs-utils';
15
+ import { KIND_BLOCK_OPERATOR } from '../operatorManager';
16
+ import { assetManager } from '../assetManager';
17
+
18
+ export async function getBlockInstanceContainerName(systemId: string, instanceId: string, blockType?: string) {
19
+ if (!blockType) {
20
+ const instance = await assetManager.getBlockInstance(systemId, instanceId);
21
+ if (!instance) {
22
+ throw new Error(`Instance ${instanceId} not found in plan ${systemId}`);
23
+ }
24
+ const block = await assetManager.getAsset(instance.block.ref);
25
+ if (!block) {
26
+ throw new Error(`Block ${instance.block.ref} not found`);
27
+ }
28
+ blockType = block.data.kind;
29
+ }
30
+ const typeDefinition = await definitionsManager.getDefinition(blockType);
31
+ if (!typeDefinition) {
32
+ throw new Error(`Block type ${blockType} not found`);
33
+ }
34
+ if (
35
+ parseKapetaUri(typeDefinition.definition.kind).fullName === KIND_BLOCK_OPERATOR &&
36
+ typeDefinition.definition.spec?.local?.singleton
37
+ ) {
38
+ return `kapeta-instance-operator-${md5(systemId + blockType)}`;
39
+ }
14
40
 
15
- export function getBlockInstanceContainerName(systemId: string, instanceId: string) {
16
41
  return `kapeta-block-instance-${md5(systemId + instanceId)}`;
17
42
  }
18
43
 
44
+ export function toPortInfo(port: PortInfo) {
45
+ if (typeof port === 'number' || typeof port === 'string') {
46
+ return { port: parseInt(`${port}`), type: 'tcp' };
47
+ }
48
+
49
+ if (!port.type) {
50
+ port.type = 'tcp';
51
+ }
52
+
53
+ return port;
54
+ }
55
+
19
56
  export function getRemoteUrl(id: string, defautValue: string) {
20
57
  const remoteConfig = ClusterConfiguration.getClusterConfig().remote;
21
58
  return remoteConfig?.[id] ?? defautValue;