@kapeta/local-cluster-service 0.19.6 → 0.20.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.
@@ -6,11 +6,16 @@ import { storageService } from './storageService';
6
6
  import { EVENT_INSTANCE_CREATED, EVENT_INSTANCE_EXITED, EVENT_STATUS_CHANGED, socketManager } from './socketManager';
7
7
  import { serviceManager } from './serviceManager';
8
8
  import { assetManager } from './assetManager';
9
- import { containerManager, HEALTH_CHECK_TIMEOUT } from './containerManager';
9
+ import {
10
+ containerManager,
11
+ DockerContainerHealth,
12
+ DockerContainerStatus,
13
+ HEALTH_CHECK_TIMEOUT,
14
+ } from './containerManager';
10
15
  import { configManager } from './configManager';
11
16
  import { DesiredInstanceStatus, InstanceInfo, InstanceOwner, InstanceStatus, InstanceType, LogEntry } from './types';
12
17
  import { BlockDefinitionSpec, BlockInstance, Plan } from '@kapeta/schemas';
13
- import { getBlockInstanceContainerName, normalizeKapetaUri } from './utils/utils';
18
+ import { getBlockInstanceContainerName, getResolvedConfiguration, normalizeKapetaUri } from './utils/utils';
14
19
  import { KIND_OPERATOR, operatorManager } from './operatorManager';
15
20
  import { parseKapetaUri } from '@kapeta/nodejs-utils';
16
21
  import { definitionsManager } from './definitionsManager';
@@ -336,6 +341,10 @@ export class InstanceManager {
336
341
  return;
337
342
  }
338
343
 
344
+ if (instance.status === InstanceStatus.STOPPING) {
345
+ return;
346
+ }
347
+
339
348
  if (changeDesired && instance.desiredStatus !== DesiredInstanceStatus.EXTERNAL) {
340
349
  instance.desiredStatus = DesiredInstanceStatus.STOP;
341
350
  }
@@ -488,15 +497,20 @@ export class InstanceManager {
488
497
  }
489
498
 
490
499
  const instanceConfig = await configManager.getConfigForSection(systemId, instanceId);
