@kapeta/local-cluster-service 0.34.2 → 0.35.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 (37) hide show
  1. package/.github/workflows/check-license.yml +0 -1
  2. package/CHANGELOG.md +7 -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 +4 -4
  6. package/dist/cjs/src/instanceManager.d.ts +2 -1
  7. package/dist/cjs/src/instanceManager.js +43 -1
  8. package/dist/cjs/src/operatorManager.d.ts +2 -2
  9. package/dist/cjs/src/operatorManager.js +2 -7
  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 +39 -0
  13. package/dist/cjs/src/utils/BlockInstanceRunner.js +16 -12
  14. package/dist/cjs/src/utils/utils.d.ts +5 -1
  15. package/dist/cjs/src/utils/utils.js +11 -1
  16. package/dist/esm/src/config/routes.js +9 -0
  17. package/dist/esm/src/containerManager.d.ts +2 -8
  18. package/dist/esm/src/containerManager.js +4 -4
  19. package/dist/esm/src/instanceManager.d.ts +2 -1
  20. package/dist/esm/src/instanceManager.js +43 -1
  21. package/dist/esm/src/operatorManager.d.ts +2 -2
  22. package/dist/esm/src/operatorManager.js +2 -7
  23. package/dist/esm/src/serviceManager.d.ts +1 -0
  24. package/dist/esm/src/serviceManager.js +9 -9
  25. package/dist/esm/src/types.d.ts +39 -0
  26. package/dist/esm/src/utils/BlockInstanceRunner.js +16 -12
  27. package/dist/esm/src/utils/utils.d.ts +5 -1
  28. package/dist/esm/src/utils/utils.js +11 -1
  29. package/package.json +1 -1
  30. package/src/config/routes.ts +15 -0
  31. package/src/containerManager.ts +5 -12
  32. package/src/instanceManager.ts +72 -4
  33. package/src/operatorManager.ts +5 -13
  34. package/src/serviceManager.ts +11 -8
  35. package/src/types.ts +35 -0
  36. package/src/utils/BlockInstanceRunner.ts +21 -14
  37. package/src/utils/utils.ts +13 -2
@@ -15,4 +15,3 @@ jobs:
15
15
  holder: 'Kapeta Inc.'
16
16
  license: 'BUSL-1.1'
17
17
  sources: 'src/**/* test/*'
