@kapeta/local-cluster-service 0.17.0 → 0.19.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 (67) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/cjs/index.js +4 -2
  3. package/dist/cjs/src/assetManager.d.ts +2 -2
  4. package/dist/cjs/src/assetManager.js +16 -16
  5. package/dist/cjs/src/assets/routes.js +2 -2
  6. package/dist/cjs/src/authManager.d.ts +12 -0
  7. package/dist/cjs/src/authManager.js +60 -0
  8. package/dist/cjs/src/codeGeneratorManager.d.ts +1 -1
  9. package/dist/cjs/src/codeGeneratorManager.js +3 -3
  10. package/dist/cjs/src/configManager.js +2 -2
  11. package/dist/cjs/src/containerManager.d.ts +15 -0
  12. package/dist/cjs/src/containerManager.js +190 -37
  13. package/dist/cjs/src/definitionsManager.d.ts +7 -6
  14. package/dist/cjs/src/definitionsManager.js +102 -18
  15. package/dist/cjs/src/instanceManager.d.ts +1 -1
  16. package/dist/cjs/src/instanceManager.js +7 -12
  17. package/dist/cjs/src/instances/routes.js +2 -2
  18. package/dist/cjs/src/operatorManager.d.ts +1 -1
  19. package/dist/cjs/src/operatorManager.js +7 -9
  20. package/dist/cjs/src/providerManager.d.ts +2 -1
  21. package/dist/cjs/src/providerManager.js +23 -15
  22. package/dist/cjs/src/repositoryManager.d.ts +2 -2
  23. package/dist/cjs/src/repositoryManager.js +8 -9
  24. package/dist/cjs/src/socketManager.d.ts +2 -2
  25. package/dist/cjs/src/socketManager.js +39 -14
  26. package/dist/cjs/src/utils/BlockInstanceRunner.js +6 -8
  27. package/dist/esm/index.js +4 -2
  28. package/dist/esm/src/assetManager.d.ts +2 -2
  29. package/dist/esm/src/assetManager.js +16 -16
  30. package/dist/esm/src/assets/routes.js +2 -2
  31. package/dist/esm/src/authManager.d.ts +12 -0
  32. package/dist/esm/src/authManager.js +60 -0
  33. package/dist/esm/src/codeGeneratorManager.d.ts +1 -1
  34. package/dist/esm/src/codeGeneratorManager.js +3 -3
  35. package/dist/esm/src/configManager.js +2 -2
  36. package/dist/esm/src/containerManager.d.ts +15 -0
  37. package/dist/esm/src/containerManager.js +190 -37
  38. package/dist/esm/src/definitionsManager.d.ts +7 -6
  39. package/dist/esm/src/definitionsManager.js +102 -18
  40. package/dist/esm/src/instanceManager.d.ts +1 -1
  41. package/dist/esm/src/instanceManager.js +7 -12
  42. package/dist/esm/src/instances/routes.js +2 -2
  43. package/dist/esm/src/operatorManager.d.ts +1 -1
  44. package/dist/esm/src/operatorManager.js +7 -9
  45. package/dist/esm/src/providerManager.d.ts +2 -1
  46. package/dist/esm/src/providerManager.js +23 -15
  47. package/dist/esm/src/repositoryManager.d.ts +2 -2
  48. package/dist/esm/src/repositoryManager.js +8 -9
  49. package/dist/esm/src/socketManager.d.ts +2 -2
  50. package/dist/esm/src/socketManager.js +39 -14
  51. package/dist/esm/src/utils/BlockInstanceRunner.js +6 -8
  52. package/index.ts +4 -2
  53. package/package.json +1 -1
  54. package/src/assetManager.ts +18 -16
  55. package/src/assets/routes.ts +2 -2
  56. package/src/authManager.ts +62 -0
  57. package/src/codeGeneratorManager.ts +3 -3
  58. package/src/configManager.ts +2 -2
  59. package/src/containerManager.ts +210 -40
  60. package/src/definitionsManager.ts +132 -17
  61. package/src/instanceManager.ts +7 -14
  62. package/src/instances/routes.ts +2 -2
  63. package/src/operatorManager.ts +7 -12
  64. package/src/providerManager.ts +27 -19
  65. package/src/repositoryManager.ts +8 -11
  66. package/src/socketManager.ts +42 -15
  67. package/src/utils/BlockInstanceRunner.ts +6 -8
