@kapeta/local-cluster-service 0.10.1 → 0.11.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 (38) 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 +6 -2
  7. package/dist/cjs/src/instanceManager.js +240 -233
  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.js +9 -64
  13. package/dist/cjs/src/utils/utils.d.ts +1 -1
  14. package/dist/cjs/src/utils/utils.js +3 -2
  15. package/dist/esm/src/containerManager.d.ts +6 -4
  16. package/dist/esm/src/containerManager.js +100 -45
  17. package/dist/esm/src/definitionsManager.d.ts +1 -0
  18. package/dist/esm/src/definitionsManager.js +7 -0
  19. package/dist/esm/src/instanceManager.d.ts +6 -2
  20. package/dist/esm/src/instanceManager.js +240 -233
  21. package/dist/esm/src/instances/routes.js +10 -4
  22. package/dist/esm/src/operatorManager.js +8 -6
  23. package/dist/esm/src/repositoryManager.js +4 -4
  24. package/dist/esm/src/types.d.ts +0 -9
  25. package/dist/esm/src/utils/BlockInstanceRunner.js +9 -64
  26. package/dist/esm/src/utils/utils.d.ts +1 -1
  27. package/dist/esm/src/utils/utils.js +3 -2
  28. package/package.json +3 -1
  29. package/src/containerManager.ts +126 -49
  30. package/src/definitionsManager.ts +8 -0
  31. package/src/instanceManager.ts +270 -255
  32. package/src/instances/routes.ts +9 -4
  33. package/src/operatorManager.ts +9 -8
  34. package/src/repositoryManager.ts +5 -5
  35. package/src/types.ts +0 -7
  36. package/src/utils/BlockInstanceRunner.ts +10 -66
  37. package/src/utils/LogData.ts +1 -0
  38. package/src/utils/utils.ts +3 -2
