@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
@@ -19,6 +19,7 @@ const clusterService_1 = require("../clusterService");
19
19
  const types_1 = require("../types");
20
20
  const definitionsManager_1 = require("../definitionsManager");
21
21
  const node_os_1 = __importDefault(require("node:os"));
22
+ const taskManager_1 = require("../taskManager");
22
23
  const KIND_BLOCK_TYPE_OPERATOR = 'core/block-type-operator';
23
24
  const KAPETA_SYSTEM_ID = 'KAPETA_SYSTEM_ID';
24
25
  const KAPETA_BLOCK_REF = 'KAPETA_BLOCK_REF';
@@ -155,7 +156,7 @@ class BlockInstanceRunner {
155
156
  if (!dockerImage) {
156
157
  throw new Error(`Missing docker image information: ${JSON.stringify(localContainer)}`);
157
158
  }
158
- const containerName = (0, utils_1.getBlockInstanceContainerName)(this._systemId, blockInstance.id);
159
+ const containerName = await (0, utils_1.getBlockInstanceContainerName)(this._systemId, blockInstance.id, targetKindUri.id);
159
160
  const startCmd = localContainer.handlers?.onCreate ? localContainer.handlers.onCreate : '';
160
161
  const dockerOpts = localContainer.options ?? {};
161
162
  const homeDir = localContainer.userHome ? localContainer.userHome : '/root';
@@ -241,7 +242,7 @@ class BlockInstanceRunner {
241
242
  throw new Error(`Block type not found: ${kindUri.id}`);
242
243
  }
243
244
  const { PortBindings, ExposedPorts, addonEnv } = await this.getDockerPortBindings(blockInstance, assetVersion, providerVersion);
244
- const containerName = (0, utils_1.getBlockInstanceContainerName)(this._systemId, blockInstance.id);
245
+ const containerName = await (0, utils_1.getBlockInstanceContainerName)(this._systemId, blockInstance.id, kindUri.id);
245
246
  // For windows we need to default to root
246
247
  const innerHome = process.platform === 'win32' ? '/root/.kapeta' : local_cluster_config_1.default.getKapetaBasedir();
247
248
  const systemUri = (0, nodejs_utils_1.parseKapetaUri)(this._systemId);
@@ -268,15 +269,6 @@ class BlockInstanceRunner {
268
269
  },
269
270
  });
270
271
  }
