@kapeta/local-cluster-service 0.10.0 → 0.11.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 (40) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/cjs/src/containerManager.d.ts +6 -4
  3. package/dist/cjs/src/containerManager.js +100 -45
  4. package/dist/cjs/src/definitionsManager.d.ts +1 -0
  5. package/dist/cjs/src/definitionsManager.js +7 -0
  6. package/dist/cjs/src/instanceManager.d.ts +2 -1
  7. package/dist/cjs/src/instanceManager.js +29 -46
  8. package/dist/cjs/src/instances/routes.js +10 -4
  9. package/dist/cjs/src/operatorManager.js +8 -6
  10. package/dist/cjs/src/repositoryManager.js +4 -4
  11. package/dist/cjs/src/types.d.ts +0 -9
  12. package/dist/cjs/src/utils/BlockInstanceRunner.d.ts +3 -2
  13. package/dist/cjs/src/utils/BlockInstanceRunner.js +49 -95
  14. package/dist/cjs/src/utils/utils.d.ts +1 -1
  15. package/dist/cjs/src/utils/utils.js +3 -2
  16. package/dist/esm/src/containerManager.d.ts +6 -4
  17. package/dist/esm/src/containerManager.js +100 -45
  18. package/dist/esm/src/definitionsManager.d.ts +1 -0
  19. package/dist/esm/src/definitionsManager.js +7 -0
  20. package/dist/esm/src/instanceManager.d.ts +2 -1
  21. package/dist/esm/src/instanceManager.js +29 -46
  22. package/dist/esm/src/instances/routes.js +10 -4
  23. package/dist/esm/src/operatorManager.js +8 -6
  24. package/dist/esm/src/repositoryManager.js +4 -4
  25. package/dist/esm/src/types.d.ts +0 -9
  26. package/dist/esm/src/utils/BlockInstanceRunner.d.ts +3 -2
  27. package/dist/esm/src/utils/BlockInstanceRunner.js +49 -95
  28. package/dist/esm/src/utils/utils.d.ts +1 -1
  29. package/dist/esm/src/utils/utils.js +3 -2
  30. package/package.json +1 -1
  31. package/src/containerManager.ts +126 -49
  32. package/src/definitionsManager.ts +8 -0
  33. package/src/instanceManager.ts +35 -50
  34. package/src/instances/routes.ts +9 -4
  35. package/src/operatorManager.ts +9 -8
  36. package/src/repositoryManager.ts +5 -5
  37. package/src/types.ts +0 -7
  38. package/src/utils/BlockInstanceRunner.ts +74 -109
  39. package/src/utils/LogData.ts +1 -0
  40. package/src/utils/utils.ts +3 -2
@@ -67,6 +67,37 @@ export class InstanceManager {
67
67
  return this._instances.find((i) => i.systemId === systemId && i.instanceId === instanceId);
68
68
  }
69
69
 
