@kapeta/local-cluster-service 0.38.0 → 0.39.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 (44) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/cjs/index.js +4 -1
  3. package/dist/cjs/src/config/routes.js +2 -2
  4. package/dist/cjs/src/containerManager.js +5 -3
  5. package/dist/cjs/src/instanceManager.d.ts +4 -2
  6. package/dist/cjs/src/instanceManager.js +60 -28
  7. package/dist/cjs/src/operatorManager.d.ts +4 -2
  8. package/dist/cjs/src/operatorManager.js +32 -23
  9. package/dist/cjs/src/serviceManager.d.ts +0 -1
  10. package/dist/cjs/src/serviceManager.js +2 -8
  11. package/dist/cjs/src/types.d.ts +1 -29
  12. package/dist/cjs/src/types.js +2 -1
  13. package/dist/cjs/src/utils/BlockInstanceRunner.js +30 -30
  14. package/dist/cjs/src/utils/InternalConfigProvider.d.ts +38 -0
  15. package/dist/cjs/src/utils/InternalConfigProvider.js +146 -0
  16. package/dist/cjs/src/utils/utils.d.ts +25 -3
  17. package/dist/cjs/src/utils/utils.js +46 -7
  18. package/dist/esm/index.js +4 -1
  19. package/dist/esm/src/config/routes.js +2 -2
  20. package/dist/esm/src/containerManager.js +5 -3
  21. package/dist/esm/src/instanceManager.d.ts +4 -2
  22. package/dist/esm/src/instanceManager.js +60 -28
  23. package/dist/esm/src/operatorManager.d.ts +4 -2
  24. package/dist/esm/src/operatorManager.js +32 -23
  25. package/dist/esm/src/serviceManager.d.ts +0 -1
  26. package/dist/esm/src/serviceManager.js +2 -8
  27. package/dist/esm/src/types.d.ts +1 -29
  28. package/dist/esm/src/types.js +2 -1
  29. package/dist/esm/src/utils/BlockInstanceRunner.js +30 -30
  30. package/dist/esm/src/utils/InternalConfigProvider.d.ts +38 -0
  31. package/dist/esm/src/utils/InternalConfigProvider.js +146 -0
  32. package/dist/esm/src/utils/utils.d.ts +25 -3
  33. package/dist/esm/src/utils/utils.js +46 -7
  34. package/index.ts +5 -2
  35. package/package.json +6 -5
  36. package/src/config/routes.ts +4 -2
  37. package/src/containerManager.ts +6 -4
  38. package/src/instanceManager.ts +74 -38
  39. package/src/operatorManager.ts +46 -37
  40. package/src/serviceManager.ts +3 -11
  41. package/src/types.ts +2 -31
  42. package/src/utils/BlockInstanceRunner.ts +48 -38
  43. package/src/utils/InternalConfigProvider.ts +214 -0
  44. package/src/utils/utils.ts +51 -8
@@ -5,7 +5,7 @@
5
5
 
6
6
  import FSExtra from 'fs-extra';
7
7
  import ClusterConfig, { DefinitionInfo } from '@kapeta/local-cluster-config';