271
- /**
272
- *
273
- * @param blockInstance
274
- * @param blockUri
275
- * @param providerDefinition
276
- * @param {{[key:string]:string}} env
277
- * @return {Promise<ProcessDetails>}
278
- * @private
279
- */
280
272
  async _startOperatorProcess(blockInstance, blockUri, providerDefinition, env) {
281
273
  const { assetFile } = local_cluster_config_1.default.getRepositoryAssetInfoPath(blockUri.handle, blockUri.name, blockUri.version);
282
274
  const kapetaYmlPath = assetFile;
@@ -288,79 +280,99 @@ class BlockInstanceRunner {
288
280
  if (!spec?.local?.image) {
289
281
  throw new Error(`Provider did not have local image: ${providerRef}`);
290
282
  }
291
- const dockerImage = spec?.local?.image;
292
- //We only want 1 operator per operator type - across all local systems
293
- const containerName = (0, utils_1.getBlockInstanceContainerName)(this._systemId, blockInstance.id);
294
- const logs = new LogData_1.LogData();
295
- const bindHost = (0, utils_1.getBindHost)();
296
- const ExposedPorts = {};
297
- const addonEnv = {};
298
- const PortBindings = {};
299
- let HealthCheck = undefined;
300
- let Mounts = [];
301
- const localPorts = spec.local.ports;
302
- const promises = Object.entries(localPorts).map(async ([portType, value]) => {
303
- const dockerPort = `${value.port}/${value.type}`;
304
- ExposedPorts[dockerPort] = {};
305
- addonEnv[`KAPETA_LOCAL_SERVER_PORT_${portType.toUpperCase()}`] = value.port;
306
- const publicPort = await serviceManager_1.serviceManager.ensureServicePort(this._systemId, blockInstance.id, portType);
307
- PortBindings[dockerPort] = [
308
- {
309
- HostIp: bindHost,
310
- HostPort: `${publicPort}`,
311
- },
312
- ];
313
- });
314
- await Promise.all(promises);
315
- if (spec.local?.env) {
316
- Object.entries(spec.local.env).forEach(([key, value]) => {
317
- addonEnv[key] = value;
318
- });
319
- }
320
- if (spec.local?.mounts) {
321
- const mounts = await containerManager_1.containerManager.createMounts(this._systemId, blockUri.id, spec.local.mounts);
322
- Mounts = containerManager_1.containerManager.toDockerMounts(mounts);
323
- }
324
- if (spec.local?.health) {
325
- HealthCheck = containerManager_1.containerManager.toDockerHealth(spec.local?.health);
283
+ const local = spec.local;
284
+ const dockerImage = local.image;
285
+ const operatorUri = local.singleton ? (0, nodejs_utils_1.parseKapetaUri)(providerRef) : blockUri;
286
+ const operatorId = local.singleton ? providerRef : blockInstance.id;
287
+ const operatorRef = local.singleton ? providerRef : blockInstance.ref;
288
+ if (local.singleton && env) {
289
+ env[KAPETA_BLOCK_REF] = operatorRef;
290
+ env[KAPETA_INSTANCE_ID] = operatorId;
326
291
  }
327
- // For windows we need to default to root
328
- const innerHome = process.platform === 'win32' ? '/root/.kapeta' : local_cluster_config_1.default.getKapetaBasedir();
329
- const systemUri = (0, nodejs_utils_1.parseKapetaUri)(this._systemId);
330
- logs.addLog(`Creating new container for block: ${containerName}`);
331
- const out = await this.ensureContainer({
332
- Image: dockerImage,
333
- name: containerName,
334
- ExposedPorts,
335
- HealthCheck,
336
- HostConfig: {
337
- Binds: [
292
+ const containerName = await (0, utils_1.getBlockInstanceContainerName)(this._systemId, blockInstance.id, providerRef);
293
+ const task = taskManager_1.taskManager.add(`container:start:${containerName}`, async () => {
294
+ const logs = new LogData_1.LogData();
295
+ const bindHost = (0, utils_1.getBindHost)();
296
+ const ExposedPorts = {};
297
+ const addonEnv = {};
298
+ const PortBindings = {};
299
+ let HealthCheck = undefined;
300
+ let Mounts = [];
301
+ const localPorts = local.ports ?? {};
302
+ const labels = {};
303
+ const promises = Object.entries(localPorts).map(async ([portType, value]) => {
304
+ const portInfo = (0, utils_1.toPortInfo)(value);
305
+ const dockerPort = `${portInfo.port}/${portInfo.type}`;
306
+ ExposedPorts[dockerPort] = {};
307
+ addonEnv[`KAPETA_LOCAL_SERVER_PORT_${portType.toUpperCase()}`] = `${portInfo.port}`;
308
+ const publicPort = await serviceManager_1.serviceManager.ensureServicePort(this._systemId, operatorId, portType);
309
+ PortBindings[dockerPort] = [
310
+ {
311
+ HostIp: bindHost,
312
+ HostPort: `${publicPort}`,
313
+ },
314
+ ];
315
+ labels[containerManager_1.CONTAINER_LABEL_PORT_PREFIX + publicPort] = portType;
316
+ });
317
+ await Promise.all(promises);
318
+ if (local.env) {
319
+ Object.entries(local.env).forEach(([key, value]) => {
320
+ addonEnv[key] = value;
321
+ });
322
+ }
323
+ if (local.mounts) {
324
+ Mounts = await containerManager_1.containerManager.createVolumes(this._systemId, operatorUri.id, local.mounts);
325
+ }
326
+ if (local.health) {
327
+ HealthCheck = containerManager_1.containerManager.toDockerHealth(local.health);
328
+ }
329
+ // For windows we need to default to root
330
+ const innerHome = process.platform === 'win32' ? '/root/.kapeta' : local_cluster_config_1.default.getKapetaBasedir();
331
+ const Binds = local.singleton
332
+ ? [`${(0, containerManager_1.toLocalBindVolume)(local_cluster_config_1.default.getKapetaBasedir())}:${innerHome}`]
333
+ : [
338
334
  `${(0, containerManager_1.toLocalBindVolume)(kapetaYmlPath)}:/kapeta.yml:ro`,
339
335
  `${(0, containerManager_1.toLocalBindVolume)(local_cluster_config_1.default.getKapetaBasedir())}:${innerHome}`,
336
+ ];
337
+ const systemUri = (0, nodejs_utils_1.parseKapetaUri)(this._systemId);
338
+ console.log(`Ensuring container for operator block: ${containerName} [singleton: ${!!local.singleton}]`);
339
+ logs.addLog(`Ensuring container for operator block: ${containerName}`);
340
+ const out = await this.ensureContainer({
341
+ Image: dockerImage,
342
+ name: containerName,
343
+ ExposedPorts,
344
+ HealthCheck,
345
+ HostConfig: {
346
+ Binds,
347
+ PortBindings,
348
+ Mounts,
349
+ },
350
+ Labels: {
351
+ ...labels,
352
+ instance: operatorId,
353
+ [containerManager_1.COMPOSE_LABEL_PROJECT]: systemUri.id.replace(/[^a-z0-9]/gi, '_'),
354
+ [containerManager_1.COMPOSE_LABEL_SERVICE]: operatorUri.id.replace(/[^a-z0-9]/gi, '_'),
355
+ },
356
+ Env: [
357
+ `KAPETA_INSTANCE_NAME=${operatorRef}`,
358
+ `KAPETA_LOCAL_CLUSTER_PORT=${clusterService_1.clusterService.getClusterServicePort()}`,
359
+ ...DOCKER_ENV_VARS,
360
+ ...Object.entries({
361
+ ...env,
362
+ ...addonEnv,
363
+ }).map(([key, value]) => `${key}=${value}`),
340
364
  ],
341
- PortBindings,
342
- Mounts,
343
- },
344
- Labels: {
345
- instance: blockInstance.id,
346
- [containerManager_1.COMPOSE_LABEL_PROJECT]: systemUri.id.replace(/[^a-z0-9]/gi, '_'),
347
- [containerManager_1.COMPOSE_LABEL_SERVICE]: blockUri.id.replace(/[^a-z0-9]/gi, '_'),
348
- },
349
- Env: [
350
- `KAPETA_INSTANCE_NAME=${blockInstance.ref}`,
351
- `KAPETA_LOCAL_CLUSTER_PORT=${clusterService_1.clusterService.getClusterServicePort()}`,
352
- ...DOCKER_ENV_VARS,
353
- ...Object.entries({
354
- ...env,
355
- ...addonEnv,
356
- }).map(([key, value]) => `${key}=${value}`),
357
- ],
365
+ });
366
+ const portTypes = local.ports ? Object.keys(local.ports) : [];
367
+ if (portTypes.length > 0) {
368
+ out.portType = portTypes[0];
369
+ }
370
+ return out;
371
+ }, {
372
+ name: `Starting container for ${providerRef}`,
373
+ systemId: this._systemId,
358
374
  });
359
- const portTypes = spec.local.ports ? Object.keys(spec.local.ports) : [];
360
- if (portTypes.length > 0) {
361
- out.portType = portTypes[0];
362
- }
363
- return out;
375
+ return task.wait();
364
376
  }
365
377
  async getDockerPortBindings(blockInstance, assetVersion, providerVersion) {
366
378
  const bindHost = (0, utils_1.getBindHost)();
@@ -3,8 +3,12 @@
3
3
  * SPDX-License-Identifier: BUSL-1.1
4
4
  */
5
5
  import { EntityList } from '@kapeta/schemas';
6
- import { AnyMap } from '../types';
7
- export declare function getBlockInstanceContainerName(systemId: string, instanceId: string): string;
6
+ import { AnyMap, PortInfo } from '../types';
7
+ export declare function getBlockInstanceContainerName(systemId: string, instanceId: string, blockType?: string): Promise<string>;
8
+ export declare function toPortInfo(port: PortInfo): {
9
+ port: number;
10
+ type: string;
11
+ };
8
12
  export declare function getRemoteUrl(id: string, defautValue: string): any;
9
13
  export declare function readYML(path: string): any;
10
14
  export declare function isWindows(): boolean;
@@ -7,16 +7,49 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
7
7
  return (mod && mod.__esModule) ? mod : { "default": mod };
8
8
  };
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
- exports.getResolvedConfiguration = exports.getBindHost = exports.isLinux = exports.isMac = exports.isWindows = exports.readYML = exports.getRemoteUrl = exports.getBlockInstanceContainerName = void 0;
10
+ exports.getResolvedConfiguration = exports.getBindHost = exports.isLinux = exports.isMac = exports.isWindows = exports.readYML = exports.getRemoteUrl = exports.toPortInfo = exports.getBlockInstanceContainerName = void 0;
11
11
  const node_fs_1 = __importDefault(require("node:fs"));
12
12
  const yaml_1 = __importDefault(require("yaml"));
13
13
  const md5_1 = __importDefault(require("md5"));
14
14
  const lodash_1 = __importDefault(require("lodash"));
15
15
  const local_cluster_config_1 = __importDefault(require("@kapeta/local-cluster-config"));
16
- function getBlockInstanceContainerName(systemId, instanceId) {
16
+ const definitionsManager_1 = require("../definitionsManager");
17
+ const nodejs_utils_1 = require("@kapeta/nodejs-utils");
18
+ const operatorManager_1 = require("../operatorManager");
19
+ const assetManager_1 = require("../assetManager");
20
+ async function getBlockInstanceContainerName(systemId, instanceId, blockType) {
21
+ if (!blockType) {
22
+ const instance = await assetManager_1.assetManager.getBlockInstance(systemId, instanceId);
23
+ if (!instance) {
24
+ throw new Error(`Instance ${instanceId} not found in plan ${systemId}`);
25
+ }
26
+ const block = await assetManager_1.assetManager.getAsset(instance.block.ref);
27
+ if (!block) {
28
+ throw new Error(`Block ${instance.block.ref} not found`);
29
+ }
30
+ blockType = block.data.kind;
31
+ }
32
+ const typeDefinition = await definitionsManager_1.definitionsManager.getDefinition(blockType);
33
+ if (!typeDefinition) {
34
+ throw new Error(`Block type ${blockType} not found`);
35
+ }
36
+ if ((0, nodejs_utils_1.parseKapetaUri)(typeDefinition.definition.kind).fullName === operatorManager_1.KIND_BLOCK_OPERATOR &&
37
+ typeDefinition.definition.spec?.local?.singleton) {
38
+ return `kapeta-instance-operator-${(0, md5_1.default)(systemId + blockType)}`;
39
+ }
17
40
  return `kapeta-block-instance-${(0, md5_1.default)(systemId + instanceId)}`;
18
41
  }
19
42
  exports.getBlockInstanceContainerName = getBlockInstanceContainerName;
43
+ function toPortInfo(port) {
44
+ if (typeof port === 'number' || typeof port === 'string') {
45
+ return { port: parseInt(`${port}`), type: 'tcp' };
46
+ }
47
+ if (!port.type) {
48
+ port.type = 'tcp';
49
+ }
50
+ return port;
51
+ }
52
+ exports.toPortInfo = toPortInfo;
20
53
  function getRemoteUrl(id, defautValue) {
21
54
  const remoteConfig = local_cluster_config_1.default.getClusterConfig().remote;
22
55
  return remoteConfig?.[id] ?? defautValue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kapeta/local-cluster-service",
3
- "version": "0.34.2",
3
+ "version": "0.36.0",
4
4
  "description": "Manages configuration, ports and service discovery for locally running Kapeta systems",
5
5
  "type": "commonjs",
6
6
  "exports": {
@@ -181,4 +181,19 @@ router.get('/consumes/:resourceName/:type', (req: KapetaRequest, res) => {
181
181
  );
182
182
  });
183
183
 
184
+ /**
185
+ * Used by services to information about a block operator
186
+ *
187
+ * If the remote service is not already running it will be started
188
+ */
189
+ router.get('/operator/:instanceId', async (req: KapetaRequest, res) => {
190
+ const operatorInfo = await instanceManager.getInstanceOperator(
191
+ req.kapeta!.systemId,
192
+ req.params.instanceId,
193
+ req.kapeta!.environment
194
+ );
195
+
196
+ res.send(operatorInfo);
197
+ });
198
+
184
199
  export default router;
@@ -14,7 +14,7 @@ import ClusterConfiguration from '@kapeta/local-cluster-config';
14
14
  import uuid from 'node-uuid';
15
15
  import md5 from 'md5';
16
16
  import { getBlockInstanceContainerName } from './utils/utils';
17
- import { InstanceInfo, LogEntry, LogSource } from './types';
17
+ import { Health, InstanceInfo, LogEntry, LogSource } from './types';
18
18
  import { KapetaAPI } from '@kapeta/nodejs-api-client';
19
19
  import { taskManager, Task } from './taskManager';
20
20
  import { EventEmitter } from 'node:events';
@@ -76,13 +76,6 @@ interface JSONMessage<T = string> {
76
76
  aux?: any;
77
77
  }
78
78
 
79
- interface Health {
80
- cmd: string;
81
- interval?: number;
82
- timeout?: number;
83
- retries?: number;
84
- }
85
-
86
79
  export const CONTAINER_LABEL_PORT_PREFIX = 'kapeta_port-';
87
80
  const NANO_SECOND = 1000000;
88
81
  const HEALTH_CHECK_INTERVAL = 3000;
@@ -255,7 +248,7 @@ class ContainerManager {
255
248
 
256
249
  async createVolumes(
257
250
  systemId: string,
258
- kind: string,
251
+ serviceId: string,
259
252
  mountOpts: StringMap | null | undefined
260
253
  ): Promise<DockerMounts[]> {
261
254
  const Mounts: DockerMounts[] = [];
@@ -263,7 +256,7 @@ class ContainerManager {
263
256
  if (mountOpts) {
264
257
  const mountOptList = Object.entries(mountOpts);
265
258
  for (const [mountName, containerPath] of mountOptList) {
266
- const volumeName = `${systemId}_${kind}_${mountName}`.replace(/[^a-z0-9]/gi, '_');
259
+ const volumeName = `${systemId}_${serviceId}_${mountName}`.replace(/[^a-z0-9]/gi, '_');
267
260
 
268
261
  Mounts.push({
269
262
  Target: containerPath,
@@ -273,7 +266,7 @@ class ContainerManager {
273
266
  Consistency: 'consistent',
274
267
  Labels: {
275
268
  [COMPOSE_LABEL_PROJECT]: systemId.replace(/[^a-z0-9]/gi, '_'),
276
- [COMPOSE_LABEL_SERVICE]: kind.replace(/[^a-z0-9]/gi, '_'),
269
+ [COMPOSE_LABEL_SERVICE]: serviceId.replace(/[^a-z0-9]/gi, '_'),
277
270
  },
278
271
  });
279
272
  }
@@ -701,7 +694,7 @@ class ContainerManager {
701
694
  }
702
695
 
703
696
  async getLogs(instance: InstanceInfo): Promise<LogEntry[]> {
704
- const containerName = getBlockInstanceContainerName(instance.systemId, instance.instanceId);
697
+ const containerName = await getBlockInstanceContainerName(instance.systemId, instance.instanceId);
705
698
  const containerInfo = await this.getContainerByName(containerName);
706
699
  if (!containerInfo) {
707
700
  return [
@@ -718,7 +711,7 @@ class ContainerManager {
718
711
  }
719
712
 
720
713
  async stopLogListening(systemId: string, instanceId: string) {
721
- const containerName = getBlockInstanceContainerName(systemId, instanceId);
714
+ const containerName = await getBlockInstanceContainerName(systemId, instanceId);
722
715
  if (this.logStreams[containerName]) {
723
716
  if (this.logStreams[containerName]?.timer) {
724
717
  clearTimeout(this.logStreams[containerName].timer);
@@ -736,7 +729,7 @@ class ContainerManager {
736
729
  }
737
730
 
738
731
  async ensureLogListening(systemId: string, instanceId: string, handler: (log: LogEntry) => void) {
739
- const containerName = getBlockInstanceContainerName(systemId, instanceId);
732
+ const containerName = await getBlockInstanceContainerName(systemId, instanceId);
740
733
  try {
741
734
  if (this.logStreams[containerName]?.stream) {
742
735
  // Already listening - will shut itself down
@@ -997,7 +990,7 @@ export class ContainerInfo {
997
990
  return;
998
991
  }
999
992
 
1000
- const hostPort = name.substr(CONTAINER_LABEL_PORT_PREFIX.length);
993
+ const hostPort = name.substring(CONTAINER_LABEL_PORT_PREFIX.length);
1001
994
 
1002
995
  portTypes[hostPort] = portType;
1003
996
  });