@@ -64,9 +64,9 @@ class AssetManager {
64
64
  * @param {string[]} [assetKinds]
65
65
  * @returns {{path: *, ref: string, data: *, editable: boolean, kind: *, exists: boolean}[]}
66
66
  */
67
- getAssets(assetKinds?: string[]): EnrichedAsset[] {
67
+ async getAssets(assetKinds?: string[]): Promise<EnrichedAsset[]> {
68
68
  if (!assetKinds) {
69
- const blockTypeProviders = definitionsManager.getDefinitions([
69
+ const blockTypeProviders = await definitionsManager.getDefinitions([
70
70
  'core/block-type',
71
71
  'core/block-type-operator',
72
72
  ]);
@@ -76,12 +76,12 @@ class AssetManager {
76
76
  assetKinds.push('core/plan');
77
77
  }
78
78
 
79
- const assets = definitionsManager.getDefinitions(assetKinds);
79
+ const assets = await definitionsManager.getDefinitions(assetKinds);
80
80
 
81
81
  return assets.map(enrichAsset);
82
82
  }
83
83
 
84
- getPlans(): EnrichedAsset[] {
84
+ async getPlans(): Promise<EnrichedAsset[]> {
85
85
  return this.getAssets(['core/plan']);
86
86
  }
87
87
 
@@ -110,18 +110,18 @@ class AssetManager {
110
110
  await repositoryManager.ensureAsset(uri.handle, uri.name, uri.version, true);
111
111
  }
112
112
 
113
- let asset = definitionsManager
114
- .getDefinitions()
115
- .map(enrichAsset)
116
- .find((a) => parseKapetaUri(a.ref).equals(uri));
117
- if (autoFetch && !asset) {
113
+ const definitionInfo = await definitionsManager.getDefinition(ref);
114
+ if (autoFetch && !definitionInfo) {
118
115
  throw new Error('Asset not found: ' + ref);
119
116
  }
120
- if (asset) {
117
+
118
+ if (definitionInfo) {
119
+ const asset = enrichAsset(definitionInfo);
121
120
  cacheManager.set(cacheKey, asset, CACHE_TTL);
121
+ return asset;
122
122
  }
123
123
 
124
- return asset;
124
+ return undefined;
125
125
  }
126
126
 
127
127
  async createAsset(
@@ -151,7 +151,7 @@ class AssetManager {
151
151
 
152
152
  const ref = `kapeta://${yaml.metadata.name}:local`;
153
153
 
154
- this.maybeGenerateCode(ref, path, yaml);
154
+ await this.maybeGenerateCode(ref, path, yaml);
155
155
 
156
156
  return asset;
157
157
  }
@@ -178,7 +178,7 @@ class AssetManager {
178
178
  cacheManager.remove(toKey(ref));
179
179
  definitionsManager.clearCache();
180
180
 
181
- this.maybeGenerateCode(asset.ref, asset.ymlPath, yaml);
181
+ await this.maybeGenerateCode(asset.ref, asset.ymlPath, yaml);
182
182
  }
183
183
 
184
184
  async importFile(filePath: string) {
@@ -206,7 +206,9 @@ class AssetManager {
206
206
 
207
207
  definitionsManager.clearCache();
208
208
 
209
- return this.getAssets().filter((a) => refs.some((ref) => compareRefs(ref, a.ref)));
209
+ const assets = await this.getAssets();
210
+
211
+ return assets.filter((a) => refs.some((ref) => compareRefs(ref, a.ref)));
210
212
  }
211
213
 
212
214
  async unregisterAsset(ref: string) {
@@ -236,9 +238,9 @@ class AssetManager {
236
238
  return await repositoryManager.ensureAsset(uri.handle, uri.name, uri.version, false);
237
239
  }
238
240
 
239
- private maybeGenerateCode(ref: string, ymlPath: string, block: BlockDefinition) {
241
+ private async maybeGenerateCode(ref: string, ymlPath: string, block: BlockDefinition) {
240
242
  ref = normalizeKapetaUri(ref);
241
- if (codeGeneratorManager.canGenerateCode(block)) {
243
+ if (await codeGeneratorManager.canGenerateCode(block)) {
242
244
  const assetTitle = block.metadata.title ? block.metadata.title : parseKapetaUri(block.metadata.name).name;
243
245
  taskManager.add(
244
246
  `codegen:${ref}`,
@@ -31,8 +31,8 @@ router.use('/', stringBody);
31
31
  /**
32
32
  * Get all local assets available
33
33
  */
34
- router.get('/', (req: Request, res: Response) => {
35
- res.send(assetManager.getAssets([]));
34
+ router.get('/', async (req: Request, res: Response) => {
35
+ res.send(await assetManager.getAssets([]));
36
36
  });
37
37
 
38
38
  /**
@@ -0,0 +1,62 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import Path from 'node:path';
3
+ import chokidar, { FSWatcher } from 'chokidar';
4
+ import ClusterConfiguration from '@kapeta/local-cluster-config';
5
+ import { WatchEventName } from './types';
6
+ import { definitionsManager } from './definitionsManager';
7
+ import { KapetaAPI } from '@kapeta/nodejs-api-client';
8
+ import { socketManager } from './socketManager';
9
+
10
+ class AuthManager extends EventEmitter {
11
+ private watcher?: FSWatcher;
12
+
13
+ private hadToken: boolean;
14
+
15
+ constructor() {
16
+ super();
17
+ this.hadToken = this.hasToken();
18
+ }
19
+
20
+ public listenForChanges() {
21
+ const parentDir = Path.dirname(ClusterConfiguration.getKapetaBasedir());
22
+ //We watch the parent dir to catch changes to the base dir itself
23
+ this.watcher = chokidar.watch(parentDir, {
24
+ followSymlinks: false,
25
+ ignorePermissionErrors: true,
26
+ disableGlobbing: true,
27
+ persistent: true,
28
+ ignoreInitial: true,
29
+ depth: 1,
30
+ ignored: (path) => {
31
+ return !path.startsWith(ClusterConfiguration.getKapetaBasedir());
32
+ },
33
+ });
34
+ this.watcher.add(ClusterConfiguration.getKapetaBasedir());
35
+ this.watcher.on('all', this.handleFileChange.bind(this));
36
+ this.watcher.on('error', (error) => {
37
+ console.log('Error watching repository', error);
38
+ });
39
+ this.watcher.on('ready', () => {
40
+ console.log('Watching for auth changes: %s', ClusterConfiguration.getKapetaBasedir());
41
+ });
42
+ }
43
+
44
+ private hasToken() {
45
+ const api = new KapetaAPI();
46
+ return api.hasToken();
47
+ }
48
+
49
+ private async handleFileChange(eventName: WatchEventName, path: string) {
50
+ const hasTokenNow = this.hasToken();
51
+ if (this.hadToken !== hasTokenNow) {
52
+ socketManager.emitGlobal('auth-change', {});
53
+ if (hasTokenNow) {
54
+ // Clear the cache in case we need to rewrite the sample plan
55
+ definitionsManager.clearCache();
56
+ }
57
+ this.hadToken = hasTokenNow;
58
+ }
59
+ }
60
+ }
61
+
62
+ export const authManager = new AuthManager();
@@ -10,7 +10,7 @@ const BLOCK_TYPE_KIND = 'core/block-type';
10
10
  class CodeGeneratorManager {
11
11
  async reload() {
12
12
  Targets.reset();
13
- const languageTargets = definitionsManager.getDefinitions(TARGET_KIND);
13
+ const languageTargets = await definitionsManager.getDefinitions(TARGET_KIND);
14
14
  for (const languageTarget of languageTargets) {
15
15
  const key = `${languageTarget.definition.metadata.name}:${languageTarget.version}`;
16
16
  try {
@@ -26,13 +26,13 @@ class CodeGeneratorManager {
26
26
  }
27
27
  }
28
28
 
29
- canGenerateCode(yamlContent: BlockDefinition): boolean {
29
+ async canGenerateCode(yamlContent: BlockDefinition): Promise<boolean> {
30
30
  if (!yamlContent.spec.target?.kind) {
31
31
  //Not all block types have targets
32
32
  return false;
33
33
  }
34
34
 
35
- const blockTypes = definitionsManager.getDefinitions(BLOCK_TYPE_KIND);
35
+ const blockTypes = await definitionsManager.getDefinitions(BLOCK_TYPE_KIND);
36
36
  const blockTypeKinds = blockTypes.map(
37
37
  (blockType) => blockType.definition.metadata.name.toLowerCase() + ':' + blockType.version
38
38
  );
@@ -80,7 +80,7 @@ class ConfigManager {
80
80
  if (systemId) {
81
81
  systemId = normalizeKapetaUri(systemId);
82
82
  }
83
- const planAssets = assetManager.getPlans();
83
+ const planAssets = await assetManager.getPlans();
84
84
 
85
85
  const blockUri = parseKapetaUri(blockRef);
86
86
 
@@ -132,7 +132,7 @@ class ConfigManager {
132
132
  async verifyIdentity(blockRef: string, systemId: string, instanceId: string) {
133
133
  blockRef = normalizeKapetaUri(blockRef);
134
134
  systemId = normalizeKapetaUri(systemId);
135
- const planAssets = assetManager.getPlans();
135
+ const planAssets = await assetManager.getPlans();
136
136
  const systemUri = systemId ? parseKapetaUri(systemId) : null;
137
137
  const blockUri = parseKapetaUri(blockRef);
138
138
  let found = false;
@@ -13,6 +13,7 @@ import { getBlockInstanceContainerName } from './utils/utils';
13
13
  import { InstanceInfo, LogEntry, LogSource } from './types';
14
14
  import { KapetaAPI } from '@kapeta/nodejs-api-client';
15
15
  import { taskManager, Task } from './taskManager';
16
+ import { EventEmitter } from 'node:events';
16
17
 
17
18
  type StringMap = { [key: string]: string };
18
19
 
@@ -78,6 +79,7 @@ class ContainerManager {
78
79
  private _mountDir: string;
79
80
  private _version: string;
80
81
  private _lastDockerAccessCheck: number = 0;
82
+ private logStreams: { [p: string]: { stream?: ClosableLogStream; timer?: NodeJS.Timeout } } = {};
81
83
 
82
84
  constructor() {
83
85
  this._docker = null;
@@ -607,6 +609,193 @@ class ContainerManager {
607
609
 
608
610
  return containerInfo.getLogs();
609
611
  }
612
+
613
+ async stopLogListening(systemId: string, instanceId: string) {
614
+ const containerName = getBlockInstanceContainerName(systemId, instanceId);
615
+ if (this.logStreams[containerName]) {
616
+ if (this.logStreams[containerName]?.timer) {
617
+ clearTimeout(this.logStreams[containerName].timer);
618
+ }
619
+
620
+ console.log('Stopped listening for logs on container: %s', containerName);
621
+ try {
622
+ const stream = this.logStreams[containerName].stream;
623
+ if (stream) {
624
+ await stream.close();
625
+ }
626
+ } catch (err) {
627
+ // Ignore
628
+ }
629
+ delete this.logStreams[containerName];
630
+ }
631
+ }
632
+
633
+ async ensureLogListening(systemId: string, instanceId: string, handler: (log: LogEntry) => void) {
634
+ const containerName = getBlockInstanceContainerName(systemId, instanceId);
635
+ try {
636
+ if (this.logStreams[containerName]?.stream) {
637
+ // Already listening - will shut itself down
638
+ return;
639
+ }
640
+
641
+ if (this.logStreams[containerName]?.timer) {
642
+ clearTimeout(this.logStreams[containerName].timer);
643
+ }
644
+
645
+ const tryLater = () => {
646
+ this.logStreams[containerName] = {
647
+ timer: setTimeout(() => {
648
+ // Keep trying until user decides to not listen anymore
649
+ this.ensureLogListening(systemId, instanceId, handler);
650
+ }, 5000),
651
+ };
652
+ };
653
+
654
+ const containerInfo = await this.getContainerByName(containerName);
655
+ if (!containerInfo || !(await containerInfo.isRunning())) {
656
+ // Container not currently running - try again in 5 seconds
657
+ tryLater();
658
+ return;
659
+ }
660
+
661
+ const stream = await containerInfo.getLogStream();
662
+ stream.onLog((log) => {
663
+ try {
664
+ handler(log);
665
+ } catch (err) {
666
+ console.warn('Error handling log', err);
667
+ }
668
+ });
669
+ stream.onEnd(() => {
670
+ // We get here if the container is stopped
671
+ delete this.logStreams[containerName];
672
+ tryLater();
673
+ });
674
+ stream.onError((err) => {
675
+ // We get here if the container crashes
676
+ delete this.logStreams[containerName];
677
+ tryLater();
678
+ });
679
+
680
+ this.logStreams[containerName] = {
681
+ stream,
682
+ };
683
+ } catch (err) {
684
+ // Ignore
685
+ }
686
+ }
687
+ }
688
+
689
+ function readLogBuffer(logBuffer: Buffer) {
690
+ const out: LogEntry[] = [];
691
+ let offset = 0;
692
+ while (offset < logBuffer.length) {
693
+ try {
694
+ // Read the docker log format - explained here:
695
+ // https://docs.docker.com/engine/api/v1.41/#operation/ContainerAttach
696
+ // or here : https://ahmet.im/blog/docker-logs-api-binary-format-explained/
697
+
698
+ // First byte is stream type
699
+ const streamTypeInt = logBuffer.readInt8(offset);
700
+ const streamType: LogSource = streamTypeInt === 1 ? 'stdout' : 'stderr';
701
+ if (streamTypeInt !== 1 && streamTypeInt !== 2) {
702
+ console.error('Unknown stream type: %s', streamTypeInt, out[out.length - 1]);
703
+ break;
704
+ }
705
+
706
+ // Bytes 4-8 is frame size
707
+ const messageLength = logBuffer.readInt32BE(offset + 4);
708
+
709
+ // After that is the message - with the message length
710
+ const dataWithoutStreamType = logBuffer.subarray(offset + 8, offset + 8 + messageLength);
711
+ const raw = dataWithoutStreamType.toString();
712
+
713
+ // Split the message into date and message
714
+ const firstSpaceIx = raw.indexOf(' ');
715
+ const dateString = raw.substring(0, firstSpaceIx);
716
+ const line = raw.substring(firstSpaceIx + 1);
717
+ offset = offset + messageLength + 8;
718
+ if (!dateString) {
719
+ break;
720
+ }
721
+ out.push({
722
+ time: new Date(dateString).getTime(),
723
+ message: line,
724
+ level: 'INFO',
725
+ source: streamType,
726
+ });
727
+ } catch (err) {
728
+ console.error('Error parsing log entry', err);
729
+ offset = logBuffer.length;
730
+ break;
731
+ }
732
+ }
733
+ return out;
734
+ }
735
+
736
+ class ClosableLogStream {
737
+ private readonly stream: FSExtra.ReadStream;
738
+
739
+ private readonly eventEmitter: EventEmitter;
740
+
741
+ constructor(stream: FSExtra.ReadStream) {
742
+ this.stream = stream;
743
+ this.eventEmitter = new EventEmitter();
744
+ stream.on('data', (data) => {
745
+ const logs = readLogBuffer(data as Buffer);
746
+ logs.forEach((log) => {
747
+ this.eventEmitter.emit('log', log);
748
+ });
749
+ });
750
+
751
+ stream.on('end', () => {
752
+ this.eventEmitter.emit('end');
753
+ });
754
+
755
+ stream.on('error', (error) => {
756
+ this.eventEmitter.emit('error', error);
757
+ });
758
+
759
+ stream.on('close', () => {
760
+ this.eventEmitter.emit('end');
761
+ });
762
+ }
763
+
764
+ onLog(listener: (log: LogEntry) => void) {
765
+ this.eventEmitter.on('log', listener);
766
+ return () => {
767
+ this.eventEmitter.removeListener('log', listener);
768
+ };
769
+ }
770
+
771
+ onEnd(listener: () => void) {
772
+ this.eventEmitter.on('end', listener);
773
+ return () => {
774
+ this.eventEmitter.removeListener('end', listener);
775
+ };
776
+ }
777
+
778
+ onError(listener: (error: Error) => void) {
779
+ this.eventEmitter.on('error', listener);
780
+ return () => {
781
+ this.eventEmitter.removeListener('error', listener);
782
+ };
783
+ }
784
+
785
+ close() {
786
+ return new Promise<void>((resolve, reject) => {
787
+ try {
788
+ this.stream.close((err) => {
789
+ if (err) {
790
+ console.warn('Error closing log stream', err);
791
+ }
792
+ resolve();
793
+ });
794
+ } catch (err) {
795
+ // Ignore
796
+ }
797
+ });
798
+ }
610
799
  }
611
800
 
612
801
  export class ContainerInfo {
@@ -718,57 +907,38 @@ export class ContainerInfo {
718
907
  return ports;
719
908
  }
720
909
 
910
+ async getLogStream() {
911
+ try {
912
+ const logStream = (await this.native.logs({
913
+ stdout: true,
914
+ stderr: true,
915
+ follow: true,
916
+ tail: 0,
917
+ timestamps: true,
918
+ })) as ReadStream;
919
+
920
+ return new ClosableLogStream(logStream);
921
+ } catch (err) {
922
+ console.log('Error getting log stream', err);
923
+ throw err;
924
+ }
925
+ }
926
+
721
927
  async getLogs(): Promise<LogEntry[]> {
722
928
  const logStream = (await this.native.logs({
723
929
  stdout: true,
724
930
  stderr: true,
725
931
  follow: false,
726
- tail: 100,
727
932
  timestamps: true,
728
933
  })) as ReadStream;
729
934
 
730
- const out = [] as LogEntry[];
935
+ const chunks: Buffer[] = [];
731
936
  await promisifyStream(logStream, (data) => {
732
- const buf = data as Buffer;
733
- let offset = 0;
734
- while (offset < buf.length) {
735
- try {
736
- // Read the docker log format - explained here:
737
- // https://docs.docker.com/engine/api/v1.41/#operation/ContainerAttach
738
- // or here : https://ahmet.im/blog/docker-logs-api-binary-format-explained/
739
-
740
- // First byte is stream type
741
- const streamTypeInt = buf.readInt8(offset);
742
- const streamType: LogSource = streamTypeInt === 1 ? 'stdout' : 'stderr';
743
-
744
- // Bytes 4-8 is frame size
745
- const messageLength = buf.readInt32BE(offset + 4);
746
-
747
- // After that is the message - with the message length
748
- const dataWithoutStreamType = buf.subarray(offset + 8, offset + 8 + messageLength);
749
- const raw = dataWithoutStreamType.toString();
750
-
751
- // Split the message into date and message
752
- const firstSpaceIx = raw.indexOf(' ');
753
- const dateString = raw.substring(0, firstSpaceIx);
754
- const line = raw.substring(firstSpaceIx + 1);
755
- offset = offset + messageLength + 8;
756
- if (!dateString) {
757
- continue;
758
- }
759
- out.push({
760
- time: new Date(dateString).getTime(),
761
- message: line,
762
- level: 'INFO',
763
- source: streamType,
764
- });
765
- } catch (err) {
766
- console.error('Error parsing log entry', err);
767
- offset = buf.length;
768
- }
769
- }
937
+ chunks.push(data as Buffer);
770
938
  });
771
939
 
940
+ const out = readLogBuffer(Buffer.concat(chunks));
941
+
772
942
  if (out.length === 0) {
773
943
  out.push({
774
944
  time: Date.now(),
@@ -1,39 +1,154 @@
1
1
  import ClusterConfiguration, { DefinitionInfo } from '@kapeta/local-cluster-config';
2
2
  import { parseKapetaUri } from '@kapeta/nodejs-utils';
3
3
  import { cacheManager, doCached } from './cacheManager';
4
+ import { KapetaAPI } from '@kapeta/nodejs-api-client';
5
+ import { Plan } from '@kapeta/schemas';
6
+ import FS from 'fs-extra';
7
+ import { normalizeKapetaUri } from './utils/utils';
8
+ import YAML from 'yaml';
9
+ import { Actions } from '@kapeta/nodejs-registry-utils';
10
+ import { ProgressListener } from './progressListener';
11
+ import Path from 'path';
12
+
13
+ export const SAMPLE_PLAN_NAME = 'kapeta/sample-nodejs-plan';
14
+
15
+ function applyHandleChange(definition: DefinitionInfo, targetHandle: string) {
16
+ const originalUri = parseKapetaUri(definition.definition.metadata.name);
17
+ definition.definition.metadata.name = `${targetHandle}/${originalUri.name}`;
18
+ return definition;
19
+ }
20
+
21
+ function normalizeFilters(kindFilter?: string | string[]) {
22
+ let resolvedFilters: string[] = [];
23
+
24
+ if (kindFilter) {
25
+ if (Array.isArray(kindFilter)) {
26
+ resolvedFilters = [...kindFilter];
27
+ } else {
28
+ resolvedFilters = [kindFilter];
29
+ }
30
+ }
31
+
32
+ return resolvedFilters.map((k) => k.toLowerCase());
33
+ }
4
34
 
5
35
  class DefinitionsManager {
6
- private getHash(kindFilter?: string | string[]) {
7
- if (kindFilter) {
8
- if (Array.isArray(kindFilter)) {
9
- return kindFilter.join(',');
36
+ private async resolveDefinitionsAndSamples() {
37
+ const definitions = ClusterConfiguration.getDefinitions();
38
+ const samplePlan = definitions.find(
39
+ (d) => d.version === 'local' && d.definition.metadata.name === SAMPLE_PLAN_NAME
40
+ );
41
+
42
+ if (!samplePlan) {
43
+ return definitions;
44
+ }
45
+
46
+ // We will only rewrite the sample plan once since we change the handle to be the users handle
47
+ const api = new KapetaAPI();
48
+ if (!api.hasToken()) {
49
+ // Not logged in yet, so we can't rewrite the sample plan
50
+ return definitions;
51
+ }
52
+ const profile = await api.getCurrentIdentity();
53
+ if (!profile) {
54
+ // Not logged in yet, so we can't rewrite the sample plan
55
+ return definitions;
56
+ }
57
+
58
+ console.log('Rewriting sample plan to use handle %s', profile.handle);
59
+
60
+ applyHandleChange(samplePlan, profile.handle);
61
+
62
+ const planDef = samplePlan.definition as Plan;
63
+
64
+ const blockRefs = new Set<string>();
65
+
66
+ planDef.spec.blocks.forEach((b) => {
67
+ const blockUri = parseKapetaUri(b.block.ref);
68
+ if (blockUri.version === 'local') {
69
+ blockRefs.add(blockUri.id);
70
+ b.block.ref = normalizeKapetaUri(`${profile.handle}/${blockUri.name}:local`);
10
71
  }
11
- return kindFilter;
72
+ });
73
+
74
+ // Rewrite all blocks that are referenced by the sample plan
75
+ const rewrittenBlocks = Array.from(blockRefs)
76
+ .map((ref) =>
77
+ definitions.find(
78
+ (d) => normalizeKapetaUri(d.definition.metadata.name + ':' + d.version) === normalizeKapetaUri(ref)
79
+ )
80
+ )
81
+ .filter((d) => d !== undefined)
82
+ .map((d) => applyHandleChange(d!, profile.handle));
83
+
84
+ // Persist the rewritten assets
85
+ const progressListener = new ProgressListener();
86
+ const rewrittenAssets = [samplePlan, ...rewrittenBlocks];
87
+ const originalRefs = [`${SAMPLE_PLAN_NAME}:local`, ...Array.from(blockRefs)];
88
+
89
+ // Store the original paths on the assets - we'll need them later
90
+ for (const asset of rewrittenAssets) {
91
+ asset.path = await FS.readlink(asset.path);
92
+ asset.ymlPath = Path.join(asset.path, Path.basename(asset.ymlPath));
93
+ }
94
+
95
+ // Uninstall the original assets
96
+ // This removes the symlinks
97
+ console.log('Uninstalling original assets', originalRefs);
98
+ try {
99
+ await Actions.uninstall(progressListener, originalRefs);
100
+ } catch (err) {
101
+ console.warn('Failed to uninstall original assets', err);
12
102
  }
13
- return 'none';
103
+
104
+ for (const asset of rewrittenAssets) {
105
+ console.log('Updating %s ', asset.ymlPath);
106
+ await FS.writeFile(asset.ymlPath, YAML.stringify(asset.definition));
107
+
108
+ console.log('Linking %s ', asset.path);
109
+ await Actions.link(progressListener, asset.path);
110
+ }
111
+
112
+ console.log('Rewrite done for sample plan');
113
+
114
+ // Return the rewritten definitions
115
+ return ClusterConfiguration.getDefinitions();
14
116
  }
15
117
 
16
- private getFullKey(kindFilter?: string | string[]) {
17
- return `definitionsManager:${this.getHash(kindFilter)}`;
118
+ private applyFilters(definitions: DefinitionInfo[], kindFilter: string[]): DefinitionInfo[] {
119
+ if (kindFilter.length === 0) {
120
+ return definitions;
121
+ }
122
+
123
+ return definitions.filter((d) => {
124
+ return kindFilter.includes(d.definition.kind.toLowerCase());
125
+ });
18
126
  }
19
127
 
20
- public getDefinitions(kindFilter?: string | string[]): DefinitionInfo[] {
21
- const key = this.getFullKey(kindFilter);
128
+ public async getDefinitions(kindFilter?: string | string[]): Promise<DefinitionInfo[]> {
129
+ kindFilter = normalizeFilters(kindFilter);
130
+
131
+ const definitions = await doCached<Promise<DefinitionInfo[]>>('definitionsManager:all', () =>
132
+ this.resolveDefinitionsAndSamples()
133
+ );
22
134
 
23
- return doCached<DefinitionInfo[]>(key, () => ClusterConfiguration.getDefinitions(kindFilter));
135
+ return this.applyFilters(definitions, kindFilter);
24
136
  }
25
137
 
26
- public exists(ref: string) {
27
- return !!this.getDefinition(ref);
138
+ public async exists(ref: string) {
139
+ return !!(await this.getDefinition(ref));
28
140
  }
29
141
 
30
- public getProviderDefinitions(): DefinitionInfo[] {
31
- return doCached<DefinitionInfo[]>('providers', () => ClusterConfiguration.getProviderDefinitions());
142
+ public async getProviderDefinitions(): Promise<DefinitionInfo[]> {
143
+ return doCached<DefinitionInfo[]>('definitionsManager:providers', () =>
144
+ ClusterConfiguration.getProviderDefinitions()
145
+ );
32
146
  }
33
147
 
34
- public getDefinition(ref: string) {
148
+ public async getDefinition(ref: string) {
35
149
  const uri = parseKapetaUri(ref);
36
- return this.getDefinitions().find((d) => {
150
+ const definitions = await this.getDefinitions();
151
+ return definitions.find((d) => {
37
152
  if (!uri.version) {
38
153
  return d.definition.metadata.name === uri.fullName;
39
154
  }