@kapeta/local-cluster-service 0.11.1 → 0.12.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/definitions.d.ts +7 -0
  3. package/dist/cjs/src/config/routes.js +1 -1
  4. package/dist/cjs/src/containerManager.d.ts +3 -2
  5. package/dist/cjs/src/containerManager.js +127 -34
  6. package/dist/cjs/src/definitionsManager.d.ts +1 -0
  7. package/dist/cjs/src/definitionsManager.js +7 -4
  8. package/dist/cjs/src/instanceManager.d.ts +8 -1
  9. package/dist/cjs/src/instanceManager.js +56 -21
  10. package/dist/cjs/src/instances/routes.js +2 -0
  11. package/dist/cjs/src/operatorManager.d.ts +2 -0
  12. package/dist/cjs/src/operatorManager.js +70 -67
  13. package/dist/cjs/src/socketManager.d.ts +1 -0
  14. package/dist/cjs/src/socketManager.js +3 -0
  15. package/dist/cjs/src/types.d.ts +1 -0
  16. package/dist/cjs/src/utils/BlockInstanceRunner.js +2 -3
  17. package/dist/esm/src/config/routes.js +1 -1
  18. package/dist/esm/src/containerManager.d.ts +3 -2
  19. package/dist/esm/src/containerManager.js +128 -35
  20. package/dist/esm/src/definitionsManager.d.ts +1 -0
  21. package/dist/esm/src/definitionsManager.js +8 -5
  22. package/dist/esm/src/instanceManager.d.ts +8 -1
  23. package/dist/esm/src/instanceManager.js +56 -21
  24. package/dist/esm/src/instances/routes.js +2 -0
  25. package/dist/esm/src/operatorManager.d.ts +2 -0
  26. package/dist/esm/src/operatorManager.js +68 -65
  27. package/dist/esm/src/socketManager.d.ts +1 -0
  28. package/dist/esm/src/socketManager.js +3 -0
  29. package/dist/esm/src/types.d.ts +1 -0
  30. package/dist/esm/src/utils/BlockInstanceRunner.js +2 -3
  31. package/dist/esm/src/utils/utils.js +1 -1
  32. package/package.json +1 -1
  33. package/src/config/routes.ts +1 -1
  34. package/src/containerManager.ts +181 -60
  35. package/src/definitionsManager.ts +9 -5
  36. package/src/instanceManager.ts +82 -42
  37. package/src/instances/routes.ts +3 -1
  38. package/src/operatorManager.ts +73 -69
  39. package/src/socketManager.ts +4 -0
  40. package/src/types.ts +1 -1
  41. package/src/utils/BlockInstanceRunner.ts +12 -24
  42. package/src/utils/utils.ts +2 -2
@@ -10,6 +10,9 @@ import { containerManager, HEALTH_CHECK_TIMEOUT } from './containerManager';
10
10
  import { configManager } from './configManager';
11
11
  import { DesiredInstanceStatus, InstanceOwner, InstanceStatus, InstanceType } from './types';
12
12
  import { getBlockInstanceContainerName, normalizeKapetaUri } from './utils/utils';
13
+ import { KIND_OPERATOR, operatorManager } from './operatorManager';
14
+ import { parseKapetaUri } from '@kapeta/nodejs-utils';
15
+ import { definitionsManager } from './definitionsManager';
13
16
  const CHECK_INTERVAL = 5000;
14
17
  const DEFAULT_HEALTH_PORT_TYPE = 'rest';
15
18
  const EVENT_STATUS_CHANGED = 'status-changed';
@@ -46,7 +49,13 @@ export class InstanceManager {
46
49
  return [];
47
50
  }
48
51
  systemId = normalizeKapetaUri(systemId);
