@kapeta/local-cluster-service 0.11.0 → 0.12.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/definitions.d.ts +7 -0
  3. package/dist/cjs/src/config/routes.js +1 -1
  4. package/dist/cjs/src/containerManager.d.ts +2 -1
  5. package/dist/cjs/src/containerManager.js +125 -21
  6. package/dist/cjs/src/definitionsManager.d.ts +1 -0
  7. package/dist/cjs/src/definitionsManager.js +7 -4
  8. package/dist/cjs/src/instanceManager.d.ts +12 -2
  9. package/dist/cjs/src/instanceManager.js +253 -200
  10. package/dist/cjs/src/operatorManager.d.ts +2 -0
  11. package/dist/cjs/src/operatorManager.js +69 -67
  12. package/dist/cjs/src/socketManager.d.ts +1 -0
  13. package/dist/cjs/src/socketManager.js +3 -0
  14. package/dist/cjs/src/types.d.ts +1 -0
  15. package/dist/cjs/src/utils/BlockInstanceRunner.js +2 -2
  16. package/dist/esm/src/config/routes.js +1 -1
  17. package/dist/esm/src/containerManager.d.ts +2 -1
  18. package/dist/esm/src/containerManager.js +126 -22
  19. package/dist/esm/src/definitionsManager.d.ts +1 -0
  20. package/dist/esm/src/definitionsManager.js +8 -5
  21. package/dist/esm/src/instanceManager.d.ts +12 -2
  22. package/dist/esm/src/instanceManager.js +253 -200
  23. package/dist/esm/src/operatorManager.d.ts +2 -0
  24. package/dist/esm/src/operatorManager.js +67 -65
  25. package/dist/esm/src/socketManager.d.ts +1 -0
  26. package/dist/esm/src/socketManager.js +3 -0
  27. package/dist/esm/src/types.d.ts +1 -0
  28. package/dist/esm/src/utils/BlockInstanceRunner.js +2 -2
  29. package/dist/esm/src/utils/utils.js +1 -1
  30. package/package.json +3 -1
  31. package/src/config/routes.ts +1 -1
  32. package/src/containerManager.ts +178 -43
  33. package/src/definitionsManager.ts +9 -5
  34. package/src/instanceManager.ts +288 -228
  35. package/src/instances/routes.ts +1 -1
  36. package/src/operatorManager.ts +72 -70
  37. package/src/socketManager.ts +4 -0
  38. package/src/types.ts +1 -1
  39. package/src/utils/BlockInstanceRunner.ts +12 -22
  40. package/src/utils/utils.ts +2 -2
