@kapeta/local-cluster-service 0.35.0 → 0.36.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.
@@ -22,6 +22,7 @@ import { AnyMap, BlockProcessParams, InstanceType, LocalImageOptions, ProcessInf
22
22
  import { definitionsManager } from '../definitionsManager';
23
23
  import Docker from 'dockerode';
24
24
  import OS from 'node:os';
25
+ import { taskManager } from '../taskManager';
25
26
 
26
27
  const KIND_BLOCK_TYPE_OPERATOR = 'core/block-type-operator';
27
28
  const KAPETA_SYSTEM_ID = 'KAPETA_SYSTEM_ID';
@@ -47,17 +48,24 @@ async function getProvider(uri: KapetaURI) {
47
48
  }
48
49
 
49
50
  export function resolvePortType(portType: string) {
50
- if (HTTP_PORTS.includes(portType)) {
51
+ if (portType && HTTP_PORTS.includes(portType.toLowerCase())) {
51
52
  return HTTP_PORT_TYPE;
52
53
  }
53
54
  return portType;
54
55
  }
55
56
 
56
- function getProviderPorts(assetVersion: DefinitionInfo, providerVersion: DefinitionInfo): string[] {
57
+ /**
58
+ * Get the port types for a non-operator block instance
59
+ */
60
+ function getServiceProviderPorts(assetVersion: DefinitionInfo, providerVersion: DefinitionInfo): string[] {
57
61
  const out =
58
62
  assetVersion.definition?.spec?.providers
63
+ ?.filter((provider: any) => {
64
+ // We only support HTTP provider ports for now. Need to figure out how to handle other types
65
+ return HTTP_PORTS.includes(provider.spec?.port?.type?.toLowerCase());
66
+ })
59
67
  ?.map((provider: any) => {
60
- return resolvePortType(provider.spec?.port?.type);
68
+ return resolvePortType(provider.spec?.port?.type?.toLowerCase());
61
69
  })
62
70
  .filter((t: any) => !!t) ?? [];
63
71
 
@@ -136,7 +144,7 @@ export class BlockInstanceRunner {
136
144
  processInfo = await this._startOperatorProcess(blockInstance, blockUri, providerVersion, env);
137
145
  } else {
138
146
  //We need a port type to know how to connect to the block consistently
139
- const portTypes = getProviderPorts(assetVersion, providerVersion);
147
+ const portTypes = getServiceProviderPorts(assetVersion, providerVersion);
140
148
 
141
149
  if (blockUri.version === 'local') {
142
150
  processInfo = await this._startLocalProcess(blockInstance, blockUri, env, assetVersion);
@@ -201,7 +209,7 @@ export class BlockInstanceRunner {
201
209
  throw new Error(`Missing docker image information: ${JSON.stringify(localContainer)}`);
202
210
  }
203
211
 
204
- const containerName = getBlockInstanceContainerName(this._systemId, blockInstance.id);
212
+ const containerName = await getBlockInstanceContainerName(this._systemId, blockInstance.id, targetKindUri.id);
205
213
  const startCmd = localContainer.handlers?.onCreate ? localContainer.handlers.onCreate : '';
206
214
  const dockerOpts = localContainer.options ?? {};
207
215
  const homeDir = localContainer.userHome ? localContainer.userHome : '/root';
@@ -216,7 +224,7 @@ export class BlockInstanceRunner {
216
224
  delete localContainer.Labels;
217
225
  delete localContainer.Env;
218
226
 
219
- const { PortBindings, ExposedPorts, addonEnv } = await this.getDockerPortBindings(
227
+ const { PortBindings, ExposedPorts, addonEnv } = await this.getServiceBlockPortBindings(
220
228
  blockInstance,
221
229
  assetVersion,
222
230
  providerVersion
@@ -316,13 +324,13 @@ export class BlockInstanceRunner {
316
324
  throw new Error(`Block type not found: ${kindUri.id}`);
317
325
  }
318
326
 
319
- const { PortBindings, ExposedPorts, addonEnv } = await this.getDockerPortBindings(
327
+ const { PortBindings, ExposedPorts, addonEnv } = await this.getServiceBlockPortBindings(
320
328
  blockInstance,
321
329
  assetVersion,
322
330
  providerVersion
323
331
  );
324
332
 
325
- const containerName = getBlockInstanceContainerName(this._systemId, blockInstance.id);
333
+ const containerName = await getBlockInstanceContainerName(this._systemId, blockInstance.id, kindUri.id);
326
334
 
327
335
  // For windows we need to default to root
328
336
  const innerHome = process.platform === 'win32' ? '/root/.kapeta' : ClusterConfig.getKapetaBasedir();
@@ -352,16 +360,7 @@ export class BlockInstanceRunner {
352
360
  });
353
361
  }
354
362
 
355
- /**
356
- *
357
- * @param blockInstance
358
- * @param blockUri
359
- * @param providerDefinition
360
- * @param {{[key:string]:string}} env
361
- * @return {Promise<ProcessDetails>}
362
- * @private
363
- */
364
- async _startOperatorProcess(
363
+ private async _startOperatorProcess(
365
364
  blockInstance: BlockProcessParams,
366
365
  blockUri: KapetaURI,
367
366
  providerDefinition: DefinitionInfo,
@@ -388,96 +387,127 @@ export class BlockInstanceRunner {
388
387
  const local = spec.local as LocalImageOptions;
389
388
 
390
389
  const dockerImage = local.image;
390
+ const operatorUri = local.singleton ? parseKapetaUri(providerRef) : blockUri;
391
+ const operatorId = local.singleton ? providerRef : blockInstance.id;
392
+ const operatorRef = local.singleton ? providerRef : blockInstance.ref;
391
393
 
392
- //We only want 1 operator per operator type - across all local systems
393
- const containerName = getBlockInstanceContainerName(this._systemId, blockInstance.id);
394
- const logs = new LogData();
395
-
396
- const bindHost = getBindHost();
397
-
398
- const ExposedPorts: AnyMap = {};
399
- const addonEnv: StringMap = {};
400
- const PortBindings: AnyMap = {};
401
- let HealthCheck = undefined;
402
- let Mounts: DockerMounts[] = [];
403
- const localPorts = local.ports ?? {};
404
- const labels: { [key: string]: string } = {};
405
- const promises = Object.entries(localPorts).map(async ([portType, value]) => {
406
- const portInfo = toPortInfo(value);
407
- const dockerPort = `${portInfo.port}/${portInfo.type}`;
408
- ExposedPorts[dockerPort] = {};
409
- addonEnv[`KAPETA_LOCAL_SERVER_PORT_${portType.toUpperCase()}`] = `${portInfo.port}`;
410
- const publicPort = await serviceManager.ensureServicePort(this._systemId, blockInstance.id, portType);
411
- PortBindings[dockerPort] = [
412
- {
413
- HostIp: bindHost,
414
- HostPort: `${publicPort}`,
415
- },
416
- ];
417
-
418
- labels[CONTAINER_LABEL_PORT_PREFIX + publicPort] = portType;
419
- });
420
-
421
- await Promise.all(promises);
422
-
423
- if (local.env) {
424
- Object.entries(local.env).forEach(([key, value]) => {
425
- addonEnv[key] = value as string;
426
- });
427
- }
428
-
429
- if (local.mounts) {
430
- Mounts = await containerManager.createVolumes(this._systemId, blockUri.id, local.mounts);
394
+ if (local.singleton && env) {
395
+ env[KAPETA_BLOCK_REF] = operatorRef;
396
+ env[KAPETA_INSTANCE_ID] = operatorId;
431
397
  }
432
398
 
433
- if (local.health) {
434
- HealthCheck = containerManager.toDockerHealth(local.health);
435
- }
436
-
437
- // For windows we need to default to root
438
- const innerHome = process.platform === 'win32' ? '/root/.kapeta' : ClusterConfig.getKapetaBasedir();
439
-
440
- const systemUri = parseKapetaUri(this._systemId);
441
- logs.addLog(`Creating new container for block: ${containerName}`);
442
- const out = await this.ensureContainer({
443
- Image: dockerImage,
444
- name: containerName,
445
- ExposedPorts,
446
- HealthCheck,
447
- HostConfig: {
448
- Binds: [
449
- `${toLocalBindVolume(kapetaYmlPath)}:/kapeta.yml:ro`,
450
- `${toLocalBindVolume(ClusterConfig.getKapetaBasedir())}:${innerHome}`,
451
- ],
452
- PortBindings,
453
- Mounts,
454
- },
455
- Labels: {
456
- ...labels,
457
- instance: blockInstance.id,
458
- [COMPOSE_LABEL_PROJECT]: systemUri.id.replace(/[^a-z0-9]/gi, '_'),
459
- [COMPOSE_LABEL_SERVICE]: blockUri.id.replace(/[^a-z0-9]/gi, '_'),
399
+ const containerName = await getBlockInstanceContainerName(this._systemId, blockInstance.id, providerRef);
400
+
401
+ const task = taskManager.add(
402
+ `container:start:${containerName}`,
403
+ async () => {
404
+ const logs = new LogData();
405
+
406
+ const bindHost = getBindHost();
407
+
408
+ const ExposedPorts: AnyMap = {};
409
+ const addonEnv: StringMap = {};
410
+ const PortBindings: AnyMap = {};
411
+ let HealthCheck = undefined;
412
+ let Mounts: DockerMounts[] = [];
413
+ const localPorts = local.ports ?? {};
414
+ const labels: { [key: string]: string } = {};
415
+ const promises = Object.entries(localPorts).map(async ([portType, value]) => {
416
+ const portInfo = toPortInfo(value);
417
+ const dockerPort = `${portInfo.port}/${portInfo.type}`;
418
+ ExposedPorts[dockerPort] = {};
419
+ addonEnv[`KAPETA_LOCAL_SERVER_PORT_${portType.toUpperCase()}`] = `${portInfo.port}`;
420
+ const publicPort = await serviceManager.ensureServicePort(this._systemId, operatorId, portType);
421
+ PortBindings[dockerPort] = [
422
+ {
423
+ HostIp: bindHost,
424
+ HostPort: `${publicPort}`,
425
+ },
426
+ ];
427
+
428
+ labels[CONTAINER_LABEL_PORT_PREFIX + publicPort] = portType;
429
+ });
430
+
431
+ await Promise.all(promises);
432
+
433
+ if (local.env) {
434
+ Object.entries(local.env).forEach(([key, value]) => {
435
+ addonEnv[key] = value as string;
436
+ });
437
+ }
438
+
439
+ if (local.mounts) {
440
+ Mounts = await containerManager.createVolumes(this._systemId, operatorUri.id, local.mounts);
441
+ }
442
+
443
+ if (local.health) {
444
+ HealthCheck = containerManager.toDockerHealth(local.health);
445
+ }
446
+
447
+ // For windows we need to default to root
448
+ const innerHome = process.platform === 'win32' ? '/root/.kapeta' : ClusterConfig.getKapetaBasedir();
449
+
450
+ const Binds = local.singleton
451
+ ? [`${toLocalBindVolume(ClusterConfig.getKapetaBasedir())}:${innerHome}`]
452
+ : [
453
+ `${toLocalBindVolume(kapetaYmlPath)}:/kapeta.yml:ro`,
454
+ `${toLocalBindVolume(ClusterConfig.getKapetaBasedir())}:${innerHome}`,
455
+ ];
456
+
457
+ const systemUri = parseKapetaUri(this._systemId);
458
+
459
+ console.log(
460
+ `Ensuring container for operator block: ${containerName} [singleton: ${!!local.singleton}]`
461
+ );
462
+
463
+ logs.addLog(`Ensuring container for operator block: ${containerName}`);
464
+ const out = await this.ensureContainer({
465
+ Image: dockerImage,
466
+ name: containerName,
467
+ ExposedPorts,
468
+ HealthCheck,
469
+ HostConfig: {
470
+ Binds,
471
+ PortBindings,
472
+ Mounts,
473
+ },
474
+ Labels: {
475
+ ...labels,
476
+ instance: operatorId,
477
+ [COMPOSE_LABEL_PROJECT]: systemUri.id.replace(/[^a-z0-9]/gi, '_'),
478
+ [COMPOSE_LABEL_SERVICE]: operatorUri.id.replace(/[^a-z0-9]/gi, '_'),
479
+ },
480
+ Env: [
481
+ `KAPETA_INSTANCE_NAME=${operatorRef}`,
482
+ `KAPETA_LOCAL_CLUSTER_PORT=${clusterService.getClusterServicePort()}`,
483
+ ...DOCKER_ENV_VARS,
484
+ ...Object.entries({
485
+ ...env,
486
+ ...addonEnv,
487
+ }).map(([key, value]) => `${key}=${value}`),
488
+ ],
489
+ });
490
+
491
+ const portTypes = local.ports ? Object.keys(local.ports) : [];
492
+ if (portTypes.length > 0) {
493
+ out.portType = portTypes[0];
494
+ }
495
+
496
+ return out;
460
497
  },
461
- Env: [
462
- `KAPETA_INSTANCE_NAME=${blockInstance.ref}`,
463
- `KAPETA_LOCAL_CLUSTER_PORT=${clusterService.getClusterServicePort()}`,
464
- ...DOCKER_ENV_VARS,
465
- ...Object.entries({
466
- ...env,
467
- ...addonEnv,
468
- }).map(([key, value]) => `${key}=${value}`),
469
- ],
470
- });
471
-
472
- const portTypes = local.ports ? Object.keys(local.ports) : [];
473
- if (portTypes.length > 0) {
474
- out.portType = portTypes[0];
475
- }
498
+ {
499
+ name: `Starting container for ${providerRef}`,
500
+ systemId: this._systemId,
501
+ }
502
+ );
476
503
 
477
- return out;
504
+ return task.wait();
478
505
  }
479
506
 
480
- private async getDockerPortBindings(
507
+ /**
508
+ * Get the port bindings for a non-operator block
509
+ */
510
+ private async getServiceBlockPortBindings(
481
511
  blockInstance: BlockProcessParams,
482
512
  assetVersion: DefinitionInfo,
483
513
  providerVersion: DefinitionInfo
@@ -487,7 +517,7 @@ export class BlockInstanceRunner {
487
517
  const addonEnv: StringMap = {};
488
518
  const PortBindings: AnyMap = {};
489
519
 
490
- const portTypes = getProviderPorts(assetVersion, providerVersion);
520
+ const portTypes = getServiceProviderPorts(assetVersion, providerVersion);
491
521
  let port = 80;
492
522
  const promises = portTypes.map(async (portType) => {
493
523
  const publicPort = await serviceManager.ensureServicePort(this._systemId, blockInstance.id, portType);
@@ -10,9 +10,35 @@ import { EntityList } from '@kapeta/schemas';
10
10
  import _ from 'lodash';
11
11
  import { AnyMap, PortInfo } from '../types';
12
12
  import ClusterConfiguration from '@kapeta/local-cluster-config';
13
+ import { definitionsManager } from '../definitionsManager';
14
+ import { normalizeKapetaUri, 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(normalizeKapetaUri(systemId) + normalizeKapetaUri(blockType))}`;
39
+ }
13
40
 
14
- export function getBlockInstanceContainerName(systemId: string, instanceId: string) {
15
- return `kapeta-block-instance-${md5(systemId + instanceId)}`;
41
+ return `kapeta-block-instance-${md5(normalizeKapetaUri(systemId) + instanceId)}`;
16
42
  }
17
43
 
18
44
  export function toPortInfo(port: PortInfo) {