@kapeta/local-cluster-service 0.35.0 → 0.36.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.
@@ -10,7 +10,7 @@ import { BlockInstanceRunner } from './utils/BlockInstanceRunner';
10
10
  import { storageService } from './storageService';
11
11
  import { EVENT_INSTANCE_CREATED, EVENT_STATUS_CHANGED, socketManager } from './socketManager';
12
12
  import { serviceManager } from './serviceManager';
13
- import { assetManager } from './assetManager';
13
+ import { assetManager, EnrichedAsset } from './assetManager';
14
14
  import {
15
15
  containerManager,
16
16
  DockerContainerHealth,
@@ -27,13 +27,12 @@ import {
27
27
  InstanceType,
28
28
  LocalImageOptions,
29
29
  LogEntry,
30
- OperatorInfo,
31
30
  OperatorInstanceInfo,
32
31
  OperatorInstancePort,
33
32
  } from './types';
34
33
  import { BlockDefinitionSpec, BlockInstance, Plan } from '@kapeta/schemas';
35
34
  import { getBlockInstanceContainerName, getResolvedConfiguration } from './utils/utils';
36
- import { KIND_OPERATOR, operatorManager } from './operatorManager';
35
+ import { KIND_BLOCK_OPERATOR, KIND_RESOURCE_OPERATOR, operatorManager } from './operatorManager';
37
36
  import { normalizeKapetaUri, parseKapetaUri } from '@kapeta/nodejs-utils';
38
37
  import { definitionsManager } from './definitionsManager';
39
38
  import { Task, taskManager } from './taskManager';
@@ -347,71 +346,6 @@ export class InstanceManager {
347
346
  );
348
347
  }
349
348
 
350
- public async stop(systemId: string, instanceId: string) {
351
- return this.stopInner(systemId, instanceId, true);
352
- }
353
-
354
- private async stopInner(systemId: string, instanceId: string, changeDesired: boolean = false) {
355
- return this.exclusive(systemId, instanceId, async () => {
356
- systemId = normalizeKapetaUri(systemId);
357
- const instance = this.getInstance(systemId, instanceId);
358
- if (!instance) {
359
- return;
360
- }
361
-
362
- if (instance.status === InstanceStatus.STOPPED) {
363
- return;
364
- }
365
-
366
- if (instance.status === InstanceStatus.STOPPING) {
367
- return;
368
- }
369
-
370
- if (changeDesired && instance.desiredStatus !== DesiredInstanceStatus.EXTERNAL) {
371
- instance.desiredStatus = DesiredInstanceStatus.STOP;
372
- }
373
-
374
- instance.status = InstanceStatus.STOPPING;
375
-
376
- socketManager.emitSystemEvent(systemId, EVENT_STATUS_CHANGED, instance);
377
- console.log('Stopping instance: %s::%s [desired: %s]', systemId, instanceId, instance.desiredStatus);
378
- this.save();
379
-
380
- try {
381
- if (instance.type === 'docker') {
382
- const containerName = getBlockInstanceContainerName(instance.systemId, instance.instanceId);
383
- const container = await containerManager.getContainerByName(containerName);
384
- if (container) {
385
- try {
386
- await container.stop();
387
- instance.status = InstanceStatus.STOPPED;
388
- socketManager.emitSystemEvent(systemId, EVENT_STATUS_CHANGED, instance);
389
- this.save();
390
- } catch (e) {
391
- console.error('Failed to stop container', e);
392
- }
393
- } else {
394
- console.warn('Container not found', containerName);
395
- }
396
- return;
397
- }
398
-
399
- if (!instance.pid) {
400
- instance.status = InstanceStatus.STOPPED;
401
- this.save();
402
- return;
403
- }
404
-
405
- process.kill(instance.pid as number, 'SIGTERM');
406
- instance.status = InstanceStatus.STOPPED;
407
- socketManager.emitSystemEvent(systemId, EVENT_STATUS_CHANGED, instance);
408
- this.save();
409
- } catch (e) {
410
- console.error('Failed to stop process', e);
411
- }
412
- });
413
- }
414
-
415
349
  public stopAllForPlan(systemId: string) {
416
350
  systemId = normalizeKapetaUri(systemId);
417
351
  const instancesForPlan = this._instances.filter((instance) => instance.systemId === systemId);
@@ -482,9 +416,17 @@ export class InstanceManager {
482
416
  };
483
417
  }