70
+
71
+ public async getLogs(systemId: string, instanceId: string):Promise<LogEntry[]> {
72
+ const instance = this.getInstance(systemId, instanceId);
73
+ if (!instance) {
74
+ throw new Error(`Instance ${systemId}/${instanceId} not found`);
75
+ }
76
+
77
+ switch (instance.type) {
78
+ case InstanceType.DOCKER:
79
+ return await containerManager.getLogs(instance);
80
+
81
+ case InstanceType.UNKNOWN:
82
+ return [{
83
+ level: 'INFO',
84
+ message: 'Instance is starting...',
85
+ time: Date.now(),
86
+ source: 'stdout',
87
+ }];
88
+
89
+ case InstanceType.LOCAL:
90
+ return [{
91
+ level: 'INFO',
92
+ message: 'Instance started outside Kapeta - logs not available...',
93
+ time: Date.now(),
94
+ source: 'stdout',
95
+ }];
96
+ }
97
+
98
+ return [];
99
+ }
100
+
70
101
  public async saveInternalInstance(instance: InstanceInfo) {
71
102
  instance.systemId = normalizeKapetaUri(instance.systemId);
72
103
  if (instance.ref) {
@@ -143,7 +174,6 @@ export class InstanceManager {
143
174
 
144
175
  instance.desiredStatus = info.desiredStatus;
145
176
  instance.owner = info.owner;
146
- instance.internal = undefined;
147
177
  instance.status = InstanceStatus.STARTING;
148
178
  instance.startedAt = Date.now();
149
179
  }
@@ -260,7 +290,7 @@ export class InstanceManager {
260
290
 
261
291
  try {
262
292
  if (instance.type === 'docker') {
263
- const containerName = getBlockInstanceContainerName(instance.instanceId);
293
+ const containerName = getBlockInstanceContainerName(instance.systemId, instance.instanceId);
264
294
  const container = await containerManager.getContainerByName(containerName);
265
295
  if (container) {
266
296
  try {
@@ -350,7 +380,7 @@ export class InstanceManager {
350
380
  name: blockAsset.data.metadata.name,
351
381
  desiredStatus: DesiredInstanceStatus.RUN,
352
382
  owner: InstanceOwner.INTERNAL,
353
- type: InstanceType.UNKNOWN,
383
+ type: existingInstance?.type ?? InstanceType.UNKNOWN,
354
384
  status: InstanceStatus.STARTING,
355
385
  startedAt: Date.now(),
356
386
  };
@@ -374,45 +404,6 @@ export class InstanceManager {
374
404
  const startTime = Date.now();
375
405
  try {
376
406
  const processInfo = await runner.start(blockRef, instanceId, instanceConfig);
377
- //emit stdout/stderr via sockets
378
- processInfo.output.on('data', (data: Buffer) => {
379
- const payload = {
380
- source: 'stdout',
381
- level: 'INFO',
382
- message: data.toString(),
383
- time: Date.now(),
384
- };
385
- this.emitInstanceEvent(systemId, instanceId, EVENT_INSTANCE_LOG, payload);
386
- });
387
-
388
- processInfo.output.on('exit', (exitCode: number) => {
389
- const timeRunning = Date.now() - startTime;
390
- const instance = this.getInstance(systemId, instanceId);
391
- if (instance?.status === InstanceStatus.READY) {
392
- //It's already been running
393
- return;
394
- }
395
-
396
- if (exitCode === 143 || exitCode === 137) {
397
- //Process got SIGTERM (143) or SIGKILL (137)
398
- //TODO: Windows?
399
- return;
400
- }
401
-
402
- if (exitCode !== 0 || timeRunning < MIN_TIME_RUNNING) {
403
- const instance = this.getInstance(systemId, instanceId);
404
- if (instance) {
405
- instance.status = InstanceStatus.FAILED;
406
- this.save();
407
- }
408
-
409
- this.emitSystemEvent(systemId, EVENT_INSTANCE_EXITED, {
410
- error: 'Failed to start instance',
411
- status: EVENT_INSTANCE_EXITED,
412
- instanceId: blockInstance.id,
413
- });
414
- }
415
- });
416
407
 
417
408
  instance.status = InstanceStatus.READY;
418
409
 
@@ -423,10 +414,6 @@ export class InstanceManager {
423
414
  health: null,
424
415
  portType: processInfo.portType,
425
416
  status: InstanceStatus.READY,
426
- internal: {
427
- logs: processInfo.logs,
428
- output: processInfo.output,
429
- },
430
417
  });
431
418
  } catch (e: any) {
432
419
  console.warn('Failed to start instance', e);
@@ -482,9 +469,7 @@ export class InstanceManager {
482
469
  storageService.put(
483
470
  'instances',
484
471
  this._instances.map((instance) => {
485
- const copy = { ...instance };
486
- delete copy.internal;
487
- return copy;
472
+ return { ...instance };
488
473
  })
489
474
  );
490
475
  } catch (e) {
@@ -612,7 +597,7 @@ export class InstanceManager {
612
597
 
613
598
  private async getExternalStatus(instance: InstanceInfo): Promise<InstanceStatus> {
614
599
  if (instance.type === InstanceType.DOCKER) {
615
- const containerName = getBlockInstanceContainerName(instance.instanceId);
600
+ const containerName = getBlockInstanceContainerName(instance.systemId, instance.instanceId);
616
601
  const container = await containerManager.getContainerByName(containerName);
617
602
  if (!container) {
618
603
  // If the container doesn't exist, we consider the instance stopped
@@ -74,16 +74,21 @@ router.post('/:systemId/:instanceId/stop', async (req: Request, res: Response) =
74
74
  /**
75
75
  * Get logs for instance in a plan
76
76
  */
77
- router.get('/:systemId/:instanceId/logs', (req: Request, res: Response) => {
77
+ router.get('/:systemId/:instanceId/logs', async (req: Request, res: Response) => {
78
78
  const instanceInfo = instanceManager.getInstance(req.params.systemId, req.params.instanceId);
79
79
  if (!instanceInfo) {
80
80
  res.status(404).send({ ok: false });
81
81
  return;
82
82
  }
83
83
 
84
- res.status(202).send({
85
- logs: instanceInfo.internal?.logs() ?? [],
86
- });
84
+ try {
85
+ const logs = await instanceManager.getLogs(req.params.systemId, req.params.instanceId);
86
+ res.status(200).send({
87
+ logs,
88
+ });
89
+ } catch (e:any) {
90
+ res.status(500).send({ ok: false, error: e.message });
91
+ }
87
92
  });
88
93
 
89
94
  /**
@@ -162,16 +162,12 @@ class OperatorManager {
162
162
 
163
163
  portTypes.sort();
164
164
 
165
- const containerBaseName = 'kapeta-resource';
166
-
167
- const nameParts = [resourceType.toLowerCase()];
168
-
169
165
  const ports: AnyMap = {};
170
166
 
171
167
  for (let i = 0; i < portTypes.length; i++) {
172
168
  const portType = portTypes[i];
173
169
  let containerPortInfo = operatorData.ports[portType];
174
- const hostPort = await serviceManager.ensureServicePort(resourceType, portType);
170
+ const hostPort = await serviceManager.ensureServicePort(systemId, resourceType, portType);
175
171
 
176
172
  if (typeof containerPortInfo === 'number' || typeof containerPortInfo === 'string') {
177
173
  containerPortInfo = { port: containerPortInfo, type: 'tcp' };
@@ -182,7 +178,6 @@ class OperatorManager {
182
178
  }
183
179
 
184
180
  const portId = containerPortInfo.port + '/' + containerPortInfo.type;
185
- nameParts.push(portType + '-' + portId + '-' + hostPort);
186
181
 
187
182
  ports[portId] = {
188
183
  type: portType,
@@ -190,9 +185,15 @@ class OperatorManager {
190
185
  };
191
186
  }
192
187
 
193
- const mounts = containerManager.createMounts(resourceType, operatorData.mounts);
188
+ const mounts = await containerManager.createMounts(systemId, resourceType, operatorData.mounts);
189
+
190
+ const nameParts = [
191
+ systemId,
192
+ resourceType.toLowerCase(),
193
+ version
194
+ ];
194
195
 
195
- const containerName = containerBaseName + '-' + md5(nameParts.join('_'));
196
+ const containerName = `kapeta-resource-${md5(nameParts.join('_'))}`;
196
197
 
197
198
  const PortBindings: { [key: string]: any } = {};
198
199
  const Env: string[] = [];
@@ -2,7 +2,7 @@ import FS from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import Path from 'node:path';
4
4
  import watch from 'recursive-watch';
5
- import FSExtra, { FSWatcher } from 'fs-extra';
5
+ import FSExtra from 'fs-extra';
6
6
  import ClusterConfiguration from '@kapeta/local-cluster-config';
7
7
  import { parseKapetaUri } from '@kapeta/nodejs-utils';
8
8
  import { socketManager } from './socketManager';
@@ -119,9 +119,11 @@ class RepositoryManager {
119
119
  this._installQueue.push(async () => {
120
120
  try {
121
121
  const normalizedRefs = refs.map((ref) => parseKapetaUri(ref).id);
122
- const filteredRefs = normalizedRefs.filter((ref) => !INSTALL_ATTEMPTED[ref]);
123
- console.log(filteredRefs);
122
+ const filteredRefs = normalizedRefs
123
+ .filter((ref) => !INSTALL_ATTEMPTED[ref])
124
+ .filter((ref) => !definitionsManager.exists(ref));
124
125
  if (filteredRefs.length > 0) {
126
+ console.log(`Auto-installing dependencies: ${filteredRefs.join(', ')}`);
125
127
  filteredRefs.forEach((ref) => (INSTALL_ATTEMPTED[ref] = true));
126
128
  //Auto-install missing asset
127
129
  try {
@@ -218,13 +220,11 @@ class RepositoryManager {
218
220
 
219
221
  this._cache[ref] = true;
220
222
  if (!installedAsset) {
221
- console.log(`Auto-installing missing asset: ${ref}`);
222
223
  await this._install([ref]);
223
224
  } else {
224
225
  //Ensure dependencies are installed
225
226
  const refs = assetVersion.dependencies.map((dep: Dependency) => dep.name);
226
227
  if (refs.length > 0) {
227
- console.log(`Auto-installing dependencies: ${refs.join(', ')}`);
228
228
  await this._install(refs);
229
229
  }
230
230
  }
package/src/types.ts CHANGED
@@ -53,10 +53,7 @@ export enum DesiredInstanceStatus {
53
53
  export type ProcessInfo = {
54
54
  type: InstanceType;
55
55
  pid?: number | string | null;
56
- output: EventEmitter;
57
56
  portType?: string;
58
- logs: () => LogEntry[];
59
- stop: () => Promise<void> | void;
60
57
  };
61
58
 
62
59
  export type InstanceInfo = {
@@ -75,10 +72,6 @@ export type InstanceInfo = {
75
72
  pid?: number | string | null;
76
73
  portType?: string;
77
74
 
78
- internal?: {
79
- output: EventEmitter;
80
- logs: () => LogEntry[];
81
- };
82
75
  };
83
76
 
84
77
  interface ResourceRef {
@@ -117,7 +117,7 @@ export class BlockInstanceRunner {
117
117
  if (blockUri.version === 'local') {
118
118
  processInfo = await this._startLocalProcess(blockInstance, blockUri, env, assetVersion);
119
119
  } else {
120
- processInfo = await this._startDockerProcess(blockInstance, blockUri, env);
120
+ processInfo = await this._startDockerProcess(blockInstance, blockUri, env, assetVersion);
121
121
  }
122
122
 
123
123
  if (portTypes.length > 0) {
@@ -169,37 +169,17 @@ export class BlockInstanceRunner {
169
169
  throw new Error(`Missing docker image information: ${JSON.stringify(localContainer)}`);
170
170
  }
171
171
 
172
- const containerName = getBlockInstanceContainerName(blockInstance.id);
173
-
172
+ const containerName = getBlockInstanceContainerName(this._systemId, blockInstance.id);
174
173
  const startCmd = localContainer.handlers?.onCreate ? localContainer.handlers.onCreate : '';
175
174
  const dockerOpts = localContainer.options ?? {};
176
175
  const homeDir = localContainer.userHome ? localContainer.userHome : '/root';
177
176
  const workingDir = localContainer.workingDir ? localContainer.workingDir : '/workspace';
178
177
 
179
- const bindHost = getBindHost();
180
-
181
- const ExposedPorts: AnyMap = {};
182
- const addonEnv: StringMap = {};
183
- const PortBindings: AnyMap = {};
184
-
185
- const portTypes = getProviderPorts(assetVersion);
186
- let port = 80;
187
- const promises = portTypes.map(async (portType) => {
188
- const publicPort = await serviceManager.ensureServicePort(this._systemId, blockInstance.id, portType);
189
- const thisPort = port++; //TODO: Not sure how we should handle multiple ports or non-HTTP ports
190
- const dockerPort = `${thisPort}/tcp`;
191
- ExposedPorts[dockerPort] = {};
192
- addonEnv[`KAPETA_LOCAL_SERVER_PORT_${portType.toUpperCase()}`] = '' + thisPort;
193
-
194
- PortBindings[dockerPort] = [
195
- {
196
- HostIp: bindHost,
197
- HostPort: `${publicPort}`,
198
- },
199
- ];
200
- });
201
-
202
- await Promise.all(promises);
178
+ const {
179
+ PortBindings,
180
+ ExposedPorts,
181
+ addonEnv
182
+ } = await this.getDockerPortBindings(blockInstance, assetVersion);
203
183
 
204
184
  let HealthCheck = undefined;
205
185
  if (localContainer.healthcheck) {
@@ -235,83 +215,7 @@ export class BlockInstanceRunner {
235
215
  });
236
216
  }
237
217
 
238
- private async ensureContainer(opts: any) {
239
- const logs = new LogData();
240
-
241
- const container = await containerManager.ensureContainer(opts);
242
-
243
- try {
244
- if (opts.HealthCheck) {
245
- await containerManager.waitForHealthy(container);
246
- } else {
247
- await containerManager.waitForReady(container);
248
- }
249
- } catch (e: any) {
250
- logs.addLog(e.message, 'ERROR');
251
- }
252
-
253
- return this._handleContainer(container, logs);
254
- }
255
-
256
- private async _handleContainer(
257
- container: Container,
258
- logs: LogData,
259
- deleteOnExit: boolean = false
260
- ): Promise<ProcessInfo> {
261
- let localContainer: Container | null = container;
262
- const logStream = (await container.logs({
263
- follow: true,
264
- stdout: true,
265
- stderr: true,
266
- tail: LogData.MAX_LINES,
267
- })) as EventEmitter;
268
-
269
- const outputEvents = new EventEmitter();
270
- logStream.on('data', (data) => {
271
- logs.addLog(data.toString());
272
- outputEvents.emit('data', data);
273
- });
274
-
275
- logStream.on('error', (data) => {
276
- logs.addLog(data.toString());
277
- outputEvents.emit('data', data);
278
- });
279
-
280
- logStream.on('close', async () => {
281
- const status = await container.status();
282
- const data = status.data as any;
283
- if (deleteOnExit) {
284
- try {
285
- await containerManager.remove(container);
286
- } catch (e: any) {}
287
- }
288
- outputEvents.emit('exit', data?.State?.ExitCode ?? 0);
289
- });
290
-
291
- return {
292
- type: InstanceType.DOCKER,
293
- pid: container.id,
294
- output: outputEvents,
295
- stop: async () => {
296
- if (!localContainer) {
297
- return;
298
- }
299
-
300
- try {
301
- await localContainer.stop();
302
- if (deleteOnExit) {
303
- await containerManager.remove(localContainer);
304
- }
305
- } catch (e) {}
306
- localContainer = null;
307
- },
308
- logs: () => {
309
- return logs.getLogs();
310
- },
311
- };
312
- }
313
-
314
- private async _startDockerProcess(blockInstance: BlockProcessParams, blockInfo: KapetaURI, env: StringMap) {
218
+ private async _startDockerProcess(blockInstance: BlockProcessParams, blockInfo: KapetaURI, env: StringMap, assetVersion: DefinitionInfo) {
315
219
  const { versionFile } = ClusterConfig.getRepositoryAssetInfoPath(
316
220
  blockInfo.handle,
317
221
  blockInfo.name,
@@ -333,8 +237,13 @@ export class BlockInstanceRunner {
333
237
  throw new Error(`Missing docker image information: ${JSON.stringify(versionInfo?.artifact?.details)}`);
334
238
  }
335
239
 
336
- const containerName = getBlockInstanceContainerName(blockInstance.id);
337
- const logs = new LogData();
240
+ const {
241
+ PortBindings,
242
+ ExposedPorts,
243
+ addonEnv
244
+ } = await this.getDockerPortBindings(blockInstance, assetVersion);
245
+
246
+ const containerName = getBlockInstanceContainerName(this._systemId, blockInstance.id);
338
247
 
339
248
  // For windows we need to default to root
340
249
  const innerHome = process.platform === 'win32' ? '/root/.kapeta' : ClusterConfig.getKapetaBasedir();
@@ -342,16 +251,22 @@ export class BlockInstanceRunner {
342
251
  return this.ensureContainer({
343
252
  Image: dockerImage,
344
253
  name: containerName,
254
+ ExposedPorts,
345
255
  Labels: {
346
256
  instance: blockInstance.id,
347
257
  },
348
258
  Env: [
349
259
  ...DOCKER_ENV_VARS,
350
260
  `KAPETA_LOCAL_CLUSTER_PORT=${clusterService.getClusterServicePort()}`,
351
- ...Object.entries(env).map(([key, value]) => `${key}=${value}`),
261
+ ...Object.entries({
262
+ ...env,
263
+ ...addonEnv
264
+ }).map(([key, value]) => `${key}=${value}`),
265
+
352
266
  ],
353
267
  HostConfig: {
354
268
  Binds: [`${toLocalBindVolume(ClusterConfig.getKapetaBasedir())}:${innerHome}`],
269
+ PortBindings,
355
270
  },
356
271
  });
357
272
  }
@@ -391,7 +306,8 @@ export class BlockInstanceRunner {
391
306
 
392
307
  const dockerImage = spec?.local?.image;
393
308
 
394
- const containerName = getBlockInstanceContainerName(blockInstance.id);
309
+ //We only want 1 operator per operator type - across all local systems
310
+ const containerName = getBlockInstanceContainerName(this._systemId, blockInstance.id);
395
311
  const logs = new LogData();
396
312
 
397
313
  const bindHost = getBindHost();
@@ -425,7 +341,7 @@ export class BlockInstanceRunner {
425
341
  }
426
342
 
427
343
  if (spec.local?.mounts) {
428
- const mounts = containerManager.createMounts(blockUri.id, spec.local.mounts);
344
+ const mounts = await containerManager.createMounts(this._systemId, blockUri.id, spec.local.mounts);
429
345
  Mounts = containerManager.toDockerMounts(mounts);
430
346
  }
431
347
 
@@ -471,4 +387,53 @@ export class BlockInstanceRunner {
471
387
 
472
388
  return out;
473
389
  }
390
+
391
+
392
+ private async getDockerPortBindings(blockInstance: BlockProcessParams, assetVersion: DefinitionInfo) {
393
+ const bindHost = getBindHost();
394
+ const ExposedPorts: AnyMap = {};
395
+ const addonEnv: StringMap = {};
396
+ const PortBindings: AnyMap = {};
397
+
398
+ const portTypes = getProviderPorts(assetVersion);
399
+ let port = 80;
400
+ const promises = portTypes.map(async (portType) => {
401
+ const publicPort = await serviceManager.ensureServicePort(this._systemId, blockInstance.id, portType);
402
+ const thisPort = port++; //TODO: Not sure how we should handle multiple ports or non-HTTP ports
403
+ const dockerPort = `${thisPort}/tcp`;
404
+ ExposedPorts[dockerPort] = {};
405
+ addonEnv[`KAPETA_LOCAL_SERVER_PORT_${portType.toUpperCase()}`] = '' + thisPort;
406
+
407
+ PortBindings[dockerPort] = [
408
+ {
409
+ HostIp: bindHost,
410
+ HostPort: `${publicPort}`,
411
+ },
412
+ ];
413
+ });
414
+
415
+
416
+ await Promise.all(promises);
417
+
418
+ return {PortBindings,ExposedPorts, addonEnv};
419
+ }
420
+
421
+ private async ensureContainer(opts: any) {
422
+ const container = await containerManager.ensureContainer(opts);
423
+
424
+ await containerManager.waitForReady(container);
425
+
426
+ return this._handleContainer(container);
427
+
428
+ }
429
+
430
+ private async _handleContainer(
431
+ container: Container
432
+ ): Promise<ProcessInfo> {
433
+
434
+ return {
435
+ type: InstanceType.DOCKER,
436
+ pid: container.id
437
+ };
438
+ }
474
439
  }
@@ -20,6 +20,7 @@ export class LogData {
20
20
  if (!msg.endsWith('\n')) {
21
21
  msg += '\n';
22
22
  }
23
+
23
24
  this.entries.push({
24
25
  time: Date.now(),
25
26
  message: msg,
@@ -1,9 +1,10 @@
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
5
 
5
- export function getBlockInstanceContainerName(instanceId: string) {
6
- return `kapeta-block-instance-${instanceId}`;
6
+ export function getBlockInstanceContainerName(systemId:string, instanceId: string) {
7
+ return `kapeta-block-instance-${md5(systemId + instanceId)}`;
7
8
  }
8
9
 
9
10
  export function normalizeKapetaUri(uri: string) {