@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.
- package/CHANGELOG.md +14 -0
- package/dist/cjs/index.js +4 -2
- package/dist/cjs/src/assetManager.d.ts +2 -2
- package/dist/cjs/src/assetManager.js +16 -16
- package/dist/cjs/src/assets/routes.js +2 -2
- package/dist/cjs/src/authManager.d.ts +12 -0
- package/dist/cjs/src/authManager.js +60 -0
- package/dist/cjs/src/codeGeneratorManager.d.ts +1 -1
- package/dist/cjs/src/codeGeneratorManager.js +3 -3
- package/dist/cjs/src/configManager.js +2 -2
- package/dist/cjs/src/containerManager.d.ts +15 -0
- package/dist/cjs/src/containerManager.js +190 -37
- package/dist/cjs/src/definitionsManager.d.ts +7 -6
- package/dist/cjs/src/definitionsManager.js +102 -18
- package/dist/cjs/src/instanceManager.d.ts +1 -1
- package/dist/cjs/src/instanceManager.js +7 -12
- package/dist/cjs/src/instances/routes.js +2 -2
- package/dist/cjs/src/operatorManager.d.ts +1 -1
- package/dist/cjs/src/operatorManager.js +7 -9
- package/dist/cjs/src/providerManager.d.ts +2 -1
- package/dist/cjs/src/providerManager.js +23 -15
- package/dist/cjs/src/repositoryManager.d.ts +2 -2
- package/dist/cjs/src/repositoryManager.js +8 -9
- package/dist/cjs/src/socketManager.d.ts +2 -2
- package/dist/cjs/src/socketManager.js +39 -14
- package/dist/cjs/src/utils/BlockInstanceRunner.js +6 -8
- package/dist/esm/index.js +4 -2
- package/dist/esm/src/assetManager.d.ts +2 -2
- package/dist/esm/src/assetManager.js +16 -16
- package/dist/esm/src/assets/routes.js +2 -2
- package/dist/esm/src/authManager.d.ts +12 -0
- package/dist/esm/src/authManager.js +60 -0
- package/dist/esm/src/codeGeneratorManager.d.ts +1 -1
- package/dist/esm/src/codeGeneratorManager.js +3 -3
- package/dist/esm/src/configManager.js +2 -2
- package/dist/esm/src/containerManager.d.ts +15 -0
- package/dist/esm/src/containerManager.js +190 -37
- package/dist/esm/src/definitionsManager.d.ts +7 -6
- package/dist/esm/src/definitionsManager.js +102 -18
- package/dist/esm/src/instanceManager.d.ts +1 -1
- package/dist/esm/src/instanceManager.js +7 -12
- package/dist/esm/src/instances/routes.js +2 -2
- package/dist/esm/src/operatorManager.d.ts +1 -1
- package/dist/esm/src/operatorManager.js +7 -9
- package/dist/esm/src/providerManager.d.ts +2 -1
- package/dist/esm/src/providerManager.js +23 -15
- package/dist/esm/src/repositoryManager.d.ts +2 -2
- package/dist/esm/src/repositoryManager.js +8 -9
- package/dist/esm/src/socketManager.d.ts +2 -2
- package/dist/esm/src/socketManager.js +39 -14
- package/dist/esm/src/utils/BlockInstanceRunner.js +6 -8
- package/index.ts +4 -2
- package/package.json +1 -1
- package/src/assetManager.ts +18 -16
- package/src/assets/routes.ts +2 -2
- package/src/authManager.ts +62 -0
- package/src/codeGeneratorManager.ts +3 -3
- package/src/configManager.ts +2 -2
- package/src/containerManager.ts +210 -40
- package/src/definitionsManager.ts +132 -17
- package/src/instanceManager.ts +7 -14
- package/src/instances/routes.ts +2 -2
- package/src/operatorManager.ts +7 -12
- package/src/providerManager.ts +27 -19
- package/src/repositoryManager.ts +8 -11
- package/src/socketManager.ts +42 -15
- package/src/utils/BlockInstanceRunner.ts +6 -8
package/src/assetManager.ts
CHANGED
@@ -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
|
-
|
114
|
-
|
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
|
-
|
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
|
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
|
-
|
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}`,
|
package/src/assets/routes.ts
CHANGED
@@ -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
|
);
|
package/src/configManager.ts
CHANGED
@@ -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;
|
package/src/containerManager.ts
CHANGED
@@ -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
|
935
|
+
const chunks: Buffer[] = [];
|
731
936
|
await promisifyStream(logStream, (data) => {
|
732
|
-
|
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
|
7
|
-
|
8
|
-
|
9
|
-
|
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
|
-
|
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
|
-
|
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
|
17
|
-
|
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
|
-
|
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
|
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', () =>
|
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
|
-
|
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
|
}
|