49
- return this._instances.filter((instance) => instance.systemId === systemId);
52
+ const planInfo = definitionsManager.getDefinition(systemId);
53
+ if (!planInfo) {
54
+ return [];
55
+ }
56
+ const plan = planInfo.definition;
57
+ const instanceIds = plan.spec.blocks.map((block) => block.id);
58
+ return this._instances.filter((instance) => instance.systemId === systemId && instanceIds.includes(instance.instanceId));
50
59
  }
51
60
  getInstance(systemId, instanceId) {
52
61
  systemId = normalizeKapetaUri(systemId);
@@ -55,7 +64,10 @@ export class InstanceManager {
55
64
  async exclusive(systemId, instanceId, fn) {
56
65
  systemId = normalizeKapetaUri(systemId);
57
66
  const key = `${systemId}/${instanceId}`;
58
- return this.instanceLocks.acquire(key, fn);
67
+ //console.log(`Acquiring lock for ${key}`, this.instanceLocks.isBusy(key));
68
+ const result = await this.instanceLocks.acquire(key, fn);
69
+ //console.log(`Releasing lock for ${key}`, this.instanceLocks.isBusy(key));
70
+ return result;
59
71
  }
60
72
  async getLogs(systemId, instanceId) {
61
73
  const instance = this.getInstance(systemId, instanceId);
@@ -66,19 +78,23 @@ export class InstanceManager {
66
78
  case InstanceType.DOCKER:
67
79
  return await containerManager.getLogs(instance);
68
80
  case InstanceType.UNKNOWN:
69
- return [{
81
+ return [
82
+ {
70
83
  level: 'INFO',
71
84
  message: 'Instance is starting...',
72
85
  time: Date.now(),
73
86
  source: 'stdout',
74
- }];
87
+ },
88
+ ];
75
89
  case InstanceType.LOCAL:
76
- return [{
90
+ return [
91
+ {
77
92
  level: 'INFO',
78
93
  message: 'Instance started outside Kapeta - logs not available...',
79
94
  time: Date.now(),
80
95
  source: 'stdout',
81
- }];
96
+ },
97
+ ];
82
98
  }
83
99
  return [];
84
100
  }
@@ -119,7 +135,8 @@ export class InstanceManager {
119
135
  const address = await serviceManager.getProviderAddress(systemId, instanceId, info.portType ?? DEFAULT_HEALTH_PORT_TYPE);
120
136
  const healthUrl = this.getHealthUrl(info, address);
121
137
  if (instance) {
122
- if (instance.status === InstanceStatus.STOPPING && instance.desiredStatus === DesiredInstanceStatus.STOP) {
138
+ if (instance.status === InstanceStatus.STOPPING &&
139
+ instance.desiredStatus === DesiredInstanceStatus.STOP) {
123
140
  //If instance is stopping do not interfere
124
141
  return;
125
142
  }
@@ -229,8 +246,7 @@ export class InstanceManager {
229
246
  if (instance.status === InstanceStatus.STOPPED) {
230
247
  return;
231
248
  }
232
- if (changeDesired &&
233
- instance.desiredStatus !== DesiredInstanceStatus.EXTERNAL) {
249
+ if (changeDesired && instance.desiredStatus !== DesiredInstanceStatus.EXTERNAL) {
234
250
  instance.desiredStatus = DesiredInstanceStatus.STOP;
235
251
  }
236
252
  instance.status = InstanceStatus.STOPPING;
@@ -324,6 +340,24 @@ export class InstanceManager {
324
340
  console.log('Starting instance: %s::%s [desired: %s]', systemId, instanceId, instance.desiredStatus);
325
341
  // Save the instance before starting it, so that we can track the status
326
342
  await this.saveInternalInstance(instance);
343
+ const blockSpec = blockAsset.data.spec;
344
+ if (blockSpec.consumers) {
345
+ const promises = blockSpec.consumers.map((consumer) => {
346
+ const consumerUri = parseKapetaUri(consumer.kind);
347
+ const asset = definitionsManager.getDefinition(consumer.kind);
348
+ if (!asset) {
349
+ // Definition not found
350
+ return Promise.resolve();
351
+ }
352
+ if (KIND_OPERATOR.toLowerCase() !== asset.definition.kind.toLowerCase()) {
353
+ // Not an operator
354
+ return Promise.resolve();
355
+ }
356
+ console.log('Ensuring resource: %s in %s', consumerUri.id, systemId);
357
+ return operatorManager.ensureResource(systemId, consumerUri.fullName, consumerUri.version);
358
+ });
359
+ await Promise.all(promises);
360
+ }
327
361
  if (existingInstance) {
328
362
  // Check if the instance is already running - but after we've commmuicated the desired status
329
363
  const currentStatus = await this.requestInstanceStatus(existingInstance);
@@ -348,7 +382,7 @@ export class InstanceManager {
348
382
  });
349
383
  }
350
384
  catch (e) {
351
- console.warn('Failed to start instance', e);
385
+ console.warn('Failed to start instance: ', systemId, instanceId, blockRef, e.message);
352
386
  const logs = [
353
387
  {
354
388
  source: 'stdout',
@@ -359,11 +393,12 @@ export class InstanceManager {
359
393
  ];
360
394
  const out = await this.saveInternalInstance({
361
395
  ...instance,
362
- type: InstanceType.LOCAL,
396
+ type: InstanceType.UNKNOWN,
363
397
  pid: null,
364
398
  health: null,
365
399
  portType: DEFAULT_HEALTH_PORT_TYPE,
366
400
  status: InstanceStatus.FAILED,
401
+ errorMessage: e.message ?? 'Failed to start - Check logs for details.',
367
402
  });
368
403
  this.emitInstanceEvent(systemId, instanceId, EVENT_INSTANCE_LOG, logs[0]);
369
404
  this.emitInstanceEvent(systemId, blockInstance.id, EVENT_INSTANCE_EXITED, {
@@ -375,15 +410,16 @@ export class InstanceManager {
375
410
  }
376
411
  });
377
412
  }
378
- async restart(systemId, instanceId) {
413
+ /**
414
+ * Stops an instance but does not remove it from the list of active instances
415
+ *
416
+ * It will be started again next time the system checks the status of the instance
417
+ *
418
+ * We do it this way to not cause the user to wait for the instance to start again
419
+ */
420
+ async prepareForRestart(systemId, instanceId) {
379
421
  systemId = normalizeKapetaUri(systemId);
380
422
  await this.stopInner(systemId, instanceId);
381
- const existingInstance = this.getInstance(systemId, instanceId);
382
- if (existingInstance?.desiredStatus === DesiredInstanceStatus.STOP) {
383
- // Internal instance was marked as stopped - abort restart
384
- return existingInstance;
385
- }
386
- return this.start(systemId, instanceId);
387
423
  }
388
424
  async stopAll() {
389
425
  return this.stopInstances(this._instances);
@@ -441,8 +477,7 @@ export class InstanceManager {
441
477
  const oldStatus = instance.status;
442
478
  const skipUpdate = (newStatus === InstanceStatus.STOPPED && instance.status === InstanceStatus.FAILED) ||
443
479
  ([InstanceStatus.READY, InstanceStatus.UNHEALTHY].includes(newStatus) &&
444
- instance.status === InstanceStatus.STOPPING &&
445
- instance.desiredStatus === DesiredInstanceStatus.STOP) ||
480
+ instance.status === InstanceStatus.STOPPING) ||
446
481
  (newStatus === InstanceStatus.STOPPED &&
447
482
  instance.status === InstanceStatus.STARTING &&
448
483
  instance.desiredStatus === DesiredInstanceStatus.RUN);
@@ -482,7 +517,7 @@ export class InstanceManager {
482
517
  //If the instance is unhealthy, try to restart it
483
518
  console.log('Restarting unhealthy instance', instance);
484
519
  try {
485
- await this.restart(instance.systemId, instance.instanceId);
520
+ await this.prepareForRestart(instance.systemId, instance.instanceId);
486
521
  }
487
522
  catch (e) {
488
523
  console.warn('Failed to restart instance', instance.systemId, instance.instanceId, e);
@@ -117,8 +117,10 @@ router.put('/', async (req, res) => {
117
117
  const oldInstance = instanceManager.getInstance(req.kapeta.systemId, req.kapeta.instanceId);
118
118
  if (oldInstance) {
119
119
  instance.pid = oldInstance.pid;
120
+ instance.desiredStatus = oldInstance.desiredStatus;
120
121
  }
121
122
  instance.type = InstanceType.DOCKER;
123
+ instance.owner = InstanceOwner.INTERNAL;
122
124
  }
123
125
  else {
124
126
  // Coming from user starting the instance outside of kapeta
@@ -1,5 +1,6 @@
1
1
  import { ContainerInfo } from './containerManager';
2
2
  import { EnvironmentType, OperatorInfo } from './types';
3
+ export declare const KIND_OPERATOR = "core/resource-type-operator";
3
4
  declare class Operator {
4
5
  private _data;
5
6
  constructor(data: any);
@@ -8,6 +9,7 @@ declare class Operator {
8
9
  }
9
10
  declare class OperatorManager {
10
11
  private _mountDir;
12
+ private operatorLock;
11
13
  constructor();
12
14
  _getMountPoint(operatorType: string, mountName: string): string;
13
15
  /**
@@ -8,7 +8,8 @@ import FSExtra from 'fs-extra';
8
8
  import { definitionsManager } from './definitionsManager';
9
9
  import { getBindHost, normalizeKapetaUri } from './utils/utils';
10
10
  import _ from 'lodash';
11
- const KIND_OPERATOR = 'core/resource-type-operator';
11
+ import AsyncLock from 'async-lock';
12
+ export const KIND_OPERATOR = 'core/resource-type-operator';
12
13
  class Operator {
13
14
  _data;
14
15
  constructor(data) {
@@ -23,6 +24,7 @@ class Operator {
23
24
  }
24
25
  class OperatorManager {
25
26
  _mountDir;
27
+ operatorLock = new AsyncLock();
26
28
  constructor() {
27
29
  this._mountDir = Path.join(storageService.getKapetaBasedir(), 'mounts');
28
30
  FSExtra.mkdirpSync(this._mountDir);
@@ -107,74 +109,75 @@ class OperatorManager {
107
109
  * @return {Promise<ContainerInfo>}
108
110
  */
109
111
  async ensureResource(systemId, resourceType, version) {
110
- const operator = this.getOperator(resourceType, version);
111
- const operatorData = operator.getData();
112
- const portTypes = Object.keys(operatorData.ports);
113
- portTypes.sort();
114
- const ports = {};
115
- for (let i = 0; i < portTypes.length; i++) {
116
- const portType = portTypes[i];
117
- let containerPortInfo = operatorData.ports[portType];
118
- const hostPort = await serviceManager.ensureServicePort(systemId, resourceType, portType);
119
- if (typeof containerPortInfo === 'number' || typeof containerPortInfo === 'string') {
120
- containerPortInfo = { port: containerPortInfo, type: 'tcp' };
121
- }
122
- if (!containerPortInfo.type) {
123
- containerPortInfo.type = 'tcp';
112
+ systemId = normalizeKapetaUri(systemId);
113
+ const key = `${systemId}#${resourceType}:${version}`;
114
+ return await this.operatorLock.acquire(key, async () => {
115
+ const operator = this.getOperator(resourceType, version);
116
+ const operatorData = operator.getData();
117
+ const portTypes = Object.keys(operatorData.ports);
118
+ portTypes.sort();
119
+ const ports = {};
120
+ for (let i = 0; i < portTypes.length; i++) {
121
+ const portType = portTypes[i];
122
+ let containerPortInfo = operatorData.ports[portType];
123
+ const hostPort = await serviceManager.ensureServicePort(systemId, resourceType, portType);
124
+ if (typeof containerPortInfo === 'number' || typeof containerPortInfo === 'string') {
125
+ containerPortInfo = { port: containerPortInfo, type: 'tcp' };
126
+ }
127
+ if (!containerPortInfo.type) {
128
+ containerPortInfo.type = 'tcp';
129
+ }
130
+ const portId = containerPortInfo.port + '/' + containerPortInfo.type;
131
+ ports[portId] = {
132
+ type: portType,
133
+ hostPort,
134
+ };
124
135
  }
125
- const portId = containerPortInfo.port + '/' + containerPortInfo.type;
126
- ports[portId] = {
127
- type: portType,
128
- hostPort,
136
+ const mounts = await containerManager.createMounts(systemId, resourceType, operatorData.mounts);
137
+ const nameParts = [systemId, resourceType.toLowerCase(), version];
138
+ const containerName = `kapeta-resource-${md5(nameParts.join('_'))}`;
139
+ const PortBindings = {};
140
+ const Env = [];
141
+ const Labels = {
142
+ kapeta: 'true',
129
143
  };
130
- }
131
- const mounts = await containerManager.createMounts(systemId, resourceType, operatorData.mounts);
132
- const nameParts = [
133
- systemId,
134
- resourceType.toLowerCase(),
135
- version
136
- ];
137
- const containerName = `kapeta-resource-${md5(nameParts.join('_'))}`;
138
- const PortBindings = {};
139
- const Env = [];
140
- const Labels = {
141
- kapeta: 'true',
142
- };
143
- const bindHost = getBindHost();
144
- const ExposedPorts = {};
145
- _.forEach(ports, (portInfo, containerPort) => {
146
- ExposedPorts['' + containerPort] = {};
147
- PortBindings['' + containerPort] = [
148
- {
149
- HostPort: '' + portInfo.hostPort,
150
- HostIp: bindHost,
144
+ const bindHost = getBindHost();
145
+ const ExposedPorts = {};
146
+ _.forEach(ports, (portInfo, containerPort) => {
147
+ ExposedPorts['' + containerPort] = {};
148
+ PortBindings['' + containerPort] = [
149
+ {
150
+ HostPort: '' + portInfo.hostPort,
151
+ HostIp: bindHost,
152
+ },
153
+ ];
154
+ Labels[CONTAINER_LABEL_PORT_PREFIX + portInfo.hostPort] = portInfo.type;
155
+ });
156
+ const Mounts = containerManager.toDockerMounts(mounts);
157
+ _.forEach(operatorData.env, (value, name) => {
158
+ Env.push(name + '=' + value);
159
+ });
160
+ let HealthCheck = undefined;
161
+ if (operatorData.health) {
162
+ HealthCheck = containerManager.toDockerHealth(operatorData.health);
163
+ }
164
+ const container = await containerManager.ensureContainer({
165
+ name: containerName,
166
+ Image: operatorData.image,
167
+ Hostname: containerName + '.kapeta',
168
+ Labels,
169
+ Cmd: operatorData.cmd,
170
+ ExposedPorts,
171
+ Env,
172
+ HealthCheck,
173
+ HostConfig: {
174
+ PortBindings,
175
+ Mounts,
151
176
  },
152
- ];
153
- Labels[CONTAINER_LABEL_PORT_PREFIX + portInfo.hostPort] = portInfo.type;
154
- });
155
- const Mounts = containerManager.toDockerMounts(mounts);
156
- _.forEach(operatorData.env, (value, name) => {
157
- Env.push(name + '=' + value);
158
- });
159
- let HealthCheck = undefined;
160
- if (operatorData.health) {
161
- HealthCheck = containerManager.toDockerHealth(operatorData.health);
162
- }
163
- const container = await containerManager.ensureContainer({
164
- name: containerName,
165
- Image: operatorData.image,
166
- Hostname: containerName + '.kapeta',
167
- Labels,
168
- Cmd: operatorData.cmd,
169
- ExposedPorts,
170
- Env,
171
- HealthCheck,
172
- HostConfig: {
173
- PortBindings,
174
- Mounts,
175
- },
177
+ });
178
+ await containerManager.waitForReady(container);
179
+ return new ContainerInfo(container);
176
180
  });
177
- return new ContainerInfo(container);
178
181
  }
179
182
  }
180
183
  export const operatorManager = new OperatorManager();
@@ -7,6 +7,7 @@ export declare class SocketManager {
7
7
  isAlive(): boolean;
8
8
  private get io();
9
9
  emit(context: string, type: string, payload: any): void;
10
+ emitGlobal(type: string, payload: any): void;
10
11
  _bindIO(): void;
11
12
  _handleSocketCreated(socket: Socket): void;
12
13
  _bindSocket(socket: Socket): void;
@@ -23,6 +23,9 @@ export class SocketManager {
23
23
  emit(context, type, payload) {
24
24
  this.io.to(context).emit(type, { context, payload });
25
25
  }
26
+ emitGlobal(type, payload) {
27
+ this.io.emit(type, { payload });
28
+ }
26
29
  _bindIO() {
27
30
  this.io.on('connection', (socket) => this._handleSocketCreated(socket));
28
31
  }
@@ -58,6 +58,7 @@ export type InstanceInfo = {
58
58
  type: InstanceType;
59
59
  owner: InstanceOwner;
60
60
  status: InstanceStatus;
61
+ errorMessage?: string;
61
62
  desiredStatus: DesiredInstanceStatus;
62
63
  address?: string;
63
64
  startedAt?: number;
@@ -194,7 +194,7 @@ export class BlockInstanceRunner {
194
194
  `KAPETA_LOCAL_CLUSTER_PORT=${clusterService.getClusterServicePort()}`,
195
195
  ...Object.entries({
196
196
  ...env,
197
- ...addonEnv
197
+ ...addonEnv,
198
198
  }).map(([key, value]) => `${key}=${value}`),
199
199
  ],
200
200
  HostConfig: {
@@ -318,13 +318,12 @@ export class BlockInstanceRunner {
318
318
  }
319
319
  async ensureContainer(opts) {
320
320
  const container = await containerManager.ensureContainer(opts);
321
- await containerManager.waitForReady(container);
322
321
  return this._handleContainer(container);
323
322
  }
324
323
  async _handleContainer(container) {
325
324
  return {
326
325
  type: InstanceType.DOCKER,
327
- pid: container.id
326
+ pid: container.id,
328
327
  };
329
328
  }
330
329
  }
@@ -1,7 +1,7 @@
1
1
  import FS from 'node:fs';
2
2
  import YAML from 'yaml';
3
3
  import { parseKapetaUri } from '@kapeta/nodejs-utils';
4
- import md5 from "md5";
4
+ import md5 from 'md5';
5
5
  export function getBlockInstanceContainerName(systemId, instanceId) {
6
6
  return `kapeta-block-instance-${md5(systemId + instanceId)}`;
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kapeta/local-cluster-service",
3
- "version": "0.11.1",
3
+ "version": "0.12.1",
4
4
  "description": "Manages configuration, ports and service discovery for locally running Kapeta systems",
5
5
  "type": "commonjs",
6
6
  "exports": {
@@ -41,7 +41,7 @@ router.put('/instance', async (req: KapetaBodyRequest, res) => {
41
41
  if (req.kapeta!.instanceId) {
42
42
  configManager.setConfigForSection(req.kapeta!.systemId, req.kapeta!.instanceId, config);
43
43
  //Restart the instance if it is running after config change
44
- await instanceManager.restart(req.kapeta!.systemId, req.kapeta!.instanceId);
44
+ await instanceManager.prepareForRestart(req.kapeta!.systemId, req.kapeta!.instanceId);
45
45
  } else {
46
46
  configManager.setConfigForSystem(req.kapeta!.systemId, config);
47
47
  }