@kapeta/local-cluster-service 0.18.0 → 0.19.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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## [0.19.1](https://github.com/kapetacom/local-cluster-service/compare/v0.19.0...v0.19.1) (2023-09-03)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * Removed debug ([feeac46](https://github.com/kapetacom/local-cluster-service/commit/feeac462856a6a375d0f80e1098a25bbcaba0a70))
7
+
8
+ # [0.19.0](https://github.com/kapetacom/local-cluster-service/compare/v0.18.0...v0.19.0) (2023-09-03)
9
+
10
+
11
+ ### Features
12
+
13
+ * Listen for docker logs when user joins room ([#67](https://github.com/kapetacom/local-cluster-service/issues/67)) ([53bd6b6](https://github.com/kapetacom/local-cluster-service/commit/53bd6b6b6de27dc2f9011887176f270d60a0d9dc))
14
+
1
15
  # [0.18.0](https://github.com/kapetacom/local-cluster-service/compare/v0.17.0...v0.18.0) (2023-09-02)
2
16
 
3
17
 
@@ -1,3 +1,5 @@
1
+ /// <reference types="node" />
2
+ import FSExtra from 'fs-extra';
1
3
  import { Docker } from 'node-docker-api';
2
4
  import { Container } from 'node-docker-api/lib/container';
3
5
  import { InstanceInfo, LogEntry } from './types';
@@ -50,6 +52,7 @@ declare class ContainerManager {
50
52
  private _mountDir;
51
53
  private _version;
52
54
  private _lastDockerAccessCheck;
55
+ private logStreams;
53
56
  constructor();
54
57
  initialize(): Promise<void>;
55
58
  checkAlive(): Promise<boolean>;
@@ -83,6 +86,17 @@ declare class ContainerManager {
83
86
  */
84
87
  get(name: string): Promise<ContainerInfo | null>;
85
88
  getLogs(instance: InstanceInfo): Promise<LogEntry[]>;
89
+ stopLogListening(systemId: string, instanceId: string): Promise<void>;
90
+ ensureLogListening(systemId: string, instanceId: string, handler: (log: LogEntry) => void): Promise<void>;
91
+ }
92
+ declare class ClosableLogStream {
93
+ private readonly stream;
94
+ private readonly eventEmitter;
95
+ constructor(stream: FSExtra.ReadStream);
96
+ onLog(listener: (log: LogEntry) => void): () => void;
97
+ onEnd(listener: () => void): () => void;
98
+ onError(listener: (error: Error) => void): () => void;
99
+ close(): Promise<void>;
86
100
  }
87
101
  export declare class ContainerInfo {
88
102
  private readonly _container;
@@ -107,6 +121,7 @@ export declare class ContainerInfo {
107
121
  inspect(): Promise<any>;
108
122
  status(): Promise<DockerState>;
109
123
  getPorts(): Promise<PortMap | false>;
124
+ getLogStream(): Promise<ClosableLogStream>;
110
125
  getLogs(): Promise<LogEntry[]>;
111
126
  }
112
127
  export declare function getExtraHosts(dockerVersion: string): string[] | undefined;
@@ -17,6 +17,7 @@ const md5_1 = __importDefault(require("md5"));
17
17
  const utils_1 = require("./utils/utils");
18
18
  const nodejs_api_client_1 = require("@kapeta/nodejs-api-client");
19
19
  const taskManager_1 = require("./taskManager");
20
+ const node_events_1 = require("node:events");
20
21
  exports.CONTAINER_LABEL_PORT_PREFIX = 'kapeta_port-';
21
22
  const NANO_SECOND = 1000000;
22
23
  const HEALTH_CHECK_INTERVAL = 3000;
@@ -33,6 +34,7 @@ class ContainerManager {
33
34
  _mountDir;
34
35
  _version;
35
36
  _lastDockerAccessCheck = 0;
37
+ logStreams = {};
36
38
  constructor() {
37
39
  this._docker = null;
38
40
  this._alive = false;
@@ -472,6 +474,174 @@ class ContainerManager {
472
474
  }
473
475
  return containerInfo.getLogs();
474
476
  }
477
+ async stopLogListening(systemId, instanceId) {
478
+ const containerName = (0, utils_1.getBlockInstanceContainerName)(systemId, instanceId);
479
+ if (this.logStreams[containerName]) {
480
+ if (this.logStreams[containerName]?.timer) {
481
+ clearTimeout(this.logStreams[containerName].timer);
482
+ }
483
+ try {
484
+ const stream = this.logStreams[containerName].stream;
485
+ if (stream) {
486
+ await stream.close();
487
+ }
488
+ }
489
+ catch (err) {
490
+ // Ignore
491
+ }
492
+ delete this.logStreams[containerName];
493
+ }
494
+ }
495
+ async ensureLogListening(systemId, instanceId, handler) {
496
+ const containerName = (0, utils_1.getBlockInstanceContainerName)(systemId, instanceId);
497
+ try {
498
+ if (this.logStreams[containerName]?.stream) {
499
+ // Already listening - will shut itself down
500
+ return;
501
+ }
502
+ if (this.logStreams[containerName]?.timer) {
503
+ clearTimeout(this.logStreams[containerName].timer);
504
+ }
505
+ const tryLater = () => {
506
+ this.logStreams[containerName] = {
507
+ timer: setTimeout(() => {
508
+ // Keep trying until user decides to not listen anymore
509
+ this.ensureLogListening(systemId, instanceId, handler);
510
+ }, 5000),
511
+ };
512
+ };
513
+ const containerInfo = await this.getContainerByName(containerName);
514
+ if (!containerInfo || !(await containerInfo.isRunning())) {
515
+ // Container not currently running - try again in 5 seconds
516
+ tryLater();
517
+ return;
518
+ }
519
+ const stream = await containerInfo.getLogStream();
520
+ stream.onLog((log) => {
521
+ try {
522
+ handler(log);
523
+ }
524
+ catch (err) {
525
+ console.warn('Error handling log', err);
526
+ }
527
+ });
528
+ stream.onEnd(() => {
529
+ // We get here if the container is stopped
530
+ delete this.logStreams[containerName];
531
+ tryLater();
532
+ });
533
+ stream.onError((err) => {
534
+ // We get here if the container crashes
535
+ delete this.logStreams[containerName];
536
+ tryLater();
537
+ });
538
+ this.logStreams[containerName] = {
539
+ stream,
540
+ };
541
+ }
542
+ catch (err) {
543
+ // Ignore
544
+ }
545
+ }
546
+ }
547
+ function readLogBuffer(logBuffer) {
548
+ const out = [];
549
+ let offset = 0;
550
+ while (offset < logBuffer.length) {
551
+ try {
552
+ // Read the docker log format - explained here:
553
+ // https://docs.docker.com/engine/api/v1.41/#operation/ContainerAttach
554
+ // or here : https://ahmet.im/blog/docker-logs-api-binary-format-explained/
555
+ // First byte is stream type
556
+ const streamTypeInt = logBuffer.readInt8(offset);
557
+ const streamType = streamTypeInt === 1 ? 'stdout' : 'stderr';
558
+ if (streamTypeInt !== 1 && streamTypeInt !== 2) {
559
+ console.error('Unknown stream type: %s', streamTypeInt, out[out.length - 1]);
560
+ break;
561
+ }
562
+ // Bytes 4-8 is frame size
563
+ const messageLength = logBuffer.readInt32BE(offset + 4);
564
+ // After that is the message - with the message length
565
+ const dataWithoutStreamType = logBuffer.subarray(offset + 8, offset + 8 + messageLength);
566
+ const raw = dataWithoutStreamType.toString();
567
+ // Split the message into date and message
568
+ const firstSpaceIx = raw.indexOf(' ');
569
+ const dateString = raw.substring(0, firstSpaceIx);
570
+ const line = raw.substring(firstSpaceIx + 1);
571
+ offset = offset + messageLength + 8;
572
+ if (!dateString) {
573
+ break;
574
+ }
575
+ out.push({
576
+ time: new Date(dateString).getTime(),
577
+ message: line,
578
+ level: 'INFO',
579
+ source: streamType,
580
+ });
581
+ }
582
+ catch (err) {
583
+ console.error('Error parsing log entry', err);
584
+ offset = logBuffer.length;
585
+ break;
586
+ }
587
+ }
588
+ return out;
589
+ }
590
+ class ClosableLogStream {
591
+ stream;
592
+ eventEmitter;
593
+ constructor(stream) {
594
+ this.stream = stream;
595
+ this.eventEmitter = new node_events_1.EventEmitter();
596
+ stream.on('data', (data) => {
597
+ const logs = readLogBuffer(data);
598
+ logs.forEach((log) => {
599
+ this.eventEmitter.emit('log', log);
600
+ });
601
+ });
602
+ stream.on('end', () => {
603
+ this.eventEmitter.emit('end');
604
+ });
605
+ stream.on('error', (error) => {
606
+ this.eventEmitter.emit('error', error);
607
+ });
608
+ stream.on('close', () => {
609
+ this.eventEmitter.emit('end');
610
+ });
611
+ }
612
+ onLog(listener) {
613
+ this.eventEmitter.on('log', listener);
614
+ return () => {
615
+ this.eventEmitter.removeListener('log', listener);
616
+ };
617
+ }
618
+ onEnd(listener) {
619
+ this.eventEmitter.on('end', listener);
620
+ return () => {
621
+ this.eventEmitter.removeListener('end', listener);
622
+ };
623
+ }
624
+ onError(listener) {
625
+ this.eventEmitter.on('error', listener);
626
+ return () => {
627
+ this.eventEmitter.removeListener('error', listener);
628
+ };
629
+ }
630
+ close() {
631
+ return new Promise((resolve, reject) => {
632
+ try {
633
+ this.stream.close((err) => {
634
+ if (err) {
635
+ console.warn('Error closing log stream', err);
636
+ }
637
+ resolve();
638
+ });
639
+ }
640
+ catch (err) {
641
+ // Ignore
642
+ }
643
+ });
644
+ }
475
645
  }
476
646
  class ContainerInfo {
477
647
  _container;
@@ -555,52 +725,34 @@ class ContainerInfo {
555
725
  });
556
726
  return ports;
557
727
  }
728
+ async getLogStream() {
729
+ try {
730
+ const logStream = (await this.native.logs({
731
+ stdout: true,
732
+ stderr: true,
733
+ follow: true,
734
+ tail: 0,
735
+ timestamps: true,
736
+ }));
737
+ return new ClosableLogStream(logStream);
738
+ }
739
+ catch (err) {
740
+ console.log('Error getting log stream', err);
741
+ throw err;
742
+ }
743
+ }
558
744
  async getLogs() {
559
745
  const logStream = (await this.native.logs({
560
746
  stdout: true,
561
747
  stderr: true,
562
748
  follow: false,
563
- tail: 100,
564
749
  timestamps: true,
565
750
  }));
566
- const out = [];
751
+ const chunks = [];
567
752
  await promisifyStream(logStream, (data) => {
568
- const buf = data;
569
- let offset = 0;
570
- while (offset < buf.length) {
571
- try {
572
- // Read the docker log format - explained here:
573
- // https://docs.docker.com/engine/api/v1.41/#operation/ContainerAttach
574
- // or here : https://ahmet.im/blog/docker-logs-api-binary-format-explained/
575
- // First byte is stream type
576
- const streamTypeInt = buf.readInt8(offset);
577
- const streamType = streamTypeInt === 1 ? 'stdout' : 'stderr';
578
- // Bytes 4-8 is frame size
579
- const messageLength = buf.readInt32BE(offset + 4);
580
- // After that is the message - with the message length
581
- const dataWithoutStreamType = buf.subarray(offset + 8, offset + 8 + messageLength);
582
- const raw = dataWithoutStreamType.toString();
583
- // Split the message into date and message
584
- const firstSpaceIx = raw.indexOf(' ');
585
- const dateString = raw.substring(0, firstSpaceIx);
586
- const line = raw.substring(firstSpaceIx + 1);
587
- offset = offset + messageLength + 8;
588
- if (!dateString) {
589
- continue;
590
- }
591
- out.push({
592
- time: new Date(dateString).getTime(),
593
- message: line,
594
- level: 'INFO',
595
- source: streamType,
596
- });
597
- }
598
- catch (err) {
599
- console.error('Error parsing log entry', err);
600
- offset = buf.length;
601
- }
602
- }
753
+ chunks.push(data);
603
754
  });
755
+ const out = readLogBuffer(Buffer.concat(chunks));
604
756
  if (out.length === 0) {
605
757
  out.push({
606
758
  time: Date.now(),
@@ -422,11 +422,6 @@ class InstanceManager {
422
422
  errorMessage: e.message ?? 'Failed to start - Check logs for details.',
423
423
  });
424
424
  socketManager_1.socketManager.emitInstanceLog(systemId, instanceId, logs[0]);
425
- socketManager_1.socketManager.emitInstanceEvent(systemId, blockInstance.id, socketManager_1.EVENT_INSTANCE_EXITED, {
426
- error: `Failed to start instance: ${e.message}`,
427
- status: socketManager_1.EVENT_INSTANCE_EXITED,
428
- instanceId: blockInstance.id,
429
- });
430
425
  return out;
431
426
  }
432
427
  }, {
@@ -486,6 +481,7 @@ class InstanceManager {
486
481
  try {
487
482
  const plan = await assetManager_1.assetManager.getAsset(instance.systemId, true, false);
488
483
  if (!plan) {
484
+ console.log('Plan not found - reset to stop', instance.ref, instance.systemId);
489
485
  instance.desiredStatus = types_1.DesiredInstanceStatus.STOP;
490
486
  changed = true;
491
487
  return;
@@ -493,12 +489,14 @@ class InstanceManager {
493
489
  const planData = plan.data;
494
490
  const planInstance = planData?.spec?.blocks?.find((b) => b.id === instance.instanceId);
495
491
  if (!planInstance || !planInstance?.block?.ref) {
492
+ console.log('Plan instance not found - reset to stop', instance.ref, instance.systemId);
496
493
  instance.desiredStatus = types_1.DesiredInstanceStatus.STOP;
497
494
  changed = true;
498
495
  return;
499
496
  }
500
497
  const blockDef = await assetManager_1.assetManager.getAsset(instance.ref, true, false);
501
498
  if (!blockDef) {
499
+ console.log('Block definition not found - reset to stop', instance.ref, instance.systemId);
502
500
  instance.desiredStatus = types_1.DesiredInstanceStatus.STOP;
503
501
  changed = true;
504
502
  return;
@@ -676,6 +674,3 @@ class InstanceManager {
676
674
  }
677
675
  exports.InstanceManager = InstanceManager;
678
676
  exports.instanceManager = new InstanceManager();
679
- process.on('exit', async () => {
680
- await exports.instanceManager.stopAll();
681
- });
@@ -19,10 +19,10 @@ export declare class SocketManager {
19
19
  emitInstanceLog(systemId: string, instanceId: string, payload: LogEntry): void;
20
20
  emitSystemLog(systemId: string, payload: LogEntry): void;
21
21
  emitGlobalLog(payload: LogEntry): void;
22
- emitInstanceEvent(systemId: string, instanceId: string, type: string, payload: any): void;
23
22
  private _bindIO;
24
23
  private _handleSocketCreated;
25
24
  private _bindSocket;
26
- private _handleSocketDestroyed;
25
+ private handleJoinRoom;
26
+ private handleLeaveRoom;
27
27
  }
28
28
  export declare const socketManager: SocketManager;
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.socketManager = exports.SocketManager = exports.EVENT_LOG = exports.EVENT_SYSTEM_LOG = exports.EVENT_INSTANCE_LOG = exports.EVENT_INSTANCE_EXITED = exports.EVENT_INSTANCE_CREATED = exports.EVENT_STATUS_CHANGED = void 0;
7
7
  const lodash_1 = __importDefault(require("lodash"));
8
8
  const utils_1 = require("./utils/utils");
9
+ const containerManager_1 = require("./containerManager");
9
10
  exports.EVENT_STATUS_CHANGED = 'status-changed';
10
11
  exports.EVENT_INSTANCE_CREATED = 'instance-created';
11
12
  exports.EVENT_INSTANCE_EXITED = 'instance-exited';
@@ -48,30 +49,28 @@ class SocketManager {
48
49
  emitSystemEvent(systemId, type, payload) {
49
50
  systemId = (0, utils_1.normalizeKapetaUri)(systemId);
50
51
  try {
51
- exports.socketManager.emit(`${systemId}/instances`, type, payload);
52
+ const contextId = `system-events/${encodeURIComponent(systemId)}`;
53
+ this.emit(contextId, type, payload);
52
54
  }
53
55
  catch (e) {
54
56
  console.warn('Failed to emit instance event: %s', e.message);
55
57
  }
56
58
  }
57
59
  emitInstanceLog(systemId, instanceId, payload) {
58
- this.emitInstanceEvent(systemId, instanceId, exports.EVENT_INSTANCE_LOG, payload);
59
- }
60
- emitSystemLog(systemId, payload) {
61
- this.emitSystemEvent(systemId, exports.EVENT_SYSTEM_LOG, payload);
62
- }
63
- emitGlobalLog(payload) {
64
- this.emitGlobal(exports.EVENT_LOG, payload);
65
- }
66
- emitInstanceEvent(systemId, instanceId, type, payload) {
67
60
  systemId = (0, utils_1.normalizeKapetaUri)(systemId);
68
61
  try {
69
- exports.socketManager.emit(`${systemId}/instances/${instanceId}`, type, payload);
62
+ this.emit(`instance-logs/${encodeURIComponent(systemId)}/${encodeURIComponent(instanceId)}`, exports.EVENT_INSTANCE_LOG, payload);
70
63
  }
71
64
  catch (e) {
72
65
  console.warn('Failed to emit instance event: %s', e.message);
73
66
  }
74
67
  }
68
+ emitSystemLog(systemId, payload) {
69
+ this.emitSystemEvent(systemId, exports.EVENT_SYSTEM_LOG, payload);
70
+ }
71
+ emitGlobalLog(payload) {
72
+ this.emitGlobal(exports.EVENT_LOG, payload);
73
+ }
75
74
  _bindIO() {
76
75
  this.io.on('connection', (socket) => this._handleSocketCreated(socket));
77
76
  }
@@ -80,16 +79,42 @@ class SocketManager {
80
79
  this._sockets.push(socket);
81
80
  }
82
81
  _bindSocket(socket) {
83
- socket.on('disconnect', () => this._handleSocketDestroyed(socket));
82
+ socket.on('disconnect', () => {
83
+ lodash_1.default.pull(this._sockets, socket);
84
+ socket.rooms.forEach((roomId) => {
85
+ this.handleLeaveRoom(roomId);
86
+ });
87
+ });
84
88
  socket.on('join', (id) => {
85
89
  socket.join(id);
90
+ this.handleJoinRoom(id);
86
91
  });
87
92
  socket.on('leave', (id) => {
88
93
  socket.leave(id);
94
+ this.handleLeaveRoom(id);
89
95
  });
90
96
  }
91
- _handleSocketDestroyed(socket) {
92
- lodash_1.default.pull(this._sockets, socket);
97
+ handleJoinRoom(id) {
98
+ if (id.startsWith('instance-logs/')) {
99
+ let [, systemId, instanceId] = id.split(/\//g);
100
+ systemId = decodeURIComponent(systemId);
101
+ instanceId = decodeURIComponent(instanceId);
102
+ console.log('Start listening for logs', systemId, instanceId);
103
+ containerManager_1.containerManager
104
+ .ensureLogListening(systemId, instanceId, (log) => {
105
+ this.emitInstanceLog(systemId, instanceId, log);
106
+ })
107
+ .catch((e) => { });
108
+ }
109
+ }
110
+ handleLeaveRoom(id) {
111
+ if (id.startsWith('instance-logs/')) {
112
+ let [, systemId, instanceId] = id.split(/\//g);
113
+ systemId = decodeURIComponent(systemId);
114
+ instanceId = decodeURIComponent(instanceId);
115
+ console.log('Stop listening for logs', systemId, instanceId);
116
+ containerManager_1.containerManager.stopLogListening(systemId, instanceId).catch((e) => { });
117
+ }
93
118
  }
94
119
  }
95
120
  exports.SocketManager = SocketManager;
@@ -1,3 +1,5 @@
1
+ /// <reference types="node" />
2
+ import FSExtra from 'fs-extra';
1
3
  import { Docker } from 'node-docker-api';
2
4
  import { Container } from 'node-docker-api/lib/container';
3
5
  import { InstanceInfo, LogEntry } from './types';
@@ -50,6 +52,7 @@ declare class ContainerManager {
50
52
  private _mountDir;
51
53
  private _version;
52
54
  private _lastDockerAccessCheck;
55
+ private logStreams;
53
56
  constructor();
54
57
  initialize(): Promise<void>;
55
58
  checkAlive(): Promise<boolean>;
@@ -83,6 +86,17 @@ declare class ContainerManager {
83
86
  */
84
87
  get(name: string): Promise<ContainerInfo | null>;
85
88
  getLogs(instance: InstanceInfo): Promise<LogEntry[]>;
89
+ stopLogListening(systemId: string, instanceId: string): Promise<void>;
90
+ ensureLogListening(systemId: string, instanceId: string, handler: (log: LogEntry) => void): Promise<void>;
91
+ }
92
+ declare class ClosableLogStream {
93
+ private readonly stream;
94
+ private readonly eventEmitter;
95
+ constructor(stream: FSExtra.ReadStream);
96
+ onLog(listener: (log: LogEntry) => void): () => void;
97
+ onEnd(listener: () => void): () => void;
98
+ onError(listener: (error: Error) => void): () => void;
99
+ close(): Promise<void>;
86
100
  }
87
101
  export declare class ContainerInfo {
88
102
  private readonly _container;
@@ -107,6 +121,7 @@ export declare class ContainerInfo {
107
121
  inspect(): Promise<any>;
108
122
  status(): Promise<DockerState>;
109
123
  getPorts(): Promise<PortMap | false>;
124
+ getLogStream(): Promise<ClosableLogStream>;
110
125
  getLogs(): Promise<LogEntry[]>;
111
126
  }
112
127
  export declare function getExtraHosts(dockerVersion: string): string[] | undefined;