@kapeta/local-cluster-service 0.27.0 → 0.28.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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ # [0.28.0](https://github.com/kapetacom/local-cluster-service/compare/v0.27.0...v0.28.0) (2023-11-15)
2
+
3
+
4
+ ### Features
5
+
6
+ * Auto-upgrade providers every 10th minute ([#100](https://github.com/kapetacom/local-cluster-service/issues/100)) ([2c35569](https://github.com/kapetacom/local-cluster-service/commit/2c35569587f456374529bb1f9a9e8f5c03be2189))
7
+
1
8
  # [0.27.0](https://github.com/kapetacom/local-cluster-service/compare/v0.26.0...v0.27.0) (2023-11-13)
2
9
 
3
10
 
package/definitions.d.ts CHANGED
@@ -13,12 +13,14 @@ declare module '@kapeta/nodejs-registry-utils' {
13
13
  export interface AssetVersion {
14
14
  content: Kind;
15
15
  dependencies: Dependency[];
16
+ version: string;
16
17
  }
17
18
 
18
19
  export class RegistryService {
19
20
  constructor(url: string);
20
21
 
21
22
  getVersion(fullName: string, version: string): Promise<AssetVersion>;
23
+ getLatestVersion(name): Promise<AssetVersion>;
22
24
  }
23
25
 
24
26
  export const Config: any;
package/dist/cjs/index.js CHANGED
@@ -58,6 +58,7 @@ const DefaultProviderInstaller_1 = require("./src/utils/DefaultProviderInstaller
58
58
  const authManager_1 = require("./src/authManager");
59
59
  const codeGeneratorManager_1 = require("./src/codeGeneratorManager");
60
60
  const Sentry = __importStar(require("@sentry/node"));
61
+ const assetManager_1 = require("./src/assetManager");
61
62
  Sentry.init({
62
63
  dsn: 'https://0b7cc946d82c591473d6f95fff5e210b@o4505820837249024.ingest.sentry.io/4506212692000768',
63
64
  enabled: process.env.NODE_ENV !== 'development',
@@ -231,6 +232,7 @@ exports.default = {
231
232
  catch (e) {
232
233
  console.error('Failed to install default providers.', e);
233
234
  }
235
+ assetManager_1.assetManager.startUpgradeInterval();
234
236
  resolve({ host, port, dockerStatus: containerManager_1.containerManager.isAlive() });
235
237
  });
236
238
  currentServer.host = host;
@@ -10,13 +10,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
10
10
  const express_promise_router_1 = __importDefault(require("express-promise-router"));
11
11
  const cors_1 = require("./middleware/cors");
12
12
  const nodejs_api_client_1 = require("@kapeta/nodejs-api-client");
13
- const local_cluster_config_1 = __importDefault(require("@kapeta/local-cluster-config"));
13
+ const nodejs_registry_utils_1 = require("@kapeta/nodejs-registry-utils");
14
14
  const { createAPIRoute } = require('@kapeta/web-microfrontend/server');
15
15
  const packageJson = require('../package.json');
16
16
  const router = (0, express_promise_router_1.default)();
17
- const remoteServices = local_cluster_config_1.default.getClusterConfig().remoteServices ?? {};
18
17
  router.use('/', cors_1.corsHandler);
19
- router.use('/registry', createAPIRoute(remoteServices.registry ?? 'https://registry.kapeta.com', {
18
+ router.use('/registry', createAPIRoute(nodejs_registry_utils_1.Config.data?.registry?.url ?? 'https://registry.kapeta.com', {
20
19
  nonce: false,
21
20
  userAgent: `KapetaDesktopCluster/${packageJson.version}`,
22
21
  tokenFetcher: () => {
@@ -16,6 +16,7 @@ export interface EnrichedAsset {
16
16
  ymlPath: string;
17
17
  }
18
18
  declare class AssetManager {
19
+ startUpgradeInterval(): void;
19
20
  /**
20
21
  *
21
22
  * @param {string[]} [assetKinds]
@@ -31,6 +32,8 @@ declare class AssetManager {
31
32
  importFile(filePath: string): Promise<EnrichedAsset[]>;
32
33
  unregisterAsset(ref: string): Promise<void>;
33
34
  installAsset(ref: string, wait?: boolean): Promise<import("./taskManager").Task<void>[] | undefined>;
35
+ private cleanupUnusedProviders;
36
+ private upgradeAllProviders;
34
37
  private maybeGenerateCode;
35
38
  }
36
39
  export declare const assetManager: AssetManager;
@@ -20,7 +20,9 @@ const definitionsManager_1 = require("./definitionsManager");
20
20
  const taskManager_1 = require("./taskManager");
21
21
  const cacheManager_1 = require("./cacheManager");
22
22
  const node_uuid_1 = __importDefault(require("node-uuid"));
23
+ const node_os_1 = __importDefault(require("node:os"));
23
24
  const CACHE_TTL = 60 * 60 * 1000; // 1 hour
25
+ const UPGRADE_CHECK_INTERVAL = 10 * 60 * 1000; // 10 minutes
24
26
  const toKey = (ref) => `assetManager:asset:${ref}`;
25
27
  function enrichAsset(asset) {
26
28
  return {
@@ -47,6 +49,21 @@ function parseRef(ref) {
47
49
  return [out[0].toLowerCase(), out[1].toLowerCase()];
48
50
  }
49
51
  class AssetManager {
52
+ startUpgradeInterval() {
53
+ console.debug('Checking for upgrades...');
54
+ this.upgradeAllProviders()
55
+ .then((task) => {
56
+ return task && task.wait();
57
+ })
58
+ .catch((e) => {
59
+ console.error('Failed to upgrade providers', e);
60
+ })
61
+ .finally(() => {
62
+ setTimeout(() => {
63
+ this.startUpgradeInterval();
64
+ }, UPGRADE_CHECK_INTERVAL);
65
+ });
66
+ }
50
67
  /**
51
68
  *
52
69
  * @param {string[]} [assetKinds]
@@ -191,6 +208,44 @@ class AssetManager {
191
208
  definitionsManager_1.definitionsManager.clearCache();
192
209
  return await repositoryManager_1.repositoryManager.ensureAsset(uri.handle, uri.name, uri.version, wait);
193
210
  }
211
+ async cleanupUnusedProviders() {
212
+ const unusedProviders = await repositoryManager_1.repositoryManager.getUnusedProviders();
213
+ if (unusedProviders.length < 1) {
214
+ return;
215
+ }
216
+ console.log('Cleaning up unused providers: ', unusedProviders);
217
+ await Promise.all(unusedProviders.map((ref) => {
218
+ return this.unregisterAsset(ref);
219
+ }));
220
+ }
221
+ async upgradeAllProviders() {
222
+ const providers = await definitionsManager_1.definitionsManager.getProviderDefinitions();
223
+ const names = providers.map((p) => p.definition.metadata.name);
224
+ const refs = await repositoryManager_1.repositoryManager.getUpdatableAssets(names);
225
+ if (refs.length < 1) {
226
+ await this.cleanupUnusedProviders();
227
+ return;
228
+ }
229
+ console.log('Installing updates', refs);
230
+ const updateAll = async () => {
231
+ try {
232
+ //We change to a temp dir to avoid issues with the current working directory
233
+ process.chdir(node_os_1.default.tmpdir());
234
+ await nodejs_registry_utils_1.Actions.install(new progressListener_1.ProgressListener(), refs, {});
235
+ await this.cleanupUnusedProviders();
236
+ }
237
+ catch (e) {
238
+ console.error(`Failed to update assets: ${refs.join(',')}`, e);
239
+ throw e;
240
+ }
241
+ cacheManager_1.cacheManager.flush();
242
+ definitionsManager_1.definitionsManager.clearCache();
243
+ };
244
+ return taskManager_1.taskManager.add(`asset:update`, updateAll, {
245
+ name: `Installing ${refs.length} updates`,
246
+ group: 'asset:update:check',
247
+ });
248
+ }
194
249
  async maybeGenerateCode(ref, ymlPath, block) {
195
250
  ref = (0, nodejs_utils_1.normalizeKapetaUri)(ref);
196
251
  if (await codeGeneratorManager_1.codeGeneratorManager.canGenerateCode(block)) {
@@ -11,6 +11,7 @@ declare class DefinitionsManager {
11
11
  exists(ref: string): Promise<boolean>;
12
12
  getProviderDefinitions(): Promise<DefinitionInfo[]>;
13
13
  getDefinition(ref: string): Promise<DefinitionInfo | undefined>;
14
+ getLatestDefinition(name: string): Promise<DefinitionInfo | undefined>;
14
15
  getVersions(assetName: string): Promise<DefinitionInfo[]>;
15
16
  clearCache(): void;
16
17
  }
@@ -136,6 +136,19 @@ class DefinitionsManager {
136
136
  return (0, nodejs_utils_1.parseKapetaUri)(`${d.definition.metadata.name}:${d.version}`).id === uri.id;
137
137
  });
138
138
  }
139
+ async getLatestDefinition(name) {
140
+ const definitions = await this.getDefinitions();
141
+ const allVersions = definitions.filter((d) => {
142
+ return d.version !== 'local' && d.definition.metadata.name === name;
143
+ });
144
+ if (allVersions.length === 0) {
145
+ return;
146
+ }
147
+ allVersions.sort((a, b) => {
148
+ return (0, nodejs_utils_1.parseVersion)(a.version).compareTo((0, nodejs_utils_1.parseVersion)(b.version)) * -1;
149
+ });
150
+ return allVersions[0];
151
+ }
139
152
  async getVersions(assetName) {
140
153
  const uri = (0, nodejs_utils_1.parseKapetaUri)(assetName);
141
154
  const definitions = await this.getDefinitions();
@@ -19,6 +19,15 @@ declare class RepositoryManager extends EventEmitter {
19
19
  setSourceOfChangeFor(file: string, source: SourceOfChange): Promise<void>;
20
20
  clearSourceOfChangeFor(file: string): Promise<void>;
21
21
  ensureDefaultProviders(): Promise<void>;
22
+ /**
23
+ * Will go through all available assets and get a list of
24
+ * providers that are not referenced anywhere.
25
+ *
26
+ * It will also make sure to not include the latest version of an asset.
27
+ *
28
+ */
29
+ getUnusedProviders(): Promise<string[]>;
30
+ getUpdatableAssets(allNames: string[]): Promise<string[]>;
22
31
  private scheduleInstallation;
23
32
  ensureAsset(handle: string, name: string, version: string, wait?: boolean): Promise<undefined | Task[]>;
24
33
  }
@@ -10,6 +10,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
10
10
  exports.repositoryManager = void 0;
11
11
  const node_os_1 = __importDefault(require("node:os"));
12
12
  const socketManager_1 = require("./socketManager");
13
+ const schemas_1 = require("@kapeta/schemas");
13
14
  const nodejs_registry_utils_1 = require("@kapeta/nodejs-registry-utils");
14
15
  const definitionsManager_1 = require("./definitionsManager");
15
16
  const taskManager_1 = require("./taskManager");
@@ -69,6 +70,97 @@ class RepositoryManager extends node_events_1.EventEmitter {
69
70
  socketManager_1.socketManager.emitGlobal(EVENT_DEFAULT_PROVIDERS_END, {});
70
71
  });
71
72
  }
73
+ /**
74
+ * Will go through all available assets and get a list of
75
+ * providers that are not referenced anywhere.
76
+ *
77
+ * It will also make sure to not include the latest version of an asset.
78
+ *
79
+ */
80
+ async getUnusedProviders() {
81
+ const allDefinitions = await definitionsManager_1.definitionsManager.getDefinitions();
82
+ const blocks = [];
83
+ const plans = [];
84
+ const providerMap = new Map();
85
+ const providerVersions = {};
86
+ const unusedProviders = new Set();
87
+ allDefinitions.forEach((d) => {
88
+ if (d.definition.kind === 'core/plan') {
89
+ plans.push(d);
90
+ return;
91
+ }
92
+ if (d.definition.kind.startsWith('core/')) {
93
+ const ref = (0, nodejs_utils_1.normalizeKapetaUri)(`${d.definition.metadata.name}:${d.version}`);
94
+ providerMap.set(ref, d);
95
+ if (!providerVersions[d.definition.metadata.name]) {
96
+ providerVersions[d.definition.metadata.name] = new Set();
97
+ }
98
+ providerVersions[d.definition.metadata.name].add(d.version);
99
+ unusedProviders.add(ref);
100
+ return;
101
+ }
102
+ blocks.push(d);
103
+ });
104
+ const latestVersions = {};
105
+ Object.entries(providerVersions).forEach(([name, versions]) => {
106
+ const versionArray = Array.from(versions);
107
+ versionArray.sort((a, b) => {
108
+ return (0, nodejs_utils_1.parseVersion)(a).compareTo((0, nodejs_utils_1.parseVersion)(b)) * -1;
109
+ });
110
+ latestVersions[name] = versionArray[0];
111
+ });
112
+ function markDependencyAsUsed(dep) {
113
+ const uri = (0, nodejs_utils_1.parseKapetaUri)(dep.name);
114
+ const ref = uri.toNormalizedString();
115
+ if (unusedProviders.has(ref)) {
116
+ unusedProviders.delete(ref);
117
+ }
118
+ }
119
+ plans.forEach((plan) => {
120
+ const dependencies = (0, schemas_1.resolveDependencies)(plan.definition);
121
+ dependencies.forEach(markDependencyAsUsed);
122
+ });
123
+ blocks.forEach((block) => {
124
+ const blockTypeKind = (0, nodejs_utils_1.normalizeKapetaUri)(block.definition.kind);
125
+ unusedProviders.delete(blockTypeKind);
126
+ const blockTypeProvider = providerMap.get(blockTypeKind);
127
+ if (!blockTypeProvider) {
128
+ console.warn('No provider found for block type', block.definition.kind);
129
+ return;
130
+ }
131
+ const dependencies = (0, schemas_1.resolveDependencies)(block.definition, blockTypeProvider.definition);
132
+ dependencies.forEach(markDependencyAsUsed);
133
+ });
134
+ return Array.from(unusedProviders).filter((ref) => {
135
+ const uri = (0, nodejs_utils_1.parseKapetaUri)(ref);
136
+ if (uri.version == 'local') {
137
+ // Don't delete local assets
138
+ return false;
139
+ }
140
+ // Don't delete the latest version of an asset
141
+ return latestVersions[uri.fullName] !== uri.version;
142
+ });
143
+ }
144
+ async getUpdatableAssets(allNames) {
145
+ const names = Array.from(new Set(allNames));
146
+ const currentVersions = await Promise.all(names.map((name) => definitionsManager_1.definitionsManager.getLatestDefinition(name).catch(() => undefined)));
147
+ const latestVersions = await Promise.all(names.map((name) => this._registryService.getLatestVersion(name).catch(() => undefined)));
148
+ return names
149
+ .map((name, index) => {
150
+ const currentVersion = currentVersions[index];
151
+ const latestVersion = latestVersions[index];
152
+ if (!currentVersion || !latestVersion) {
153
+ // Shouldn't happen unless the registry is down or an asset was deleted
154
+ return undefined;
155
+ }
156
+ const ref = (0, nodejs_utils_1.normalizeKapetaUri)(`${name}:${latestVersion.version}`);
157
+ if (currentVersion.version === latestVersion.version) {
158
+ return undefined;
159
+ }
160
+ return ref;
161
+ })
162
+ .filter((ref) => !!ref);
163
+ }
72
164
  async scheduleInstallation(refs) {
73
165
  //We make sure to only install one asset at a time - otherwise unexpected things might happen
74
166
  const createInstaller = (ref) => {
package/dist/esm/index.js CHANGED
@@ -58,6 +58,7 @@ const DefaultProviderInstaller_1 = require("./src/utils/DefaultProviderInstaller
58
58
  const authManager_1 = require("./src/authManager");
59
59
  const codeGeneratorManager_1 = require("./src/codeGeneratorManager");
60
60
  const Sentry = __importStar(require("@sentry/node"));
61
+ const assetManager_1 = require("./src/assetManager");
61
62
  Sentry.init({
62
63
  dsn: 'https://0b7cc946d82c591473d6f95fff5e210b@o4505820837249024.ingest.sentry.io/4506212692000768',
63
64
  enabled: process.env.NODE_ENV !== 'development',
@@ -231,6 +232,7 @@ exports.default = {
231
232
  catch (e) {
232
233
  console.error('Failed to install default providers.', e);
233
234
  }
235
+ assetManager_1.assetManager.startUpgradeInterval();
234
236
  resolve({ host, port, dockerStatus: containerManager_1.containerManager.isAlive() });
235
237
  });
236
238
  currentServer.host = host;
@@ -10,13 +10,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
10
10
  const express_promise_router_1 = __importDefault(require("express-promise-router"));
11
11
  const cors_1 = require("./middleware/cors");
12
12
  const nodejs_api_client_1 = require("@kapeta/nodejs-api-client");
13
- const local_cluster_config_1 = __importDefault(require("@kapeta/local-cluster-config"));
13
+ const nodejs_registry_utils_1 = require("@kapeta/nodejs-registry-utils");
14
14
  const { createAPIRoute } = require('@kapeta/web-microfrontend/server');
15
15
  const packageJson = require('../package.json');
16
16
  const router = (0, express_promise_router_1.default)();
17
- const remoteServices = local_cluster_config_1.default.getClusterConfig().remoteServices ?? {};
18
17
  router.use('/', cors_1.corsHandler);
19
- router.use('/registry', createAPIRoute(remoteServices.registry ?? 'https://registry.kapeta.com', {
18
+ router.use('/registry', createAPIRoute(nodejs_registry_utils_1.Config.data?.registry?.url ?? 'https://registry.kapeta.com', {
20
19
  nonce: false,
21
20
  userAgent: `KapetaDesktopCluster/${packageJson.version}`,
22
21
  tokenFetcher: () => {
@@ -16,6 +16,7 @@ export interface EnrichedAsset {
16
16
  ymlPath: string;
17
17
  }
18
18
  declare class AssetManager {
19
+ startUpgradeInterval(): void;
19
20
  /**
20
21
  *
21
22
  * @param {string[]} [assetKinds]
@@ -31,6 +32,8 @@ declare class AssetManager {
31
32
  importFile(filePath: string): Promise<EnrichedAsset[]>;
32
33
  unregisterAsset(ref: string): Promise<void>;
33
34
  installAsset(ref: string, wait?: boolean): Promise<import("./taskManager").Task<void>[] | undefined>;
35
+ private cleanupUnusedProviders;
36
+ private upgradeAllProviders;
34
37
  private maybeGenerateCode;
35
38
  }
36
39
  export declare const assetManager: AssetManager;
@@ -20,7 +20,9 @@ const definitionsManager_1 = require("./definitionsManager");
20
20
  const taskManager_1 = require("./taskManager");
21
21
  const cacheManager_1 = require("./cacheManager");
22
22
  const node_uuid_1 = __importDefault(require("node-uuid"));
23
+ const node_os_1 = __importDefault(require("node:os"));
23
24
  const CACHE_TTL = 60 * 60 * 1000; // 1 hour
25
+ const UPGRADE_CHECK_INTERVAL = 10 * 60 * 1000; // 10 minutes
24
26
  const toKey = (ref) => `assetManager:asset:${ref}`;
25
27
  function enrichAsset(asset) {
26
28
  return {
@@ -47,6 +49,21 @@ function parseRef(ref) {
47
49
  return [out[0].toLowerCase(), out[1].toLowerCase()];
48
50
  }
49
51
  class AssetManager {
52
+ startUpgradeInterval() {
53
+ console.debug('Checking for upgrades...');
54
+ this.upgradeAllProviders()
55
+ .then((task) => {
56
+ return task && task.wait();
57
+ })
58
+ .catch((e) => {
59
+ console.error('Failed to upgrade providers', e);
60
+ })
61
+ .finally(() => {
62
+ setTimeout(() => {
63
+ this.startUpgradeInterval();
64
+ }, UPGRADE_CHECK_INTERVAL);
65
+ });
66
+ }
50
67
  /**
51
68
  *
52
69
  * @param {string[]} [assetKinds]
@@ -191,6 +208,44 @@ class AssetManager {
191
208
  definitionsManager_1.definitionsManager.clearCache();
192
209
  return await repositoryManager_1.repositoryManager.ensureAsset(uri.handle, uri.name, uri.version, wait);
193
210
  }
211
+ async cleanupUnusedProviders() {
212
+ const unusedProviders = await repositoryManager_1.repositoryManager.getUnusedProviders();
213
+ if (unusedProviders.length < 1) {
214
+ return;
215
+ }
216
+ console.log('Cleaning up unused providers: ', unusedProviders);
217
+ await Promise.all(unusedProviders.map((ref) => {
218
+ return this.unregisterAsset(ref);
219
+ }));
220
+ }
221
+ async upgradeAllProviders() {
222
+ const providers = await definitionsManager_1.definitionsManager.getProviderDefinitions();
223
+ const names = providers.map((p) => p.definition.metadata.name);
224
+ const refs = await repositoryManager_1.repositoryManager.getUpdatableAssets(names);
225
+ if (refs.length < 1) {
226
+ await this.cleanupUnusedProviders();
227
+ return;
228
+ }
229
+ console.log('Installing updates', refs);
230
+ const updateAll = async () => {
231
+ try {
232
+ //We change to a temp dir to avoid issues with the current working directory
233
+ process.chdir(node_os_1.default.tmpdir());
234
+ await nodejs_registry_utils_1.Actions.install(new progressListener_1.ProgressListener(), refs, {});
235
+ await this.cleanupUnusedProviders();
236
+ }
237
+ catch (e) {
238
+ console.error(`Failed to update assets: ${refs.join(',')}`, e);
239
+ throw e;
240
+ }
241
+ cacheManager_1.cacheManager.flush();
242
+ definitionsManager_1.definitionsManager.clearCache();
243
+ };
244
+ return taskManager_1.taskManager.add(`asset:update`, updateAll, {
245
+ name: `Installing ${refs.length} updates`,
246
+ group: 'asset:update:check',
247
+ });
248
+ }
194
249
  async maybeGenerateCode(ref, ymlPath, block) {
195
250
  ref = (0, nodejs_utils_1.normalizeKapetaUri)(ref);
196
251
  if (await codeGeneratorManager_1.codeGeneratorManager.canGenerateCode(block)) {
@@ -11,6 +11,7 @@ declare class DefinitionsManager {
11
11
  exists(ref: string): Promise<boolean>;
12
12
  getProviderDefinitions(): Promise<DefinitionInfo[]>;
13
13
  getDefinition(ref: string): Promise<DefinitionInfo | undefined>;
14
+ getLatestDefinition(name: string): Promise<DefinitionInfo | undefined>;
14
15
  getVersions(assetName: string): Promise<DefinitionInfo[]>;
15
16
  clearCache(): void;
16
17
  }
@@ -136,6 +136,19 @@ class DefinitionsManager {
136
136
  return (0, nodejs_utils_1.parseKapetaUri)(`${d.definition.metadata.name}:${d.version}`).id === uri.id;
137
137
  });
138
138
  }
139
+ async getLatestDefinition(name) {
140
+ const definitions = await this.getDefinitions();
141
+ const allVersions = definitions.filter((d) => {
142
+ return d.version !== 'local' && d.definition.metadata.name === name;
143
+ });
144
+ if (allVersions.length === 0) {
145
+ return;
146
+ }
147
+ allVersions.sort((a, b) => {
148
+ return (0, nodejs_utils_1.parseVersion)(a.version).compareTo((0, nodejs_utils_1.parseVersion)(b.version)) * -1;
149
+ });
150
+ return allVersions[0];
151
+ }
139
152
  async getVersions(assetName) {
140
153
  const uri = (0, nodejs_utils_1.parseKapetaUri)(assetName);
141
154
  const definitions = await this.getDefinitions();
@@ -19,6 +19,15 @@ declare class RepositoryManager extends EventEmitter {
19
19
  setSourceOfChangeFor(file: string, source: SourceOfChange): Promise<void>;
20
20
  clearSourceOfChangeFor(file: string): Promise<void>;
21
21
  ensureDefaultProviders(): Promise<void>;
22
+ /**
23
+ * Will go through all available assets and get a list of
24
+ * providers that are not referenced anywhere.
25
+ *
26
+ * It will also make sure to not include the latest version of an asset.
27
+ *
28
+ */
29
+ getUnusedProviders(): Promise<string[]>;
30
+ getUpdatableAssets(allNames: string[]): Promise<string[]>;
22
31
  private scheduleInstallation;
23
32
  ensureAsset(handle: string, name: string, version: string, wait?: boolean): Promise<undefined | Task[]>;
24
33
  }
@@ -10,6 +10,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
10
10
  exports.repositoryManager = void 0;
11
11
  const node_os_1 = __importDefault(require("node:os"));
12
12
  const socketManager_1 = require("./socketManager");
13
+ const schemas_1 = require("@kapeta/schemas");
13
14
  const nodejs_registry_utils_1 = require("@kapeta/nodejs-registry-utils");
14
15
  const definitionsManager_1 = require("./definitionsManager");
15
16
  const taskManager_1 = require("./taskManager");
@@ -69,6 +70,97 @@ class RepositoryManager extends node_events_1.EventEmitter {
69
70
  socketManager_1.socketManager.emitGlobal(EVENT_DEFAULT_PROVIDERS_END, {});
70
71
  });
71
72
  }
73
+ /**
74
+ * Will go through all available assets and get a list of
75
+ * providers that are not referenced anywhere.
76
+ *
77
+ * It will also make sure to not include the latest version of an asset.
78
+ *
79
+ */
80
+ async getUnusedProviders() {
81
+ const allDefinitions = await definitionsManager_1.definitionsManager.getDefinitions();
82
+ const blocks = [];
83
+ const plans = [];
84
+ const providerMap = new Map();
85
+ const providerVersions = {};
86
+ const unusedProviders = new Set();
87
+ allDefinitions.forEach((d) => {
88
+ if (d.definition.kind === 'core/plan') {
89
+ plans.push(d);
90
+ return;
91
+ }
92
+ if (d.definition.kind.startsWith('core/')) {
93
+ const ref = (0, nodejs_utils_1.normalizeKapetaUri)(`${d.definition.metadata.name}:${d.version}`);
94
+ providerMap.set(ref, d);
95
+ if (!providerVersions[d.definition.metadata.name]) {
96
+ providerVersions[d.definition.metadata.name] = new Set();
97
+ }
98
+ providerVersions[d.definition.metadata.name].add(d.version);
99
+ unusedProviders.add(ref);
100
+ return;
101
+ }
102
+ blocks.push(d);
103
+ });
104
+ const latestVersions = {};
105
+ Object.entries(providerVersions).forEach(([name, versions]) => {
106
+ const versionArray = Array.from(versions);
107
+ versionArray.sort((a, b) => {
108
+ return (0, nodejs_utils_1.parseVersion)(a).compareTo((0, nodejs_utils_1.parseVersion)(b)) * -1;
109
+ });
110
+ latestVersions[name] = versionArray[0];
111
+ });
112
+ function markDependencyAsUsed(dep) {
113
+ const uri = (0, nodejs_utils_1.parseKapetaUri)(dep.name);
114
+ const ref = uri.toNormalizedString();
115
+ if (unusedProviders.has(ref)) {
116
+ unusedProviders.delete(ref);
117
+ }
118
+ }
119
+ plans.forEach((plan) => {
120
+ const dependencies = (0, schemas_1.resolveDependencies)(plan.definition);
121
+ dependencies.forEach(markDependencyAsUsed);
122
+ });
123
+ blocks.forEach((block) => {
124
+ const blockTypeKind = (0, nodejs_utils_1.normalizeKapetaUri)(block.definition.kind);
125
+ unusedProviders.delete(blockTypeKind);
126
+ const blockTypeProvider = providerMap.get(blockTypeKind);
127
+ if (!blockTypeProvider) {
128
+ console.warn('No provider found for block type', block.definition.kind);
129
+ return;
130
+ }
131
+ const dependencies = (0, schemas_1.resolveDependencies)(block.definition, blockTypeProvider.definition);
132
+ dependencies.forEach(markDependencyAsUsed);
133
+ });
134
+ return Array.from(unusedProviders).filter((ref) => {
135
+ const uri = (0, nodejs_utils_1.parseKapetaUri)(ref);
136
+ if (uri.version == 'local') {
137
+ // Don't delete local assets
138
+ return false;
139
+ }
140
+ // Don't delete the latest version of an asset
141
+ return latestVersions[uri.fullName] !== uri.version;
142
+ });
143
+ }
144
+ async getUpdatableAssets(allNames) {
145
+ const names = Array.from(new Set(allNames));
146
+ const currentVersions = await Promise.all(names.map((name) => definitionsManager_1.definitionsManager.getLatestDefinition(name).catch(() => undefined)));
147
+ const latestVersions = await Promise.all(names.map((name) => this._registryService.getLatestVersion(name).catch(() => undefined)));
148
+ return names
149
+ .map((name, index) => {
150
+ const currentVersion = currentVersions[index];
151
+ const latestVersion = latestVersions[index];
152
+ if (!currentVersion || !latestVersion) {
153
+ // Shouldn't happen unless the registry is down or an asset was deleted
154
+ return undefined;
155
+ }
156
+ const ref = (0, nodejs_utils_1.normalizeKapetaUri)(`${name}:${latestVersion.version}`);
157
+ if (currentVersion.version === latestVersion.version) {
158
+ return undefined;
159
+ }
160
+ return ref;
161
+ })
162
+ .filter((ref) => !!ref);
163
+ }
72
164
  async scheduleInstallation(refs) {
73
165
  //We make sure to only install one asset at a time - otherwise unexpected things might happen
74
166
  const createInstaller = (ref) => {
package/index.ts CHANGED
@@ -32,6 +32,7 @@ import { defaultProviderInstaller } from './src/utils/DefaultProviderInstaller';
32
32
  import { authManager } from './src/authManager';
33
33
  import { codeGeneratorManager } from './src/codeGeneratorManager';
34
34
  import * as Sentry from '@sentry/node';
35
+ import { assetManager } from './src/assetManager';
35
36
 
36
37
  Sentry.init({
37
38
  dsn: 'https://0b7cc946d82c591473d6f95fff5e210b@o4505820837249024.ingest.sentry.io/4506212692000768',
@@ -245,6 +246,8 @@ export default {
245
246
  console.error('Failed to install default providers.', e);
246
247
  }
247
248
 
249
+ assetManager.startUpgradeInterval();
250
+
248
251
  resolve({ host, port, dockerStatus: containerManager.isAlive() });
249
252
  });
250
253
  currentServer.host = host;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kapeta/local-cluster-service",
3
- "version": "0.27.0",
3
+ "version": "0.28.0",
4
4
  "description": "Manages configuration, ports and service discovery for locally running Kapeta systems",
5
5
  "type": "commonjs",
6
6
  "exports": {
package/src/api.ts CHANGED
@@ -6,18 +6,17 @@
6
6
  import Router from 'express-promise-router';
7
7
  import { corsHandler } from './middleware/cors';
8
8
  import { KapetaAPI } from '@kapeta/nodejs-api-client';
9
- import ClusterConfiguration from '@kapeta/local-cluster-config';
9
+ import { Config } from '@kapeta/nodejs-registry-utils';
10
10
  const { createAPIRoute } = require('@kapeta/web-microfrontend/server');
11
11
  const packageJson = require('../package.json');
12
12
 
13
13
  const router = Router();
14
14
 
15
- const remoteServices = ClusterConfiguration.getClusterConfig().remoteServices ?? {};
16
15
  router.use('/', corsHandler);
17
16
 
18
17
  router.use(
19
18
  '/registry',
20
- createAPIRoute(remoteServices.registry ?? 'https://registry.kapeta.com', {
19
+ createAPIRoute(Config.data?.registry?.url ?? 'https://registry.kapeta.com', {
21
20
  nonce: false,
22
21
  userAgent: `KapetaDesktopCluster/${packageJson.version}`,
23
22
  tokenFetcher: () => {
@@ -18,8 +18,10 @@ import { taskManager } from './taskManager';
18
18
  import { SourceOfChange } from './types';
19
19
  import { cacheManager } from './cacheManager';
20
20
  import uuid from 'node-uuid';
21
+ import os from 'node:os';
21
22
 
22
23
  const CACHE_TTL = 60 * 60 * 1000; // 1 hour
24
+ const UPGRADE_CHECK_INTERVAL = 10 * 60 * 1000; // 10 minutes
23
25
 
24
26
  const toKey = (ref: string) => `assetManager:asset:${ref}`;
25
27
 
@@ -64,6 +66,22 @@ function parseRef(ref: string) {
64
66
  }
65
67
 
66
68
  class AssetManager {
69
+ public startUpgradeInterval() {
70
+ console.debug('Checking for upgrades...');
71
+ this.upgradeAllProviders()
72
+ .then((task) => {
73
+ return task && task.wait();
74
+ })
75
+ .catch((e) => {
76
+ console.error('Failed to upgrade providers', e);
77
+ })
78
+ .finally(() => {
79
+ setTimeout(() => {
80
+ this.startUpgradeInterval();
81
+ }, UPGRADE_CHECK_INTERVAL);
82
+ });
83
+ }
84
+
67
85
  /**
68
86
  *
69
87
  * @param {string[]} [assetKinds]
@@ -258,6 +276,52 @@ class AssetManager {
258
276
  return await repositoryManager.ensureAsset(uri.handle, uri.name, uri.version, wait);
259
277
  }
260
278
 
279
+ private async cleanupUnusedProviders(): Promise<void> {
280
+ const unusedProviders = await repositoryManager.getUnusedProviders();
281
+ if (unusedProviders.length < 1) {
282
+ return;
283
+ }
284
+
285
+ console.log('Cleaning up unused providers: ', unusedProviders);
286
+ await Promise.all(
287
+ unusedProviders.map((ref) => {
288
+ return this.unregisterAsset(ref);
289
+ })
290
+ );
291
+ }
292
+
293
+ private async upgradeAllProviders() {
294
+ const providers = await definitionsManager.getProviderDefinitions();
295
+ const names = providers.map((p) => p.definition.metadata.name);
296
+
297
+ const refs = await repositoryManager.getUpdatableAssets(names);
298
+
299
+ if (refs.length < 1) {
300
+ await this.cleanupUnusedProviders();
301
+ return;
302
+ }
303
+
304
+ console.log('Installing updates', refs);
305
+ const updateAll = async () => {
306
+ try {
307
+ //We change to a temp dir to avoid issues with the current working directory
308
+ process.chdir(os.tmpdir());
309
+ await Actions.install(new ProgressListener(), refs, {});
310
+ await this.cleanupUnusedProviders();
311
+ } catch (e) {
312
+ console.error(`Failed to update assets: ${refs.join(',')}`, e);
313
+ throw e;
314
+ }
315
+ cacheManager.flush();
316
+ definitionsManager.clearCache();
317
+ };
318
+
319
+ return taskManager.add(`asset:update`, updateAll, {
320
+ name: `Installing ${refs.length} updates`,
321
+ group: 'asset:update:check',
322
+ });
323
+ }
324
+
261
325
  private async maybeGenerateCode(ref: string, ymlPath: string, block: Definition) {
262
326
  ref = normalizeKapetaUri(ref);
263
327
  if (await codeGeneratorManager.canGenerateCode(block)) {
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import ClusterConfiguration, { DefinitionInfo } from '@kapeta/local-cluster-config';
7
- import { parseKapetaUri, normalizeKapetaUri } from '@kapeta/nodejs-utils';
7
+ import { parseKapetaUri, normalizeKapetaUri, parseVersion } from '@kapeta/nodejs-utils';
8
8
  import { cacheManager, doCached } from './cacheManager';
9
9
  import { KapetaAPI } from '@kapeta/nodejs-api-client';
10
10
  import { Plan } from '@kapeta/schemas';
@@ -170,6 +170,23 @@ class DefinitionsManager {
170
170
  });
171
171
  }
172
172
 
173
+ public async getLatestDefinition(name: string) {
174
+ const definitions = await this.getDefinitions();
175
+ const allVersions = definitions.filter((d) => {
176
+ return d.version !== 'local' && d.definition.metadata.name === name;
177
+ });
178
+
179
+ if (allVersions.length === 0) {
180
+ return;
181
+ }
182
+
183
+ allVersions.sort((a, b) => {
184
+ return parseVersion(a.version).compareTo(parseVersion(b.version)) * -1;
185
+ });
186
+
187
+ return allVersions[0];
188
+ }
189
+
173
190
  public async getVersions(assetName: string) {
174
191
  const uri = parseKapetaUri(assetName);
175
192
  const definitions = await this.getDefinitions();
@@ -5,16 +5,17 @@
5
5
 
6
6
  import os from 'node:os';
7
7
  import { socketManager } from './socketManager';
8
- import { Dependency } from '@kapeta/schemas';
9
- import { Actions, Config, RegistryService } from '@kapeta/nodejs-registry-utils';
8
+ import { DependencyReference, Dependency, resolveDependencies } from '@kapeta/schemas';
9
+ import { Actions, AssetVersion, Config, RegistryService } from '@kapeta/nodejs-registry-utils';
10
10
  import { definitionsManager } from './definitionsManager';
11
11
  import { Task, taskManager } from './taskManager';
12
- import { normalizeKapetaUri } from '@kapeta/nodejs-utils';
12
+ import { normalizeKapetaUri, parseKapetaUri, parseVersion } from '@kapeta/nodejs-utils';
13
13
  import { ProgressListener } from './progressListener';
14
14
  import { RepositoryWatcher } from './RepositoryWatcher';
15
15
  import { SourceOfChange } from './types';
16
16
  import { cacheManager } from './cacheManager';
17
17
  import { EventEmitter } from 'node:events';
18
+ import { DefinitionInfo } from '@kapeta/local-cluster-config';
18
19
 
19
20
  const EVENT_DEFAULT_PROVIDERS_START = 'default-providers-start';
20
21
  const EVENT_DEFAULT_PROVIDERS_END = 'default-providers-end';
@@ -77,6 +78,116 @@ class RepositoryManager extends EventEmitter {
77
78
  });
78
79
  }
79
80
 
81
+ /**
82
+ * Will go through all available assets and get a list of
83
+ * providers that are not referenced anywhere.
84
+ *
85
+ * It will also make sure to not include the latest version of an asset.
86
+ *
87
+ */
88
+ public async getUnusedProviders(): Promise<string[]> {
89
+ const allDefinitions: DefinitionInfo[] = await definitionsManager.getDefinitions();
90
+ const blocks: DefinitionInfo[] = [];
91
+ const plans: DefinitionInfo[] = [];
92
+ const providerMap = new Map<string, DefinitionInfo>();
93
+ const providerVersions: { [name: string]: Set<string> } = {};
94
+ const unusedProviders = new Set<string>();
95
+ allDefinitions.forEach((d) => {
96
+ if (d.definition.kind === 'core/plan') {
97
+ plans.push(d);
98
+ return;
99
+ }
100
+
101
+ if (d.definition.kind.startsWith('core/')) {
102
+ const ref = normalizeKapetaUri(`${d.definition.metadata.name}:${d.version}`);
103
+ providerMap.set(ref, d);
104
+ if (!providerVersions[d.definition.metadata.name]) {
105
+ providerVersions[d.definition.metadata.name] = new Set<string>();
106
+ }
107
+ providerVersions[d.definition.metadata.name].add(d.version);
108
+ unusedProviders.add(ref);
109
+ return;
110
+ }
111
+ blocks.push(d);
112
+ });
113
+
114
+ const latestVersions: { [name: string]: string } = {};
115
+ Object.entries(providerVersions).forEach(([name, versions]) => {
116
+ const versionArray = Array.from(versions);
117
+ versionArray.sort((a, b) => {
118
+ return parseVersion(a).compareTo(parseVersion(b)) * -1;
119
+ });
120
+ latestVersions[name] = versionArray[0];
121
+ });
122
+
123
+ function markDependencyAsUsed(dep: DependencyReference) {
124
+ const uri = parseKapetaUri(dep.name);
125
+ const ref = uri.toNormalizedString();
126
+ if (unusedProviders.has(ref)) {
127
+ unusedProviders.delete(ref);
128
+ }
129
+ }
130
+
131
+ plans.forEach((plan) => {
132
+ const dependencies = resolveDependencies(plan.definition);
133
+ dependencies.forEach(markDependencyAsUsed);
134
+ });
135
+
136
+ blocks.forEach((block) => {
137
+ const blockTypeKind = normalizeKapetaUri(block.definition.kind);
138
+ unusedProviders.delete(blockTypeKind);
139
+ const blockTypeProvider = providerMap.get(blockTypeKind);
140
+ if (!blockTypeProvider) {
141
+ console.warn('No provider found for block type', block.definition.kind);
142
+ return;
143
+ }
144
+ const dependencies = resolveDependencies(block.definition, blockTypeProvider.definition);
145
+ dependencies.forEach(markDependencyAsUsed);
146
+ });
147
+
148
+ return Array.from(unusedProviders).filter((ref) => {
149
+ const uri = parseKapetaUri(ref);
150
+ if (uri.version == 'local') {
151
+ // Don't delete local assets
152
+ return false;
153
+ }
154
+
155
+ // Don't delete the latest version of an asset
156
+ return latestVersions[uri.fullName] !== uri.version;
157
+ });
158
+ }
159
+
160
+ public async getUpdatableAssets(allNames: string[]): Promise<string[]> {
161
+ const names = Array.from(new Set<string>(allNames));
162
+
163
+ const currentVersions = await Promise.all(
164
+ names.map((name) => definitionsManager.getLatestDefinition(name).catch(() => undefined))
165
+ );
166
+
167
+ const latestVersions = await Promise.all(
168
+ names.map((name) => this._registryService.getLatestVersion(name).catch(() => undefined))
169
+ );
170
+
171
+ return names
172
+ .map((name, index) => {
173
+ const currentVersion: DefinitionInfo | undefined = currentVersions[index];
174
+ const latestVersion: AssetVersion | undefined = latestVersions[index];
175
+ if (!currentVersion || !latestVersion) {
176
+ // Shouldn't happen unless the registry is down or an asset was deleted
177
+ return undefined;
178
+ }
179
+
180
+ const ref = normalizeKapetaUri(`${name}:${latestVersion.version}`);
181
+
182
+ if (currentVersion.version === latestVersion.version) {
183
+ return undefined;
184
+ }
185
+
186
+ return ref;
187
+ })
188
+ .filter((ref) => !!ref) as string[];
189
+ }
190
+
80
191
  private async scheduleInstallation(refs: string[]): Promise<Task[]> {
81
192
  //We make sure to only install one asset at a time - otherwise unexpected things might happen
82
193
  const createInstaller = (ref: string) => {