18
-
package/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ # [0.35.0](https://github.com/kapetacom/local-cluster-service/compare/v0.34.2...v0.35.0) (2024-01-31)
2
+
3
+
4
+ ### Features
5
+
6
+ * Adds support for getting instance operator info ([#122](https://github.com/kapetacom/local-cluster-service/issues/122)) ([8fa18ac](https://github.com/kapetacom/local-cluster-service/commit/8fa18ac58226ce9776f79a64d3a99cf56456e834))
7
+
1
8
  ## [0.34.2](https://github.com/kapetacom/local-cluster-service/compare/v0.34.1...v0.34.2) (2024-01-22)
2
9
 
3
10
 
@@ -148,4 +148,13 @@ router.get('/consumes/resource/:resourceType/:portType/:name', async (req, res)
148
148
  router.get('/consumes/:resourceName/:type', (req, res) => {
149
149
  res.send(serviceManager_1.serviceManager.getConsumerAddress(req.kapeta.systemId, req.kapeta.instanceId, req.params.resourceName, req.params.type, req.kapeta.environment));
150
150
  });
151
+ /**
152
+ * Used by services to information about a block operator
153
+ *
154
+ * If the remote service is not already running it will be started
155
+ */
156
+ router.get('/operator/:instanceId', async (req, res) => {
157
+ const operatorInfo = await instanceManager_1.instanceManager.getInstanceOperator(req.kapeta.systemId, req.params.instanceId, req.kapeta.environment);
158
+ res.send(operatorInfo);
159
+ });
151
160
  exports.default = router;
@@ -5,7 +5,7 @@
5
5
  /// <reference types="node" />
6
6
  import FSExtra from 'fs-extra';
7
7
  import Docker from 'dockerode';
8
- import { InstanceInfo, LogEntry } from './types';
8
+ import { Health, InstanceInfo, LogEntry } from './types';
9
9
  type StringMap = {
10
10
  [key: string]: string;
11
11
  };
@@ -26,12 +26,6 @@ export interface DockerMounts {
26
26
  }
27
27
  export type DockerContainerStatus = 'created' | 'running' | 'paused' | 'restarting' | 'removing' | 'exited' | 'dead';
28
28
  export type DockerContainerHealth = 'starting' | 'healthy' | 'unhealthy' | 'none';
29
- interface Health {
30
- cmd: string;
31
- interval?: number;
32
- timeout?: number;
33
- retries?: number;
34
- }
35
29
  export declare const CONTAINER_LABEL_PORT_PREFIX = "kapeta_port-";
36
30
  export declare const COMPOSE_LABEL_PROJECT = "com.docker.compose.project";
37
31
  export declare const COMPOSE_LABEL_SERVICE = "com.docker.compose.service";
@@ -50,7 +44,7 @@ declare class ContainerManager {
50
44
  isAlive(): boolean;
51
45
  getMountPoint(systemId: string, ref: string, mountName: string): string;
52
46
  createMounts(systemId: string, kind: string, mountOpts: StringMap | null | undefined): Promise<StringMap>;
53
- createVolumes(systemId: string, kind: string, mountOpts: StringMap | null | undefined): Promise<DockerMounts[]>;
47
+ createVolumes(systemId: string, serviceId: string, mountOpts: StringMap | null | undefined): Promise<DockerMounts[]>;
54
48
  ping(): Promise<void>;
55
49
  docker(): Docker;
56
50
  getContainerByName(containerName: string): Promise<ContainerInfo | undefined>;
@@ -168,12 +168,12 @@ class ContainerManager {
168
168
  }
169
169
  return mounts;
170
170
  }
171
- async createVolumes(systemId, kind, mountOpts) {
171
+ async createVolumes(systemId, serviceId, mountOpts) {
172
172
  const Mounts = [];
173
173
  if (mountOpts) {
174
174
  const mountOptList = Object.entries(mountOpts);
175
175
  for (const [mountName, containerPath] of mountOptList) {
176
- const volumeName = `${systemId}_${kind}_${mountName}`.replace(/[^a-z0-9]/gi, '_');
176
+ const volumeName = `${systemId}_${serviceId}_${mountName}`.replace(/[^a-z0-9]/gi, '_');
177
177
  Mounts.push({
178
178
  Target: containerPath,
179
179
  Source: volumeName,
@@ -182,7 +182,7 @@ class ContainerManager {
182
182
  Consistency: 'consistent',
183
183
  Labels: {
184
184
  [exports.COMPOSE_LABEL_PROJECT]: systemId.replace(/[^a-z0-9]/gi, '_'),
185
- [exports.COMPOSE_LABEL_SERVICE]: kind.replace(/[^a-z0-9]/gi, '_'),
185
+ [exports.COMPOSE_LABEL_SERVICE]: serviceId.replace(/[^a-z0-9]/gi, '_'),
186
186
  },
187
187
  });
188
188
  }
@@ -791,7 +791,7 @@ class ContainerInfo {
791
791
  if (!name.startsWith(exports.CONTAINER_LABEL_PORT_PREFIX)) {
792
792
  return;
793
793
  }
794
- const hostPort = name.substr(exports.CONTAINER_LABEL_PORT_PREFIX.length);
794
+ const hostPort = name.substring(exports.CONTAINER_LABEL_PORT_PREFIX.length);
795
795
  portTypes[hostPort] = portType;
796
796
  });
797
797
  lodash_1.default.forEach(inspectResult.HostConfig.PortBindings, (portBindings, containerPortSpec) => {
@@ -2,7 +2,7 @@
2
2
  * Copyright 2023 Kapeta Inc.
3
3
  * SPDX-License-Identifier: BUSL-1.1
4
4
  */
5
- import { InstanceInfo, LogEntry } from './types';
5
+ import { EnvironmentType, InstanceInfo, LogEntry, OperatorInstanceInfo } from './types';
6
6
  import { Task } from './taskManager';
7
7
  export declare class InstanceManager {
8
8
  private _interval;
@@ -27,6 +27,7 @@ export declare class InstanceManager {
27
27
  stop(systemId: string, instanceId: string): Promise<void>;
28
28
  private stopInner;
29
29
  stopAllForPlan(systemId: string): Task<void>;
30
+ getInstanceOperator(systemId: string, instanceId: string, environment?: EnvironmentType): Promise<OperatorInstanceInfo>;
30
31
  start(systemId: string, instanceId: string): Promise<InstanceInfo | Task<InstanceInfo>>;
31
32
  /**
32
33
  * Stops an instance but does not remove it from the list of active instances
@@ -323,6 +323,48 @@ class InstanceManager {
323
323
  name: `Stopping plan ${systemId}`,
324
324
  });
325
325
  }
326
+ async getInstanceOperator(systemId, instanceId, environment) {
327
+ const blockInstance = await assetManager_1.assetManager.getBlockInstance(systemId, instanceId);
328
+ if (!blockInstance) {
329
+ throw new Error(`Instance not found: ${systemId}/${instanceId}`);
330
+ }
331
+ const blockRef = (0, nodejs_utils_1.normalizeKapetaUri)(blockInstance.block.ref);
332
+ const block = await assetManager_1.assetManager.getAsset(blockRef, true);
333
+ if (!block) {
334
+ throw new Error(`Block not found: ${blockRef}`);
335
+ }
336
+ const operatorDefinition = await definitionsManager_1.definitionsManager.getDefinition(block.kind);
337
+ if (!operatorDefinition?.definition.spec.local) {
338
+ throw new Error(`Operator block has no local definition: ${blockRef}`);
339
+ }
340
+ const localConfig = operatorDefinition.definition.spec.local;
341
+ let instance = await this.start(systemId, instanceId);
342
+ if (instance instanceof taskManager_1.Task) {
343
+ instance = await instance.wait();
344
+ }
345
+ const container = await containerManager_1.containerManager.get(instance.pid);
346
+ if (!container) {
347
+ throw new Error(`Container not found: ${instance.pid}`);
348
+ }
349
+ const portInfo = await container.getPorts();
350
+ if (!portInfo) {
351
+ throw new Error(`No ports found for instance: ${instanceId}`);
352
+ }
353
+ const hostname = serviceManager_1.serviceManager.getLocalHost(environment);
354
+ const ports = {};
355
+ Object.entries(portInfo).forEach(([key, value]) => {
356
+ ports[key] = {
357
+ protocol: value.protocol,
358
+ port: parseInt(value.hostPort),
359
+ };
360
+ });
361
+ return {
362
+ hostname,
363
+ ports,
364
+ credentials: localConfig.credentials,
365
+ options: localConfig.options,
366
+ };
367
+ }
326
368
  async start(systemId, instanceId) {
327
369
  return this.exclusive(systemId, instanceId, async () => {
328
370
  systemId = (0, nodejs_utils_1.normalizeKapetaUri)(systemId);
@@ -333,7 +375,7 @@ class InstanceManager {
333
375
  throw new Error('Block not found: ' + blockRef);
334
376
  }
335
377
  const existingInstance = this.getInstance(systemId, instanceId);
336
- if (existingInstance) {
378
+ if (existingInstance && existingInstance.pid) {
337
379
  if (existingInstance.status === types_1.InstanceStatus.READY) {
338
380
  // Instance is already running
339
381
  return existingInstance;
@@ -4,12 +4,12 @@
4
4
  */
5
5
  import { DefinitionInfo } from '@kapeta/local-cluster-config';
6
6
  import { ContainerInfo } from './containerManager';
7
- import { EnvironmentType, OperatorInfo } from './types';
7
+ import { EnvironmentType, LocalImageOptions, OperatorInfo } from './types';
8
8
  export declare const KIND_OPERATOR = "core/resource-type-operator";
9
9
  declare class Operator {
10
10
  private readonly _data;
11
11
  constructor(data: DefinitionInfo);
12
- getLocalData(): any;
12
+ getLocalData(): LocalImageOptions;
13
13
  getDefinitionInfo(): DefinitionInfo;
14
14
  getCredentials(): any;
15
15
  }
@@ -137,13 +137,8 @@ class OperatorManager {
137
137
  const portType = portTypes[i];
138
138
  let containerPortInfo = operatorData.ports[portType];
139
139
  const hostPort = await serviceManager_1.serviceManager.ensureServicePort(systemId, resourceType, portType);
140
- if (typeof containerPortInfo === 'number' || typeof containerPortInfo === 'string') {
141
- containerPortInfo = { port: containerPortInfo, type: 'tcp' };
142
- }
143
- if (!containerPortInfo.type) {
144
- containerPortInfo.type = 'tcp';
145
- }
146
- const portId = containerPortInfo.port + '/' + containerPortInfo.type;
140
+ const portInfo = (0, utils_1.toPortInfo)(containerPortInfo);
141
+ const portId = portInfo.port + '/' + portInfo.type;
147
142
  ports[portId] = {
148
143
  type: portType,
149
144
  hostPort,
@@ -9,6 +9,7 @@ export declare const HTTP_PORTS: string[];
9
9
  declare class ServiceManager {
10
10
  private _systems;
11
11
  constructor();
12
+ getLocalHost(environmentType?: EnvironmentType): string;
12
13
  _forLocal(port: string | number, path?: string, environmentType?: EnvironmentType): string;
13
14
  _ensureSystem(systemId: string): any;
14
15
  _ensureService(systemId: string, serviceId: string): any;
@@ -31,22 +31,22 @@ class ServiceManager {
31
31
  });
32
32
  });
33
33
  }
34
- _forLocal(port, path, environmentType) {
35
- if (!path) {
36
- path = '';
37
- }
38
- let host;
34
+ getLocalHost(environmentType) {
39
35
  if (environmentType === 'docker') {
40
36
  //We're inside a docker container, so we can use this special host name to access the host machine
41
- host = 'host.docker.internal';
37
+ return 'host.docker.internal';
42
38
  }
43
- else {
44
- host = clusterService_1.clusterService.getClusterServiceHost();
39
+ return clusterService_1.clusterService.getClusterServiceHost();
40
+ }
41
+ _forLocal(port, path, environmentType) {
42
+ if (!path) {
43
+ path = '';
45
44
  }
45
+ const hostname = this.getLocalHost(environmentType);
46
46
  if (path.startsWith('/')) {
47
47
  path = path.substring(1);
48
48
  }
49
- return `http://${host}:${port}/${path}`;
49
+ return `http://${hostname}:${port}/${path}`;
50
50
  }
51
51
  _ensureSystem(systemId) {
52
52
  systemId = (0, nodejs_utils_1.normalizeKapetaUri)(systemId);
@@ -56,6 +56,30 @@ export type ProcessInfo = {
56
56
  pid?: number | string | null;
57
57
  portType?: string;
58
58
  };
59
+ export interface Health {
60
+ cmd: string;
61
+ interval?: number;
62
+ timeout?: number;
63
+ retries?: number;
64
+ }
65
+ export type PortInfo = {
66
+ port: number;
67
+ type: 'tcp' | 'udp';
68
+ } | number | string;
69
+ export type LocalImageOptions<Credentials = AnyMap, Options = AnyMap> = {
70
+ image: string;
71
+ ports: {
72
+ [key: string]: PortInfo;
73
+ };
74
+ credentials?: Credentials;
75
+ options?: Options;
76
+ cmd?: string;
77
+ env?: AnyMap;
78
+ health?: Health;
79
+ mounts?: {
80
+ [key: string]: string;
81
+ };
82
+ };
59
83
  export type InstanceInfo = {
60
84
  systemId: string;
61
85
  instanceId: string;
@@ -73,6 +97,21 @@ export type InstanceInfo = {
73
97
  portType?: string;
74
98
  };
75
99
  export type ProxyRequestHandler = (req: StringBodyRequest, res: express.Response, info: ProxyRequestInfo) => void;
100
+ export interface OperatorInstancePort {
101
+ protocol: string;
102
+ port: number;
103
+ }
104
+ export interface OperatorInstanceInfo {
105
+ hostname: string;
106
+ ports: {
107
+ [portType: string]: OperatorInstancePort;
108
+ };
109
+ path?: string;
110
+ query?: string;
111
+ hash?: string;
112
+ options?: AnyMap;
113
+ credentials?: AnyMap;
114
+ }
76
115
  export interface OperatorInfo {
77
116
  host: string;
78
117
  port: string;
@@ -288,7 +288,8 @@ class BlockInstanceRunner {
288
288
  if (!spec?.local?.image) {
289
289
  throw new Error(`Provider did not have local image: ${providerRef}`);
290
290
  }
291
- const dockerImage = spec?.local?.image;
291
+ const local = spec.local;
292
+ const dockerImage = local.image;
292
293
  //We only want 1 operator per operator type - across all local systems
293
294
  const containerName = (0, utils_1.getBlockInstanceContainerName)(this._systemId, blockInstance.id);
294
295
  const logs = new LogData_1.LogData();
@@ -298,11 +299,13 @@ class BlockInstanceRunner {
298
299
  const PortBindings = {};
299
300
  let HealthCheck = undefined;
300
301
  let Mounts = [];
301
- const localPorts = spec.local.ports;
302
+ const localPorts = local.ports ?? {};
303
+ const labels = {};
302
304
  const promises = Object.entries(localPorts).map(async ([portType, value]) => {
303
- const dockerPort = `${value.port}/${value.type}`;
305
+ const portInfo = (0, utils_1.toPortInfo)(value);
306
+ const dockerPort = `${portInfo.port}/${portInfo.type}`;
304
307
  ExposedPorts[dockerPort] = {};
305
- addonEnv[`KAPETA_LOCAL_SERVER_PORT_${portType.toUpperCase()}`] = value.port;
308
+ addonEnv[`KAPETA_LOCAL_SERVER_PORT_${portType.toUpperCase()}`] = `${portInfo.port}`;
306
309
  const publicPort = await serviceManager_1.serviceManager.ensureServicePort(this._systemId, blockInstance.id, portType);
307
310
  PortBindings[dockerPort] = [
308
311
  {
@@ -310,19 +313,19 @@ class BlockInstanceRunner {
310
313
  HostPort: `${publicPort}`,
311
314
  },
312
315
  ];
316
+ labels[containerManager_1.CONTAINER_LABEL_PORT_PREFIX + publicPort] = portType;
313
317
  });
314
318
  await Promise.all(promises);
315
- if (spec.local?.env) {
316
- Object.entries(spec.local.env).forEach(([key, value]) => {
319
+ if (local.env) {
320
+ Object.entries(local.env).forEach(([key, value]) => {
317
321
  addonEnv[key] = value;
318
322
  });
319
323
  }
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);
324
+ if (local.mounts) {
325
+ Mounts = await containerManager_1.containerManager.createVolumes(this._systemId, blockUri.id, local.mounts);
323
326
  }
324
- if (spec.local?.health) {
325
- HealthCheck = containerManager_1.containerManager.toDockerHealth(spec.local?.health);
327
+ if (local.health) {
328
+ HealthCheck = containerManager_1.containerManager.toDockerHealth(local.health);
326
329
  }
327
330
  // For windows we need to default to root
328
331
  const innerHome = process.platform === 'win32' ? '/root/.kapeta' : local_cluster_config_1.default.getKapetaBasedir();
@@ -342,6 +345,7 @@ class BlockInstanceRunner {
342
345
  Mounts,
343
346
  },
344
347
  Labels: {
348
+ ...labels,
345
349
  instance: blockInstance.id,
346
350
  [containerManager_1.COMPOSE_LABEL_PROJECT]: systemUri.id.replace(/[^a-z0-9]/gi, '_'),
347
351
  [containerManager_1.COMPOSE_LABEL_SERVICE]: blockUri.id.replace(/[^a-z0-9]/gi, '_'),
@@ -356,7 +360,7 @@ class BlockInstanceRunner {
356
360
  }).map(([key, value]) => `${key}=${value}`),
357
361
  ],
358
362
  });
359
- const portTypes = spec.local.ports ? Object.keys(spec.local.ports) : [];
363
+ const portTypes = local.ports ? Object.keys(local.ports) : [];
360
364
  if (portTypes.length > 0) {
361
365
  out.portType = portTypes[0];
362
366
  }
@@ -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';
6
+ import { AnyMap, PortInfo } from '../types';
7
7
  export declare function getBlockInstanceContainerName(systemId: string, instanceId: string): 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,7 +7,7 @@ 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"));
@@ -17,6 +17,16 @@ function getBlockInstanceContainerName(systemId, instanceId) {
17
17
  return `kapeta-block-instance-${(0, md5_1.default)(systemId + instanceId)}`;
18
18
  }
19
19
  exports.getBlockInstanceContainerName = getBlockInstanceContainerName;
20
+ function toPortInfo(port) {
21
+ if (typeof port === 'number' || typeof port === 'string') {
22
+ return { port: parseInt(`${port}`), type: 'tcp' };
23
+ }
24
+ if (!port.type) {
25
+ port.type = 'tcp';
26
+ }
27
+ return port;
28
+ }
29
+ exports.toPortInfo = toPortInfo;
20
30
  function getRemoteUrl(id, defautValue) {
21
31
  const remoteConfig = local_cluster_config_1.default.getClusterConfig().remote;
22
32
  return remoteConfig?.[id] ?? defautValue;
@@ -148,4 +148,13 @@ router.get('/consumes/resource/:resourceType/:portType/:name', async (req, res)
148
148
  router.get('/consumes/:resourceName/:type', (req, res) => {
149
149
  res.send(serviceManager_1.serviceManager.getConsumerAddress(req.kapeta.systemId, req.kapeta.instanceId, req.params.resourceName, req.params.type, req.kapeta.environment));
150
150
  });
151
+ /**
152
+ * Used by services to information about a block operator
153
+ *
154
+ * If the remote service is not already running it will be started
155
+ */
156
+ router.get('/operator/:instanceId', async (req, res) => {
157
+ const operatorInfo = await instanceManager_1.instanceManager.getInstanceOperator(req.kapeta.systemId, req.params.instanceId, req.kapeta.environment);
158
+ res.send(operatorInfo);
159
+ });
151
160
  exports.default = router;
@@ -5,7 +5,7 @@
5
5
  /// <reference types="node" />
6
6
  import FSExtra from 'fs-extra';
7
7
  import Docker from 'dockerode';
8
- import { InstanceInfo, LogEntry } from './types';
8
+ import { Health, InstanceInfo, LogEntry } from './types';
9
9
  type StringMap = {
10
10
  [key: string]: string;
11
11
  };
@@ -26,12 +26,6 @@ export interface DockerMounts {
26
26
  }
27
27
  export type DockerContainerStatus = 'created' | 'running' | 'paused' | 'restarting' | 'removing' | 'exited' | 'dead';
28
28
  export type DockerContainerHealth = 'starting' | 'healthy' | 'unhealthy' | 'none';
29
- interface Health {
30
- cmd: string;
31
- interval?: number;
32
- timeout?: number;
33
- retries?: number;
34
- }
35
29
  export declare const CONTAINER_LABEL_PORT_PREFIX = "kapeta_port-";
36
30
  export declare const COMPOSE_LABEL_PROJECT = "com.docker.compose.project";
37
31
  export declare const COMPOSE_LABEL_SERVICE = "com.docker.compose.service";
@@ -50,7 +44,7 @@ declare class ContainerManager {
50
44
  isAlive(): boolean;
51
45
  getMountPoint(systemId: string, ref: string, mountName: string): string;
52
46
  createMounts(systemId: string, kind: string, mountOpts: StringMap | null | undefined): Promise<StringMap>;
53
- createVolumes(systemId: string, kind: string, mountOpts: StringMap | null | undefined): Promise<DockerMounts[]>;
47
+ createVolumes(systemId: string, serviceId: string, mountOpts: StringMap | null | undefined): Promise<DockerMounts[]>;
54
48
  ping(): Promise<void>;
55
49
  docker(): Docker;
56
50
  getContainerByName(containerName: string): Promise<ContainerInfo | undefined>;
@@ -168,12 +168,12 @@ class ContainerManager {
168
168
  }
169
169
  return mounts;
170
170
  }
171
- async createVolumes(systemId, kind, mountOpts) {
171
+ async createVolumes(systemId, serviceId, mountOpts) {
172
172
  const Mounts = [];
173
173
  if (mountOpts) {
174
174
  const mountOptList = Object.entries(mountOpts);
175
175
  for (const [mountName, containerPath] of mountOptList) {
176
- const volumeName = `${systemId}_${kind}_${mountName}`.replace(/[^a-z0-9]/gi, '_');
176
+ const volumeName = `${systemId}_${serviceId}_${mountName}`.replace(/[^a-z0-9]/gi, '_');
177
177
  Mounts.push({
178
178
  Target: containerPath,
179
179
  Source: volumeName,
@@ -182,7 +182,7 @@ class ContainerManager {
182
182
  Consistency: 'consistent',
183
183
  Labels: {
184
184
  [exports.COMPOSE_LABEL_PROJECT]: systemId.replace(/[^a-z0-9]/gi, '_'),
185
- [exports.COMPOSE_LABEL_SERVICE]: kind.replace(/[^a-z0-9]/gi, '_'),
185
+ [exports.COMPOSE_LABEL_SERVICE]: serviceId.replace(/[^a-z0-9]/gi, '_'),
186
186
  },
187
187
  });
188
188
  }
@@ -791,7 +791,7 @@ class ContainerInfo {
791
791
  if (!name.startsWith(exports.CONTAINER_LABEL_PORT_PREFIX)) {
792
792
  return;
793
793
  }
794
- const hostPort = name.substr(exports.CONTAINER_LABEL_PORT_PREFIX.length);
794
+ const hostPort = name.substring(exports.CONTAINER_LABEL_PORT_PREFIX.length);
795
795
  portTypes[hostPort] = portType;
796
796
  });
797
797
  lodash_1.default.forEach(inspectResult.HostConfig.PortBindings, (portBindings, containerPortSpec) => {
@@ -2,7 +2,7 @@
2
2
  * Copyright 2023 Kapeta Inc.
3
3
  * SPDX-License-Identifier: BUSL-1.1
4
4
  */
5
- import { InstanceInfo, LogEntry } from './types';
5
+ import { EnvironmentType, InstanceInfo, LogEntry, OperatorInstanceInfo } from './types';
6
6
  import { Task } from './taskManager';
7
7
  export declare class InstanceManager {
8
8
  private _interval;
@@ -27,6 +27,7 @@ export declare class InstanceManager {
27
27
  stop(systemId: string, instanceId: string): Promise<void>;
28
28
  private stopInner;
29
29
  stopAllForPlan(systemId: string): Task<void>;
30
+ getInstanceOperator(systemId: string, instanceId: string, environment?: EnvironmentType): Promise<OperatorInstanceInfo>;
30
31
  start(systemId: string, instanceId: string): Promise<InstanceInfo | Task<InstanceInfo>>;
31
32
  /**
32
33
  * Stops an instance but does not remove it from the list of active instances
@@ -323,6 +323,48 @@ class InstanceManager {
323
323
  name: `Stopping plan ${systemId}`,
324
324
  });
325
325
  }
326
+ async getInstanceOperator(systemId, instanceId, environment) {
327
+ const blockInstance = await assetManager_1.assetManager.getBlockInstance(systemId, instanceId);
328
+ if (!blockInstance) {
329
+ throw new Error(`Instance not found: ${systemId}/${instanceId}`);
330
+ }
331
+ const blockRef = (0, nodejs_utils_1.normalizeKapetaUri)(blockInstance.block.ref);
332
+ const block = await assetManager_1.assetManager.getAsset(blockRef, true);
333
+ if (!block) {
334
+ throw new Error(`Block not found: ${blockRef}`);
335
+ }
336
+ const operatorDefinition = await definitionsManager_1.definitionsManager.getDefinition(block.kind);
337
+ if (!operatorDefinition?.definition.spec.local) {
338
+ throw new Error(`Operator block has no local definition: ${blockRef}`);
339
+ }
340
+ const localConfig = operatorDefinition.definition.spec.local;
341
+ let instance = await this.start(systemId, instanceId);
342
+ if (instance instanceof taskManager_1.Task) {
343
+ instance = await instance.wait();
344
+ }
345
+ const container = await containerManager_1.containerManager.get(instance.pid);
346
+ if (!container) {
347
+ throw new Error(`Container not found: ${instance.pid}`);
348
+ }
349
+ const portInfo = await container.getPorts();
350
+ if (!portInfo) {
351
+ throw new Error(`No ports found for instance: ${instanceId}`);
352
+ }
353
+ const hostname = serviceManager_1.serviceManager.getLocalHost(environment);
354
+ const ports = {};
355
+ Object.entries(portInfo).forEach(([key, value]) => {
356
+ ports[key] = {
357
+ protocol: value.protocol,
358
+ port: parseInt(value.hostPort),
359
+ };
360
+ });
361
+ return {
362
+ hostname,
363
+ ports,
364
+ credentials: localConfig.credentials,
365
+ options: localConfig.options,
366
+ };
367
+ }
326
368
  async start(systemId, instanceId) {
327
369
  return this.exclusive(systemId, instanceId, async () => {
328
370
  systemId = (0, nodejs_utils_1.normalizeKapetaUri)(systemId);
@@ -333,7 +375,7 @@ class InstanceManager {
333
375
  throw new Error('Block not found: ' + blockRef);
334
376
  }
335
377
  const existingInstance = this.getInstance(systemId, instanceId);
336
- if (existingInstance) {
378
+ if (existingInstance && existingInstance.pid) {
337
379
  if (existingInstance.status === types_1.InstanceStatus.READY) {
338
380
  // Instance is already running
339
381
  return existingInstance;
@@ -4,12 +4,12 @@
4
4
  */
5
5
  import { DefinitionInfo } from '@kapeta/local-cluster-config';
6
6
  import { ContainerInfo } from './containerManager';
7
- import { EnvironmentType, OperatorInfo } from './types';
7
+ import { EnvironmentType, LocalImageOptions, OperatorInfo } from './types';
8
8
  export declare const KIND_OPERATOR = "core/resource-type-operator";
9
9
  declare class Operator {
10
10
  private readonly _data;
11
11
  constructor(data: DefinitionInfo);
12
- getLocalData(): any;
12
+ getLocalData(): LocalImageOptions;
13
13
  getDefinitionInfo(): DefinitionInfo;
14
14
  getCredentials(): any;
15
15
  }
@@ -137,13 +137,8 @@ class OperatorManager {
137
137
  const portType = portTypes[i];
138
138
  let containerPortInfo = operatorData.ports[portType];
139
139
  const hostPort = await serviceManager_1.serviceManager.ensureServicePort(systemId, resourceType, portType);
140
- if (typeof containerPortInfo === 'number' || typeof containerPortInfo === 'string') {
141
- containerPortInfo = { port: containerPortInfo, type: 'tcp' };
142
- }
143
- if (!containerPortInfo.type) {
144
- containerPortInfo.type = 'tcp';
145
- }
146
- const portId = containerPortInfo.port + '/' + containerPortInfo.type;
140
+ const portInfo = (0, utils_1.toPortInfo)(containerPortInfo);
141
+ const portId = portInfo.port + '/' + portInfo.type;
147
142
  ports[portId] = {
148
143
  type: portType,
149
144
  hostPort,
@@ -9,6 +9,7 @@ export declare const HTTP_PORTS: string[];
9
9
  declare class ServiceManager {
10
10
  private _systems;
11
11
  constructor();
12
+ getLocalHost(environmentType?: EnvironmentType): string;
12
13
  _forLocal(port: string | number, path?: string, environmentType?: EnvironmentType): string;
13
14
  _ensureSystem(systemId: string): any;
14
15
  _ensureService(systemId: string, serviceId: string): any;
@@ -31,22 +31,22 @@ class ServiceManager {
31
31
  });
32
32
  });
33
33
  }
34
- _forLocal(port, path, environmentType) {
35
- if (!path) {
36
- path = '';
37
- }
38
- let host;
34
+ getLocalHost(environmentType) {
39
35
  if (environmentType === 'docker') {
40
36
  //We're inside a docker container, so we can use this special host name to access the host machine
41
- host = 'host.docker.internal';
37
+ return 'host.docker.internal';
42
38
  }
43
- else {
44
- host = clusterService_1.clusterService.getClusterServiceHost();
39
+ return clusterService_1.clusterService.getClusterServiceHost();
40
+ }
41
+ _forLocal(port, path, environmentType) {
42
+ if (!path) {
43
+ path = '';
45
44
  }
45
+ const hostname = this.getLocalHost(environmentType);
46
46
  if (path.startsWith('/')) {
47
47
  path = path.substring(1);
48
48
  }
49
- return `http://${host}:${port}/${path}`;
49
+ return `http://${hostname}:${port}/${path}`;
50
50
  }
51
51
  _ensureSystem(systemId) {
52
52
  systemId = (0, nodejs_utils_1.normalizeKapetaUri)(systemId);
@@ -56,6 +56,30 @@ export type ProcessInfo = {
56
56
  pid?: number | string | null;
57
57
  portType?: string;
58
58
  };
59
+ export interface Health {
60
+ cmd: string;
61
+ interval?: number;
62
+ timeout?: number;
63
+ retries?: number;
64
+ }
65
+ export type PortInfo = {
66
+ port: number;
67
+ type: 'tcp' | 'udp';
68
+ } | number | string;
69
+ export type LocalImageOptions<Credentials = AnyMap, Options = AnyMap> = {
70
+ image: string;
71
+ ports: {
72
+ [key: string]: PortInfo;
73
+ };
74
+ credentials?: Credentials;
75
+ options?: Options;
76
+ cmd?: string;
77
+ env?: AnyMap;
78
+ health?: Health;
79
+ mounts?: {
80
+ [key: string]: string;
81
+ };
82
+ };
59
83
  export type InstanceInfo = {
60
84
  systemId: string;
61
85
  instanceId: string;
@@ -73,6 +97,21 @@ export type InstanceInfo = {
73
97
  portType?: string;
74
98
  };
75
99
  export type ProxyRequestHandler = (req: StringBodyRequest, res: express.Response, info: ProxyRequestInfo) => void;
100
+ export interface OperatorInstancePort {
101
+ protocol: string;
102
+ port: number;
103
+ }
104
+ export interface OperatorInstanceInfo {
105
+ hostname: string;
106
+ ports: {
107
+ [portType: string]: OperatorInstancePort;
108
+ };
109
+ path?: string;
110
+ query?: string;
111
+ hash?: string;
112
+ options?: AnyMap;
113
+ credentials?: AnyMap;
114
+ }
76
115
  export interface OperatorInfo {
77
116
  host: string;
78
117
  port: string;
@@ -288,7 +288,8 @@ class BlockInstanceRunner {
288
288
  if (!spec?.local?.image) {
289
289
  throw new Error(`Provider did not have local image: ${providerRef}`);
290
290
  }
291
- const dockerImage = spec?.local?.image;
291
+ const local = spec.local;
292
+ const dockerImage = local.image;
292
293
  //We only want 1 operator per operator type - across all local systems
293
294
  const containerName = (0, utils_1.getBlockInstanceContainerName)(this._systemId, blockInstance.id);
294
295
  const logs = new LogData_1.LogData();
@@ -298,11 +299,13 @@ class BlockInstanceRunner {
298
299
  const PortBindings = {};
299
300
  let HealthCheck = undefined;
300
301
  let Mounts = [];
301
- const localPorts = spec.local.ports;
302
+ const localPorts = local.ports ?? {};
303
+ const labels = {};
302
304
  const promises = Object.entries(localPorts).map(async ([portType, value]) => {
303
- const dockerPort = `${value.port}/${value.type}`;
305
+ const portInfo = (0, utils_1.toPortInfo)(value);
306
+ const dockerPort = `${portInfo.port}/${portInfo.type}`;
304
307
  ExposedPorts[dockerPort] = {};
305
- addonEnv[`KAPETA_LOCAL_SERVER_PORT_${portType.toUpperCase()}`] = value.port;
308
+ addonEnv[`KAPETA_LOCAL_SERVER_PORT_${portType.toUpperCase()}`] = `${portInfo.port}`;
306
309
  const publicPort = await serviceManager_1.serviceManager.ensureServicePort(this._systemId, blockInstance.id, portType);
307
310
  PortBindings[dockerPort] = [
308
311
  {
@@ -310,19 +313,19 @@ class BlockInstanceRunner {
310
313
  HostPort: `${publicPort}`,
311
314
  },
312
315
  ];
316
+ labels[containerManager_1.CONTAINER_LABEL_PORT_PREFIX + publicPort] = portType;
313
317
  });
314
318
  await Promise.all(promises);
315
- if (spec.local?.env) {
316
- Object.entries(spec.local.env).forEach(([key, value]) => {
319
+ if (local.env) {
320
+ Object.entries(local.env).forEach(([key, value]) => {
317
321
  addonEnv[key] = value;
318
322
  });
319
323
  }
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);
324
+ if (local.mounts) {
325
+ Mounts = await containerManager_1.containerManager.createVolumes(this._systemId, blockUri.id, local.mounts);
323
326
  }
324
- if (spec.local?.health) {
325
- HealthCheck = containerManager_1.containerManager.toDockerHealth(spec.local?.health);
327
+ if (local.health) {
328
+ HealthCheck = containerManager_1.containerManager.toDockerHealth(local.health);
326
329
  }
327
330
  // For windows we need to default to root
328
331
  const innerHome = process.platform === 'win32' ? '/root/.kapeta' : local_cluster_config_1.default.getKapetaBasedir();
@@ -342,6 +345,7 @@ class BlockInstanceRunner {
342
345
  Mounts,
343
346
  },
344
347
  Labels: {
348
+ ...labels,
345
349
  instance: blockInstance.id,
346
350
  [containerManager_1.COMPOSE_LABEL_PROJECT]: systemUri.id.replace(/[^a-z0-9]/gi, '_'),
347
351
  [containerManager_1.COMPOSE_LABEL_SERVICE]: blockUri.id.replace(/[^a-z0-9]/gi, '_'),
@@ -356,7 +360,7 @@ class BlockInstanceRunner {
356
360
  }).map(([key, value]) => `${key}=${value}`),
357
361
  ],
358
362
  });
359
- const portTypes = spec.local.ports ? Object.keys(spec.local.ports) : [];
363
+ const portTypes = local.ports ? Object.keys(local.ports) : [];
360
364
  if (portTypes.length > 0) {
361
365
  out.portType = portTypes[0];
362
366
  }
@@ -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';
6
+ import { AnyMap, PortInfo } from '../types';
7
7
  export declare function getBlockInstanceContainerName(systemId: string, instanceId: string): 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,7 +7,7 @@ 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"));
@@ -17,6 +17,16 @@ function getBlockInstanceContainerName(systemId, instanceId) {
17
17
  return `kapeta-block-instance-${(0, md5_1.default)(systemId + instanceId)}`;
18
18
  }
19
19
  exports.getBlockInstanceContainerName = getBlockInstanceContainerName;
20
+ function toPortInfo(port) {
21
+ if (typeof port === 'number' || typeof port === 'string') {
22
+ return { port: parseInt(`${port}`), type: 'tcp' };
23
+ }
24
+ if (!port.type) {
25
+ port.type = 'tcp';
26
+ }
27
+ return port;
28
+ }
29
+ exports.toPortInfo = toPortInfo;
20
30
  function getRemoteUrl(id, defautValue) {
21
31
  const remoteConfig = local_cluster_config_1.default.getClusterConfig().remote;
22
32
  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.35.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
  }
@@ -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
  });
@@ -18,7 +18,19 @@ import {
18
18
  HEALTH_CHECK_TIMEOUT,
19
19
  } from './containerManager';
20
20
  import { configManager } from './configManager';
21
- import { DesiredInstanceStatus, InstanceInfo, InstanceOwner, InstanceStatus, InstanceType, LogEntry } from './types';
21
+ import {
22
+ DesiredInstanceStatus,
23
+ EnvironmentType,
24
+ InstanceInfo,
25
+ InstanceOwner,
26
+ InstanceStatus,
27
+ InstanceType,
28
+ LocalImageOptions,
29
+ LogEntry,
30
+ OperatorInfo,
31
+ OperatorInstanceInfo,
32
+ OperatorInstancePort,
33
+ } from './types';
22
34
  import { BlockDefinitionSpec, BlockInstance, Plan } from '@kapeta/schemas';
23
35
  import { getBlockInstanceContainerName, getResolvedConfiguration } from './utils/utils';
24
36
  import { KIND_OPERATOR, operatorManager } from './operatorManager';
@@ -414,6 +426,62 @@ export class InstanceManager {
414
426
  );
415
427
  }
416
428
 
429
+ public async getInstanceOperator(
430
+ systemId: string,
431
+ instanceId: string,
432
+ environment?: EnvironmentType
433
+ ): Promise<OperatorInstanceInfo> {
434
+ const blockInstance = await assetManager.getBlockInstance(systemId, instanceId);
435
+ if (!blockInstance) {
436
+ throw new Error(`Instance not found: ${systemId}/${instanceId}`);
437
+ }
438
+ const blockRef = normalizeKapetaUri(blockInstance.block.ref);
439
+ const block = await assetManager.getAsset(blockRef, true);
440
+ if (!block) {
441
+ throw new Error(`Block not found: ${blockRef}`);
442
+ }
443
+
444
+ const operatorDefinition = await definitionsManager.getDefinition(block.kind);
445
+
446
+ if (!operatorDefinition?.definition.spec.local) {
447
+ throw new Error(`Operator block has no local definition: ${blockRef}`);
448
+ }
449
+
450
+ const localConfig = operatorDefinition.definition.spec.local as LocalImageOptions;
451
+
452
+ let instance = await this.start(systemId, instanceId);
453
+ if (instance instanceof Task) {
454
+ instance = await instance.wait();
455
+ }
456
+
457
+ const container = await containerManager.get(instance.pid as string);
458
+ if (!container) {
459
+ throw new Error(`Container not found: ${instance.pid}`);
460
+ }
461
+
462
+ const portInfo = await container.getPorts();
463
+ if (!portInfo) {
464
+ throw new Error(`No ports found for instance: ${instanceId}`);
465
+ }
466
+
467
+ const hostname = serviceManager.getLocalHost(environment);
468
+ const ports: { [key: string]: OperatorInstancePort } = {};
469
+
470
+ Object.entries(portInfo).forEach(([key, value]) => {
471
+ ports[key] = {
472
+ protocol: value.protocol,
473
+ port: parseInt(value.hostPort),
474
+ };
475
+ });
476
+
477
+ return {
478
+ hostname,
479
+ ports,
480
+ credentials: localConfig.credentials,
481
+ options: localConfig.options,
482
+ };
483
+ }
484
+
417
485
  public async start(systemId: string, instanceId: string): Promise<InstanceInfo | Task<InstanceInfo>> {
418
486
  return this.exclusive(systemId, instanceId, async () => {
419
487
  systemId = normalizeKapetaUri(systemId);
@@ -427,7 +495,7 @@ export class InstanceManager {
427
495
 
428
496
  const existingInstance = this.getInstance(systemId, instanceId);
429
497
 
430
- if (existingInstance) {
498
+ if (existingInstance && existingInstance.pid) {
431
499
  if (existingInstance.status === InstanceStatus.READY) {
432
500
  // Instance is already running
433
501
  return existingInstance;
@@ -481,8 +549,8 @@ export class InstanceManager {
481
549
  return Promise.resolve();
482
550
  }
483
551
  // Check if the operator has a local definition, if not we skip it since we can't start it
484
- if(!asset.definition.spec.local) {
485
- console.log('Skipping operator since it as no local definition: %s', consumer.kind)
552
+ if (!asset.definition.spec.local) {
553
+ console.log('Skipping operator since it as no local definition: %s', consumer.kind);
486
554
  return Promise.resolve();
487
555
  }
488
556
  console.log('Ensuring resource: %s in %s', consumerUri.id, systemId);
@@ -16,10 +16,10 @@ import {
16
16
  containerManager,
17
17
  } from './containerManager';
18
18
  import FSExtra from 'fs-extra';
19
- import { AnyMap, EnvironmentType, OperatorInfo, StringMap } from './types';
19
+ import { AnyMap, EnvironmentType, LocalImageOptions, OperatorInfo, StringMap } from './types';
20
20
  import { BlockInstance, Resource } from '@kapeta/schemas';
21
21
  import { definitionsManager } from './definitionsManager';
22
- import { getBindHost } from './utils/utils';
22
+ import { getBindHost, toPortInfo } from './utils/utils';
23
23
  import { parseKapetaUri, normalizeKapetaUri } from '@kapeta/nodejs-utils';
24
24
  import _ from 'lodash';
25
25
  import AsyncLock from 'async-lock';
@@ -34,7 +34,7 @@ class Operator {
34
34
  this._data = data;
35
35
  }
36
36
 
37
- getLocalData() {
37
+ getLocalData(): LocalImageOptions {
38
38
  return this._data.definition.spec.local;
39
39
  }
40
40
 
@@ -187,16 +187,8 @@ class OperatorManager {
187
187
  const portType = portTypes[i];
188
188
  let containerPortInfo = operatorData.ports[portType];
189
189
  const hostPort = await serviceManager.ensureServicePort(systemId, resourceType, portType);
190
-
191
- if (typeof containerPortInfo === 'number' || typeof containerPortInfo === 'string') {
192
- containerPortInfo = { port: containerPortInfo, type: 'tcp' };
193
- }
194
-
195
- if (!containerPortInfo.type) {
196
- containerPortInfo.type = 'tcp';
197
- }
198
-
199
- const portId = containerPortInfo.port + '/' + containerPortInfo.type;
190
+ const portInfo = toPortInfo(containerPortInfo);
191
+ const portId = portInfo.port + '/' + portInfo.type;
200
192
 
201
193
  ports[portId] = {
202
194
  type: portType,
@@ -34,22 +34,25 @@ class ServiceManager {
34
34
  });
35
35
  }
36
36
 
37
+ public getLocalHost(environmentType?: EnvironmentType) {
38
+ if (environmentType === 'docker') {
39
+ //We're inside a docker container, so we can use this special host name to access the host machine
40
+ return 'host.docker.internal';
41
+ }
42
+
43
+ return clusterService.getClusterServiceHost();
44
+ }
45
+
37
46
  _forLocal(port: string | number, path?: string, environmentType?: EnvironmentType) {
38
47
  if (!path) {
39
48
  path = '';
40
49
  }
41
- let host;
42
- if (environmentType === 'docker') {
43
- //We're inside a docker container, so we can use this special host name to access the host machine
44
- host = 'host.docker.internal';
45
- } else {
46
- host = clusterService.getClusterServiceHost();
47
- }
50
+ const hostname = this.getLocalHost(environmentType);
48
51
 
49
52
  if (path.startsWith('/')) {
50
53
  path = path.substring(1);
51
54
  }
52
- return `http://${host}:${port}/${path}`;
55
+ return `http://${hostname}:${port}/${path}`;
53
56
  }
54
57
 
55
58
  _ensureSystem(systemId: string) {
package/src/types.ts CHANGED
@@ -61,6 +61,26 @@ export type ProcessInfo = {
61
61
  portType?: string;
62
62
  };
63
63
 
64
+ export interface Health {
65
+ cmd: string;
66
+ interval?: number;
67
+ timeout?: number;
68
+ retries?: number;
69
+ }
70
+
71
+ export type PortInfo = { port: number; type: 'tcp' | 'udp' } | number | string;
72
+
73
+ export type LocalImageOptions<Credentials = AnyMap, Options = AnyMap> = {
74
+ image: string;
75
+ ports: { [key: string]: PortInfo };
76
+ credentials?: Credentials;
77
+ options?: Options;
78
+ cmd?: string;
79
+ env?: AnyMap;
80
+ health?: Health;
81
+ mounts?: { [key: string]: string };
82
+ };
83
+
64
84
  export type InstanceInfo = {
65
85
  systemId: string;
66
86
  instanceId: string;
@@ -86,6 +106,21 @@ interface ResourceRef {
86
106
 
87
107
  export type ProxyRequestHandler = (req: StringBodyRequest, res: express.Response, info: ProxyRequestInfo) => void;
88
108
 
109
+ export interface OperatorInstancePort {
110
+ protocol: string;
111
+ port: number;
112
+ }
113
+
114
+ export interface OperatorInstanceInfo {
115
+ hostname: string;
116
+ ports: { [portType: string]: OperatorInstancePort };
117
+ path?: string;
118
+ query?: string;
119
+ hash?: string;
120
+ options?: AnyMap;
121
+ credentials?: AnyMap;
122
+ }
123
+
89
124
  export interface OperatorInfo {
90
125
  host: string;
91
126
  port: string;
@@ -5,19 +5,20 @@
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';
@@ -384,7 +385,9 @@ export class BlockInstanceRunner {
384
385
  throw new Error(`Provider did not have local image: ${providerRef}`);
385
386
  }
386
387
 
387
- const dockerImage = spec?.local?.image;
388
+ const local = spec.local as LocalImageOptions;
389
+
390
+ const dockerImage = local.image;
388
391
 
389
392
  //We only want 1 operator per operator type - across all local systems
390
393
  const containerName = getBlockInstanceContainerName(this._systemId, blockInstance.id);
@@ -397,11 +400,13 @@ export class BlockInstanceRunner {
397
400
  const PortBindings: AnyMap = {};
398
401
  let HealthCheck = undefined;
399
402
  let Mounts: DockerMounts[] = [];
400
- const localPorts = spec.local.ports as { [p: string]: { port: string; type: string } };
403
+ const localPorts = local.ports ?? {};
404
+ const labels: { [key: string]: string } = {};
401
405
  const promises = Object.entries(localPorts).map(async ([portType, value]) => {
402
- const dockerPort = `${value.port}/${value.type}`;
406
+ const portInfo = toPortInfo(value);
407
+ const dockerPort = `${portInfo.port}/${portInfo.type}`;
403
408
  ExposedPorts[dockerPort] = {};
404
- addonEnv[`KAPETA_LOCAL_SERVER_PORT_${portType.toUpperCase()}`] = value.port;
409
+ addonEnv[`KAPETA_LOCAL_SERVER_PORT_${portType.toUpperCase()}`] = `${portInfo.port}`;
405
410
  const publicPort = await serviceManager.ensureServicePort(this._systemId, blockInstance.id, portType);
406
411
  PortBindings[dockerPort] = [
407
412
  {
@@ -409,23 +414,24 @@ export class BlockInstanceRunner {
409
414
  HostPort: `${publicPort}`,
410
415
  },
411
416
  ];
417
+
418
+ labels[CONTAINER_LABEL_PORT_PREFIX + publicPort] = portType;
412
419
  });
413
420
 
414
421
  await Promise.all(promises);
415
422
 
416
- if (spec.local?.env) {
417
- Object.entries(spec.local.env).forEach(([key, value]) => {
423
+ if (local.env) {
424
+ Object.entries(local.env).forEach(([key, value]) => {
418
425
  addonEnv[key] = value as string;
419
426
  });
420
427
  }
421
428
 
422
- if (spec.local?.mounts) {
423
- const mounts = await containerManager.createMounts(this._systemId, blockUri.id, spec.local.mounts);
424
- Mounts = containerManager.toDockerMounts(mounts);
429
+ if (local.mounts) {
430
+ Mounts = await containerManager.createVolumes(this._systemId, blockUri.id, local.mounts);
425
431
  }
426
432
 
427
- if (spec.local?.health) {
428
- HealthCheck = containerManager.toDockerHealth(spec.local?.health);
433
+ if (local.health) {
434
+ HealthCheck = containerManager.toDockerHealth(local.health);
429
435
  }
430
436
 
431
437
  // For windows we need to default to root
@@ -447,6 +453,7 @@ export class BlockInstanceRunner {
447
453
  Mounts,
448
454
  },
449
455
  Labels: {
456
+ ...labels,
450
457
  instance: blockInstance.id,
451
458
  [COMPOSE_LABEL_PROJECT]: systemUri.id.replace(/[^a-z0-9]/gi, '_'),
452
459
  [COMPOSE_LABEL_SERVICE]: blockUri.id.replace(/[^a-z0-9]/gi, '_'),
@@ -462,7 +469,7 @@ export class BlockInstanceRunner {
462
469
  ],
463
470
  });
464
471
 
465
- const portTypes = spec.local.ports ? Object.keys(spec.local.ports) : [];
472
+ const portTypes = local.ports ? Object.keys(local.ports) : [];
466
473
  if (portTypes.length > 0) {
467
474
  out.portType = portTypes[0];
468
475
  }
@@ -5,17 +5,28 @@
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';
14
13
 
15
14
  export function getBlockInstanceContainerName(systemId: string, instanceId: string) {
16
15
  return `kapeta-block-instance-${md5(systemId + instanceId)}`;
17
16
  }
18
17
 
18
+ export function toPortInfo(port: PortInfo) {
19
+ if (typeof port === 'number' || typeof port === 'string') {
20
+ return { port: parseInt(`${port}`), type: 'tcp' };
21
+ }
22
+
23
+ if (!port.type) {
24
+ port.type = 'tcp';
25
+ }
26
+
27
+ return port;
28
+ }
29
+
19
30
  export function getRemoteUrl(id: string, defautValue: string) {
20
31
  const remoteConfig = ClusterConfiguration.getClusterConfig().remote;
21
32
  return remoteConfig?.[id] ?? defautValue;