500
+ const resolvedConfig = getResolvedConfiguration(
501
+ blockSpec.configuration,
502
+ instanceConfig,
503
+ blockInstance.defaultConfiguration
504
+ );
491
505
  const task = taskManager.add(
492
506
  `instance:start:${systemId}:${instanceId}`,
493
507
  async () => {
494
508
  const runner = new BlockInstanceRunner(systemId);
495
509
  const startTime = Date.now();
496
510
  try {
497
- const processInfo = await runner.start(blockRef, instanceId, instanceConfig);
511
+ const processInfo = await runner.start(blockRef, instanceId, resolvedConfig);
498
512
 
499
- instance.status = InstanceStatus.READY;
513
+ instance.status = InstanceStatus.STARTING;
500
514
 
501
515
  return this.saveInternalInstance({
502
516
  ...instance,
@@ -504,10 +518,10 @@ export class InstanceManager {
504
518
  pid: processInfo.pid ?? -1,
505
519
  health: null,
506
520
  portType: processInfo.portType,
507
- status: InstanceStatus.READY,
521
+ status: InstanceStatus.STARTING,
508
522
  });
509
523
  } catch (e: any) {
510
- console.warn('Failed to start instance: ', systemId, instanceId, blockRef, e.message);
524
+ console.warn('Failed to start instance: ', systemId, instanceId, blockRef, e);
511
525
  const logs: LogEntry[] = [
512
526
  {
513
527
  source: 'stdout',
@@ -686,7 +700,12 @@ export class InstanceManager {
686
700
  try {
687
701
  await this.start(instance.systemId, instance.instanceId);
688
702
  } catch (e: any) {
689
- console.warn('Failed to start instance', instance.systemId, instance.instanceId, e);
703
+ console.warn(
704
+ 'Failed to start previously stopped instance',
705
+ instance.systemId,
706
+ instance.instanceId,
707
+ e
708
+ );
690
709
  }
691
710
  return;
692
711
  }
@@ -738,37 +757,47 @@ export class InstanceManager {
738
757
  return InstanceStatus.STOPPED;
739
758
  }
740
759
  const state = await container.status();
760
+ if (!state) {
761
+ return InstanceStatus.STOPPED;
762
+ }
741
763
 
742
- if (state.Status === 'running') {
743
- if (state.Health?.Status === 'healthy') {
744
- return InstanceStatus.READY;
745
- }
746
- if (state.Health?.Status === 'starting') {
747
- return InstanceStatus.STARTING;
748
- }
749
- if (state.Health?.Status === 'unhealthy') {
750
- return InstanceStatus.UNHEALTHY;
751
- }
764
+ const statusType = state.Status as DockerContainerStatus;
752
765
 
766
+ if (statusType === 'running') {
767
+ if (state.Health?.Status) {
768
+ const healthStatusType = state.Health.Status as DockerContainerHealth;
769
+ if (healthStatusType === 'healthy' || healthStatusType === 'none') {
770
+ return InstanceStatus.READY;
771
+ }
772
+
773
+ if (healthStatusType === 'starting') {
774
+ return InstanceStatus.STARTING;
775
+ }
776
+
777
+ if (healthStatusType === 'unhealthy') {
778
+ return InstanceStatus.UNHEALTHY;
779
+ }
780
+ }
753
781
  return InstanceStatus.READY;
754
782
  }
755
- if (state.Status === 'created') {
783
+
784
+ if (statusType === 'created') {
756
785
  return InstanceStatus.STARTING;
757
786
  }
758
787
 
759
- if (state.Status === 'exited' || state.Status === 'dead') {
788
+ if (statusType === 'exited' || statusType === 'dead') {
760
789
  return InstanceStatus.STOPPED;
761
790
  }
762
791
 
763
- if (state.Status === 'removing') {
792
+ if (statusType === 'removing') {
764
793
  return InstanceStatus.BUSY;
765
794
  }
766
795
 
767
- if (state.Status === 'restarting') {
796
+ if (statusType === 'restarting') {
768
797
  return InstanceStatus.BUSY;
769
798
  }
770
799
 
771
- if (state.Status === 'paused') {
800
+ if (statusType === 'paused') {
772
801
  return InstanceStatus.BUSY;
773
802
  }
774
803
 
@@ -4,7 +4,13 @@ import md5 from 'md5';
4
4
  import { parseKapetaUri } from '@kapeta/nodejs-utils';
5
5
  import { serviceManager } from './serviceManager';
6
6
  import { storageService } from './storageService';
7
- import { CONTAINER_LABEL_PORT_PREFIX, ContainerInfo, containerManager } from './containerManager';
7
+ import {
8
+ COMPOSE_LABEL_PROJECT,
9
+ COMPOSE_LABEL_SERVICE,
10
+ CONTAINER_LABEL_PORT_PREFIX,
11
+ ContainerInfo,
12
+ containerManager,
13
+ } from './containerManager';
8
14
  import FSExtra from 'fs-extra';
9
15
  import { AnyMap, EnvironmentType, OperatorInfo, StringMap } from './types';
10
16
  import { BlockInstance, Resource } from '@kapeta/schemas';
@@ -199,8 +205,12 @@ class OperatorManager {
199
205
  const PortBindings: { [key: string]: any } = {};
200
206
  const Env: string[] = [];
201
207
 
208
+ const systemUri = parseKapetaUri(systemId);
209
+
202
210
  const Labels: StringMap = {
203
211
  kapeta: 'true',
212
+ [COMPOSE_LABEL_PROJECT]: systemUri.id.replace(/[^a-z0-9]/gi, '_'),
213
+ [COMPOSE_LABEL_SERVICE]: [resourceType, version].join('_').replace(/[^a-z0-9]/gi, '_'),
204
214
  };
205
215
 
206
216
  const operatorMetadata = operator.getDefinitionInfo().definition.metadata;
@@ -139,7 +139,9 @@ class TaskManager {
139
139
 
140
140
  socketManager.emitGlobal(EVENT_TASK_ADDED, task.toData());
141
141
 
142
- this.invokeTask(task).catch(() => {});
142
+ this.invokeTask(task).catch((err) => {
143
+ console.warn(`Task ${task.id} failed`, err);
144
+ });
143
145
 
144
146
  return task;
145
147
  }
@@ -205,6 +207,7 @@ class TaskManager {
205
207
  task.future.resolve(result);
206
208
  task.emitUpdate();
207
209
  } catch (e: any) {
210
+ console.warn(`Task ${task.id} failed while waiting for it to resolve`, e);
208
211
  task.errorMessage = e.message;
209
212
  task.status = TaskStatus.FAILED;
210
213
  task.future.reject(e);
@@ -3,12 +3,18 @@ import ClusterConfig, { DefinitionInfo } from '@kapeta/local-cluster-config';
3
3
  import { getBindHost, getBlockInstanceContainerName, normalizeKapetaUri, readYML } from './utils';
4
4
  import { KapetaURI, parseKapetaUri } from '@kapeta/nodejs-utils';
5
5
  import { serviceManager } from '../serviceManager';
6
- import { containerManager, DockerMounts, toLocalBindVolume } from '../containerManager';
6
+ import {
7
+ COMPOSE_LABEL_PROJECT,
8
+ COMPOSE_LABEL_SERVICE,
9
+ containerManager,
10
+ DockerMounts,
11
+ toLocalBindVolume,
12
+ } from '../containerManager';
7
13
  import { LogData } from './LogData';
8
14
  import { clusterService } from '../clusterService';
9
15
  import { AnyMap, BlockProcessParams, InstanceType, ProcessInfo, StringMap } from '../types';
10
- import { Container } from 'node-docker-api/lib/container';
11
16
  import { definitionsManager } from '../definitionsManager';
17
+ import Docker from 'dockerode';
12
18
 
13
19
  const KIND_BLOCK_TYPE_OPERATOR = 'core/block-type-operator';
14
20
  const KAPETA_SYSTEM_ID = 'KAPETA_SYSTEM_ID';
@@ -185,6 +191,8 @@ export class BlockInstanceRunner {
185
191
  HealthCheck = containerManager.toDockerHealth({ cmd: localContainer.healthcheck });
186
192
  }
187
193
 
194
+ const systemUri = parseKapetaUri(this._systemId);
195
+
188
196
  return this.ensureContainer({
189
197
  ...dockerOpts,
190
198
  Image: dockerImage,
@@ -193,6 +201,8 @@ export class BlockInstanceRunner {
193
201
  Labels: {
194
202
  ...customLabels,
195
203
  instance: blockInstance.id,
204
+ [COMPOSE_LABEL_PROJECT]: systemUri.id.replace(/[^a-z0-9]/gi, '_'),
205
+ [COMPOSE_LABEL_SERVICE]: blockInfo.id.replace(/[^a-z0-9]/gi, '_'),
196
206
  },
197
207
  HealthCheck,
198
208
  ExposedPorts,
@@ -250,6 +260,7 @@ export class BlockInstanceRunner {
250
260
 
251
261
  // For windows we need to default to root
252
262
  const innerHome = process.platform === 'win32' ? '/root/.kapeta' : ClusterConfig.getKapetaBasedir();
263
+ const systemUri = parseKapetaUri(this._systemId);
253
264
 
254
265
  return this.ensureContainer({
255
266
  Image: dockerImage,
@@ -257,6 +268,8 @@ export class BlockInstanceRunner {
257
268
  ExposedPorts,
258
269
  Labels: {
259
270
  instance: blockInstance.id,
271
+ [COMPOSE_LABEL_PROJECT]: systemUri.id.replace(/[^a-z0-9]/gi, '_'),
272
+ [COMPOSE_LABEL_SERVICE]: blockInfo.id.replace(/[^a-z0-9]/gi, '_'),
260
273
  },
261
274
  Env: [
262
275
  ...DOCKER_ENV_VARS,
@@ -354,6 +367,7 @@ export class BlockInstanceRunner {
354
367
  // For windows we need to default to root
355
368
  const innerHome = process.platform === 'win32' ? '/root/.kapeta' : ClusterConfig.getKapetaBasedir();
356
369
 
370
+ const systemUri = parseKapetaUri(this._systemId);
357
371
  logs.addLog(`Creating new container for block: ${containerName}`);
358
372
  const out = await this.ensureContainer({
359
373
  Image: dockerImage,
@@ -370,6 +384,8 @@ export class BlockInstanceRunner {
370
384
  },
371
385
  Labels: {
372
386
  instance: blockInstance.id,
387
+ [COMPOSE_LABEL_PROJECT]: systemUri.id.replace(/[^a-z0-9]/gi, '_'),
388
+ [COMPOSE_LABEL_SERVICE]: blockUri.id.replace(/[^a-z0-9]/gi, '_'),
373
389
  },
374
390
  Env: [
375
391
  `KAPETA_INSTANCE_NAME=${blockInstance.ref}`,
@@ -424,7 +440,7 @@ export class BlockInstanceRunner {
424
440
  return this._handleContainer(container);
425
441
  }
426
442
 
427
- private async _handleContainer(container: Container): Promise<ProcessInfo> {
443
+ private async _handleContainer(container: Docker.Container): Promise<ProcessInfo> {
428
444
  return {
429
445
  type: InstanceType.DOCKER,
430
446
  pid: container.id,
@@ -2,6 +2,9 @@ import FS from 'node:fs';
2
2
  import YAML from 'yaml';
3
3
  import { parseKapetaUri } from '@kapeta/nodejs-utils';
4
4
  import md5 from 'md5';
5
+ import { EntityList } from '@kapeta/schemas';
6
+ import _ from 'lodash';
7
+ import { AnyMap } from '../types';
5
8
 
6
9
  export function getBlockInstanceContainerName(systemId: string, instanceId: string) {
7
10
  return `kapeta-block-instance-${md5(systemId + instanceId)}`;
@@ -47,3 +50,29 @@ export function getBindHost(preferredHost = '127.0.0.1') {
47
50
  // TODO: This might pose a security risk - so we should authenticate all requests using a shared secret/nonce that we pass around.
48
51
  return isLinux() ? '0.0.0.0' : preferredHost;
49
52
  }
53
+
54
+ export function getResolvedConfiguration(entities?: EntityList, config?: AnyMap, globalConfiguration?: AnyMap): AnyMap {
55
+ if (!entities || !globalConfiguration) {
56
+ return config || {};
57
+ }
58
+
59
+ const mergedConfig = config ? _.cloneDeep(config) : {};
60
+ entities.types?.forEach((type) => {
61
+ if (!type.properties) {
62
+ return;
63
+ }
64
+ Object.entries(type.properties).forEach(([propertyName, property]) => {
65
+ if (!property.global) {
66
+ return;
67
+ }
68
+
69
+ const configPath = type.name + '.' + propertyName;
70
+ const defaultValue = globalConfiguration ? _.get(globalConfiguration, configPath) : undefined;
71
+ if (!_.has(mergedConfig, configPath)) {
72
+ _.set(mergedConfig, configPath, defaultValue);
73
+ }
74
+ });
75
+ });
76
+
77
+ return mergedConfig;
78
+ }