@kapeta/local-cluster-service 0.9.1 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,10 +3,11 @@ import md5 from 'md5';
3
3
  import { parseKapetaUri } from '@kapeta/nodejs-utils';
4
4
  import { serviceManager } from './serviceManager';
5
5
  import { storageService } from './storageService';
6
- import { containerManager } from './containerManager';
6
+ import { CONTAINER_LABEL_PORT_PREFIX, ContainerInfo, containerManager } from './containerManager';
7
7
  import FSExtra from 'fs-extra';
8
8
  import { definitionsManager } from './definitionsManager';
9
- import { normalizeKapetaUri } from './utils/utils';
9
+ import { getBindHost, normalizeKapetaUri } from './utils/utils';
10
+ import _ from 'lodash';
10
11
  const KIND_OPERATOR = 'core/resource-type-operator';
11
12
  class Operator {
12
13
  _data;
@@ -132,32 +133,46 @@ class OperatorManager {
132
133
  }
133
134
  const mounts = containerManager.createMounts(resourceType, operatorData.mounts);
134
135
  const containerName = containerBaseName + '-' + md5(nameParts.join('_'));
135
- let container = await containerManager.get(containerName);
136
- const isRunning = container ? await container.isRunning() : false;
137
- if (container && !isRunning) {
138
- await container.start();
139
- }
140
- if (!container) {
141
- container = await containerManager.run(operatorData.image, containerName, {
142
- mounts,
143
- ports,
144
- health: operatorData.health,
145
- env: operatorData.env,
146
- cmd: operatorData.cmd,
147
- });
148
- }
149
- try {
150
- if (operatorData.health) {
151
- await containerManager.waitForHealthy(container.native);
152
- }
153
- else {
154
- await containerManager.waitForReady(container.native);
155
- }
156
- }
157
- catch (e) {
158
- console.error(e.message);
136
+ const PortBindings = {};
137
+ const Env = [];
138
+ const Labels = {
139
+ kapeta: 'true',
140
+ };
141
+ const bindHost = getBindHost();
142
+ const ExposedPorts = {};
143
+ _.forEach(ports, (portInfo, containerPort) => {
144
+ ExposedPorts['' + containerPort] = {};
145
+ PortBindings['' + containerPort] = [
146
+ {
147
+ HostPort: '' + portInfo.hostPort,
148
+ HostIp: bindHost,
149
+ },
150
+ ];
151
+ Labels[CONTAINER_LABEL_PORT_PREFIX + portInfo.hostPort] = portInfo.type;
152
+ });
153
+ const Mounts = containerManager.toDockerMounts(mounts);
154
+ _.forEach(operatorData.env, (value, name) => {
155
+ Env.push(name + '=' + value);
156
+ });
157
+ let HealthCheck = undefined;
158
+ if (operatorData.health) {
159
+ HealthCheck = containerManager.toDockerHealth(operatorData.health);
159
160
  }
160
- return container;
161
+ const container = await containerManager.ensureContainer({
162
+ name: containerName,
163
+ Image: operatorData.image,
164
+ Hostname: containerName + '.kapeta',
165
+ Labels,
166
+ Cmd: operatorData.cmd,
167
+ ExposedPorts,
168
+ Env,
169
+ HealthCheck,
170
+ HostConfig: {
171
+ PortBindings,
172
+ Mounts,
173
+ },
174
+ });
175
+ return new ContainerInfo(container);
161
176
  }
162
177
  }
163
178
  export const operatorManager = new OperatorManager();
@@ -14,7 +14,6 @@ export declare class BlockInstanceRunner {
14
14
  * Starts local process
15
15
  */
16
16
  private _startLocalProcess;
17
- private _handleContainer;
18
17
  private _startDockerProcess;
19
18
  /**
20
19
  *
@@ -26,4 +25,7 @@ export declare class BlockInstanceRunner {
26
25
  * @private
27
26
  */
28
27
  _startOperatorProcess(blockInstance: BlockProcessParams, blockUri: KapetaURI, providerDefinition: DefinitionInfo, env: StringMap): Promise<ProcessInfo>;
28
+ private getDockerPortBindings;
29
+ private ensureContainer;
30
+ private _handleContainer;
29
31
  }
