@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
package/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## [0.11.1](https://github.com/kapetacom/local-cluster-service/compare/v0.11.0...v0.11.1) (2023-07-31)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * Ensure we do not attempt to start / stop the same instance at the ([493e077](https://github.com/kapetacom/local-cluster-service/commit/493e077d0c6acdbcc371dae2ef8b6fbf2478c950))
7
+
8
+ # [0.11.0](https://github.com/kapetacom/local-cluster-service/compare/v0.10.1...v0.11.0) (2023-07-31)
9
+
10
+
11
+ ### Features
12
+
13
+ * Always get logs from docker ([#53](https://github.com/kapetacom/local-cluster-service/issues/53)) ([5cab8cb](https://github.com/kapetacom/local-cluster-service/commit/5cab8cbf18b38edf99d538e1819e135f0a5bd7e3))
14
+
1
15
  ## [0.10.1](https://github.com/kapetacom/local-cluster-service/compare/v0.10.0...v0.10.1) (2023-07-27)
2
16
 
3
17
 
@@ -1,5 +1,6 @@
1
1
  import { Docker } from 'node-docker-api';
2
2
  import { Container } from 'node-docker-api/lib/container';
3
+ import { InstanceInfo, LogEntry } from "./types";
3
4
  type StringMap = {
4
5
  [key: string]: string;
5
6
  };
@@ -52,8 +53,8 @@ declare class ContainerManager {
52
53
  initialize(): Promise<void>;
53
54
  checkAlive(): Promise<boolean>;
54
55
  isAlive(): boolean;
55
- getMountPoint(kind: string, mountName: string): string;
56
- createMounts(kind: string, mountOpts: StringMap): StringMap;
56
+ getMountPoint(systemId: string, ref: string, mountName: string): string;
57
+ createMounts(systemId: string, kind: string, mountOpts: StringMap | null | undefined): Promise<StringMap>;
57
58
  ping(): Promise<void>;
58
59
  docker(): Docker;
59
60
  getContainerByName(containerName: string): Promise<ContainerInfo | undefined>;
@@ -67,11 +68,10 @@ declare class ContainerManager {
67
68
  };
68
69
  private applyHash;
69
70
  ensureContainer(opts: any): Promise<Container>;
71
+ private createOrUpdateContainer;
70
72
  startContainer(opts: any): Promise<Container>;
71
73
  waitForReady(container: Container, attempt?: number): Promise<void>;
72
- waitForHealthy(container: Container, attempt?: number): Promise<void>;
73
74
  _isReady(container: Container): Promise<any>;
74
- _isHealthy(container: Container): Promise<boolean>;
75
75
  remove(container: Container, opts?: {
76
76
  force?: boolean;
77
77
  }): Promise<void>;
@@ -81,6 +81,7 @@ declare class ContainerManager {
81
81
  * @return {Promise<ContainerInfo>}
82
82
  */
83
83
  get(name: string): Promise<ContainerInfo | null>;
84
+ getLogs(instance: InstanceInfo): Promise<LogEntry[]>;
84
85
  }
85
86
  export declare class ContainerInfo {
86
87
  private readonly _container;
@@ -105,6 +106,7 @@ export declare class ContainerInfo {
105
106
  inspect(): Promise<any>;
106
107
  status(): Promise<DockerState>;
107
108
  getPorts(): Promise<PortMap | false>;
109
+ getLogs(): Promise<LogEntry[]>;
108
110
  }
109
111
  export declare function getExtraHosts(dockerVersion: string): string[] | undefined;
110
112
  /**
@@ -14,6 +14,7 @@ const nodejs_utils_1 = require("@kapeta/nodejs-utils");
14
14
  const local_cluster_config_1 = __importDefault(require("@kapeta/local-cluster-config"));
15
15
  const node_uuid_1 = __importDefault(require("node-uuid"));
16
16
  const md5_1 = __importDefault(require("md5"));
17
+ const utils_1 = require("./utils/utils");
17
18
  exports.CONTAINER_LABEL_PORT_PREFIX = 'kapeta_port-';
18
19
  const NANO_SECOND = 1000000;
19
20
  const HEALTH_CHECK_INTERVAL = 3000;
@@ -21,8 +22,8 @@ const HEALTH_CHECK_MAX = 20;
21
22
  const IMAGE_PULL_CACHE_TTL = 30 * 60 * 1000;
22
23
  const IMAGE_PULL_CACHE = {};
23
24
  exports.HEALTH_CHECK_TIMEOUT = HEALTH_CHECK_INTERVAL * HEALTH_CHECK_MAX * 2;
24
- const promisifyStream = (stream) => new Promise((resolve, reject) => {
25
- stream.on('data', (d) => console.log(d.toString()));
25
+ const promisifyStream = (stream, handler) => new Promise((resolve, reject) => {
26
+ stream.on('data', handler);
26
27
  stream.on('end', resolve);
27
28
  stream.on('error', reject);
28
29
  });
@@ -101,17 +102,21 @@ class ContainerManager {
101
102
  isAlive() {
102
103
  return this._alive;
103
104
  }
104
- getMountPoint(kind, mountName) {
105
- const kindUri = (0, nodejs_utils_1.parseKapetaUri)(kind);
106
- return path_1.default.join(this._mountDir, kindUri.handle, kindUri.name, mountName);
105
+ getMountPoint(systemId, ref, mountName) {
106
+ const kindUri = (0, nodejs_utils_1.parseKapetaUri)(ref);
107
+ const systemUri = (0, nodejs_utils_1.parseKapetaUri)(systemId);
108
+ return path_1.default.join(this._mountDir, systemUri.handle, systemUri.name, systemUri.version, kindUri.handle, kindUri.name, kindUri.version, mountName);
107
109
  }
108
- createMounts(kind, mountOpts) {
110
+ async createMounts(systemId, kind, mountOpts) {
109
111
  const mounts = {};
110
- lodash_1.default.forEach(mountOpts, (containerPath, mountName) => {
111
- const hostPath = this.getMountPoint(kind, mountName);
112
- fs_extra_1.default.mkdirpSync(hostPath);
113
- mounts[containerPath] = hostPath;
114
- });
112
+ if (mountOpts) {
113
+ const mountOptList = Object.entries(mountOpts);
114
+ for (const [mountName, containerPath] of mountOptList) {
115
+ const hostPath = this.getMountPoint(systemId, kind, mountName);
116
+ await fs_extra_1.default.mkdirp(hostPath);
117
+ mounts[containerPath] = hostPath;
118
+ }
119
+ }
115
120
  return mounts;
116
121
  }
117
122
  async ping() {
@@ -162,12 +167,14 @@ class ContainerManager {
162
167
  return false;
163
168
  }
164
169
  console.log('Pulling image: %s', image);
165
- await this.docker()
170
+ const stream = await this.docker()
166
171
  .image.create({}, {
167
172
  fromImage: imageName,
168
173
  tag: tag,
169
- })
170
- .then((stream) => promisifyStream(stream));
174
+ });
175
+ await promisifyStream(stream, (chunk) => {
176
+ console.log('Data from docker: "%s"', chunk.toString());
177
+ });
171
178
  IMAGE_PULL_CACHE[image] = Date.now();
172
179
  console.log('Image pulled: %s', image);
173
180
  return true;
@@ -204,6 +211,11 @@ class ContainerManager {
204
211
  dockerOpts.Labels.HASH = hash;
205
212
  }
206
213
  async ensureContainer(opts) {
214
+ const container = await this.createOrUpdateContainer(opts);
215
+ await this.waitForReady(container);
216
+ return container;
217
+ }
218
+ async createOrUpdateContainer(opts) {
207
219
  let imagePulled = false;
208
220
  try {
209
221
  imagePulled = await this.pull(opts.Image);
@@ -283,28 +295,6 @@ class ContainerManager {
283
295
  }, HEALTH_CHECK_INTERVAL);
284
296
  });
285
297
  }
286
- async waitForHealthy(container, attempt) {
287
- if (!attempt) {
288
- attempt = 0;
289
- }
290
- if (attempt >= HEALTH_CHECK_MAX) {
291
- throw new Error('Container did not become healthy within the timeout');
292
- }
293
- if (await this._isHealthy(container)) {
294
- return;
295
- }
296
- return new Promise((resolve, reject) => {
297
- setTimeout(async () => {
298
- try {
299
- await this.waitForHealthy(container, (attempt ?? 0) + 1);
300
- resolve();
301
- }
302
- catch (err) {
303
- reject(err);
304
- }
305
- }, HEALTH_CHECK_INTERVAL);
306
- });
307
- }
308
298
  async _isReady(container) {
309
299
  let info;
310
300
  try {
@@ -318,16 +308,12 @@ class ContainerManager {
318
308
  if (state?.Status === 'exited' || state?.Status === 'removing' || state?.Status === 'dead') {
319
309
  throw new Error('Container exited unexpectedly');
320
310
  }
321
- return infoData?.State?.Running ?? false;
322
- }
323
- async _isHealthy(container) {
324
- try {
325
- const info = await container.status();
326
- const infoData = info?.data;
327
- return infoData?.State?.Health?.Status === 'healthy';
311
+ if (infoData?.State?.Health) {
312
+ // If container has health info - wait for it to become healthy
313
+ return infoData.State.Health.Status === 'healthy';
328
314
  }
329
- catch (err) {
330
- return false;
315
+ else {
316
+ return infoData?.State?.Running ?? false;
331
317
  }
332
318
  }
333
319
  async remove(container, opts) {
@@ -357,6 +343,19 @@ class ContainerManager {
357
343
  }
358
344
  return new ContainerInfo(dockerContainer);
359
345
  }
346
+ async getLogs(instance) {
347
+ const containerName = (0, utils_1.getBlockInstanceContainerName)(instance.systemId, instance.instanceId);
348
+ const containerInfo = await this.getContainerByName(containerName);
349
+ if (!containerInfo) {
350
+ return [{
351
+ source: "stdout",
352
+ level: "ERROR",
353
+ time: Date.now(),
354
+ message: "Container not found"
355
+ }];
356
+ }
357
+ return containerInfo.getLogs();
358
+ }
360
359
  }
361
360
  class ContainerInfo {
362
361
  _container;
@@ -440,6 +439,62 @@ class ContainerInfo {
440
439
  });
441
440
  return ports;
442
441
  }
442
+ async getLogs() {
443
+ const logStream = await this.native.logs({
444
+ stdout: true,
445
+ stderr: true,
446
+ follow: false,
447
+ tail: 100,
448
+ timestamps: true,
449
+ });
450
+ const out = [];
451
+ await promisifyStream(logStream, (data) => {
452
+ const buf = data;
453
+ let offset = 0;
454
+ while (offset < buf.length) {
455
+ try {
456
+ // Read the docker log format - explained here:
457
+ // https://docs.docker.com/engine/api/v1.41/#operation/ContainerAttach
458
+ // or here : https://ahmet.im/blog/docker-logs-api-binary-format-explained/
459
+ // First byte is stream type
460
+ const streamTypeInt = buf.readInt8(offset);
461
+ const streamType = streamTypeInt === 1 ? 'stdout' : 'stderr';
462
+ // Bytes 4-8 is frame size
463
+ const messageLength = buf.readInt32BE(offset + 4);
464
+ // After that is the message - with the message length
465
+ const dataWithoutStreamType = buf.subarray(offset + 8, offset + 8 + messageLength);
466
+ const raw = dataWithoutStreamType.toString();
467
+ // Split the message into date and message
468
+ const firstSpaceIx = raw.indexOf(' ');
469
+ const dateString = raw.substring(0, firstSpaceIx);
470
+ const line = raw.substring(firstSpaceIx + 1);
471
+ offset = offset + messageLength + 8;
472
+ if (!dateString) {
473
+ continue;
474
+ }
475
+ out.push({
476
+ time: new Date(dateString).getTime(),
477
+ message: line,
478
+ level: 'INFO',
479
+ source: streamType,
480
+ });
481
+ }
482
+ catch (err) {
483
+ console.error('Error parsing log entry', err);
484
+ offset = buf.length;
485
+ }
486
+ }
487
+ });
488
+ if (out.length === 0) {
489
+ out.push({
490
+ time: Date.now(),
491
+ message: 'No logs found for container',
492
+ level: 'INFO',
493
+ source: 'stdout',
494
+ });
495
+ }
496
+ return out;
497
+ }
443
498
  }
444
499
  exports.ContainerInfo = ContainerInfo;
445
500
  function getExtraHosts(dockerVersion) {
@@ -5,6 +5,7 @@ declare class DefinitionsManager {
5
5
  clearCache(): void;
6
6
  private doCached;
7
7
  getDefinitions(kindFilter?: string | string[]): DefinitionInfo[];
8
+ exists(ref: string): boolean;
8
9
  getProviderDefinitions(): DefinitionInfo[];
9
10
  }
10
11
  export declare const definitionsManager: DefinitionsManager;
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.definitionsManager = void 0;
7
7
  const local_cluster_config_1 = __importDefault(require("@kapeta/local-cluster-config"));
8
+ const nodejs_utils_1 = require("@kapeta/nodejs-utils");
8
9
  const CACHE_TTL = 60 * 1000; // 1 min
9
10
  class DefinitionsManager {
10
11
  cache = {};
@@ -37,6 +38,12 @@ class DefinitionsManager {
37
38
  const key = this.getKey(kindFilter);
38
39
  return this.doCached(key, () => local_cluster_config_1.default.getDefinitions(kindFilter));
39
40
  }
41
+ exists(ref) {
42
+ const uri = (0, nodejs_utils_1.parseKapetaUri)(ref);
43
+ return !!this.getDefinitions().find((d) => {
44
+ return (0, nodejs_utils_1.parseKapetaUri)(`${d.definition.metadata.name}:${d.version}`).id === uri.id;
45
+ });
46
+ }
40
47
  getProviderDefinitions() {
41
48
  return this.doCached('providers', () => local_cluster_config_1.default.getProviderDefinitions());
42
49
  }
@@ -1,12 +1,15 @@
1
- import { InstanceInfo } from './types';
1
+ import { InstanceInfo, LogEntry } from './types';
2
2
  export declare class InstanceManager {
3
3
  private _interval;
4
4
  private readonly _instances;
5
+ private readonly instanceLocks;
5
6
  constructor();
6
7
  private checkInstancesLater;
7
8
  getInstances(): InstanceInfo[];
8
9
  getInstancesForPlan(systemId: string): InstanceInfo[];
9
10
  getInstance(systemId: string, instanceId: string): InstanceInfo | undefined;
11
+ private exclusive;
12
+ getLogs(systemId: string, instanceId: string): Promise<LogEntry[]>;
10
13
  saveInternalInstance(instance: InstanceInfo): Promise<InstanceInfo>;
11
14
  /**
12
15
  * Method is called when instance is started from the Kapeta SDKs (e.g. NodeJS SDK)
@@ -14,9 +17,10 @@ export declare class InstanceManager {
14
17
  */
15
18
  registerInstanceFromSDK(systemId: string, instanceId: string, info: Omit<InstanceInfo, 'systemId' | 'instanceId'>): Promise<InstanceInfo | undefined>;
16
19
  private getHealthUrl;
17
- markAsStopped(systemId: string, instanceId: string): void;
20
+ markAsStopped(systemId: string, instanceId: string): Promise<void>;
18
21
  startAllForPlan(systemId: string): Promise<InstanceInfo[]>;
19
22
  stop(systemId: string, instanceId: string): Promise<void>;
23
+ private stopInner;
20
24
  stopAllForPlan(systemId: string): Promise<void>;
21
25
  start(systemId: string, instanceId: string): Promise<InstanceInfo>;
22
26
  restart(systemId: string, instanceId: string): Promise<InstanceInfo>;