@kapeta/local-cluster-service 0.12.0 → 0.13.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 (55) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/cjs/index.js +2 -0
  3. package/dist/cjs/src/assetManager.d.ts +3 -1
  4. package/dist/cjs/src/assetManager.js +20 -4
  5. package/dist/cjs/src/assets/routes.js +22 -1
  6. package/dist/cjs/src/containerManager.d.ts +1 -1
  7. package/dist/cjs/src/containerManager.js +132 -122
  8. package/dist/cjs/src/instanceManager.d.ts +4 -3
  9. package/dist/cjs/src/instanceManager.js +87 -60
  10. package/dist/cjs/src/instances/routes.js +21 -11
  11. package/dist/cjs/src/operatorManager.d.ts +5 -3
  12. package/dist/cjs/src/operatorManager.js +34 -22
  13. package/dist/cjs/src/providerManager.js +1 -1
  14. package/dist/cjs/src/repositoryManager.d.ts +2 -4
  15. package/dist/cjs/src/repositoryManager.js +51 -66
  16. package/dist/cjs/src/socketManager.js +1 -1
  17. package/dist/cjs/src/taskManager.d.ts +64 -0
  18. package/dist/cjs/src/taskManager.js +163 -0
  19. package/dist/cjs/src/tasks/routes.d.ts +3 -0
  20. package/dist/cjs/src/tasks/routes.js +35 -0
  21. package/dist/cjs/src/utils/BlockInstanceRunner.js +0 -1
  22. package/dist/esm/index.js +2 -0
  23. package/dist/esm/src/assetManager.d.ts +3 -1
  24. package/dist/esm/src/assetManager.js +20 -4
  25. package/dist/esm/src/assets/routes.js +22 -1
  26. package/dist/esm/src/containerManager.d.ts +1 -1
  27. package/dist/esm/src/containerManager.js +132 -122
  28. package/dist/esm/src/instanceManager.d.ts +4 -3
  29. package/dist/esm/src/instanceManager.js +87 -60
  30. package/dist/esm/src/instances/routes.js +21 -11
  31. package/dist/esm/src/operatorManager.d.ts +5 -3
  32. package/dist/esm/src/operatorManager.js +34 -22
  33. package/dist/esm/src/providerManager.js +1 -1
  34. package/dist/esm/src/repositoryManager.d.ts +2 -4
  35. package/dist/esm/src/repositoryManager.js +51 -66
  36. package/dist/esm/src/socketManager.js +1 -1
  37. package/dist/esm/src/taskManager.d.ts +64 -0
  38. package/dist/esm/src/taskManager.js +159 -0
  39. package/dist/esm/src/tasks/routes.d.ts +3 -0
  40. package/dist/esm/src/tasks/routes.js +30 -0
  41. package/dist/esm/src/utils/BlockInstanceRunner.js +0 -1
  42. package/index.ts +2 -0
  43. package/package.json +1 -1
  44. package/src/assetManager.ts +28 -4
  45. package/src/assets/routes.ts +23 -1
  46. package/src/containerManager.ts +153 -142
  47. package/src/instanceManager.ts +116 -70
  48. package/src/instances/routes.ts +20 -12
  49. package/src/operatorManager.ts +46 -26
  50. package/src/providerManager.ts +1 -1
  51. package/src/repositoryManager.ts +65 -63
  52. package/src/socketManager.ts +1 -1
  53. package/src/taskManager.ts +225 -0
  54. package/src/tasks/routes.ts +38 -0
  55. package/src/utils/BlockInstanceRunner.ts +0 -4