@@ -94,7 +94,7 @@ export class BlockInstanceRunner {
94
94
  processInfo = await this._startLocalProcess(blockInstance, blockUri, env, assetVersion);
95
95
  }
96
96
  else {
97
- processInfo = await this._startDockerProcess(blockInstance, blockUri, env);
97
+ processInfo = await this._startDockerProcess(blockInstance, blockUri, env, assetVersion);
98
98
  }
99
99
  if (portTypes.length > 0) {
100
100
  processInfo.portType = portTypes[0];
@@ -128,57 +128,16 @@ export class BlockInstanceRunner {
128
128
  throw new Error(`Missing docker image information: ${JSON.stringify(localContainer)}`);
129
129
  }
130
130
  const containerName = getBlockInstanceContainerName(blockInstance.id);
131
- const logs = new LogData();
132
- logs.addLog(`Starting block ${blockInstance.ref}`);
133
- let containerInfo = await containerManager.getContainerByName(containerName);
134
- let container = containerInfo?.native;
135
- console.log('Starting dev container', containerName);
136
- if (containerInfo) {
137
- console.log(`Dev container already exists. Deleting...`);
138
- try {
139
- await containerInfo.remove({
140
- force: true,
141
- });
142
- }
143
- catch (e) {
144
- throw new Error('Failed to delete existing container: ' + e.message);
145
- }
146
- container = undefined;
147
- containerInfo = undefined;
148
- }
149
- logs.addLog(`Creating new container for block: ${containerName}`);
150
- console.log('Creating new dev container', containerName, dockerImage);
151
- await containerManager.pull(dockerImage);
152
131
  const startCmd = localContainer.handlers?.onCreate ? localContainer.handlers.onCreate : '';
153
132
  const dockerOpts = localContainer.options ?? {};
154
133
  const homeDir = localContainer.userHome ? localContainer.userHome : '/root';
155
134
  const workingDir = localContainer.workingDir ? localContainer.workingDir : '/workspace';
156
- const bindHost = getBindHost();
157
- const ExposedPorts = {};
158
- const addonEnv = {};
159
- const PortBindings = {};
160
- const portTypes = getProviderPorts(assetVersion);
161
- let port = 80;
162
- const promises = portTypes.map(async (portType) => {
163
- const publicPort = await serviceManager.ensureServicePort(this._systemId, blockInstance.id, portType);
164
- const thisPort = port++; //TODO: Not sure how we should handle multiple ports or non-HTTP ports
165
- const dockerPort = `${thisPort}/tcp`;
166
- ExposedPorts[dockerPort] = {};
167
- addonEnv[`KAPETA_LOCAL_SERVER_PORT_${portType.toUpperCase()}`] = '' + thisPort;
168
- PortBindings[dockerPort] = [
169
- {
170
- HostIp: bindHost,
171
- HostPort: `${publicPort}`,
172
- },
173
- ];
174
- });
175
- await Promise.all(promises);
135
+ const { PortBindings, ExposedPorts, addonEnv } = await this.getDockerPortBindings(blockInstance, assetVersion);
176
136
  let HealthCheck = undefined;
177
137
  if (localContainer.healthcheck) {
178
138
  HealthCheck = containerManager.toDockerHealth({ cmd: localContainer.healthcheck });
179
139
  }
180
- console.log('Starting dev container', containerName, dockerImage);
181
- container = await containerManager.startContainer({
140
+ return this.ensureContainer({
182
141
  Image: dockerImage,
183
142
  name: containerName,
184
143
  WorkingDir: workingDir,
@@ -205,8 +164,163 @@ export class BlockInstanceRunner {
205
164
  },
206
165
  ...dockerOpts,
207
166
  });
167
+ }
168
+ async _startDockerProcess(blockInstance, blockInfo, env, assetVersion) {
169
+ const { versionFile } = ClusterConfig.getRepositoryAssetInfoPath(blockInfo.handle, blockInfo.name, blockInfo.version);
170
+ const versionYml = versionFile;
171
+ if (!FS.existsSync(versionYml)) {
172
+ throw new Error(`Did not find version info at the expected path: ${versionYml}`);
173
+ }
174
+ const versionInfo = readYML(versionYml);
175
+ if (versionInfo?.artifact?.type !== 'docker') {
176
+ throw new Error(`Unsupported artifact type: ${versionInfo?.artifact?.type}`);
177
+ }
178
+ const dockerImage = versionInfo?.artifact?.details?.primary;
179
+ if (!dockerImage) {
180
+ throw new Error(`Missing docker image information: ${JSON.stringify(versionInfo?.artifact?.details)}`);
181
+ }
182
+ const { PortBindings, ExposedPorts, addonEnv } = await this.getDockerPortBindings(blockInstance, assetVersion);
183
+ const containerName = getBlockInstanceContainerName(blockInstance.id);
184
+ // For windows we need to default to root
185
+ const innerHome = process.platform === 'win32' ? '/root/.kapeta' : ClusterConfig.getKapetaBasedir();
186
+ return this.ensureContainer({
187
+ Image: dockerImage,
188
+ name: containerName,
189
+ ExposedPorts,
190
+ Labels: {
191
+ instance: blockInstance.id,
192
+ },
193
+ Env: [
194
+ ...DOCKER_ENV_VARS,
195
+ `KAPETA_LOCAL_CLUSTER_PORT=${clusterService.getClusterServicePort()}`,
196
+ ...Object.entries({
197
+ ...env,
198
+ ...addonEnv
199
+ }).map(([key, value]) => `${key}=${value}`),
200
+ ],
201
+ HostConfig: {
202
+ Binds: [`${toLocalBindVolume(ClusterConfig.getKapetaBasedir())}:${innerHome}`],
203
+ PortBindings,
204
+ },
205
+ });
206
+ }
207
+ /**
208
+ *
209
+ * @param blockInstance
210
+ * @param blockUri
211
+ * @param providerDefinition
212
+ * @param {{[key:string]:string}} env
213
+ * @return {Promise<ProcessDetails>}
214
+ * @private
215
+ */
216
+ async _startOperatorProcess(blockInstance, blockUri, providerDefinition, env) {
217
+ const { assetFile } = ClusterConfig.getRepositoryAssetInfoPath(blockUri.handle, blockUri.name, blockUri.version);
218
+ const kapetaYmlPath = assetFile;
219
+ if (!FS.existsSync(kapetaYmlPath)) {
220
+ throw new Error(`Did not find kapeta.yml at the expected path: ${kapetaYmlPath}`);
221
+ }
222
+ const spec = providerDefinition.definition.spec;
223
+ const providerRef = `${providerDefinition.definition.metadata.name}:${providerDefinition.version}`;
224
+ if (!spec?.local?.image) {
225
+ throw new Error(`Provider did not have local image: ${providerRef}`);
226
+ }
227
+ const dockerImage = spec?.local?.image;
228
+ const containerName = getBlockInstanceContainerName(blockInstance.id);
229
+ const logs = new LogData();
230
+ const bindHost = getBindHost();
231
+ const ExposedPorts = {};
232
+ const addonEnv = {};
233
+ const PortBindings = {};
234
+ let HealthCheck = undefined;
235
+ let Mounts = [];
236
+ const promises = Object.entries(spec.local.ports).map(async ([portType, value]) => {
237
+ const dockerPort = `${value.port}/${value.type}`;
238
+ ExposedPorts[dockerPort] = {};
239
+ addonEnv[`KAPETA_LOCAL_SERVER_PORT_${portType.toUpperCase()}`] = value.port;
240
+ const publicPort = await serviceManager.ensureServicePort(this._systemId, blockInstance.id, portType);
241
+ PortBindings[dockerPort] = [
242
+ {
243
+ HostIp: bindHost,
244
+ HostPort: `${publicPort}`,
245
+ },
246
+ ];
247
+ });
248
+ await Promise.all(promises);
249
+ if (spec.local?.env) {
250
+ Object.entries(spec.local.env).forEach(([key, value]) => {
251
+ addonEnv[key] = value;
252
+ });
253
+ }
254
+ if (spec.local?.mounts) {
255
+ const mounts = containerManager.createMounts(blockUri.id, spec.local.mounts);
256
+ Mounts = containerManager.toDockerMounts(mounts);
257
+ }
258
+ if (spec.local?.health) {
259
+ HealthCheck = containerManager.toDockerHealth(spec.local?.health);
260
+ }
261
+ // For windows we need to default to root
262
+ const innerHome = process.platform === 'win32' ? '/root/.kapeta' : ClusterConfig.getKapetaBasedir();
263
+ logs.addLog(`Creating new container for block: ${containerName}`);
264
+ const out = await this.ensureContainer({
265
+ Image: dockerImage,
266
+ name: containerName,
267
+ ExposedPorts,
268
+ HealthCheck,
269
+ HostConfig: {
270
+ Binds: [
271
+ `${toLocalBindVolume(kapetaYmlPath)}:/kapeta.yml:ro`,
272
+ `${toLocalBindVolume(ClusterConfig.getKapetaBasedir())}:${innerHome}`,
273
+ ],
274
+ PortBindings,
275
+ Mounts,
276
+ },
277
+ Labels: {
278
+ instance: blockInstance.id,
279
+ },
280
+ Env: [
281
+ `KAPETA_INSTANCE_NAME=${blockInstance.ref}`,
282
+ `KAPETA_LOCAL_CLUSTER_PORT=${clusterService.getClusterServicePort()}`,
283
+ ...DOCKER_ENV_VARS,
284
+ ...Object.entries({
285
+ ...env,
286
+ ...addonEnv,
287
+ }).map(([key, value]) => `${key}=${value}`),
288
+ ],
289
+ });
290
+ const portTypes = spec.local.ports ? Object.keys(spec.local.ports) : [];
291
+ if (portTypes.length > 0) {
292
+ out.portType = portTypes[0];
293
+ }
294
+ return out;
295
+ }
296
+ async getDockerPortBindings(blockInstance, assetVersion) {
297
+ const bindHost = getBindHost();
298
+ const ExposedPorts = {};
299
+ const addonEnv = {};
300
+ const PortBindings = {};
301
+ const portTypes = getProviderPorts(assetVersion);
302
+ let port = 80;
303
+ const promises = portTypes.map(async (portType) => {
304
+ const publicPort = await serviceManager.ensureServicePort(this._systemId, blockInstance.id, portType);
305
+ const thisPort = port++; //TODO: Not sure how we should handle multiple ports or non-HTTP ports
306
+ const dockerPort = `${thisPort}/tcp`;
307
+ ExposedPorts[dockerPort] = {};
308
+ addonEnv[`KAPETA_LOCAL_SERVER_PORT_${portType.toUpperCase()}`] = '' + thisPort;
309
+ PortBindings[dockerPort] = [
310
+ {
311
+ HostIp: bindHost,
312
+ HostPort: `${publicPort}`,
313
+ },
314
+ ];
315
+ });
316
+ await Promise.all(promises);
317
+ return { PortBindings, ExposedPorts, addonEnv };
318
+ }
319
+ async ensureContainer(opts) {
320
+ const logs = new LogData();
321
+ const container = await containerManager.ensureContainer(opts);
208
322
  try {
209
- if (HealthCheck) {
323
+ if (opts.HealthCheck) {
210
324
  await containerManager.waitForHealthy(container);
211
325
  }
212
326
  else {
@@ -268,201 +382,4 @@ export class BlockInstanceRunner {
268
382
  },
269
383
  };
270
384
  }
271
- async _startDockerProcess(blockInstance, blockInfo, env) {
272
- const { versionFile } = ClusterConfig.getRepositoryAssetInfoPath(blockInfo.handle, blockInfo.name, blockInfo.version);
273
- const versionYml = versionFile;
274
- if (!FS.existsSync(versionYml)) {
275
- throw new Error(`Did not find version info at the expected path: ${versionYml}`);
276
- }
277
- const versionInfo = readYML(versionYml);
278
- if (versionInfo?.artifact?.type !== 'docker') {
279
- throw new Error(`Unsupported artifact type: ${versionInfo?.artifact?.type}`);
280
- }
281
- const dockerImage = versionInfo?.artifact?.details?.primary;
282
- if (!dockerImage) {
283
- throw new Error(`Missing docker image information: ${JSON.stringify(versionInfo?.artifact?.details)}`);
284
- }
285
- const containerName = getBlockInstanceContainerName(blockInstance.id);
286
- const logs = new LogData();
287
- const containerInfo = await containerManager.getContainerByName(containerName);
288
- let container = containerInfo?.native;
289
- // For windows we need to default to root
290
- const innerHome = process.platform === 'win32' ? '/root/.kapeta' : ClusterConfig.getKapetaBasedir();
291
- if (container) {
292
- const containerData = container.data;
293
- if (containerData.State === 'running') {
294
- logs.addLog(`Found existing running container for block: ${containerName}`);
295
- }
296
- else {
297
- logs.addLog(`Found existing container for block: ${containerName}. Starting now`);
298
- await container.start();
299
- }
300
- }
301
- else {
302
- logs.addLog(`Creating new container for block: ${containerName}`);
303
- container = await containerManager.startContainer({
304
- Image: dockerImage,
305
- name: containerName,
306
- Labels: {
307
- instance: blockInstance.id,
308
- },
309
- Env: [
310
- ...DOCKER_ENV_VARS,
311
- `KAPETA_LOCAL_CLUSTER_PORT=${clusterService.getClusterServicePort()}`,
312
- ...Object.entries(env).map(([key, value]) => `${key}=${value}`),
313
- ],
314
- HostConfig: {
315
- Binds: [`${toLocalBindVolume(ClusterConfig.getKapetaBasedir())}:${innerHome}`],
316
- },
317
- });
318
- try {
319
- await containerManager.waitForReady(container);
320
- }
321
- catch (e) {
322
- logs.addLog(e.message, 'ERROR');
323
- }
324
- }
325
- return this._handleContainer(container, logs);
326
- }
327
- /**
328
- *
329
- * @param blockInstance
330
- * @param blockUri
331
- * @param providerDefinition
332
- * @param {{[key:string]:string}} env
333
- * @return {Promise<ProcessDetails>}
334
- * @private
335
- */
336
- async _startOperatorProcess(blockInstance, blockUri, providerDefinition, env) {
337
- const { assetFile } = ClusterConfig.getRepositoryAssetInfoPath(blockUri.handle, blockUri.name, blockUri.version);
338
- const kapetaYmlPath = assetFile;
339
- if (!FS.existsSync(kapetaYmlPath)) {
340
- throw new Error(`Did not find kapeta.yml at the expected path: ${kapetaYmlPath}`);
341
- }
342
- const spec = providerDefinition.definition.spec;
343
- const providerRef = `${providerDefinition.definition.metadata.name}:${providerDefinition.version}`;
344
- if (!spec?.local?.image) {
345
- throw new Error(`Provider did not have local image: ${providerRef}`);
346
- }
347
- const dockerImage = spec?.local?.image;
348
- try {
349
- await containerManager.pull(dockerImage);
350
- }
351
- catch (e) {
352
- console.warn('Failed to pull image. Continuing...', e);
353
- }
354
- const containerName = getBlockInstanceContainerName(blockInstance.id);
355
- const logs = new LogData();
356
- const containerInfo = await containerManager.getContainerByName(containerName);
357
- let container = containerInfo?.native;
358
- if (container) {
359
- const containerData = container.data;
360
- if (containerData.State === 'running') {
361
- logs.addLog(`Found existing running container for block: ${containerName}`);
362
- }
363
- else {
364
- if (containerData.State?.ExitCode > 0) {
365
- logs.addLog(`Container exited with code: ${containerData.State.ExitCode}. Deleting...`);
366
- try {
367
- await containerManager.remove(container);
368
- }
369
- catch (e) { }
370
- container = undefined;
371
- }
372
- else {
373
- logs.addLog(`Found existing container for block: ${containerName}. Starting now`);
374
- try {
375
- await container.start();
376
- }
377
- catch (e) {
378
- console.warn('Failed to start container. Deleting...', e);
379
- try {
380
- await containerManager.remove(container);
381
- }
382
- catch (e) { }
383
- container = undefined;
384
- }
385
- }
386
- }
387
- }
388
- const bindHost = getBindHost();
389
- if (!container) {
390
- const ExposedPorts = {};
391
- const addonEnv = {};
392
- const PortBindings = {};
393
- let HealthCheck = undefined;
394
- let Mounts = [];
395
- const promises = Object.entries(spec.local.ports).map(async ([portType, value]) => {
396
- const dockerPort = `${value.port}/${value.type}`;
397
- ExposedPorts[dockerPort] = {};
398
- addonEnv[`KAPETA_LOCAL_SERVER_PORT_${portType.toUpperCase()}`] = value.port;
399
- const publicPort = await serviceManager.ensureServicePort(this._systemId, blockInstance.id, portType);
400
- PortBindings[dockerPort] = [
401
- {
402
- HostIp: bindHost,
403
- HostPort: `${publicPort}`,
404
- },
405
- ];
406
- });
407
- await Promise.all(promises);
408
- if (spec.local?.env) {
409
- Object.entries(spec.local.env).forEach(([key, value]) => {
410
- addonEnv[key] = value;
411
- });
412
- }
413
- if (spec.local?.mounts) {
414
- const mounts = containerManager.createMounts(blockUri.id, spec.local.mounts);
415
- Mounts = containerManager.toDockerMounts(mounts);
416
- }
417
- if (spec.local?.health) {
418
- HealthCheck = containerManager.toDockerHealth(spec.local?.health);
419
- }
420
- // For windows we need to default to root
421
- const innerHome = process.platform === 'win32' ? '/root/.kapeta' : ClusterConfig.getKapetaBasedir();
422
- logs.addLog(`Creating new container for block: ${containerName}`);
423
- container = await containerManager.startContainer({
424
- Image: dockerImage,
425
- name: containerName,
426
- ExposedPorts,
427
- HealthCheck,
428
- HostConfig: {
429
- Binds: [
430
- `${toLocalBindVolume(kapetaYmlPath)}:/kapeta.yml:ro`,
431
- `${toLocalBindVolume(ClusterConfig.getKapetaBasedir())}:${innerHome}`,
432
- ],
433
- PortBindings,
434
- Mounts,
435
- },
436
- Labels: {
437
- instance: blockInstance.id,
438
- },
439
- Env: [
440
- `KAPETA_INSTANCE_NAME=${blockInstance.ref}`,
441
- `KAPETA_LOCAL_CLUSTER_PORT=${clusterService.getClusterServicePort()}`,
442
- ...DOCKER_ENV_VARS,
443
- ...Object.entries({
444
- ...env,
445
- ...addonEnv,
446
- }).map(([key, value]) => `${key}=${value}`),
447
- ],
448
- });
449
- try {
450
- if (HealthCheck) {
451
- await containerManager.waitForHealthy(container);
452
- }
453
- else {
454
- await containerManager.waitForReady(container);
455
- }
456
- }
457
- catch (e) {
458
- logs.addLog(e.message, 'ERROR');
459
- }
460
- }
461
- const out = await this._handleContainer(container, logs, true);
462
- const portTypes = spec.local.ports ? Object.keys(spec.local.ports) : [];
463
- if (portTypes.length > 0) {
464
- out.portType = portTypes[0];
465
- }
466
- return out;
467
- }
468
385
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kapeta/local-cluster-service",
3
- "version": "0.9.1",
3
+ "version": "0.10.1",
4
4
  "description": "Manages configuration, ports and service discovery for locally running Kapeta systems",
5
5
  "type": "commonjs",
6
6
  "exports": {