@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
@@ -11,7 +11,6 @@ const nodejs_utils_1 = require("@kapeta/nodejs-utils");
11
11
  const serviceManager_1 = require("../serviceManager");
12
12
  const containerManager_1 = require("../containerManager");
13
13
  const LogData_1 = require("./LogData");
14
- const events_1 = __importDefault(require("events"));
15
14
  const clusterService_1 = require("../clusterService");
16
15
  const types_1 = require("../types");
17
16
  const definitionsManager_1 = require("../definitionsManager");
@@ -100,7 +99,7 @@ class BlockInstanceRunner {
100
99
  processInfo = await this._startLocalProcess(blockInstance, blockUri, env, assetVersion);
101
100
  }
102
101
  else {
103
- processInfo = await this._startDockerProcess(blockInstance, blockUri, env);
102
+ processInfo = await this._startDockerProcess(blockInstance, blockUri, env, assetVersion);
104
103
  }
105
104
  if (portTypes.length > 0) {
106
105
  processInfo.portType = portTypes[0];
@@ -133,31 +132,12 @@ class BlockInstanceRunner {
133
132
  if (!dockerImage) {
134
133
  throw new Error(`Missing docker image information: ${JSON.stringify(localContainer)}`);
135
134
  }
136
- const containerName = (0, utils_1.getBlockInstanceContainerName)(blockInstance.id);
135
+ const containerName = (0, utils_1.getBlockInstanceContainerName)(this._systemId, blockInstance.id);
137
136
  const startCmd = localContainer.handlers?.onCreate ? localContainer.handlers.onCreate : '';
138
137
  const dockerOpts = localContainer.options ?? {};
139
138
  const homeDir = localContainer.userHome ? localContainer.userHome : '/root';
140
139
  const workingDir = localContainer.workingDir ? localContainer.workingDir : '/workspace';
141
- const bindHost = (0, utils_1.getBindHost)();
142
- const ExposedPorts = {};
143
- const addonEnv = {};
144
- const PortBindings = {};
145
- const portTypes = getProviderPorts(assetVersion);
146
- let port = 80;
147
- const promises = portTypes.map(async (portType) => {
148
- const publicPort = await serviceManager_1.serviceManager.ensureServicePort(this._systemId, blockInstance.id, portType);
149
- const thisPort = port++; //TODO: Not sure how we should handle multiple ports or non-HTTP ports
150
- const dockerPort = `${thisPort}/tcp`;
151
- ExposedPorts[dockerPort] = {};
152
- addonEnv[`KAPETA_LOCAL_SERVER_PORT_${portType.toUpperCase()}`] = '' + thisPort;
153
- PortBindings[dockerPort] = [
154
- {
155
- HostIp: bindHost,
156
- HostPort: `${publicPort}`,
157
- },
158
- ];
159
- });
160
- await Promise.all(promises);
140
+ const { PortBindings, ExposedPorts, addonEnv } = await this.getDockerPortBindings(blockInstance, assetVersion);
161
141
  let HealthCheck = undefined;
162
142
  if (localContainer.healthcheck) {
163
143
  HealthCheck = containerManager_1.containerManager.toDockerHealth({ cmd: localContainer.healthcheck });
@@ -190,73 +170,7 @@ class BlockInstanceRunner {
190
170
  ...dockerOpts,
191
171
  });
192
172
  }
193
- async ensureContainer(opts) {
194
- const logs = new LogData_1.LogData();
195
- const container = await containerManager_1.containerManager.ensureContainer(opts);
196
- try {
197
- if (opts.HealthCheck) {
198
- await containerManager_1.containerManager.waitForHealthy(container);
199
- }
200
- else {
201
- await containerManager_1.containerManager.waitForReady(container);
202
- }
203
- }
204
- catch (e) {
205
- logs.addLog(e.message, 'ERROR');
206
- }
207
- return this._handleContainer(container, logs);
208
- }
209
- async _handleContainer(container, logs, deleteOnExit = false) {
210
- let localContainer = container;
211
- const logStream = (await container.logs({
212
- follow: true,
213
- stdout: true,
214
- stderr: true,
215
- tail: LogData_1.LogData.MAX_LINES,
216
- }));
217
- const outputEvents = new events_1.default();
218
- logStream.on('data', (data) => {
219
- logs.addLog(data.toString());
220
- outputEvents.emit('data', data);
221
- });
222
- logStream.on('error', (data) => {
223
- logs.addLog(data.toString());
224
- outputEvents.emit('data', data);
225
- });
226
- logStream.on('close', async () => {
227
- const status = await container.status();
228
- const data = status.data;
229
- if (deleteOnExit) {
230
- try {
231
- await containerManager_1.containerManager.remove(container);
232
- }
233
- catch (e) { }
234
- }
235
- outputEvents.emit('exit', data?.State?.ExitCode ?? 0);
236
- });
237
- return {
238
- type: types_1.InstanceType.DOCKER,
239
- pid: container.id,
240
- output: outputEvents,
241
- stop: async () => {
242
- if (!localContainer) {
243
- return;
244
- }
245
- try {
246
- await localContainer.stop();
247
- if (deleteOnExit) {
248
- await containerManager_1.containerManager.remove(localContainer);
249
- }
250
- }
251
- catch (e) { }
252
- localContainer = null;
253
- },
254
- logs: () => {
255
- return logs.getLogs();
256
- },
257
- };
258
- }
259
- async _startDockerProcess(blockInstance, blockInfo, env) {
173
+ async _startDockerProcess(blockInstance, blockInfo, env, assetVersion) {
260
174
  const { versionFile } = local_cluster_config_1.default.getRepositoryAssetInfoPath(blockInfo.handle, blockInfo.name, blockInfo.version);
261
175
  const versionYml = versionFile;
262
176
  if (!node_fs_1.default.existsSync(versionYml)) {
@@ -270,23 +184,28 @@ class BlockInstanceRunner {
270
184
  if (!dockerImage) {
271
185
  throw new Error(`Missing docker image information: ${JSON.stringify(versionInfo?.artifact?.details)}`);
272
186
  }
273
- const containerName = (0, utils_1.getBlockInstanceContainerName)(blockInstance.id);
274
- const logs = new LogData_1.LogData();
187
+ const { PortBindings, ExposedPorts, addonEnv } = await this.getDockerPortBindings(blockInstance, assetVersion);
188
+ const containerName = (0, utils_1.getBlockInstanceContainerName)(this._systemId, blockInstance.id);
275
189
  // For windows we need to default to root
276
190
  const innerHome = process.platform === 'win32' ? '/root/.kapeta' : local_cluster_config_1.default.getKapetaBasedir();
277
191
  return this.ensureContainer({
278
192
  Image: dockerImage,
279
193
  name: containerName,
194
+ ExposedPorts,
280
195
  Labels: {
281
196
  instance: blockInstance.id,
282
197
  },
283
198
  Env: [
284
199
  ...DOCKER_ENV_VARS,
285
200
  `KAPETA_LOCAL_CLUSTER_PORT=${clusterService_1.clusterService.getClusterServicePort()}`,
286
- ...Object.entries(env).map(([key, value]) => `${key}=${value}`),
201
+ ...Object.entries({
202
+ ...env,
203
+ ...addonEnv
204
+ }).map(([key, value]) => `${key}=${value}`),
287
205
  ],
288
206
  HostConfig: {
289
207
  Binds: [`${(0, containerManager_1.toLocalBindVolume)(local_cluster_config_1.default.getKapetaBasedir())}:${innerHome}`],
208
+ PortBindings,
290
209
  },
291
210
  });
292
211
  }
@@ -311,7 +230,8 @@ class BlockInstanceRunner {
311
230
  throw new Error(`Provider did not have local image: ${providerRef}`);
312
231
  }
313
232
  const dockerImage = spec?.local?.image;
314
- const containerName = (0, utils_1.getBlockInstanceContainerName)(blockInstance.id);
233
+ //We only want 1 operator per operator type - across all local systems
234
+ const containerName = (0, utils_1.getBlockInstanceContainerName)(this._systemId, blockInstance.id);
315
235
  const logs = new LogData_1.LogData();
316
236
  const bindHost = (0, utils_1.getBindHost)();
317
237
  const ExposedPorts = {};
@@ -338,7 +258,7 @@ class BlockInstanceRunner {
338
258
  });
339
259
  }
340
260
  if (spec.local?.mounts) {
341
- const mounts = containerManager_1.containerManager.createMounts(blockUri.id, spec.local.mounts);
261
+ const mounts = await containerManager_1.containerManager.createMounts(this._systemId, blockUri.id, spec.local.mounts);
342
262
  Mounts = containerManager_1.containerManager.toDockerMounts(mounts);
343
263
  }
344
264
  if (spec.local?.health) {
@@ -379,5 +299,39 @@ class BlockInstanceRunner {
379
299
  }
380
300
  return out;
381
301
  }
302
+ async getDockerPortBindings(blockInstance, assetVersion) {
303
+ const bindHost = (0, utils_1.getBindHost)();
304
+ const ExposedPorts = {};
305
+ const addonEnv = {};
306
+ const PortBindings = {};
307
+ const portTypes = getProviderPorts(assetVersion);
308
+ let port = 80;
309
+ const promises = portTypes.map(async (portType) => {
310
+ const publicPort = await serviceManager_1.serviceManager.ensureServicePort(this._systemId, blockInstance.id, portType);
311
+ const thisPort = port++; //TODO: Not sure how we should handle multiple ports or non-HTTP ports
312
+ const dockerPort = `${thisPort}/tcp`;
313
+ ExposedPorts[dockerPort] = {};
314
+ addonEnv[`KAPETA_LOCAL_SERVER_PORT_${portType.toUpperCase()}`] = '' + thisPort;
315
+ PortBindings[dockerPort] = [
316
+ {
317
+ HostIp: bindHost,
318
+ HostPort: `${publicPort}`,
319
+ },
320
+ ];
321
+ });
322
+ await Promise.all(promises);
323
+ return { PortBindings, ExposedPorts, addonEnv };
324
+ }
325
+ async ensureContainer(opts) {
326
+ const container = await containerManager_1.containerManager.ensureContainer(opts);
327
+ await containerManager_1.containerManager.waitForReady(container);
328
+ return this._handleContainer(container);
329
+ }
330
+ async _handleContainer(container) {
331
+ return {
332
+ type: types_1.InstanceType.DOCKER,
333
+ pid: container.id
334
+ };
335
+ }
382
336
  }
383
337
  exports.BlockInstanceRunner = BlockInstanceRunner;
@@ -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;
@@ -7,8 +7,9 @@ exports.getBindHost = exports.isLinux = exports.isMac = exports.isWindows = expo
7
7
  const node_fs_1 = __importDefault(require("node:fs"));
8
8
  const yaml_1 = __importDefault(require("yaml"));
9
9
  const nodejs_utils_1 = require("@kapeta/nodejs-utils");
10
- function getBlockInstanceContainerName(instanceId) {
11
- return `kapeta-block-instance-${instanceId}`;
10
+ const md5_1 = __importDefault(require("md5"));
11
+ function getBlockInstanceContainerName(systemId, instanceId) {
12
+ return `kapeta-block-instance-${(0, md5_1.default)(systemId + instanceId)}`;
12
13
  }
13
14
  exports.getBlockInstanceContainerName = getBlockInstanceContainerName;
14
15
  function normalizeKapetaUri(uri) {
@@ -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
  /**
@@ -8,6 +8,7 @@ import { parseKapetaUri } from '@kapeta/nodejs-utils';
8
8
  import ClusterConfiguration from '@kapeta/local-cluster-config';
9
9
  import uuid from 'node-uuid';
10
10
  import md5 from 'md5';
11
+ import { getBlockInstanceContainerName } from "./utils/utils";
11
12
  export const CONTAINER_LABEL_PORT_PREFIX = 'kapeta_port-';
12
13
  const NANO_SECOND = 1000000;
13
14
  const HEALTH_CHECK_INTERVAL = 3000;
@@ -15,8 +16,8 @@ const HEALTH_CHECK_MAX = 20;
15
16
  const IMAGE_PULL_CACHE_TTL = 30 * 60 * 1000;
16
17
  const IMAGE_PULL_CACHE = {};
17
18
  export const HEALTH_CHECK_TIMEOUT = HEALTH_CHECK_INTERVAL * HEALTH_CHECK_MAX * 2;
18
- const promisifyStream = (stream) => new Promise((resolve, reject) => {
19
- stream.on('data', (d) => console.log(d.toString()));
19
+ const promisifyStream = (stream, handler) => new Promise((resolve, reject) => {
20
+ stream.on('data', handler);
20
21
  stream.on('end', resolve);
21
22
  stream.on('error', reject);
22
23
  });
@@ -95,17 +96,21 @@ class ContainerManager {
95
96
  isAlive() {
96
97
  return this._alive;
97
98
  }
98
- getMountPoint(kind, mountName) {
99
- const kindUri = parseKapetaUri(kind);
100
- return Path.join(this._mountDir, kindUri.handle, kindUri.name, mountName);
99
+ getMountPoint(systemId, ref, mountName) {
100
+ const kindUri = parseKapetaUri(ref);
101
+ const systemUri = parseKapetaUri(systemId);
102
+ return Path.join(this._mountDir, systemUri.handle, systemUri.name, systemUri.version, kindUri.handle, kindUri.name, kindUri.version, mountName);
101
103
  }
102
- createMounts(kind, mountOpts) {
104
+ async createMounts(systemId, kind, mountOpts) {
103
105
  const mounts = {};
104
- _.forEach(mountOpts, (containerPath, mountName) => {
105
- const hostPath = this.getMountPoint(kind, mountName);
106
- FSExtra.mkdirpSync(hostPath);
107
- mounts[containerPath] = hostPath;
108
- });
106
+ if (mountOpts) {
107
+ const mountOptList = Object.entries(mountOpts);
108
+ for (const [mountName, containerPath] of mountOptList) {
109
+ const hostPath = this.getMountPoint(systemId, kind, mountName);
110
+ await FSExtra.mkdirp(hostPath);
111
+ mounts[containerPath] = hostPath;
112
+ }
113
+ }
109
114
  return mounts;
110
115
  }
111
116
  async ping() {
@@ -156,12 +161,14 @@ class ContainerManager {
156
161
  return false;
157
162
  }
158
163
  console.log('Pulling image: %s', image);
159
- await this.docker()
164
+ const stream = await this.docker()
160
165
  .image.create({}, {
161
166
  fromImage: imageName,
162
167
  tag: tag,
163
- })
164
- .then((stream) => promisifyStream(stream));
168
+ });
169
+ await promisifyStream(stream, (chunk) => {
170
+ console.log('Data from docker: "%s"', chunk.toString());
171
+ });
165
172
  IMAGE_PULL_CACHE[image] = Date.now();
166
173
  console.log('Image pulled: %s', image);
167
174
  return true;
@@ -198,6 +205,11 @@ class ContainerManager {
198
205
  dockerOpts.Labels.HASH = hash;
199
206
  }
200
207
  async ensureContainer(opts) {
208
+ const container = await this.createOrUpdateContainer(opts);
209
+ await this.waitForReady(container);
210
+ return container;
211
+ }
212
+ async createOrUpdateContainer(opts) {
201
213
  let imagePulled = false;
202
214
  try {
203
215
  imagePulled = await this.pull(opts.Image);
@@ -277,28 +289,6 @@ class ContainerManager {
277
289
  }, HEALTH_CHECK_INTERVAL);
278
290
  });
279
291
  }
280
- async waitForHealthy(container, attempt) {
281
- if (!attempt) {
282
- attempt = 0;
283
- }
284
- if (attempt >= HEALTH_CHECK_MAX) {
285
- throw new Error('Container did not become healthy within the timeout');
286
- }
287
- if (await this._isHealthy(container)) {
288
- return;
289
- }
290
- return new Promise((resolve, reject) => {
291
- setTimeout(async () => {
292
- try {
293
- await this.waitForHealthy(container, (attempt ?? 0) + 1);
294
- resolve();
295
- }
296
- catch (err) {
297
- reject(err);
298
- }
299
- }, HEALTH_CHECK_INTERVAL);
300
- });
301
- }
302
292
  async _isReady(container) {
303
293
  let info;
304
294
  try {
@@ -312,16 +302,12 @@ class ContainerManager {
312
302
  if (state?.Status === 'exited' || state?.Status === 'removing' || state?.Status === 'dead') {
313
303
  throw new Error('Container exited unexpectedly');
314
304
  }
315
- return infoData?.State?.Running ?? false;
316
- }
317
- async _isHealthy(container) {
318
- try {
319
- const info = await container.status();
320
- const infoData = info?.data;
321
- return infoData?.State?.Health?.Status === 'healthy';
305
+ if (infoData?.State?.Health) {
306
+ // If container has health info - wait for it to become healthy
307
+ return infoData.State.Health.Status === 'healthy';
322
308
  }
323
- catch (err) {
324
- return false;
309
+ else {
310
+ return infoData?.State?.Running ?? false;
325
311
  }
326
312
  }
327
313
  async remove(container, opts) {
@@ -351,6 +337,19 @@ class ContainerManager {
351
337
  }
352
338
  return new ContainerInfo(dockerContainer);
353
339
  }
340
+ async getLogs(instance) {
341
+ const containerName = getBlockInstanceContainerName(instance.systemId, instance.instanceId);
342
+ const containerInfo = await this.getContainerByName(containerName);
343
+ if (!containerInfo) {
344
+ return [{
345
+ source: "stdout",
346
+ level: "ERROR",
347
+ time: Date.now(),
348
+ message: "Container not found"
349
+ }];
350
+ }
351
+ return containerInfo.getLogs();
352
+ }
354
353
  }
355
354
  export class ContainerInfo {
356
355
  _container;
@@ -434,6 +433,62 @@ export class ContainerInfo {
434
433
  });
435
434
  return ports;
436
435
  }
436
+ async getLogs() {
437
+ const logStream = await this.native.logs({
438
+ stdout: true,
439
+ stderr: true,
440
+ follow: false,
441
+ tail: 100,
442
+ timestamps: true,
443
+ });
444
+ const out = [];
445
+ await promisifyStream(logStream, (data) => {
446
+ const buf = data;
447
+ let offset = 0;
448
+ while (offset < buf.length) {
449
+ try {
450
+ // Read the docker log format - explained here:
451
+ // https://docs.docker.com/engine/api/v1.41/#operation/ContainerAttach
452
+ // or here : https://ahmet.im/blog/docker-logs-api-binary-format-explained/
453
+ // First byte is stream type
454
+ const streamTypeInt = buf.readInt8(offset);
455
+ const streamType = streamTypeInt === 1 ? 'stdout' : 'stderr';
456
+ // Bytes 4-8 is frame size
457
+ const messageLength = buf.readInt32BE(offset + 4);
458
+ // After that is the message - with the message length
459
+ const dataWithoutStreamType = buf.subarray(offset + 8, offset + 8 + messageLength);
460
+ const raw = dataWithoutStreamType.toString();
461
+ // Split the message into date and message
462
+ const firstSpaceIx = raw.indexOf(' ');
463
+ const dateString = raw.substring(0, firstSpaceIx);
464
+ const line = raw.substring(firstSpaceIx + 1);
465
+ offset = offset + messageLength + 8;
466
+ if (!dateString) {
467
+ continue;
468
+ }
469
+ out.push({
470
+ time: new Date(dateString).getTime(),
471
+ message: line,
472
+ level: 'INFO',
473
+ source: streamType,
474
+ });
475
+ }
476
+ catch (err) {
477
+ console.error('Error parsing log entry', err);
478
+ offset = buf.length;
479
+ }
480
+ }
481
+ });
482
+ if (out.length === 0) {
483
+ out.push({
484
+ time: Date.now(),
485
+ message: 'No logs found for container',
486
+ level: 'INFO',
487
+ source: 'stdout',
488
+ });
489
+ }
490
+ return out;
491
+ }
437
492
  }
438
493
  export function getExtraHosts(dockerVersion) {
439
494
  if (process.platform !== 'darwin' && process.platform !== 'win32') {
@@ -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;
@@ -1,4 +1,5 @@
1
1
  import ClusterConfiguration from '@kapeta/local-cluster-config';
2
+ import { parseKapetaUri } from "@kapeta/nodejs-utils";
2
3
  const CACHE_TTL = 60 * 1000; // 1 min
3
4
  class DefinitionsManager {
4
5
  cache = {};
@@ -31,6 +32,12 @@ class DefinitionsManager {
31
32
  const key = this.getKey(kindFilter);
32
33
  return this.doCached(key, () => ClusterConfiguration.getDefinitions(kindFilter));
33
34
  }
35
+ exists(ref) {
36
+ const uri = parseKapetaUri(ref);
37
+ return !!this.getDefinitions().find((d) => {
38
+ return parseKapetaUri(`${d.definition.metadata.name}:${d.version}`).id === uri.id;
39
+ });
40
+ }
34
41
  getProviderDefinitions() {
35
42
  return this.doCached('providers', () => ClusterConfiguration.getProviderDefinitions());
36
43
  }
@@ -1,4 +1,4 @@
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;
@@ -7,6 +7,7 @@ export declare class InstanceManager {
7
7
  getInstances(): InstanceInfo[];
8
8
  getInstancesForPlan(systemId: string): InstanceInfo[];
9
9
  getInstance(systemId: string, instanceId: string): InstanceInfo | undefined;
10
+ getLogs(systemId: string, instanceId: string): Promise<LogEntry[]>;
10
11
  saveInternalInstance(instance: InstanceInfo): Promise<InstanceInfo>;
11
12
  /**
12
13
  * Method is called when instance is started from the Kapeta SDKs (e.g. NodeJS SDK)
@@ -50,6 +50,31 @@ export class InstanceManager {
50
50
  systemId = normalizeKapetaUri(systemId);
51
51
  return this._instances.find((i) => i.systemId === systemId && i.instanceId === instanceId);
52
52
  }
53
+ async getLogs(systemId, instanceId) {
54
+ const instance = this.getInstance(systemId, instanceId);
55
+ if (!instance) {
56
+ throw new Error(`Instance ${systemId}/${instanceId} not found`);
57
+ }
58
+ switch (instance.type) {
59
+ case InstanceType.DOCKER:
60
+ return await containerManager.getLogs(instance);
61
+ case InstanceType.UNKNOWN:
62
+ return [{
63
+ level: 'INFO',
64
+ message: 'Instance is starting...',
65
+ time: Date.now(),
66
+ source: 'stdout',
67
+ }];
68
+ case InstanceType.LOCAL:
69
+ return [{
70
+ level: 'INFO',
71
+ message: 'Instance started outside Kapeta - logs not available...',
72
+ time: Date.now(),
73
+ source: 'stdout',
74
+ }];
75
+ }
76
+ return [];
77
+ }
53
78
  async saveInternalInstance(instance) {
54
79
  instance.systemId = normalizeKapetaUri(instance.systemId);
55
80
  if (instance.ref) {
@@ -100,7 +125,6 @@ export class InstanceManager {
100
125
  }
101
126
  instance.desiredStatus = info.desiredStatus;
102
127
  instance.owner = info.owner;
103
- instance.internal = undefined;
104
128
  instance.status = InstanceStatus.STARTING;
105
129
  instance.startedAt = Date.now();
106
130
  }
@@ -199,7 +223,7 @@ export class InstanceManager {
199
223
  this.save();
200
224
  try {
201
225
  if (instance.type === 'docker') {
202
- const containerName = getBlockInstanceContainerName(instance.instanceId);
226
+ const containerName = getBlockInstanceContainerName(instance.systemId, instance.instanceId);
203
227
  const container = await containerManager.getContainerByName(containerName);
204
228
  if (container) {
205
229
  try {
@@ -275,7 +299,7 @@ export class InstanceManager {
275
299
  name: blockAsset.data.metadata.name,
276
300
  desiredStatus: DesiredInstanceStatus.RUN,
277
301
  owner: InstanceOwner.INTERNAL,
278
- type: InstanceType.UNKNOWN,
302
+ type: existingInstance?.type ?? InstanceType.UNKNOWN,
279
303
  status: InstanceStatus.STARTING,
280
304
  startedAt: Date.now(),
281
305
  };
@@ -295,41 +319,6 @@ export class InstanceManager {
295
319
  const startTime = Date.now();
296
320
  try {
297
321
  const processInfo = await runner.start(blockRef, instanceId, instanceConfig);
298
- //emit stdout/stderr via sockets
299
- processInfo.output.on('data', (data) => {
300
- const payload = {
301
- source: 'stdout',
302
- level: 'INFO',
303
- message: data.toString(),
304
- time: Date.now(),
305
- };
306
- this.emitInstanceEvent(systemId, instanceId, EVENT_INSTANCE_LOG, payload);
307
- });
308
- processInfo.output.on('exit', (exitCode) => {
309
- const timeRunning = Date.now() - startTime;
310
- const instance = this.getInstance(systemId, instanceId);
311
- if (instance?.status === InstanceStatus.READY) {
312
- //It's already been running
313
- return;
314
- }
315
- if (exitCode === 143 || exitCode === 137) {
316
- //Process got SIGTERM (143) or SIGKILL (137)
317
- //TODO: Windows?
318
- return;
319
- }
320
- if (exitCode !== 0 || timeRunning < MIN_TIME_RUNNING) {
321
- const instance = this.getInstance(systemId, instanceId);
322
- if (instance) {
323
- instance.status = InstanceStatus.FAILED;
324
- this.save();
325
- }
326
- this.emitSystemEvent(systemId, EVENT_INSTANCE_EXITED, {
327
- error: 'Failed to start instance',
328
- status: EVENT_INSTANCE_EXITED,
329
- instanceId: blockInstance.id,
330
- });
331
- }
332
- });
333
322
  instance.status = InstanceStatus.READY;
334
323
  return this.saveInternalInstance({
335
324
  ...instance,
@@ -338,10 +327,6 @@ export class InstanceManager {
338
327
  health: null,
339
328
  portType: processInfo.portType,
340
329
  status: InstanceStatus.READY,
341
- internal: {
342
- logs: processInfo.logs,
343
- output: processInfo.output,
344
- },
345
330
  });
346
331
  }
347
332
  catch (e) {
@@ -387,9 +372,7 @@ export class InstanceManager {
387
372
  save() {
388
373
  try {
389
374
  storageService.put('instances', this._instances.map((instance) => {
390
- const copy = { ...instance };
391
- delete copy.internal;
392
- return copy;
375
+ return { ...instance };
393
376
  }));
394
377
  }
395
378
  catch (e) {
@@ -491,7 +474,7 @@ export class InstanceManager {
491
474
  }
492
475
  async getExternalStatus(instance) {
493
476
  if (instance.type === InstanceType.DOCKER) {
494
- const containerName = getBlockInstanceContainerName(instance.instanceId);
477
+ const containerName = getBlockInstanceContainerName(instance.systemId, instance.instanceId);
495
478
  const container = await containerManager.getContainerByName(containerName);
496
479
  if (!container) {
497
480
  // If the container doesn't exist, we consider the instance stopped