@@ -107,9 +107,11 @@ class RepositoryManager {
107
107
  this._installQueue.push(async () => {
108
108
  try {
109
109
  const normalizedRefs = refs.map((ref) => parseKapetaUri(ref).id);
110
- const filteredRefs = normalizedRefs.filter((ref) => !INSTALL_ATTEMPTED[ref]);
111
- console.log(filteredRefs);
110
+ const filteredRefs = normalizedRefs
111
+ .filter((ref) => !INSTALL_ATTEMPTED[ref])
112
+ .filter((ref) => !definitionsManager.exists(ref));
112
113
  if (filteredRefs.length > 0) {
114
+ console.log(`Auto-installing dependencies: ${filteredRefs.join(', ')}`);
113
115
  filteredRefs.forEach((ref) => (INSTALL_ATTEMPTED[ref] = true));
114
116
  //Auto-install missing asset
115
117
  try {
@@ -200,14 +202,12 @@ class RepositoryManager {
200
202
  }
201
203
  this._cache[ref] = true;
202
204
  if (!installedAsset) {
203
- console.log(`Auto-installing missing asset: ${ref}`);
204
205
  await this._install([ref]);
205
206
  }
206
207
  else {
207
208
  //Ensure dependencies are installed
208
209
  const refs = assetVersion.dependencies.map((dep) => dep.name);
209
210
  if (refs.length > 0) {
210
- console.log(`Auto-installing dependencies: ${refs.join(', ')}`);
211
211
  await this._install(refs);
212
212
  }
213
213
  }
@@ -1,5 +1,3 @@
1
- /// <reference types="node" />
2
- import EventEmitter from 'events';
3
1
  import express from 'express';
4
2
  import { Resource } from '@kapeta/schemas';
5
3
  import { StringBodyRequest } from './middleware/stringBody';
@@ -50,10 +48,7 @@ export declare enum DesiredInstanceStatus {
50
48
  export type ProcessInfo = {
51
49
  type: InstanceType;
52
50
  pid?: number | string | null;
53
- output: EventEmitter;
54
51
  portType?: string;
55
- logs: () => LogEntry[];
56
- stop: () => Promise<void> | void;
57
52
  };
58
53
  export type InstanceInfo = {
59
54
  systemId: string;
@@ -69,10 +64,6 @@ export type InstanceInfo = {
69
64
  health?: string | null;
70
65
  pid?: number | string | null;
71
66
  portType?: string;
72
- internal?: {
73
- output: EventEmitter;
74
- logs: () => LogEntry[];
75
- };
76
67
  };
77
68
  interface ResourceRef {
78
69
  blockId: string;
@@ -5,7 +5,6 @@ import { parseKapetaUri } from '@kapeta/nodejs-utils';
5
5
  import { serviceManager } from '../serviceManager';
6
6
  import { containerManager, toLocalBindVolume } from '../containerManager';
7
7
  import { LogData } from './LogData';
8
- import EventEmitter from 'events';
9
8
  import { clusterService } from '../clusterService';
10
9
  import { InstanceType } from '../types';
11
10
  import { definitionsManager } from '../definitionsManager';
@@ -127,7 +126,7 @@ export class BlockInstanceRunner {
127
126
  if (!dockerImage) {
128
127
  throw new Error(`Missing docker image information: ${JSON.stringify(localContainer)}`);
129
128
  }
130
- const containerName = getBlockInstanceContainerName(blockInstance.id);
129
+ const containerName = getBlockInstanceContainerName(this._systemId, blockInstance.id);
131
130
  const startCmd = localContainer.handlers?.onCreate ? localContainer.handlers.onCreate : '';
132
131
  const dockerOpts = localContainer.options ?? {};
133
132
  const homeDir = localContainer.userHome ? localContainer.userHome : '/root';
@@ -180,7 +179,7 @@ export class BlockInstanceRunner {
180
179
  throw new Error(`Missing docker image information: ${JSON.stringify(versionInfo?.artifact?.details)}`);
181
180
  }
182
181
  const { PortBindings, ExposedPorts, addonEnv } = await this.getDockerPortBindings(blockInstance, assetVersion);
183
- const containerName = getBlockInstanceContainerName(blockInstance.id);
182
+ const containerName = getBlockInstanceContainerName(this._systemId, blockInstance.id);
184
183
  // For windows we need to default to root
185
184
  const innerHome = process.platform === 'win32' ? '/root/.kapeta' : ClusterConfig.getKapetaBasedir();
186
185
  return this.ensureContainer({
@@ -225,7 +224,8 @@ export class BlockInstanceRunner {
225
224
  throw new Error(`Provider did not have local image: ${providerRef}`);
226
225
  }
227
226
  const dockerImage = spec?.local?.image;
228
- const containerName = getBlockInstanceContainerName(blockInstance.id);
227
+ //We only want 1 operator per operator type - across all local systems
228
+ const containerName = getBlockInstanceContainerName(this._systemId, blockInstance.id);
229
229
  const logs = new LogData();
230
230
  const bindHost = getBindHost();
231
231
  const ExposedPorts = {};
@@ -252,7 +252,7 @@ export class BlockInstanceRunner {
252
252
  });
253
253
  }
254
254
  if (spec.local?.mounts) {
255
- const mounts = containerManager.createMounts(blockUri.id, spec.local.mounts);
255
+ const mounts = await containerManager.createMounts(this._systemId, blockUri.id, spec.local.mounts);
256
256
  Mounts = containerManager.toDockerMounts(mounts);
257
257
  }
258
258
  if (spec.local?.health) {
@@ -317,69 +317,14 @@ export class BlockInstanceRunner {
317
317
  return { PortBindings, ExposedPorts, addonEnv };
318
318
  }
319
319
  async ensureContainer(opts) {
320
- const logs = new LogData();
321
320
  const container = await containerManager.ensureContainer(opts);
322
- try {
323
- if (opts.HealthCheck) {
324
- await containerManager.waitForHealthy(container);
325
- }
326
- else {
327
- await containerManager.waitForReady(container);
328
- }
329
- }
330
- catch (e) {
331
- logs.addLog(e.message, 'ERROR');
332
- }
333
- return this._handleContainer(container, logs);
321
+ await containerManager.waitForReady(container);
322
+ return this._handleContainer(container);
334
323
  }
335
- async _handleContainer(container, logs, deleteOnExit = false) {
336
- let localContainer = container;
337
- const logStream = (await container.logs({
338
- follow: true,
339
- stdout: true,
340
- stderr: true,
341
- tail: LogData.MAX_LINES,
342
- }));
343
- const outputEvents = new EventEmitter();
344
- logStream.on('data', (data) => {
345
- logs.addLog(data.toString());
346
- outputEvents.emit('data', data);
347
- });
348
- logStream.on('error', (data) => {
349
- logs.addLog(data.toString());
350
- outputEvents.emit('data', data);
351
- });
352
- logStream.on('close', async () => {
353
- const status = await container.status();
354
- const data = status.data;
355
- if (deleteOnExit) {
356
- try {
357
- await containerManager.remove(container);
358
- }
359
- catch (e) { }
360
- }
361
- outputEvents.emit('exit', data?.State?.ExitCode ?? 0);
362
- });
324
+ async _handleContainer(container) {
363
325
  return {
364
326
  type: InstanceType.DOCKER,
365
- pid: container.id,
366
- output: outputEvents,
367
- stop: async () => {
368
- if (!localContainer) {
369
- return;
370
- }
371
- try {
372
- await localContainer.stop();
373
- if (deleteOnExit) {
374
- await containerManager.remove(localContainer);
375
- }
376
- }
377
- catch (e) { }
378
- localContainer = null;
379
- },
380
- logs: () => {
381
- return logs.getLogs();
382
- },
327
+ pid: container.id
383
328
  };
384
329
  }
385
330
  }
@@ -1,4 +1,4 @@
1
- export declare function getBlockInstanceContainerName(instanceId: string): string;
1
+ export declare function getBlockInstanceContainerName(systemId: string, instanceId: string): string;
2
2
  export declare function normalizeKapetaUri(uri: string): string;
3
3
  export declare function readYML(path: string): any;
4
4
  export declare function isWindows(): boolean;
@@ -1,8 +1,9 @@
1
1
  import FS from 'node:fs';
2
2
  import YAML from 'yaml';
3
3
  import { parseKapetaUri } from '@kapeta/nodejs-utils';
4
- export function getBlockInstanceContainerName(instanceId) {
5
- return `kapeta-block-instance-${instanceId}`;
4
+ import md5 from "md5";
5
+ export function getBlockInstanceContainerName(systemId, instanceId) {
6
+ return `kapeta-block-instance-${md5(systemId + instanceId)}`;
6
7
  }
7
8
  export function normalizeKapetaUri(uri) {
8
9
  if (!uri) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kapeta/local-cluster-service",
3
- "version": "0.10.1",
3
+ "version": "0.11.1",
4
4
  "description": "Manages configuration, ports and service discovery for locally running Kapeta systems",
5
5
  "type": "commonjs",
6
6
  "exports": {
@@ -50,6 +50,7 @@
50
50
  "@kapeta/nodejs-utils": "<2",
51
51
  "@kapeta/schemas": "^0.0.58",
52
52
  "@kapeta/sdk-config": "<2",
53
+ "async-lock": "^1.4.0",
53
54
  "express": "4.17.1",
54
55
  "express-promise-router": "^4.1.1",
55
56
  "fs-extra": "^11.1.0",
@@ -70,6 +71,7 @@
70
71
  "@kapeta/eslint-config": "^0.6.1",
71
72
  "@kapeta/prettier-config": "^0.6.0",
72
73
  "@tsconfig/node18": "^18.2.0",
74
+ "@types/async-lock": "^1.4.0",
73
75
  "@types/express": "^4.17.17",
74
76
  "@types/fs-extra": "^11.0.1",
75
77
  "@types/lodash": "^4.14.195",
@@ -9,6 +9,10 @@ import ClusterConfiguration from '@kapeta/local-cluster-config';
9
9
  import { Container } from 'node-docker-api/lib/container';
10
10
  import uuid from 'node-uuid';
11
11
  import md5 from 'md5';
12
+ import {getBlockInstanceContainerName} from "./utils/utils";
13
+ import {InstanceInfo, LogEntry, LogSource} from "./types";
14
+ import EventEmitter from "events";
15
+ import {LogData} from "./utils/LogData";
12
16
 
13
17
  type StringMap = { [key: string]: string };
14
18
 
@@ -63,9 +67,9 @@ const IMAGE_PULL_CACHE: { [key: string]: number } = {};
63
67
 
64
68
  export const HEALTH_CHECK_TIMEOUT = HEALTH_CHECK_INTERVAL * HEALTH_CHECK_MAX * 2;
65
69
 
66
- const promisifyStream = (stream: ReadStream) =>
70
+ const promisifyStream = (stream: ReadStream, handler:(d:string|Buffer) => void) =>
67
71
  new Promise((resolve, reject) => {
68
- stream.on('data', (d) => console.log(d.toString()));
72
+ stream.on('data', handler);
69
73
  stream.on('end', resolve);
70
74
  stream.on('error', reject);
71
75
  });
@@ -151,19 +155,30 @@ class ContainerManager {
151
155
  return this._alive;
152
156
  }
153
157
 
154
- getMountPoint(kind: string, mountName: string) {
155
- const kindUri = parseKapetaUri(kind);
156
- return Path.join(this._mountDir, kindUri.handle, kindUri.name, mountName);
158
+ getMountPoint(systemId:string, ref: string, mountName: string) {
159
+ const kindUri = parseKapetaUri(ref);
160
+ const systemUri = parseKapetaUri(systemId)
161
+ return Path.join(this._mountDir,
162
+ systemUri.handle,
163
+ systemUri.name,
164
+ systemUri.version,
165
+ kindUri.handle,
166
+ kindUri.name,
167
+ kindUri.version, mountName);
157
168
  }
158
169
 
159
- createMounts(kind: string, mountOpts: StringMap): StringMap {
170
+ async createMounts(systemId:string, kind: string, mountOpts: StringMap|null|undefined): Promise<StringMap> {
160
171
  const mounts: StringMap = {};
161
172
 
162
- _.forEach(mountOpts, (containerPath, mountName) => {
163
- const hostPath = this.getMountPoint(kind, mountName);
164
- FSExtra.mkdirpSync(hostPath);
165
- mounts[containerPath] = hostPath;
166
- });
173
+ if (mountOpts) {
174
+ const mountOptList = Object.entries(mountOpts);
175
+ for(const [mountName, containerPath] of mountOptList) {
176
+ const hostPath = this.getMountPoint(systemId, kind, mountName);
177
+ await FSExtra.mkdirp(hostPath);
178
+ mounts[containerPath] = hostPath;
179
+ }
180
+ }
181
+
167
182
  return mounts;
168
183
  }
169
184
 
@@ -224,15 +239,18 @@ class ContainerManager {
224
239
  }
225
240
 
226
241
  console.log('Pulling image: %s', image);
227
- await this.docker()
242
+ const stream = await this.docker()
228
243
  .image.create(
229
244
  {},
230
245
  {
231
246
  fromImage: imageName,
232
247
  tag: tag,
233
248
  }
234
- )
235
- .then((stream) => promisifyStream(stream as ReadStream));
249
+ ) as ReadStream;
250
+
251
+ await promisifyStream(stream, (chunk) => {
252
+ console.log('Data from docker: "%s"', chunk.toString());
253
+ });
236
254
 
237
255
  IMAGE_PULL_CACHE[image] = Date.now();
238
256
 
@@ -278,7 +296,15 @@ class ContainerManager {
278
296
  dockerOpts.Labels.HASH = hash;
279
297
  }
280
298
 
281
- async ensureContainer(opts: any) {
299
+ public async ensureContainer(opts: any) {
300
+ const container = await this.createOrUpdateContainer(opts);
301
+
302
+ await this.waitForReady(container);
303
+
304
+ return container;
305
+ }
306
+
307
+ private async createOrUpdateContainer(opts: any) {
282
308
  let imagePulled = false;
283
309
  try {
284
310
  imagePulled = await this.pull(opts.Image);
@@ -369,31 +395,6 @@ class ContainerManager {
369
395
  });
370
396
  }
371
397
 
372
- async waitForHealthy(container: Container, attempt?: number): Promise<void> {
373
- if (!attempt) {
374
- attempt = 0;
375
- }
376
-
377
- if (attempt >= HEALTH_CHECK_MAX) {
378
- throw new Error('Container did not become healthy within the timeout');
379
- }
380
-
381
- if (await this._isHealthy(container)) {
382
- return;
383
- }
384
-
385
- return new Promise((resolve, reject) => {
386
- setTimeout(async () => {
387
- try {
388
- await this.waitForHealthy(container, (attempt ?? 0) + 1);
389
- resolve();
390
- } catch (err) {
391
- reject(err);
392
- }
393
- }, HEALTH_CHECK_INTERVAL);
394
- });
395
- }
396
-
397
398
  async _isReady(container: Container) {
398
399
  let info: Container;
399
400
  try {
@@ -403,19 +404,16 @@ class ContainerManager {
403
404
  }
404
405
  const infoData: any = info?.data;
405
406
  const state = infoData?.State as DockerState;
407
+
406
408
  if (state?.Status === 'exited' || state?.Status === 'removing' || state?.Status === 'dead') {
407
409
  throw new Error('Container exited unexpectedly');
408
410
  }
409
- return infoData?.State?.Running ?? false;
410
- }
411
411
 
412
- async _isHealthy(container: Container) {
413
- try {
414
- const info = await container.status();
415
- const infoData: any = info?.data;
416
- return infoData?.State?.Health?.Status === 'healthy';
417
- } catch (err) {
418
- return false;
412
+ if (infoData?.State?.Health) {
413
+ // If container has health info - wait for it to become healthy
414
+ return infoData.State.Health.Status === 'healthy';
415
+ } else {
416
+ return infoData?.State?.Running ?? false;
419
417
  }
420
418
  }
421
419
 
@@ -449,6 +447,21 @@ class ContainerManager {
449
447
 
450
448
  return new ContainerInfo(dockerContainer);
451
449
  }
450
+
451
+ async getLogs(instance: InstanceInfo):Promise<LogEntry[]> {
452
+ const containerName = getBlockInstanceContainerName(instance.systemId, instance.instanceId);
453
+ const containerInfo = await this.getContainerByName(containerName);
454
+ if (!containerInfo) {
455
+ return [{
456
+ source: "stdout",
457
+ level: "ERROR",
458
+ time: Date.now(),
459
+ message: "Container not found"
460
+ }];
461
+ }
462
+
463
+ return containerInfo.getLogs()
464
+ }
452
465
  }
453
466
 
454
467
  export class ContainerInfo {
@@ -558,6 +571,70 @@ export class ContainerInfo {
558
571
 
559
572
  return ports;
560
573
  }
574
+
575
+ async getLogs():Promise<LogEntry[]> {
576
+
577
+ const logStream = await this.native.logs({
578
+ stdout: true,
579
+ stderr: true,
580
+ follow: false,
581
+ tail: 100,
582
+ timestamps: true,
583
+ }) as ReadStream;
584
+
585
+ const out = [] as LogEntry[];
586
+ await promisifyStream(logStream, (data) => {
587
+ const buf = data as Buffer;
588
+ let offset = 0;
589
+ while(offset < buf.length) {
590
+ try {
591
+ // Read the docker log format - explained here:
592
+ // https://docs.docker.com/engine/api/v1.41/#operation/ContainerAttach
593
+ // or here : https://ahmet.im/blog/docker-logs-api-binary-format-explained/
594
+
595
+ // First byte is stream type
596
+ const streamTypeInt = buf.readInt8(offset);
597
+ const streamType:LogSource = streamTypeInt === 1 ? 'stdout' : 'stderr';
598
+
599
+ // Bytes 4-8 is frame size
600
+ const messageLength = buf.readInt32BE(offset + 4);
601
+
602
+ // After that is the message - with the message length
603
+ const dataWithoutStreamType = buf.subarray(offset + 8, offset + 8 + messageLength);
604
+ const raw = dataWithoutStreamType.toString();
605
+
606
+ // Split the message into date and message
607
+ const firstSpaceIx = raw.indexOf(' ');
608
+ const dateString = raw.substring(0, firstSpaceIx);
609
+ const line = raw.substring(firstSpaceIx + 1);
610
+ offset = offset + messageLength + 8;
611
+ if (!dateString) {
612
+ continue;
613
+ }
614
+ out.push({
615
+ time: new Date(dateString).getTime(),
616
+ message: line,
617
+ level: 'INFO',
618
+ source: streamType,
619
+ });
620
+ } catch (err) {
621
+ console.error('Error parsing log entry', err);
622
+ offset = buf.length
623
+ }
624
+ }
625
+ });
626
+
627
+ if (out.length === 0) {
628
+ out.push({
629
+ time: Date.now(),
630
+ message: 'No logs found for container',
631
+ level: 'INFO',
632
+ source: 'stdout',
633
+ });
634
+ }
635
+
636
+ return out;
637
+ }
561
638
  }
562
639
 
563
640
  export function getExtraHosts(dockerVersion: string): string[] | undefined {
@@ -1,4 +1,5 @@
1
1
  import ClusterConfiguration, { DefinitionInfo } from '@kapeta/local-cluster-config';
2
+ import {parseKapetaUri} from "@kapeta/nodejs-utils";
2
3
 
3
4
  const CACHE_TTL = 60 * 1000; // 1 min
4
5
 
@@ -46,6 +47,13 @@ class DefinitionsManager {
46
47
  return this.doCached(key, () => ClusterConfiguration.getDefinitions(kindFilter));
47
48
  }
48
49
 
50
+ public exists(ref: string) {
51
+ const uri = parseKapetaUri(ref);
52
+ return !!this.getDefinitions().find((d) => {
53
+ return parseKapetaUri(`${d.definition.metadata.name}:${d.version}`).id === uri.id;
54
+ });
55
+ }
56
+
49
57
  public getProviderDefinitions() {
50
58
  return this.doCached('providers', () => ClusterConfiguration.getProviderDefinitions());
51
59
  }