package/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ # [0.13.0](https://github.com/kapetacom/local-cluster-service/compare/v0.12.1...v0.13.0) (2023-08-03)
2
+
3
+
4
+ ### Features
5
+
6
+ * Adds background task concept ([#55](https://github.com/kapetacom/local-cluster-service/issues/55)) ([71cc63c](https://github.com/kapetacom/local-cluster-service/commit/71cc63c9c3eb8bec1dec8b31d3340694e93bd3e5))
7
+
8
+ ## [0.12.1](https://github.com/kapetacom/local-cluster-service/compare/v0.12.0...v0.12.1) (2023-08-02)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * Adjustments to make starting plans locally smoother ([fc353ad](https://github.com/kapetacom/local-cluster-service/commit/fc353adde350b7e9d4c7eb9347c4cfa0c3a6aa58))
14
+
1
15
  # [0.12.0](https://github.com/kapetacom/local-cluster-service/compare/v0.11.1...v0.12.0) (2023-07-31)
2
16
 
3
17
 
package/dist/cjs/index.js CHANGED
@@ -20,6 +20,7 @@ const routes_6 = __importDefault(require("./src/filesystem/routes"));
20
20
  const routes_7 = __importDefault(require("./src/assets/routes"));
21
21
  const routes_8 = __importDefault(require("./src/providers/routes"));
22
22
  const routes_9 = __importDefault(require("./src/attachments/routes"));
23
+ const routes_10 = __importDefault(require("./src/tasks/routes"));
23
24
  const utils_1 = require("./src/utils/utils");
24
25
  const request_1 = __importDefault(require("request"));
25
26
  let currentServer = null;
@@ -34,6 +35,7 @@ function createServer() {
34
35
  app.use('/assets', routes_7.default);
35
36
  app.use('/providers', routes_8.default);
36
37
  app.use('/attachments', routes_9.default);
38
+ app.use('/tasks', routes_10.default);
37
39
  app.get('/status', async (req, res) => {
38
40
  res.send({
39
41
  ok: true,
@@ -13,6 +13,7 @@ export interface EnrichedAsset {
13
13
  declare class AssetManager {
14
14
  private cache;
15
15
  constructor();
16
+ clearCache(): void;
16
17
  /**
17
18
  *
18
19
  * @param {string[]} [assetKinds]
@@ -21,11 +22,12 @@ declare class AssetManager {
21
22
  getAssets(assetKinds?: string[]): EnrichedAsset[];
22
23
  getPlans(): EnrichedAsset[];
23
24
  getPlan(ref: string, noCache?: boolean): Promise<Definition>;
24
- getAsset(ref: string, noCache?: boolean): Promise<EnrichedAsset | undefined>;
25
+ getAsset(ref: string, noCache?: boolean, autoFetch?: boolean): Promise<EnrichedAsset | undefined>;
25
26
  createAsset(path: string, yaml: BlockDefinition): Promise<EnrichedAsset[]>;
26
27
  updateAsset(ref: string, yaml: BlockDefinition): Promise<void>;
27
28
  importFile(filePath: string): Promise<EnrichedAsset[]>;
28
29
  unregisterAsset(ref: string): Promise<void>;
30
+ installAsset(ref: string): Promise<import("./taskManager").Task<void>[] | undefined>;
29
31
  }
30
32
  export declare const assetManager: AssetManager;
31
33
  export {};
@@ -47,6 +47,9 @@ class AssetManager {
47
47
  stdTTL: 60 * 60, // 1 hour
48
48
  });
49
49
  }
50
+ clearCache() {
51
+ this.cache.flushAll();
52
+ }
50
53
  /**
51
54
  *
52
55
  * @param {string[]} [assetKinds]
@@ -76,22 +79,26 @@ class AssetManager {
76
79
  }
77
80
  return asset.data;
78
81
  }
79
- async getAsset(ref, noCache = false) {
82
+ async getAsset(ref, noCache = false, autoFetch = true) {
80
83
  ref = (0, utils_1.normalizeKapetaUri)(ref);
81
84
  const cacheKey = `getAsset:${ref}`;
82
85
  if (!noCache && this.cache.has(cacheKey)) {
83
86
  return this.cache.get(cacheKey);
84
87
  }
85
88
  const uri = (0, nodejs_utils_1.parseKapetaUri)(ref);
86
- await repositoryManager_1.repositoryManager.ensureAsset(uri.handle, uri.name, uri.version);
89
+ if (autoFetch) {
90
+ await repositoryManager_1.repositoryManager.ensureAsset(uri.handle, uri.name, uri.version, true);
91
+ }
87
92
  let asset = definitionsManager_1.definitionsManager
88
93
  .getDefinitions()
89
94
  .map(enrichAsset)
90
95
  .find((a) => (0, nodejs_utils_1.parseKapetaUri)(a.ref).equals(uri));
91
- if (!asset) {
96
+ if (autoFetch && !asset) {
92
97
  throw new Error('Asset not found: ' + ref);
93
98
  }
94
- this.cache.set(cacheKey, asset);
99
+ if (asset) {
100
+ this.cache.set(cacheKey, asset);
101
+ }
95
102
  return asset;
96
103
  }
97
104
  async createAsset(path, yaml) {
@@ -152,5 +159,14 @@ class AssetManager {
152
159
  this.cache.flushAll();
153
160
  await nodejs_registry_utils_1.Actions.uninstall(progressListener_1.progressListener, [asset.ref]);
154
161
  }
162
+ async installAsset(ref) {
163
+ const asset = await this.getAsset(ref, true, false);
164
+ if (asset) {
165
+ throw new Error('Asset already installed: ' + ref);
166
+ }
167
+ const uri = (0, nodejs_utils_1.parseKapetaUri)(ref);
168
+ console.log('Installing %s', ref);
169
+ return await repositoryManager_1.repositoryManager.ensureAsset(uri.handle, uri.name, uri.version, false);
170
+ }
155
171
  }
156
172
  exports.assetManager = new AssetManager();
@@ -39,8 +39,15 @@ router.get('/read', async (req, res) => {
39
39
  res.status(400).send({ error: 'Query parameter "ref" is missing' });
40
40
  return;
41
41
  }
42
+ const ensure = req.query.ensure !== 'false';
42
43
  try {
43
- res.send(await assetManager_1.assetManager.getAsset(req.query.ref, true));
44
+ const asset = await assetManager_1.assetManager.getAsset(req.query.ref, true, ensure);
45
+ if (asset) {
46
+ res.send(asset);
47
+ }
48
+ else {
49
+ res.status(404).send({ error: 'Asset not found' });
50
+ }
44
51
  }
45
52
  catch (err) {
46
53
  res.status(400).send({ error: err.message });
@@ -114,4 +121,18 @@ router.put('/import', async (req, res) => {
114
121
  res.status(400).send({ error: err.message });
115
122
  }
116
123
  });
124
+ router.put('/install', async (req, res) => {
125
+ if (!req.query.ref) {
126
+ res.status(400).send({ error: 'Query parameter "ref" is missing' });
127
+ return;
128
+ }
129
+ try {
130
+ const tasks = await assetManager_1.assetManager.installAsset(req.query.ref);
131
+ const taskIds = tasks?.map((t) => t.id) ?? [];
132
+ res.status(200).send(taskIds);
133
+ }
134
+ catch (err) {
135
+ res.status(400).send({ error: err.message });
136
+ }
137
+ });
117
138
  exports.default = router;
@@ -59,7 +59,7 @@ declare class ContainerManager {
59
59
  ping(): Promise<void>;
60
60
  docker(): Docker;
61
61
  getContainerByName(containerName: string): Promise<ContainerInfo | undefined>;
62
- pull(image: string, cacheForMS?: number): Promise<boolean>;
62
+ pull(image: string): Promise<boolean>;
63
63
  toDockerMounts(mounts: StringMap): DockerMounts[];
64
64
  toDockerHealth(health: Health): {
65
65
  Test: string[];
@@ -15,15 +15,13 @@ 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
18
  const nodejs_api_client_1 = require("@kapeta/nodejs-api-client");
19
+ const taskManager_1 = require("./taskManager");
20
20
  const EVENT_IMAGE_PULL = 'docker-image-pull';
21
21
  exports.CONTAINER_LABEL_PORT_PREFIX = 'kapeta_port-';
22
22
  const NANO_SECOND = 1000000;
23
23
  const HEALTH_CHECK_INTERVAL = 3000;
24
24
  const HEALTH_CHECK_MAX = 20;
25
- const IMAGE_PULL_CACHE_TTL = 30 * 60 * 1000;
26
- const IMAGE_PULL_CACHE = {};
27
25
  exports.HEALTH_CHECK_TIMEOUT = HEALTH_CHECK_INTERVAL * HEALTH_CHECK_MAX * 2;
28
26
  const promisifyStream = (stream, handler) => new Promise((resolve, reject) => {
29
27
  stream.on('data', handler);
@@ -151,17 +149,11 @@ class ContainerManager {
151
149
  }
152
150
  return undefined;
153
151
  }
154
- async pull(image, cacheForMS = IMAGE_PULL_CACHE_TTL) {
152
+ async pull(image) {
155
153
  let [imageName, tag] = image.split(/:/);
156
154
  if (!tag) {
157
155
  tag = 'latest';
158
156
  }
159
- if (IMAGE_PULL_CACHE[image]) {
160
- const timeSince = Date.now() - IMAGE_PULL_CACHE[image];
161
- if (timeSince < cacheForMS) {
162
- return false;
163
- }
164
- }
165
157
  const imageTagList = (await this.docker().image.list())
166
158
  .map((image) => image.data)
167
159
  .filter((imageData) => !!imageData.RepoTags)
@@ -170,121 +162,141 @@ class ContainerManager {
170
162
  console.log('Image found: %s', image);
171
163
  return false;
172
164
  }
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, {
185
- fromImage: imageName,
186
- tag: tag,
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;
165
+ let friendlyImageName = image;
166
+ const imageParts = imageName.split('/');
167
+ if (imageParts.length > 2) {
168
+ //Strip the registry to make the name shorter
169
+ friendlyImageName = `${imageParts.slice(1).join('/')}:${tag}`;
170
+ }
171
+ const taskName = `Pulling image ${friendlyImageName}`;
172
+ const processor = async (task) => {
173
+ const timeStarted = Date.now();
174
+ const api = new nodejs_api_client_1.KapetaAPI();
175
+ const accessToken = await api.getAccessToken();
176
+ const auth = image.startsWith('docker.kapeta.com/')
177
+ ? {
178
+ username: 'kapeta',
179
+ password: accessToken,
180
+ serveraddress: 'docker.kapeta.com',
264
181
  }
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++;
182
+ : {};
183
+ const stream = (await this.docker().image.create(auth, {
184
+ fromImage: imageName,
185
+ tag: tag,
186
+ }));
187
+ const chunks = {};
188
+ let lastEmitted = Date.now();
189
+ await promisifyStream(stream, (rawData) => {
190
+ const lines = rawData.toString().trim().split('\n');
191
+ lines.forEach((line) => {
192
+ const data = JSON.parse(line);
193
+ if (![
194
+ 'Waiting',
195
+ 'Downloading',
196
+ 'Extracting',
197
+ 'Download complete',
198
+ 'Pull complete',
199
+ 'Already exists',
200
+ ].includes(data.status)) {
201
+ return;
202
+ }
203
+ if (!chunks[data.id]) {
204
+ chunks[data.id] = {
205
+ downloading: {
206
+ total: 0,
207
+ current: 0,
208
+ },
209
+ extracting: {
210
+ total: 0,
211
+ current: 0,
212
+ },
213
+ done: false,
214
+ };
215
+ }
216
+ const chunk = chunks[data.id];
217
+ switch (data.status) {
218
+ case 'Downloading':
219
+ chunk.downloading = data.progressDetail;
220
+ break;
221
+ case 'Extracting':
222
+ chunk.extracting = data.progressDetail;
223
+ break;
224
+ case 'Download complete':
225
+ chunk.downloading.current = chunks[data.id].downloading.total;
226
+ break;
227
+ case 'Pull complete':
228
+ chunk.extracting.current = chunks[data.id].extracting.total;
229
+ chunk.done = true;
230
+ break;
231
+ case 'Already exists':
232
+ // Force layer to be done
233
+ chunk.downloading.current = 1;
234
+ chunk.downloading.total = 1;
235
+ chunk.extracting.current = 1;
236
+ chunk.extracting.total = 1;
237
+ chunk.done = true;
238
+ break;
239
+ }
240
+ });
241
+ if (Date.now() - lastEmitted < 1000) {
242
+ return;
273
243
  }
244
+ const chunkList = Object.values(chunks);
245
+ let totals = {
246
+ downloading: {
247
+ total: 0,
248
+ current: 0,
249
+ },
250
+ extracting: {
251
+ total: 0,
252
+ current: 0,
253
+ },
254
+ total: chunkList.length,
255
+ done: 0,
256
+ };
257
+ chunkList.forEach((chunk) => {
258
+ if (chunk.downloading.current > 0) {
259
+ totals.downloading.current += chunk.downloading.current;
260
+ }
261
+ if (chunk.downloading.total > 0) {
262
+ totals.downloading.total += chunk.downloading.total;
263
+ }
264
+ if (chunk.extracting.current > 0) {
265
+ totals.extracting.current += chunk.extracting.current;
266
+ }
267
+ if (chunk.extracting.total > 0) {
268
+ totals.extracting.total += chunk.extracting.total;
269
+ }
270
+ if (chunk.done) {
271
+ totals.done++;
272
+ }
273
+ });
274
+ const progress = totals.total > 0 ? (totals.done / totals.total) * 100 : 0;
275
+ task.metadata = {
276
+ ...task.metadata,
277
+ image,
278
+ progress,
279
+ status: totals,
280
+ timeTaken: Date.now() - timeStarted,
281
+ };
282
+ task.emitUpdate();
283
+ lastEmitted = Date.now();
284
+ //console.log('Pulling image %s: %s % [done: %s, total: %s]', image, Math.round(percent), totals.done, totals.total);
274
285
  });
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, {
286
+ task.metadata = {
287
+ ...task.metadata,
278
288
  image,
279
- percent,
280
- status: totals,
289
+ progress: 100,
281
290
  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);
291
+ };
292
+ task.emitUpdate();
293
+ };
294
+ const task = taskManager_1.taskManager.add(`docker:image:pull:${image}`, processor, {
295
+ name: taskName,
296
+ image,
297
+ progress: -1,
285
298
  });
286
- IMAGE_PULL_CACHE[image] = Date.now();
287
- socketManager_1.socketManager.emitGlobal(EVENT_IMAGE_PULL, { image, percent: 100, timeTaken: Date.now() - timeStarted });
299
+ await task.wait();
288
300
  return true;
289
301
  }
290
302
  toDockerMounts(mounts) {
@@ -319,9 +331,7 @@ class ContainerManager {
319
331
  dockerOpts.Labels.HASH = hash;
320
332
  }
321
333
  async ensureContainer(opts) {
322
- const container = await this.createOrUpdateContainer(opts);
323
- await this.waitForReady(container);
324
- return container;
334
+ return await this.createOrUpdateContainer(opts);
325
335
  }
326
336
  async createOrUpdateContainer(opts) {
327
337
  let imagePulled = await this.pull(opts.Image);
@@ -1,4 +1,5 @@
1
1
  import { InstanceInfo, LogEntry } from './types';
2
+ import { Task } from './taskManager';
2
3
  export declare class InstanceManager {
3
4
  private _interval;
4
5
  private readonly _instances;
@@ -18,11 +19,11 @@ export declare class InstanceManager {
18
19
  registerInstanceFromSDK(systemId: string, instanceId: string, info: Omit<InstanceInfo, 'systemId' | 'instanceId'>): Promise<InstanceInfo | undefined>;
19
20
  private getHealthUrl;
20
21
  markAsStopped(systemId: string, instanceId: string): Promise<void>;
21
- startAllForPlan(systemId: string): Promise<InstanceInfo[]>;
22
+ startAllForPlan(systemId: string): Promise<Task<InstanceInfo[]>>;
22
23
  stop(systemId: string, instanceId: string): Promise<void>;
23
24
  private stopInner;
24
- stopAllForPlan(systemId: string): Promise<void>;
25
- start(systemId: string, instanceId: string): Promise<InstanceInfo>;
25
+ stopAllForPlan(systemId: string): Task<void>;
26
+ start(systemId: string, instanceId: string): Promise<InstanceInfo | Task<InstanceInfo>>;
26
27
  /**
27
28
  * Stops an instance but does not remove it from the list of active instances
28
29
  *