package/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ # [0.12.0](https://github.com/kapetacom/local-cluster-service/compare/v0.11.1...v0.12.0) (2023-07-31)
2
+
3
+
4
+ ### Features
5
+
6
+ * Send status events to client when pulling image ([#54](https://github.com/kapetacom/local-cluster-service/issues/54)) ([6c6f1b0](https://github.com/kapetacom/local-cluster-service/commit/6c6f1b0cf31d4bbd1fccf10f4a66a3ac97ff7171))
7
+
8
+ ## [0.11.1](https://github.com/kapetacom/local-cluster-service/compare/v0.11.0...v0.11.1) (2023-07-31)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * Ensure we do not attempt to start / stop the same instance at the ([493e077](https://github.com/kapetacom/local-cluster-service/commit/493e077d0c6acdbcc371dae2ef8b6fbf2478c950))
14
+
1
15
  # [0.11.0](https://github.com/kapetacom/local-cluster-service/compare/v0.10.1...v0.11.0) (2023-07-31)
2
16
 
3
17
 
package/definitions.d.ts CHANGED
@@ -18,4 +18,11 @@ declare module '@kapeta/nodejs-registry-utils' {
18
18
 
19
19
  export const Config: any;
20
20
  export const Actions: any;
21
+
22
+ export const handlers: {
23
+ DockerHandler: ArtifactHandlerFactory;
24
+ NPMHandler: ArtifactHandlerFactory;
25
+ MavenHandler: ArtifactHandlerFactory;
26
+ YAMLHandler: ArtifactHandlerFactory;
27
+ };
21
28
  }
@@ -37,7 +37,7 @@ router.put('/instance', async (req, res) => {
37
37
  if (req.kapeta.instanceId) {
38
38
  configManager_1.configManager.setConfigForSection(req.kapeta.systemId, req.kapeta.instanceId, config);
39
39
  //Restart the instance if it is running after config change
40
- await instanceManager_1.instanceManager.restart(req.kapeta.systemId, req.kapeta.instanceId);
40
+ await instanceManager_1.instanceManager.prepareForRestart(req.kapeta.systemId, req.kapeta.instanceId);
41
41
  }
42
42
  else {
43
43
  configManager_1.configManager.setConfigForSystem(req.kapeta.systemId, config);
@@ -1,6 +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
+ import { InstanceInfo, LogEntry } from './types';
4
4
  type StringMap = {
5
5
  [key: string]: string;
6
6
  };
@@ -49,6 +49,7 @@ declare class ContainerManager {
49
49
  private _alive;
50
50
  private _mountDir;
51
51
  private _version;
52
+ private _lastDockerAccessCheck;
52
53
  constructor();
53
54
  initialize(): Promise<void>;
54
55
  checkAlive(): Promise<boolean>;
@@ -15,6 +15,9 @@ const local_cluster_config_1 = __importDefault(require("@kapeta/local-cluster-co
15
15
  const node_uuid_1 = __importDefault(require("node-uuid"));
16
16
  const md5_1 = __importDefault(require("md5"));
17
17
  const utils_1 = require("./utils/utils");
18
+ const socketManager_1 = require("./socketManager");
19
+ const nodejs_api_client_1 = require("@kapeta/nodejs-api-client");
20
+ const EVENT_IMAGE_PULL = 'docker-image-pull';
18
21
  exports.CONTAINER_LABEL_PORT_PREFIX = 'kapeta_port-';
19
22
  const NANO_SECOND = 1000000;
20
23
  const HEALTH_CHECK_INTERVAL = 3000;
@@ -32,6 +35,7 @@ class ContainerManager {
32
35
  _alive;
33
36
  _mountDir;
34
37
  _version;
38
+ _lastDockerAccessCheck = 0;
35
39
  constructor() {
36
40
  this._docker = null;
37
41
  this._alive = false;
@@ -166,17 +170,121 @@ class ContainerManager {
166
170
  console.log('Image found: %s', image);
167
171
  return false;
168
172
  }
169
- console.log('Pulling image: %s', image);
170
- const stream = await this.docker()
171
- .image.create({}, {
173
+ const timeStarted = Date.now();
174
+ socketManager_1.socketManager.emitGlobal(EVENT_IMAGE_PULL, { image, percent: 0 });
175
+ const api = new nodejs_api_client_1.KapetaAPI();
176
+ const accessToken = await api.getAccessToken();
177
+ const auth = image.startsWith('docker.kapeta.com/')
178
+ ? {
179
+ username: 'kapeta',
180
+ password: accessToken,
181
+ serveraddress: 'docker.kapeta.com',
182
+ }
183
+ : {};
184
+ const stream = (await this.docker().image.create(auth, {
172
185
  fromImage: imageName,
173
186
  tag: tag,
174
- });
175
- await promisifyStream(stream, (chunk) => {
176
- console.log('Data from docker: "%s"', chunk.toString());
187
+ }));
188
+ const chunks = {};
189
+ let lastEmitted = Date.now();
190
+ await promisifyStream(stream, (rawData) => {
191
+ const lines = rawData.toString().trim().split('\n');
192
+ lines.forEach((line) => {
193
+ const data = JSON.parse(line);
194
+ if (![
195
+ 'Waiting',
196
+ 'Downloading',
197
+ 'Extracting',
198
+ 'Download complete',
199
+ 'Pull complete',
200
+ 'Already exists',
201
+ ].includes(data.status)) {
202
+ return;
203
+ }
204
+ if (!chunks[data.id]) {
205
+ chunks[data.id] = {
206
+ downloading: {
207
+ total: 0,
208
+ current: 0,
209
+ },
210
+ extracting: {
211
+ total: 0,
212
+ current: 0,
213
+ },
214
+ done: false,
215
+ };
216
+ }
217
+ const chunk = chunks[data.id];
218
+ switch (data.status) {
219
+ case 'Downloading':
220
+ chunk.downloading = data.progressDetail;
221
+ break;
222
+ case 'Extracting':
223
+ chunk.extracting = data.progressDetail;
224
+ break;
225
+ case 'Download complete':
226
+ chunk.downloading.current = chunks[data.id].downloading.total;
227
+ break;
228
+ case 'Pull complete':
229
+ chunk.extracting.current = chunks[data.id].extracting.total;
230
+ chunk.done = true;
231
+ break;
232
+ case 'Already exists':
233
+ // Force layer to be done
234
+ chunk.downloading.current = 1;
235
+ chunk.downloading.total = 1;
236
+ chunk.extracting.current = 1;
237
+ chunk.extracting.total = 1;
238
+ chunk.done = true;
239
+ break;
240
+ }
241
+ });
242
+ if (Date.now() - lastEmitted < 1000) {
243
+ return;
244
+ }
245
+ const chunkList = Object.values(chunks);
246
+ let totals = {
247
+ downloading: {
248
+ total: 0,
249
+ current: 0,
250
+ },
251
+ extracting: {
252
+ total: 0,
253
+ current: 0,
254
+ },
255
+ total: chunkList.length,
256
+ done: 0,
257
+ };
258
+ chunkList.forEach((chunk) => {
259
+ if (chunk.downloading.current > 0) {
260
+ totals.downloading.current += chunk.downloading.current;
261
+ }
262
+ if (chunk.downloading.total > 0) {
263
+ totals.downloading.total += chunk.downloading.total;
264
+ }
265
+ if (chunk.extracting.current > 0) {
266
+ totals.extracting.current += chunk.extracting.current;
267
+ }
268
+ if (chunk.extracting.total > 0) {
269
+ totals.extracting.total += chunk.extracting.total;
270
+ }
271
+ if (chunk.done) {
272
+ totals.done++;
273
+ }
274
+ });
275
+ const percent = totals.total > 0 ? (totals.done / totals.total) * 100 : 0;
276
+ //We emit at most every second to not spam the client
277
+ socketManager_1.socketManager.emitGlobal(EVENT_IMAGE_PULL, {
278
+ image,
279
+ percent,
280
+ status: totals,
281
+ timeTaken: Date.now() - timeStarted,
282
+ });
283
+ lastEmitted = Date.now();
284
+ //console.log('Pulling image %s: %s % [done: %s, total: %s]', image, Math.round(percent), totals.done, totals.total);
177
285
  });
178
286
  IMAGE_PULL_CACHE[image] = Date.now();
179
- console.log('Image pulled: %s', image);
287
+ socketManager_1.socketManager.emitGlobal(EVENT_IMAGE_PULL, { image, percent: 100, timeTaken: Date.now() - timeStarted });
180
288
  return true;
181
289
  }
182
290
  toDockerMounts(mounts) {
@@ -216,13 +324,7 @@ class ContainerManager {
216
324
  return container;
217
325
  }
218
326
  async createOrUpdateContainer(opts) {
219
- let imagePulled = false;
220
- try {
221
- imagePulled = await this.pull(opts.Image);
222
- }
223
- catch (e) {
224
- console.warn('Failed to pull image. Continuing...', e);
225
- }
327
+ let imagePulled = await this.pull(opts.Image);
226
328
  this.applyHash(opts);
227
329
  if (!opts.name) {
228
330
  console.log('Starting unnamed container: %s', opts.Image);
@@ -347,12 +449,14 @@ class ContainerManager {
347
449
  const containerName = (0, utils_1.getBlockInstanceContainerName)(instance.systemId, instance.instanceId);
348
450
  const containerInfo = await this.getContainerByName(containerName);
349
451
  if (!containerInfo) {
350
- return [{
351
- source: "stdout",
352
- level: "ERROR",
452
+ return [
453
+ {
454
+ source: 'stdout',
455
+ level: 'ERROR',
353
456
  time: Date.now(),
354
- message: "Container not found"
355
- }];
457
+ message: 'Container not found',
458
+ },
459
+ ];
356
460
  }
357
461
  return containerInfo.getLogs();
358
462
  }
@@ -440,13 +544,13 @@ class ContainerInfo {
440
544
  return ports;
441
545
  }
442
546
  async getLogs() {
443
- const logStream = await this.native.logs({
547
+ const logStream = (await this.native.logs({
444
548
  stdout: true,
445
549
  stderr: true,
446
550
  follow: false,
447
551
  tail: 100,
448
552
  timestamps: true,
449
- });
553
+ }));
450
554
  const out = [];
451
555
  await promisifyStream(logStream, (data) => {
452
556
  const buf = data;
@@ -7,6 +7,7 @@ declare class DefinitionsManager {
7
7
  getDefinitions(kindFilter?: string | string[]): DefinitionInfo[];
8
8
  exists(ref: string): boolean;
9
9
  getProviderDefinitions(): DefinitionInfo[];
10
+ getDefinition(ref: string): DefinitionInfo | undefined;
10
11
  }
11
12
  export declare const definitionsManager: DefinitionsManager;
12
13
  export {};
@@ -39,13 +39,16 @@ class DefinitionsManager {
39
39
  return this.doCached(key, () => local_cluster_config_1.default.getDefinitions(kindFilter));
40
40
  }
41
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
- });
42
+ return !!this.getDefinition(ref);
46
43
  }
47
44
  getProviderDefinitions() {
48
45
  return this.doCached('providers', () => local_cluster_config_1.default.getProviderDefinitions());
49
46
  }
47
+ getDefinition(ref) {
48
+ const uri = (0, nodejs_utils_1.parseKapetaUri)(ref);
49
+ return this.getDefinitions().find((d) => {
50
+ return (0, nodejs_utils_1.parseKapetaUri)(`${d.definition.metadata.name}:${d.version}`).id === uri.id;
51
+ });
52
+ }
50
53
  }
51
54
  exports.definitionsManager = new DefinitionsManager();
@@ -2,11 +2,13 @@ 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;
10
12
  getLogs(systemId: string, instanceId: string): Promise<LogEntry[]>;
11
13
  saveInternalInstance(instance: InstanceInfo): Promise<InstanceInfo>;
12
14
  /**
@@ -15,12 +17,20 @@ export declare class InstanceManager {
15
17
  */
16
18
  registerInstanceFromSDK(systemId: string, instanceId: string, info: Omit<InstanceInfo, 'systemId' | 'instanceId'>): Promise<InstanceInfo | undefined>;
17
19
  private getHealthUrl;
18
- markAsStopped(systemId: string, instanceId: string): void;
20
+ markAsStopped(systemId: string, instanceId: string): Promise<void>;
19
21
  startAllForPlan(systemId: string): Promise<InstanceInfo[]>;
20
22
  stop(systemId: string, instanceId: string): Promise<void>;
23
+ private stopInner;
21
24
  stopAllForPlan(systemId: string): Promise<void>;
22
25
  start(systemId: string, instanceId: string): Promise<InstanceInfo>;
23
- restart(systemId: string, instanceId: string): Promise<InstanceInfo>;
26
+ /**
27
+ * Stops an instance but does not remove it from the list of active instances
28
+ *
29
+ * It will be started again next time the system checks the status of the instance
30
+ *
31
+ * We do it this way to not cause the user to wait for the instance to start again
32
+ */
33
+ prepareForRestart(systemId: string, instanceId: string): Promise<void>;
24
34
  stopAll(): Promise<void>;
25
35
  private stopInstances;
26
36
  private save;