484
418
 
485
- public async start(systemId: string, instanceId: string): Promise<InstanceInfo | Task<InstanceInfo>> {
486
- return this.exclusive(systemId, instanceId, async () => {
487
- systemId = normalizeKapetaUri(systemId);
419
+ public async stop(systemId: string, instanceId: string) {
420
+ return this.stopInner(systemId, instanceId, true);
421
+ }
422
+
423
+ private async stopInner(
424
+ systemId: string,
425
+ instanceId: string,
426
+ changeDesired: boolean = false,
427
+ checkForSingleton: boolean = true
428
+ ) {
429
+ if (checkForSingleton) {
488
430
  const blockInstance = await assetManager.getBlockInstance(systemId, instanceId);
489
431
  const blockRef = normalizeKapetaUri(blockInstance.block.ref);
490
432
 
@@ -493,9 +435,123 @@ export class InstanceManager {
493
435
  throw new Error('Block not found: ' + blockRef);
494
436
  }
495
437
 
496
- const existingInstance = this.getInstance(systemId, instanceId);
438
+ if (await this.isSingletonOperator(blockAsset)) {
439
+ const instances = await this.getAllInstancesForKind(systemId, blockAsset.data.kind);
440
+ if (instances.length > 1) {
441
+ const promises = instances.map((id) => {
442
+ return this.stopInner(systemId, id, changeDesired, false);
443
+ });
444
+
445
+ await Promise.all(promises);
446
+ return;
447
+ }
448
+ }
449
+ }
450
+
451
+ return this.exclusive(systemId, instanceId, async () => {
452
+ systemId = normalizeKapetaUri(systemId);
453
+ const instance = this.getInstance(systemId, instanceId);
454
+ if (!instance) {
455
+ return;
456
+ }
457
+
458
+ if (instance.status === InstanceStatus.STOPPED) {
459
+ return;
460
+ }
461
+
462
+ if (instance.status === InstanceStatus.STOPPING) {
463
+ return;
464
+ }
465
+
466
+ if (changeDesired && instance.desiredStatus !== DesiredInstanceStatus.EXTERNAL) {
467
+ instance.desiredStatus = DesiredInstanceStatus.STOP;
468
+ }
469
+
470
+ instance.status = InstanceStatus.STOPPING;
471
+
472
+ socketManager.emitSystemEvent(systemId, EVENT_STATUS_CHANGED, instance);
473
+ console.log(
474
+ 'Stopping instance: %s::%s [desired: %s] [intentional: %s]',
475
+ systemId,
476
+ instanceId,
477
+ instance.desiredStatus,
478
+ changeDesired
479
+ );
480
+ this.save();
481
+
482
+ try {
483
+ if (instance.type === 'docker') {
484
+ const containerName = await getBlockInstanceContainerName(instance.systemId, instance.instanceId);
485
+ const container = await containerManager.getContainerByName(containerName);
486
+ if (container) {
487
+ try {
488
+ await container.stop();
489
+ instance.status = InstanceStatus.STOPPED;
490
+ socketManager.emitSystemEvent(systemId, EVENT_STATUS_CHANGED, instance);
491
+ this.save();
492
+ } catch (e) {
493
+ console.error('Failed to stop container', e);
494
+ }
495
+ } else {
496
+ console.warn('Container not found', containerName);
497
+ }
498
+ return;
499
+ }
500
+
501
+ if (!instance.pid) {
502
+ instance.status = InstanceStatus.STOPPED;
503
+ this.save();
504
+ return;
505
+ }
506
+
507
+ process.kill(instance.pid as number, 'SIGTERM');
508
+ instance.status = InstanceStatus.STOPPED;
509
+ socketManager.emitSystemEvent(systemId, EVENT_STATUS_CHANGED, instance);
510
+ this.save();
511
+ } catch (e) {
512
+ console.error('Failed to stop process', e);
513
+ }
514
+ });
515
+ }
516
+
517
+ public async start(
518
+ systemId: string,
519
+ instanceId: string,
520
+ checkForSingleton: boolean = true
521
+ ): Promise<InstanceInfo | Task<InstanceInfo>> {
522
+ systemId = normalizeKapetaUri(systemId);
523
+ const blockInstance = await assetManager.getBlockInstance(systemId, instanceId);
524
+ const blockRef = normalizeKapetaUri(blockInstance.block.ref);
525
+
526
+ const blockAsset = await assetManager.getAsset(blockRef, true);
527
+ if (!blockAsset) {
528
+ throw new Error('Block not found: ' + blockRef);
529
+ }
530
+
531
+ if (checkForSingleton && (await this.isSingletonOperator(blockAsset))) {
532
+ const instances = await this.getAllInstancesForKind(systemId, blockAsset.data.kind);
533
+ if (instances.length > 1) {
534
+ const promises = instances.map((id) => {
535
+ return this.start(systemId, id, false);
536
+ });
537
+
538
+ await Promise.all(promises);
539
+ return promises[0];
540
+ }
541
+ }
542
+
543
+ return this.exclusive(systemId, instanceId, async () => {
544
+ let existingInstance = this.getInstance(systemId, instanceId);
497
545
 
498
546
  if (existingInstance && existingInstance.pid) {
547
+ const container = await containerManager.get(existingInstance.pid as string);
548
+ if (!container) {
549
+ // The container is not running
550
+ existingInstance = undefined;
551
+ }
552
+ }
553
+
554
+ if (existingInstance?.pid) {
499
555
  if (existingInstance.status === InstanceStatus.READY) {
500
556
  // Instance is already running
501
557
  return existingInstance;
@@ -544,7 +600,7 @@ export class InstanceManager {
544
600
  return Promise.resolve();
545
601
  }
546
602
 
547
- if (KIND_OPERATOR.toLowerCase() !== asset.definition.kind.toLowerCase()) {
603
+ if (KIND_RESOURCE_OPERATOR.toLowerCase() !== asset.definition.kind.toLowerCase()) {
548
604
  // Not an operator
549
605
  return Promise.resolve();
550
606
  }
@@ -554,7 +610,7 @@ export class InstanceManager {
554
610
  return Promise.resolve();
555
611
  }
556
612
  console.log('Ensuring resource: %s in %s', consumerUri.id, systemId);
557
- return operatorManager.ensureResource(systemId, consumerUri.fullName, consumerUri.version);
613
+ return operatorManager.ensureOperator(systemId, consumerUri.fullName, consumerUri.version);
558
614
  });
559
615
 
560
616
  await Promise.all(promises);
@@ -638,6 +694,8 @@ export class InstanceManager {
638
694
  */
639
695
  public async prepareForRestart(systemId: string, instanceId: string) {
640
696
  systemId = normalizeKapetaUri(systemId);
697
+
698
+ console.log('Stopping instance during restart...', systemId, instanceId);
641
699
  await this.stopInner(systemId, instanceId);
642
700
  }
643
701
 
@@ -671,8 +729,14 @@ export class InstanceManager {
671
729
  while (all.length > 0) {
672
730
  // Check a few instances at a time - docker doesn't like too many concurrent requests
673
731
  const chunk = all.splice(0, 30);
674
- const promises = chunk.map(async (instance) => {
675
- if (!instance.systemId) {
732
+ const promises = chunk.map(async (oldInstance) => {
733
+ if (!oldInstance.systemId) {
734
+ return;
735
+ }
736
+
737
+ // Grab the latest here
738
+ const instance = this.getInstance(oldInstance.systemId, oldInstance.instanceId);
739
+ if (!instance) {
676
740
  return;
677
741
  }
678
742
 
@@ -789,6 +853,11 @@ export class InstanceManager {
789
853
  ) {
790
854
  //If the instance is running but we want it to stop, stop it
791
855
  try {
856
+ console.log(
857
+ 'Stopping instance since it is its desired state',
858
+ instance.systemId,
859
+ instance.instanceId
860
+ );
792
861
  await this.stopInner(instance.systemId, instance.instanceId);
793
862
  } catch (e) {
794
863
  console.warn('Failed to stop instance', instance.systemId, instance.instanceId, e);
@@ -823,7 +892,7 @@ export class InstanceManager {
823
892
 
824
893
  private async getExternalStatus(instance: InstanceInfo): Promise<InstanceStatus> {
825
894
  if (instance.type === InstanceType.DOCKER) {
826
- const containerName = getBlockInstanceContainerName(instance.systemId, instance.instanceId);
895
+ const containerName = await getBlockInstanceContainerName(instance.systemId, instance.instanceId);
827
896
  const container = await containerManager.getContainerByName(containerName);
828
897
  if (!container) {
829
898
  // If the container doesn't exist, we consider the instance stopped
@@ -932,6 +1001,53 @@ export class InstanceManager {
932
1001
  });
933
1002
  });
934
1003
  }
1004
+
1005
+ private async isSingletonOperator(blockAsset: EnrichedAsset): Promise<boolean> {
1006
+ const provider = await assetManager.getAsset(blockAsset.data.kind);
1007
+ if (!provider) {
1008
+ return false;
1009
+ }
1010
+
1011
+ if (parseKapetaUri(provider.kind).fullName === KIND_BLOCK_OPERATOR) {
1012
+ const localConfig = provider.data.spec.local as LocalImageOptions;
1013
+ return localConfig.singleton ?? false;
1014
+ }
1015
+
1016
+ return false;
1017
+ }
1018
+
1019
+ private async getKindForAssetRef(assetRef: string): Promise<string | null> {
1020
+ const block = await assetManager.getAsset(assetRef);
1021
+ if (!block) {
1022
+ return null;
1023
+ }
1024
+
1025
+ return block.data.kind;
1026
+ }
1027
+
1028
+ private async isUsingKind(ref: string, kind: string): Promise<boolean> {
1029
+ const assetKind = await this.getKindForAssetRef(ref);
1030
+ if (!assetKind) {
1031
+ return false;
1032
+ }
1033
+
1034
+ return parseKapetaUri(assetKind).fullName === parseKapetaUri(kind).fullName;
1035
+ }
1036
+
1037
+ private async getAllInstancesForKind(systemId: string, kind: string): Promise<string[]> {
1038
+ const plan = await assetManager.getPlan(systemId);
1039
+ if (!plan?.spec?.blocks) {
1040
+ return [];
1041
+ }
1042
+ const out: string[] = [];
1043
+ for (const block of plan.spec.blocks) {
1044
+ if (await this.isUsingKind(block.block.ref, kind)) {
1045
+ out.push(block.id);
1046
+ }
1047
+ }
1048
+
1049
+ return out;
1050
+ }
935
1051
  }
936
1052
 
937
1053
  export const instanceManager = new InstanceManager();
@@ -25,11 +25,13 @@ import _ from 'lodash';
25
25
  import AsyncLock from 'async-lock';
26
26
  import { taskManager } from './taskManager';
27
27
 
28
- export const KIND_OPERATOR = 'core/resource-type-operator';
28
+ export const KIND_RESOURCE_OPERATOR = 'core/resource-type-operator';
29
+ export const KIND_BLOCK_OPERATOR = 'core/block-type-operator';
29
30
  const KIND_PLAN = 'core/plan';
30
31
 
31
32
  class Operator {
32
33
  private readonly _data: DefinitionInfo;
34
+
33
35
  constructor(data: DefinitionInfo) {
34
36
  this._data = data;
35
37
  }
@@ -65,24 +67,24 @@ class OperatorManager {
65
67
  /**
66
68
  * Get operator definition for resource type
67
69
  */
68
- async getOperator(resourceType: string, version: string) {
69
- const operators = await definitionsManager.getDefinitions(KIND_OPERATOR);
70
+ async getOperator(fullName: string, version: string) {
71
+ const operators = await definitionsManager.getDefinitions([KIND_RESOURCE_OPERATOR, KIND_BLOCK_OPERATOR]);
70
72
 
71
73
  const operator: DefinitionInfo | undefined = operators.find(
72
74
  (operator) =>
73
75
  operator.definition &&
74
76
  operator.definition.metadata &&
75
77
  operator.definition.metadata.name &&
76
- operator.definition.metadata.name.toLowerCase() === resourceType.toLowerCase() &&
78
+ operator.definition.metadata.name.toLowerCase() === fullName.toLowerCase() &&
77
79
  operator.version === version
78
80
  );
79
81
 
80
82
  if (!operator) {
81
- throw new Error(`Unknown resource type: ${resourceType}:${version}`);
83
+ throw new Error(`Unknown operator type: ${fullName}:${version}`);
82
84
  }
83
85
 
84
86
  if (!operator.definition.spec || !operator.definition.spec.local) {
85
- throw new Error(`Operator missing local definition: ${resourceType}:${version}`);
87
+ throw new Error(`Operator missing local definition: ${fullName}:${version}`);
86
88
  }
87
89
 
88
90
  return new Operator(operator);
@@ -137,7 +139,7 @@ class OperatorManager {
137
139
  const kindUri = parseKapetaUri(blockResource.kind);
138
140
  const operator = await this.getOperator(resourceType, kindUri.version);
139
141
  const credentials = operator.getCredentials();
140
- const container = await this.ensureResource(systemId, resourceType, kindUri.version);
142
+ const container = await this.ensureOperator(systemId, resourceType, kindUri.version);
141
143
  const portInfo = await container.getPort(portType);
142
144
 
143
145
  if (!portInfo) {
@@ -164,16 +166,17 @@ class OperatorManager {
164
166
  /**
165
167
  * Ensure we have a running operator of given type
166
168
  *
167
- * @param {string} systemId
168
- * @param {string} resourceType
169
- * @param {string} version
170
- * @return {Promise<ContainerInfo>}
169
+ * @param systemId the plan ref
170
+ * @param kind the full name - e.g. myhandle/rabbitmq
171
+ * @param version the version of the operator
171
172
  */
172
- async ensureResource(systemId: string, resourceType: string, version: string): Promise<ContainerInfo> {
173
+ async ensureOperator(systemId: string, kind: string, version: string): Promise<ContainerInfo> {
173
174
  systemId = normalizeKapetaUri(systemId);
174
- const key = `${systemId}#${resourceType}:${version}`;
175
+
176
+ const key = `${systemId}#${kind}:${version}`;
177
+
175
178
  return await this.operatorLock.acquire(key, async () => {
176
- const operator = await this.getOperator(resourceType, version);
179
+ const operator = await this.getOperator(kind, version);
177
180
 
178
181
  const operatorData = operator.getLocalData();
179
182
 
@@ -186,7 +189,7 @@ class OperatorManager {
186
189
  for (let i = 0; i < portTypes.length; i++) {
187
190
  const portType = portTypes[i];
188
191
  let containerPortInfo = operatorData.ports[portType];
189
- const hostPort = await serviceManager.ensureServicePort(systemId, resourceType, portType);
192
+ const hostPort = await serviceManager.ensureServicePort(systemId, kind, portType);
190
193
  const portInfo = toPortInfo(containerPortInfo);
191
194
  const portId = portInfo.port + '/' + portInfo.type;
192
195
 
@@ -196,7 +199,7 @@ class OperatorManager {
196
199
  };
197
200
  }
198
201
 
199
- const nameParts = [systemId, resourceType.toLowerCase(), version];
202
+ const nameParts = [systemId, kind.toLowerCase(), version];
200
203
 
201
204
  const containerName = `kapeta-resource-${md5(nameParts.join('_'))}`;
202
205
 
@@ -208,7 +211,7 @@ class OperatorManager {
208
211
  const Labels: StringMap = {
209
212
  kapeta: 'true',
210
213
  [COMPOSE_LABEL_PROJECT]: systemUri.id.replace(/[^a-z0-9]/gi, '_'),
211
- [COMPOSE_LABEL_SERVICE]: [resourceType, version].join('_').replace(/[^a-z0-9]/gi, '_'),
214
+ [COMPOSE_LABEL_SERVICE]: [kind, version].join('_').replace(/[^a-z0-9]/gi, '_'),
212
215
  };
213
216
 
214
217
  const operatorMetadata = operator.getDefinitionInfo().definition.metadata;
@@ -229,7 +232,7 @@ class OperatorManager {
229
232
  Labels[CONTAINER_LABEL_PORT_PREFIX + portInfo.hostPort] = portInfo.type;
230
233
  });
231
234
 
232
- const Mounts = await containerManager.createVolumes(systemId, resourceType, operatorData.mounts);
235
+ const Mounts = await containerManager.createVolumes(systemId, kind, operatorData.mounts);
233
236
 
234
237
  _.forEach(operatorData.env, (value, name) => {
235
238
  Env.push(name + '=' + value);
package/src/types.ts CHANGED
@@ -78,6 +78,7 @@ export type LocalImageOptions<Credentials = AnyMap, Options = AnyMap> = {
78
78
  cmd?: string;
79
79
  env?: AnyMap;
80
80
  health?: Health;
81
+ singleton?: boolean;
81
82
  mounts?: { [key: string]: string };
82
83
  };
83
84