8
- import { getBindHost, getBlockInstanceContainerName, readYML, toPortInfo } from './utils';
8
+ import { getDockerHostIp, getBlockInstanceContainerName, getOperatorInstancePorts, 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 {
@@ -18,13 +18,23 @@ import {
18
18
  } from '../containerManager';
19
19
  import { LogData } from './LogData';
20
20
  import { clusterService } from '../clusterService';
21
- import { AnyMap, BlockProcessParams, InstanceType, KIND_BLOCK_TYPE_OPERATOR, ProcessInfo, StringMap } from '../types';
21
+ import {
22
+ AnyMap,
23
+ BlockProcessParams,
24
+ DOCKER_HOST_INTERNAL,
25
+ InstanceType,
26
+ KIND_BLOCK_TYPE_OPERATOR,
27
+ ProcessInfo,
28
+ StringMap,
29
+ } from '../types';
22
30
  import { definitionsManager } from '../definitionsManager';
23
31
  import Docker from 'dockerode';
24
32
  import OS from 'node:os';
25
33
  import Path from 'node:path';
26
34
  import { taskManager } from '../taskManager';
27
35
  import { LocalDevContainer, LocalInstance } from '@kapeta/schemas';
36
+ import { createInternalConfigProvider } from './InternalConfigProvider';
37
+ import { resolveKapetaVariables, writeConfigTemplates } from '@kapeta/config-mapper';
28
38
 
29
39
  const KAPETA_SYSTEM_ID = 'KAPETA_SYSTEM_ID';
30
40
  const KAPETA_BLOCK_REF = 'KAPETA_BLOCK_REF';
@@ -36,7 +46,7 @@ const KAPETA_INSTANCE_ID = 'KAPETA_INSTANCE_ID';
36
46
  */
37
47
  const DOCKER_ENV_VARS = [
38
48
  `KAPETA_LOCAL_SERVER=0.0.0.0`,
39
- `KAPETA_LOCAL_CLUSTER_HOST=host.docker.internal`,
49
+ `KAPETA_LOCAL_CLUSTER_HOST=${DOCKER_HOST_INTERNAL}`,
40
50
  `KAPETA_ENVIRONMENT_TYPE=docker`,
41
51
  ];
42
52
 
@@ -105,20 +115,6 @@ export class BlockInstanceRunner {
105
115
  }
106
116
 
107
117
  private async _execute(blockInstance: BlockProcessParams): Promise<ProcessInfo> {
108
- const env: StringMap = {};
109
-
110
- if (this._systemId) {
111
- env[KAPETA_SYSTEM_ID] = this._systemId;
112
- }
113
-
114
- if (blockInstance.ref) {
115
- env[KAPETA_BLOCK_REF] = blockInstance.ref;
116
- }
117
-
118
- if (blockInstance.id) {
119
- env[KAPETA_INSTANCE_ID] = blockInstance.id;
120
- }
121
-
122
118
  const blockUri = parseKapetaUri(blockInstance.ref);
123
119
 
124
120
  if (!blockUri.version) {
@@ -139,18 +135,32 @@ export class BlockInstanceRunner {
139
135
  throw new Error(`Kind not found: ${kindUri.id}`);
140
136
  }
141
137
 
138
+ const baseDir = ClusterConfig.getRepositoryAssetPath(blockUri.handle, blockUri.name, blockUri.version);
139
+ const realBaseDir = await FSExtra.realpath(baseDir);
140
+ const internalConfigProvider = await createInternalConfigProvider(
141
+ this._systemId,
142
+ blockInstance.id,
143
+ assetVersion
144
+ );
145
+
146
+ // Resolve the environment variables
147
+ const envVars = await resolveKapetaVariables(realBaseDir, internalConfigProvider);
148
+
149
+ // Write out the config templates if they exist
150
+ await writeConfigTemplates(envVars, realBaseDir);
151
+
142
152
  let processInfo: ProcessInfo;
143
153
 
144
154
  if (providerVersion.definition.kind === KIND_BLOCK_TYPE_OPERATOR) {
145
- processInfo = await this._startOperatorProcess(blockInstance, blockUri, providerVersion, env);
155
+ processInfo = await this._startOperatorProcess(blockInstance, blockUri, providerVersion, envVars);
146
156
  } else {
147
157
  //We need a port type to know how to connect to the block consistently
148
158
  const portTypes = getServiceProviderPorts(assetVersion, providerVersion);
149
159
 
150
160
  if (blockUri.version === 'local') {
151
- processInfo = await this._startLocalProcess(blockInstance, blockUri, env, assetVersion);
161
+ processInfo = await this._startLocalProcess(blockInstance, blockUri, envVars, assetVersion);
152
162
  } else {
153
- processInfo = await this._startDockerProcess(blockInstance, blockUri, env, assetVersion);
163
+ processInfo = await this._startDockerProcess(blockInstance, blockUri, envVars, assetVersion);
154
164
  }
155
165
 
156
166
  if (portTypes.length > 0) {
@@ -250,9 +260,13 @@ export class BlockInstanceRunner {
250
260
  HealthCheck = containerManager.toDockerHealth({ cmd: localContainer.healthcheck });
251
261
  }
252
262
 
253
- const Mounts = containerManager.toDockerMounts({
254
- [workingDir]: toLocalBindVolume(realLocalPath),
255
- });
263
+ const Mounts = isDockerImage
264
+ ? // For docker images we mount the local directory to the working directory
265
+ containerManager.toDockerMounts({
266
+ [workingDir]: toLocalBindVolume(realLocalPath),
267
+ })
268
+ : // For dockerfiles we don't mount anything
269
+ [];
256
270
 
257
271
  const systemUri = parseKapetaUri(this._systemId);
258
272
 
@@ -415,34 +429,30 @@ export class BlockInstanceRunner {
415
429
  `container:start:${containerName}`,
416
430
  async () => {
417
431
  const logs = new LogData();
418
-
419
- const bindHost = getBindHost();
432
+ const hostIp = getDockerHostIp();
420
433
 
421
434
  const ExposedPorts: AnyMap = {};
422
435
  const addonEnv: StringMap = {};
423
436
  const PortBindings: AnyMap = {};
424
437
  let HealthCheck = undefined;
425
438
  let Mounts: DockerMounts[] = [];
426
- const localPorts = local.ports ?? {};
439
+ const instancePorts = await getOperatorInstancePorts(this._systemId, operatorId, local);
427
440
  const labels: { [key: string]: string } = {};
428
- const promises = Object.entries(localPorts).map(async ([portType, value]) => {
429
- const portInfo = toPortInfo(value);
430
- const dockerPort = `${portInfo.port}/${portInfo.type}`;
441
+ instancePorts.forEach((portInfo) => {
442
+ const dockerPort = `${portInfo.port}/${portInfo.protocol}`;
431
443
  ExposedPorts[dockerPort] = {};
432
- addonEnv[`KAPETA_LOCAL_SERVER_PORT_${portType.toUpperCase()}`] = `${portInfo.port}`;
433
- const publicPort = await serviceManager.ensureServicePort(this._systemId, operatorId, portType);
444
+ addonEnv[`KAPETA_LOCAL_SERVER_PORT_${portInfo.portType.toUpperCase()}`] = `${portInfo.port}`;
445
+
434
446
  PortBindings[dockerPort] = [
435
447
  {
436
- HostIp: bindHost,
437
- HostPort: `${publicPort}`,
448
+ HostIp: hostIp,
449
+ HostPort: `${portInfo.hostPort}`,
438
450
  },
439
451
  ];
440
452
 
441
- labels[CONTAINER_LABEL_PORT_PREFIX + publicPort] = portType;
453
+ labels[CONTAINER_LABEL_PORT_PREFIX + portInfo.hostPort] = portInfo.portType;
442
454
  });
443
455
 
444
- await Promise.all(promises);
445
-
446
456
  if (local.env) {
447
457
  Object.entries(local.env).forEach(([key, value]) => {
448
458
  addonEnv[key] = value as string;
@@ -525,7 +535,7 @@ export class BlockInstanceRunner {
525
535
  assetVersion: DefinitionInfo,
526
536
  providerVersion: DefinitionInfo
527
537
  ) {
528
- const bindHost = getBindHost();
538
+ const hostIp = getDockerHostIp();
529
539
  const ExposedPorts: AnyMap = {};
530
540
  const addonEnv: StringMap = {};
531
541
  const PortBindings: AnyMap = {};
@@ -541,7 +551,7 @@ export class BlockInstanceRunner {
541
551
 
542
552
  PortBindings[dockerPort] = [
543
553
  {
544
- HostIp: bindHost,
554
+ HostIp: hostIp,
545
555
  HostPort: `${publicPort}`,
546
556
  },
547
557
  ];
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Copyright 2023 Kapeta Inc.
3
+ * SPDX-License-Identifier: BUSL-1.1
4
+ */
5
+ import {
6
+ BlockInstanceDetails,
7
+ ConfigProvider,
8
+ DefaultCredentials,
9
+ DefaultResourceOptions,
10
+ InstanceOperator,
11
+ ResourceInfo,
12
+ } from '@kapeta/sdk-config';
13
+ import { Definition, DefinitionInfo } from '@kapeta/local-cluster-config';
14
+ import { normalizeKapetaUri } from '@kapeta/nodejs-utils';
15
+ import { BlockDefinition, Plan } from '@kapeta/schemas';
16
+ import { configManager } from '../configManager';
17
+ import { AnyMap, EnvironmentType } from '../types';
18
+ import _ from 'lodash';
19
+ import { serviceManager } from '../serviceManager';
20
+ import { operatorManager } from '../operatorManager';
21
+ import { instanceManager } from '../instanceManager';
22
+ import { definitionsManager } from '../definitionsManager';
23
+ import { getBindAddressForEnvironment } from './utils';
24
+
25
+ /**
26
+ * A configuration provider that does the same as the LocalConfigProvider
27
+ * but without calling the API of the local cluster service (since it's running in the same process)
28
+ */
29
+ export class InternalConfigProvider implements ConfigProvider {
30
+ private readonly info: DefinitionInfo;
31
+ private readonly systemId: string;
32
+ private readonly instanceId: string;
33
+ private readonly config: AnyMap;
34
+ private readonly environment: EnvironmentType;
35
+
36
+ constructor(
37
+ systemId: string,
38
+ instanceId: string,
39
+ info: DefinitionInfo,
40
+ config: AnyMap,
41
+ environment: EnvironmentType = 'docker'
42
+ ) {
43
+ this.info = info;
44
+ this.systemId = normalizeKapetaUri(systemId);
45
+ this.instanceId = instanceId;
46
+ this.config = config;
47
+ this.environment = environment;
48
+ }
49
+
50
+ getBlockDefinition() {
51
+ return this.info.definition;
52
+ }
53
+ getBlockReference(): string {
54
+ return normalizeKapetaUri(this.info.definition.metadata.name + ':' + this.info.version);
55
+ }
56
+ getSystemId(): string {
57
+ return this.systemId;
58
+ }
59
+ getInstanceId(): string {
60
+ return this.instanceId;
61
+ }
62
+ getServerPort(portType?: string | undefined): Promise<string> {
63
+ return serviceManager.ensureServicePort(this.systemId, this.instanceId, portType);
64
+ }
65
+ async getServiceAddress(serviceName: string, portType: string): Promise<string | null> {
66
+ return serviceManager.getConsumerAddress(
67
+ this.systemId,
68
+ this.instanceId,
69
+ serviceName,
70
+ portType,
71
+ this.environment
72
+ );
73
+ }
74
+ getResourceInfo<Options = DefaultResourceOptions, Credentials = DefaultCredentials>(
75
+ resourceType: string,
76
+ portType: string,
77
+ resourceName: string
78
+ ): Promise<ResourceInfo<Options, Credentials> | null> {
79
+ return operatorManager.getConsumerResourceInfo(
80
+ this.systemId,
81
+ this.instanceId,
82
+ resourceType,
83
+ portType,
84
+ resourceName,
85
+ this.environment,
86
+ false
87
+ );
88
+ }
89
+ async getInstanceHost(instanceId: string): Promise<string | null> {
90
+ const instance = instanceManager.getInstance(this.systemId, instanceId);
91
+ return instance?.address ?? null;
92
+ }
93
+ async getServerHost(): Promise<string> {
94
+ return getBindAddressForEnvironment(this.environment);
95
+ }
96
+ getProviderId(): string {
97
+ return 'internal';
98
+ }
99
+ getOrDefault<T = any>(path: string, defaultValue: T): T {
100
+ return this.get(path) ?? defaultValue;
101
+ }
102
+ get<T = any>(path: string): T | undefined {
103
+ return _.get(this.config, path);
104
+ }
105
+
106
+ getInstanceOperator<Options = any, Credentials extends DefaultCredentials = DefaultCredentials>(
107
+ instanceId: string
108
+ ): Promise<InstanceOperator<Options, Credentials> | null> {
109
+ return instanceManager.getInstanceOperator(this.systemId, instanceId, this.environment, false);
110
+ }
111
+
112
+ public async getInstanceForConsumer<BlockType = BlockDefinition>(
113
+ resourceName: string
114
+ ): Promise<BlockInstanceDetails<BlockType> | null> {
115
+ const plan = await this.getPlan();
116
+ if (!plan) {
117
+ throw new Error('Could not find plan');
118
+ }
119
+ const instanceId = this.getInstanceId();
120
+ const connection = plan.spec.connections.find(
121
+ (connection) =>
122
+ connection.consumer.blockId === instanceId && connection.consumer.resourceName === resourceName
123
+ );
124
+
125
+ if (!connection) {
126
+ throw new Error(`Could not find connection for consumer ${resourceName}`);
127
+ }
128
+
129
+ const instance = plan.spec.blocks.find((b) => b.id === connection.provider.blockId);
130
+
131
+ if (!instance) {
132
+ throw new Error(`Could not find instance ${connection.provider.blockId} in plan`);
133
+ }
134
+
135
+ const block = await this.getBlock(instance.block.ref);
136
+
137
+ if (!block) {
138
+ throw new Error(`Could not find block ${instance.block.ref} in plan`);
139
+ }
140
+
141
+ return {
142
+ instanceId: connection.provider.blockId,
143
+ connections: [connection],
144
+ block: block as BlockType,
145
+ };
146
+ }
147
+
148
+ public async getInstancesForProvider<BlockType = BlockDefinition>(
149
+ resourceName: string
150
+ ): Promise<BlockInstanceDetails<BlockType>[]> {
151
+ const plan = await this.getPlan();
152
+ if (!plan) {
153
+ throw new Error('Could not find plan');
154
+ }
155
+ const instanceId = this.getInstanceId();
156
+
157
+ const blockDetails: { [key: string]: BlockInstanceDetails<BlockType> } = {};
158
+ const connections = plan.spec.connections.filter(
159
+ (connection) =>
160
+ connection.provider.blockId === instanceId && connection.provider.resourceName === resourceName
161
+ );
162
+
163
+ for (const connection of connections) {
164
+ const blockInstanceId = connection.consumer.blockId;
165
+ if (blockDetails[blockInstanceId]) {
166
+ blockDetails[blockInstanceId].connections.push(connection);
167
+ continue;
168
+ }
169
+
170
+ const instance = plan.spec.blocks.find((b) => b.id === blockInstanceId);
171
+ if (!instance) {
172
+ throw new Error(`Could not find instance ${blockInstanceId} in plan`);
173
+ }
174
+
175
+ const block = await this.getBlock(instance.block.ref);
176
+ if (!block) {
177
+ throw new Error(`Could not find block ${instance.block.ref} in plan`);
178
+ }
179
+
180
+ blockDetails[blockInstanceId] = {
181
+ instanceId: blockInstanceId,
182
+ connections: [connection],
183
+ block: block as BlockType,
184
+ };
185
+ }
186
+
187
+ return Object.values(blockDetails);
188
+ }
189
+
190
+ async getBlock(ref: any): Promise<Definition> {
191
+ const definition = await definitionsManager.getDefinition(ref);
192
+ if (!definition) {
193
+ throw new Error(`Could not find definition for ${ref}`);
194
+ }
195
+ return definition.definition;
196
+ }
197
+
198
+ async getPlan(): Promise<Plan> {
199
+ const definition = await definitionsManager.getDefinition(this.systemId);
200
+ if (!definition) {
201
+ throw new Error(`Could not find plan ${this.systemId}`);
202
+ }
203
+ return definition.definition as Plan;
204
+ }
205
+ }
206
+
207
+ export async function createInternalConfigProvider(
208
+ systemId: string,
209
+ instanceId: string,
210
+ info: DefinitionInfo
211
+ ): Promise<InternalConfigProvider> {
212
+ const config = await configManager.getConfigForBlockInstance(systemId, instanceId);
213
+ return new InternalConfigProvider(systemId, instanceId, info, config);
214
+ }
@@ -6,13 +6,15 @@
6
6
  import FS from 'node:fs';
7
7
  import YAML from 'yaml';
8
8
  import md5 from 'md5';
9
- import { EntityList, LocalInstancePort, LocalInstancePortType } from '@kapeta/schemas';
9
+ import { EntityList, LocalInstance, LocalInstancePort, LocalInstancePortType } from '@kapeta/schemas';
10
10
  import _ from 'lodash';
11
- import { AnyMap, KIND_BLOCK_TYPE_OPERATOR } from '../types';
11
+ import { AnyMap, DOCKER_HOST_INTERNAL, EnvironmentType, KIND_BLOCK_TYPE_OPERATOR } from '../types';
12
12
  import ClusterConfiguration from '@kapeta/local-cluster-config';
13
13
  import { definitionsManager } from '../definitionsManager';
14
14
  import { normalizeKapetaUri, parseKapetaUri } from '@kapeta/nodejs-utils';
15
15
  import { assetManager } from '../assetManager';
16
+ import { serviceManager } from '../serviceManager';
17
+ import { clusterService } from '../clusterService';
16
18
 
17
19
  export async function getBlockInstanceContainerName(systemId: string, instanceId: string, blockType?: string) {
18
20
  if (!blockType) {
@@ -52,6 +54,53 @@ export function toPortInfo(port: LocalInstancePort) {
52
54
  return port;
53
55
  }
54
56
 
57
+ export async function getOperatorInstancePorts(systemId: string, operatorId: string, local: LocalInstance) {
58
+ const localPorts = local.ports ?? {};
59
+
60
+ const promises = Object.entries(localPorts).map(async ([portType, value]) => {
61
+ const portInfo = toPortInfo(value);
62
+ const hostPort = await serviceManager.ensureServicePort(systemId, operatorId, portType);
63
+ return {
64
+ portType,
65
+ port: portInfo.port,
66
+ hostPort,
67
+ protocol: portInfo.type,
68
+ };
69
+ });
70
+ return await Promise.all(promises);
71
+ }
72
+
73
+ /**
74
+ * Gets the hostname where all services are available - including the cluster service.
75
+ *
76
+ * For docker this is the internal docker host - otherwise it's the local machine
77
+ * Assumed to be the same address as the cluster service outside docker.
78
+ */
79
+ export function getRemoteHostForEnvironment(environment: EnvironmentType | undefined): string {
80
+ return environment === 'docker' ? DOCKER_HOST_INTERNAL : clusterService.getClusterServiceHost();
81
+ }
82
+
83
+ /**
84
+ * Get the bind address for the given environment.
85
+ *
86
+ * Outside of docker we bind to 127.0.0.1 - inside we bind to everything (0.0.0.0)
87
+ */
88
+ export function getBindAddressForEnvironment(
89
+ environment: EnvironmentType | undefined,
90
+ preferredHost = '127.0.0.1'
91
+ ): string {
92
+ return environment === 'docker' ? '0.0.0.0' : preferredHost;
93
+ }
94
+
95
+ /**
96
+ * Get the docker host IP address for port binding.
97
+ */
98
+ export function getDockerHostIp(preferredHost = '127.0.0.1') {
99
+ // On Linux we need to bind to 0.0.0.0 to be able to connect to it from docker containers.
100
+ // TODO: This might pose a security risk - so we should authenticate all requests using a shared secret/nonce that we pass around.
101
+ return isLinux() ? '0.0.0.0' : preferredHost;
102
+ }
103
+
55
104
  export function getRemoteUrl(id: string, defautValue: string) {
56
105
  const remoteConfig = ClusterConfiguration.getClusterConfig().remote;
57
106
  return remoteConfig?.[id] ?? defautValue;
@@ -79,12 +128,6 @@ export function isLinux() {
79
128
  return !isWindows() && !isMac();
80
129
  }
81
130
 
82
- export function getBindHost(preferredHost = '127.0.0.1') {
83
- // On Linux we need to bind to 0.0.0.0 to be able to connect to it from docker containers.
84
- // TODO: This might pose a security risk - so we should authenticate all requests using a shared secret/nonce that we pass around.
85
- return isLinux() ? '0.0.0.0' : preferredHost;
86
- }
87
-
88
131
  export function getResolvedConfiguration(entities?: EntityList, config?: AnyMap, globalConfiguration?: AnyMap): AnyMap {
89
132
  if (!entities || !globalConfiguration) {
90
133
  return config || {};