@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
@@ -62,15 +62,21 @@ router.post('/:systemId/:instanceId/stop', async (req, res) => {
62
62
  /**
63
63
  * Get logs for instance in a plan
64
64
  */
65
- router.get('/:systemId/:instanceId/logs', (req, res) => {
65
+ router.get('/:systemId/:instanceId/logs', async (req, res) => {
66
66
  const instanceInfo = instanceManager.getInstance(req.params.systemId, req.params.instanceId);
67
67
  if (!instanceInfo) {
68
68
  res.status(404).send({ ok: false });
69
69
  return;
70
70
  }
71
- res.status(202).send({
72
- logs: instanceInfo.internal?.logs() ?? [],
73
- });
71
+ try {
72
+ const logs = await instanceManager.getLogs(req.params.systemId, req.params.instanceId);
73
+ res.status(200).send({
74
+ logs,
75
+ });
76
+ }
77
+ catch (e) {
78
+ res.status(500).send({ ok: false, error: e.message });
79
+ }
74
80
  });
75
81
  /**
76
82
  * Get public address for instance in a plan if available
@@ -111,13 +111,11 @@ class OperatorManager {
111
111
  const operatorData = operator.getData();
112
112
  const portTypes = Object.keys(operatorData.ports);
113
113
  portTypes.sort();
114
- const containerBaseName = 'kapeta-resource';
115
- const nameParts = [resourceType.toLowerCase()];
116
114
  const ports = {};
117
115
  for (let i = 0; i < portTypes.length; i++) {
118
116
  const portType = portTypes[i];
119
117
  let containerPortInfo = operatorData.ports[portType];
120
- const hostPort = await serviceManager.ensureServicePort(resourceType, portType);
118
+ const hostPort = await serviceManager.ensureServicePort(systemId, resourceType, portType);
121
119
  if (typeof containerPortInfo === 'number' || typeof containerPortInfo === 'string') {
122
120
  containerPortInfo = { port: containerPortInfo, type: 'tcp' };
123
121
  }
@@ -125,14 +123,18 @@ class OperatorManager {
125
123
  containerPortInfo.type = 'tcp';
126
124
  }
127
125
  const portId = containerPortInfo.port + '/' + containerPortInfo.type;
128
- nameParts.push(portType + '-' + portId + '-' + hostPort);
129
126
  ports[portId] = {
130
127
  type: portType,
131
128
  hostPort,
132
129
  };
133
130
  }
134
- const mounts = containerManager.createMounts(resourceType, operatorData.mounts);
135
- const containerName = containerBaseName + '-' + md5(nameParts.join('_'));
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('_'))}`;
136
138
  const PortBindings = {};
137
139
  const Env = [];
138
140
  const Labels = {
@@ -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;
@@ -14,8 +14,6 @@ export declare class BlockInstanceRunner {
14
14
  * Starts local process
15
15
  */
16
16
  private _startLocalProcess;
17
- private ensureContainer;
18
- private _handleContainer;
19
17
  private _startDockerProcess;
20
18
  /**
21
19
  *
@@ -27,4 +25,7 @@ export declare class BlockInstanceRunner {
27
25
  * @private
28
26
  */
29
27
  _startOperatorProcess(blockInstance: BlockProcessParams, blockUri: KapetaURI, providerDefinition: DefinitionInfo, env: StringMap): Promise<ProcessInfo>;
28
+ private getDockerPortBindings;
29
+ private ensureContainer;
30
+ private _handleContainer;
30
31
  }
@@ -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';
@@ -94,7 +93,7 @@ export class BlockInstanceRunner {
94
93
  processInfo = await this._startLocalProcess(blockInstance, blockUri, env, assetVersion);
95
94
  }
96
95
  else {
97
- processInfo = await this._startDockerProcess(blockInstance, blockUri, env);
96
+ processInfo = await this._startDockerProcess(blockInstance, blockUri, env, assetVersion);
98
97
  }
99
98
  if (portTypes.length > 0) {
100
99
  processInfo.portType = portTypes[0];
@@ -127,31 +126,12 @@ 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';
134
133
  const workingDir = localContainer.workingDir ? localContainer.workingDir : '/workspace';
135
- const bindHost = getBindHost();
136
- const ExposedPorts = {};
137
- const addonEnv = {};
138
- const PortBindings = {};
139
- const portTypes = getProviderPorts(assetVersion);
140
- let port = 80;
141
- const promises = portTypes.map(async (portType) => {
142
- const publicPort = await serviceManager.ensureServicePort(this._systemId, blockInstance.id, portType);
143
- const thisPort = port++; //TODO: Not sure how we should handle multiple ports or non-HTTP ports
144
- const dockerPort = `${thisPort}/tcp`;
145
- ExposedPorts[dockerPort] = {};
146
- addonEnv[`KAPETA_LOCAL_SERVER_PORT_${portType.toUpperCase()}`] = '' + thisPort;
147
- PortBindings[dockerPort] = [
148
- {
149
- HostIp: bindHost,
150
- HostPort: `${publicPort}`,
151
- },
152
- ];
153
- });
154
- await Promise.all(promises);
134
+ const { PortBindings, ExposedPorts, addonEnv } = await this.getDockerPortBindings(blockInstance, assetVersion);
155
135
  let HealthCheck = undefined;
156
136
  if (localContainer.healthcheck) {
157
137
  HealthCheck = containerManager.toDockerHealth({ cmd: localContainer.healthcheck });
@@ -184,73 +164,7 @@ export class BlockInstanceRunner {
184
164
  ...dockerOpts,
185
165
  });
186
166
  }
187
- async ensureContainer(opts) {
188
- const logs = new LogData();
189
- const container = await containerManager.ensureContainer(opts);
190
- try {
191
- if (opts.HealthCheck) {
192
- await containerManager.waitForHealthy(container);
193
- }
194
- else {
195
- await containerManager.waitForReady(container);
196
- }
197
- }
198
- catch (e) {
199
- logs.addLog(e.message, 'ERROR');
200
- }
201
- return this._handleContainer(container, logs);
202
- }
203
- async _handleContainer(container, logs, deleteOnExit = false) {
204
- let localContainer = container;
205
- const logStream = (await container.logs({
206
- follow: true,
207
- stdout: true,
208
- stderr: true,
209
- tail: LogData.MAX_LINES,
210
- }));
211
- const outputEvents = new EventEmitter();
212
- logStream.on('data', (data) => {
213
- logs.addLog(data.toString());
214
- outputEvents.emit('data', data);
215
- });
216
- logStream.on('error', (data) => {
217
- logs.addLog(data.toString());
218
- outputEvents.emit('data', data);
219
- });
220
- logStream.on('close', async () => {
221
- const status = await container.status();
222
- const data = status.data;
223
- if (deleteOnExit) {
224
- try {
225
- await containerManager.remove(container);
226
- }
227
- catch (e) { }
228
- }
229
- outputEvents.emit('exit', data?.State?.ExitCode ?? 0);
230
- });
231
- return {
232
- type: InstanceType.DOCKER,
233
- pid: container.id,
234
- output: outputEvents,
235
- stop: async () => {
236
- if (!localContainer) {
237
- return;
238
- }
239
- try {
240
- await localContainer.stop();
241
- if (deleteOnExit) {
242
- await containerManager.remove(localContainer);
243
- }
244
- }
245
- catch (e) { }
246
- localContainer = null;
247
- },
248
- logs: () => {
249
- return logs.getLogs();
250
- },
251
- };
252
- }
253
- async _startDockerProcess(blockInstance, blockInfo, env) {
167
+ async _startDockerProcess(blockInstance, blockInfo, env, assetVersion) {
254
168
  const { versionFile } = ClusterConfig.getRepositoryAssetInfoPath(blockInfo.handle, blockInfo.name, blockInfo.version);
255
169
  const versionYml = versionFile;
256
170
  if (!FS.existsSync(versionYml)) {
@@ -264,23 +178,28 @@ export class BlockInstanceRunner {
264
178
  if (!dockerImage) {
265
179
  throw new Error(`Missing docker image information: ${JSON.stringify(versionInfo?.artifact?.details)}`);
266
180
  }
267
- const containerName = getBlockInstanceContainerName(blockInstance.id);
268
- const logs = new LogData();
181
+ const { PortBindings, ExposedPorts, addonEnv } = await this.getDockerPortBindings(blockInstance, assetVersion);
182
+ const containerName = getBlockInstanceContainerName(this._systemId, blockInstance.id);
269
183
  // For windows we need to default to root
270
184
  const innerHome = process.platform === 'win32' ? '/root/.kapeta' : ClusterConfig.getKapetaBasedir();
271
185
  return this.ensureContainer({
272
186
  Image: dockerImage,
273
187
  name: containerName,
188
+ ExposedPorts,
274
189
  Labels: {
275
190
  instance: blockInstance.id,
276
191
  },
277
192
  Env: [
278
193
  ...DOCKER_ENV_VARS,
279
194
  `KAPETA_LOCAL_CLUSTER_PORT=${clusterService.getClusterServicePort()}`,
280
- ...Object.entries(env).map(([key, value]) => `${key}=${value}`),
195
+ ...Object.entries({
196
+ ...env,
197
+ ...addonEnv
198
+ }).map(([key, value]) => `${key}=${value}`),
281
199
  ],
282
200
  HostConfig: {
283
201
  Binds: [`${toLocalBindVolume(ClusterConfig.getKapetaBasedir())}:${innerHome}`],
202
+ PortBindings,
284
203
  },
285
204
  });
286
205
  }
@@ -305,7 +224,8 @@ export class BlockInstanceRunner {
305
224
  throw new Error(`Provider did not have local image: ${providerRef}`);
306
225
  }
307
226
  const dockerImage = spec?.local?.image;
308
- 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);
309
229
  const logs = new LogData();
310
230
  const bindHost = getBindHost();
311
231
  const ExposedPorts = {};
@@ -332,7 +252,7 @@ export class BlockInstanceRunner {
332
252
  });
333
253
  }
334
254
  if (spec.local?.mounts) {
335
- const mounts = containerManager.createMounts(blockUri.id, spec.local.mounts);
255
+ const mounts = await containerManager.createMounts(this._systemId, blockUri.id, spec.local.mounts);
336
256
  Mounts = containerManager.toDockerMounts(mounts);
337
257
  }
338
258
  if (spec.local?.health) {
@@ -373,4 +293,38 @@ export class BlockInstanceRunner {
373
293
  }
374
294
  return out;
375
295
  }
296
+ async getDockerPortBindings(blockInstance, assetVersion) {
297
+ const bindHost = getBindHost();
298
+ const ExposedPorts = {};
299
+ const addonEnv = {};
300
+ const PortBindings = {};
301
+ const portTypes = getProviderPorts(assetVersion);
302
+ let port = 80;
303
+ const promises = portTypes.map(async (portType) => {
304
+ const publicPort = await serviceManager.ensureServicePort(this._systemId, blockInstance.id, portType);
305
+ const thisPort = port++; //TODO: Not sure how we should handle multiple ports or non-HTTP ports
306
+ const dockerPort = `${thisPort}/tcp`;
307
+ ExposedPorts[dockerPort] = {};
308
+ addonEnv[`KAPETA_LOCAL_SERVER_PORT_${portType.toUpperCase()}`] = '' + thisPort;
309
+ PortBindings[dockerPort] = [
310
+ {
311
+ HostIp: bindHost,
312
+ HostPort: `${publicPort}`,
313
+ },
314
+ ];
315
+ });
316
+ await Promise.all(promises);
317
+ return { PortBindings, ExposedPorts, addonEnv };
318
+ }
319
+ async ensureContainer(opts) {
320
+ const container = await containerManager.ensureContainer(opts);
321
+ await containerManager.waitForReady(container);
322
+ return this._handleContainer(container);
323
+ }
324
+ async _handleContainer(container) {
325
+ return {
326
+ type: InstanceType.DOCKER,
327
+ pid: container.id
328
+ };
329
+ }
376
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.0",
3
+ "version": "0.11.0",
4
4
  "description": "Manages configuration, ports and service discovery for locally running Kapeta systems",
5
5
  "type": "commonjs",
6
6
  "exports": {
@@ -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
  }