@kapeta/local-cluster-service 0.31.0 → 0.32.1
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 +2 -0
- package/dist/cjs/src/RepositoryWatcher.js +30 -6
- package/dist/cjs/src/ai/aiClient.d.ts +20 -0
- package/dist/cjs/src/ai/aiClient.js +74 -0
- package/dist/cjs/src/ai/routes.d.ts +7 -0
- package/dist/cjs/src/ai/routes.js +37 -0
- package/dist/cjs/src/ai/transform.d.ts +11 -0
- package/dist/cjs/src/ai/transform.js +239 -0
- package/dist/cjs/src/ai/types.d.ts +40 -0
- package/dist/cjs/src/ai/types.js +2 -0
- package/dist/cjs/src/assetManager.js +5 -2
- package/dist/cjs/src/codeGeneratorManager.js +4 -3
- package/dist/cjs/src/containerManager.js +7 -1
- package/dist/cjs/src/definitionsManager.js +2 -2
- package/dist/cjs/src/instanceManager.js +2 -2
- package/dist/cjs/src/repositoryManager.js +1 -1
- package/dist/cjs/src/utils/utils.d.ts +1 -0
- package/dist/cjs/src/utils/utils.js +7 -1
- package/dist/esm/index.js +2 -0
- package/dist/esm/src/RepositoryWatcher.js +30 -6
- package/dist/esm/src/ai/aiClient.d.ts +20 -0
- package/dist/esm/src/ai/aiClient.js +74 -0
- package/dist/esm/src/ai/routes.d.ts +7 -0
- package/dist/esm/src/ai/routes.js +37 -0
- package/dist/esm/src/ai/transform.d.ts +11 -0
- package/dist/esm/src/ai/transform.js +239 -0
- package/dist/esm/src/ai/types.d.ts +40 -0
- package/dist/esm/src/ai/types.js +2 -0
- package/dist/esm/src/assetManager.js +5 -2
- package/dist/esm/src/codeGeneratorManager.js +4 -3
- package/dist/esm/src/containerManager.js +7 -1
- package/dist/esm/src/definitionsManager.js +2 -2
- package/dist/esm/src/instanceManager.js +2 -2
- package/dist/esm/src/repositoryManager.js +1 -1
- package/dist/esm/src/utils/utils.d.ts +1 -0
- package/dist/esm/src/utils/utils.js +7 -1
- package/index.ts +2 -0
- package/package.json +1 -1
- package/src/RepositoryWatcher.ts +34 -6
- package/src/ai/aiClient.ts +93 -0
- package/src/ai/routes.ts +39 -0
- package/src/ai/transform.ts +275 -0
- package/src/ai/types.ts +45 -0
- package/src/assetManager.ts +5 -1
- package/src/codeGeneratorManager.ts +6 -4
- package/src/containerManager.ts +7 -1
- package/src/definitionsManager.ts +2 -2
- package/src/instanceManager.ts +2 -2
- package/src/repositoryManager.ts +1 -1
- package/src/utils/utils.ts +6 -0
@@ -7,15 +7,21 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
7
7
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
8
8
|
};
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
10
|
-
exports.getResolvedConfiguration = exports.getBindHost = exports.isLinux = exports.isMac = exports.isWindows = exports.readYML = exports.getBlockInstanceContainerName = void 0;
|
10
|
+
exports.getResolvedConfiguration = exports.getBindHost = exports.isLinux = exports.isMac = exports.isWindows = exports.readYML = exports.getRemoteUrl = exports.getBlockInstanceContainerName = void 0;
|
11
11
|
const node_fs_1 = __importDefault(require("node:fs"));
|
12
12
|
const yaml_1 = __importDefault(require("yaml"));
|
13
13
|
const md5_1 = __importDefault(require("md5"));
|
14
14
|
const lodash_1 = __importDefault(require("lodash"));
|
15
|
+
const local_cluster_config_1 = __importDefault(require("@kapeta/local-cluster-config"));
|
15
16
|
function getBlockInstanceContainerName(systemId, instanceId) {
|
16
17
|
return `kapeta-block-instance-${(0, md5_1.default)(systemId + instanceId)}`;
|
17
18
|
}
|
18
19
|
exports.getBlockInstanceContainerName = getBlockInstanceContainerName;
|
20
|
+
function getRemoteUrl(id, defautValue) {
|
21
|
+
const remoteConfig = local_cluster_config_1.default.getClusterConfig().remote;
|
22
|
+
return remoteConfig?.[id] ?? defautValue;
|
23
|
+
}
|
24
|
+
exports.getRemoteUrl = getRemoteUrl;
|
19
25
|
function readYML(path) {
|
20
26
|
const rawYaml = node_fs_1.default.readFileSync(path);
|
21
27
|
try {
|
package/index.ts
CHANGED
@@ -23,6 +23,7 @@ import ProviderRoutes from './src/providers/routes';
|
|
23
23
|
import AttachmentRoutes from './src/attachments/routes';
|
24
24
|
import TaskRoutes from './src/tasks/routes';
|
25
25
|
import APIRoutes from './src/api';
|
26
|
+
import AIRoutes from './src/ai/routes';
|
26
27
|
import { getBindHost } from './src/utils/utils';
|
27
28
|
import request from 'request';
|
28
29
|
import { repositoryManager } from './src/repositoryManager';
|
@@ -72,6 +73,7 @@ function createServer() {
|
|
72
73
|
app.use('/attachments', AttachmentRoutes);
|
73
74
|
app.use('/tasks', TaskRoutes);
|
74
75
|
app.use('/api', APIRoutes);
|
76
|
+
app.use('/ai', AIRoutes);
|
75
77
|
|
76
78
|
app.get('/status', async (req, res) => {
|
77
79
|
res.send({
|
package/package.json
CHANGED
package/src/RepositoryWatcher.ts
CHANGED
@@ -15,6 +15,7 @@ import { SourceOfChange, WatchEventName } from './types';
|
|
15
15
|
import { cacheManager } from './cacheManager';
|
16
16
|
import { EventEmitter } from 'node:events';
|
17
17
|
import { assetManager } from './assetManager';
|
18
|
+
import { definitionsManager } from './definitionsManager';
|
18
19
|
|
19
20
|
interface AssetIdentity {
|
20
21
|
handle: string;
|
@@ -22,6 +23,27 @@ interface AssetIdentity {
|
|
22
23
|
version: string;
|
23
24
|
}
|
24
25
|
const KAPETA_YML_RX = /^kapeta.ya?ml$/;
|
26
|
+
|
27
|
+
let definitions: DefinitionInfo[] | undefined;
|
28
|
+
let definitionTimeout: NodeJS.Timeout | undefined;
|
29
|
+
|
30
|
+
function getDefinitionsDebounced() {
|
31
|
+
if (definitionTimeout) {
|
32
|
+
clearTimeout(definitionTimeout);
|
33
|
+
definitionTimeout = undefined;
|
34
|
+
}
|
35
|
+
if (!definitions) {
|
36
|
+
definitions = ClusterConfiguration.getDefinitions();
|
37
|
+
} else {
|
38
|
+
definitionTimeout = setTimeout(() => {
|
39
|
+
definitionTimeout = undefined;
|
40
|
+
definitions = undefined;
|
41
|
+
}, 500);
|
42
|
+
}
|
43
|
+
|
44
|
+
return definitions;
|
45
|
+
}
|
46
|
+
|
25
47
|
export class RepositoryWatcher extends EventEmitter {
|
26
48
|
private watcher?: FSWatcher;
|
27
49
|
private disabled: boolean = false;
|
@@ -208,7 +230,8 @@ export class RepositoryWatcher extends EventEmitter {
|
|
208
230
|
|
209
231
|
private async checkForChange(assetIdentity: AssetIdentity, sourceOfChange: SourceOfChange) {
|
210
232
|
const ymlPath = Path.join(this.getRepositoryPath(assetIdentity), 'kapeta.yml');
|
211
|
-
const newDefinitions =
|
233
|
+
const newDefinitions = getDefinitionsDebounced();
|
234
|
+
|
212
235
|
const newDefinition = newDefinitions.find((d) => d.ymlPath === ymlPath);
|
213
236
|
let currentDefinition = this.allDefinitions.find((d) => d.ymlPath === ymlPath);
|
214
237
|
const ymlExists = await this.exists(ymlPath);
|
@@ -290,11 +313,16 @@ export class RepositoryWatcher extends EventEmitter {
|
|
290
313
|
} catch (e) {}
|
291
314
|
|
292
315
|
if (symbolicLink) {
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
316
|
+
try {
|
317
|
+
const realPath = Path.join(await FS.realpath(path), 'kapeta.yml');
|
318
|
+
if (await this.exists(realPath)) {
|
319
|
+
//console.log('Watching symlink target %s => %s', path, realPath);
|
320
|
+
this.watcher?.add(realPath);
|
321
|
+
this.symbolicLinks[path] = realPath;
|
322
|
+
}
|
323
|
+
} catch (e) {
|
324
|
+
// Remove the symlink - it's broken
|
325
|
+
await FS.remove(path);
|
298
326
|
}
|
299
327
|
}
|
300
328
|
} catch (e) {
|
@@ -0,0 +1,93 @@
|
|
1
|
+
/**
|
2
|
+
* Copyright 2023 Kapeta Inc.
|
3
|
+
* SPDX-License-Identifier: BUSL-1.1
|
4
|
+
*/
|
5
|
+
import request from 'request';
|
6
|
+
import { PlanContext, transformToPlan } from './transform';
|
7
|
+
import { Application } from './types';
|
8
|
+
import { KapetaAPI } from '@kapeta/nodejs-api-client';
|
9
|
+
import ClusterConfiguration from '@kapeta/local-cluster-config';
|
10
|
+
import { getRemoteUrl } from '../utils/utils';
|
11
|
+
|
12
|
+
export type PromptResult = {
|
13
|
+
explanation: string;
|
14
|
+
response: string;
|
15
|
+
context?: PlanContext;
|
16
|
+
};
|
17
|
+
|
18
|
+
export interface AIMessage {
|
19
|
+
content: string;
|
20
|
+
role: 'user' | 'assistant';
|
21
|
+
}
|
22
|
+
|
23
|
+
export interface AIRequest {
|
24
|
+
messages: AIMessage[];
|
25
|
+
}
|
26
|
+
|
27
|
+
class AIClient {
|
28
|
+
private readonly _baseUrl: string;
|
29
|
+
|
30
|
+
constructor() {
|
31
|
+
this._baseUrl = getRemoteUrl('ai-service', 'https://ai.kapeta.com');
|
32
|
+
}
|
33
|
+
|
34
|
+
public async sendPrompt(handle: string, body: AIRequest): Promise<PromptResult> {
|
35
|
+
const url = `${this._baseUrl}/v1/plan?type=chat`;
|
36
|
+
|
37
|
+
const headers: { [k: string]: string } = {};
|
38
|
+
const api = new KapetaAPI();
|
39
|
+
if (api.hasToken()) {
|
40
|
+
headers['Authorization'] = `Bearer ${await api.getAccessToken()}`;
|
41
|
+
}
|
42
|
+
|
43
|
+
const options = {
|
44
|
+
url,
|
45
|
+
method: 'POST',
|
46
|
+
json: true,
|
47
|
+
body,
|
48
|
+
headers,
|
49
|
+
};
|
50
|
+
|
51
|
+
return new Promise((resolve, reject) => {
|
52
|
+
request(options, async (error, response, application: Application) => {
|
53
|
+
if (error) {
|
54
|
+
console.error(error);
|
55
|
+
reject(error);
|
56
|
+
}
|
57
|
+
|
58
|
+
if (response.statusCode !== 200) {
|
59
|
+
console.log('Prompt failed', response.statusCode, response.body);
|
60
|
+
reject(new Error(`Invalid response code: ${response.statusCode}`));
|
61
|
+
return;
|
62
|
+
}
|
63
|
+
|
64
|
+
try {
|
65
|
+
if (application?.name) {
|
66
|
+
const planContext = await transformToPlan(handle, application);
|
67
|
+
resolve({
|
68
|
+
explanation: application.explanation,
|
69
|
+
response: application.response ?? application.explanation ?? 'Plan was generated',
|
70
|
+
context: planContext,
|
71
|
+
});
|
72
|
+
} else {
|
73
|
+
resolve({
|
74
|
+
explanation: application.explanation,
|
75
|
+
response:
|
76
|
+
application.response ??
|
77
|
+
application.explanation ??
|
78
|
+
'I did not understand your request. Please rephrase.',
|
79
|
+
});
|
80
|
+
}
|
81
|
+
} catch (err: any) {
|
82
|
+
console.error(err);
|
83
|
+
resolve({
|
84
|
+
explanation: '',
|
85
|
+
response: 'I did not understand your request. Please rephrase.',
|
86
|
+
});
|
87
|
+
}
|
88
|
+
});
|
89
|
+
});
|
90
|
+
}
|
91
|
+
}
|
92
|
+
|
93
|
+
export const aiClient = new AIClient();
|
package/src/ai/routes.ts
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
/**
|
2
|
+
* Copyright 2023 Kapeta Inc.
|
3
|
+
* SPDX-License-Identifier: BUSL-1.1
|
4
|
+
*/
|
5
|
+
|
6
|
+
import Router from 'express-promise-router';
|
7
|
+
import { Response } from 'express';
|
8
|
+
|
9
|
+
import { corsHandler } from '../middleware/cors';
|
10
|
+
|
11
|
+
import { stringBody, StringBodyRequest } from '../middleware/stringBody';
|
12
|
+
import { aiClient, AIRequest } from './aiClient';
|
13
|
+
import { KapetaBodyRequest } from '../types';
|
14
|
+
import YAML from 'yaml';
|
15
|
+
|
16
|
+
const router = Router();
|
17
|
+
|
18
|
+
router.use('/', corsHandler);
|
19
|
+
router.use('/', stringBody);
|
20
|
+
|
21
|
+
router.post('/prompt/:handle', async (req: KapetaBodyRequest, res: Response) => {
|
22
|
+
const handle = req.params.handle;
|
23
|
+
try {
|
24
|
+
const aiRequest: AIRequest = JSON.parse(req.stringBody ?? '{}');
|
25
|
+
const result = await aiClient.sendPrompt(handle, aiRequest);
|
26
|
+
if (req.accepts('application/yaml')) {
|
27
|
+
res.set('Content-Type', 'application/yaml');
|
28
|
+
res.send(YAML.stringify(result));
|
29
|
+
} else {
|
30
|
+
res.json(result);
|
31
|
+
}
|
32
|
+
} catch (err: any) {
|
33
|
+
console.error('Failed to send prompt', err);
|
34
|
+
res.status(400).send({ error: err.message });
|
35
|
+
return;
|
36
|
+
}
|
37
|
+
});
|
38
|
+
|
39
|
+
export default router;
|
@@ -0,0 +1,275 @@
|
|
1
|
+
/**
|
2
|
+
* Copyright 2023 Kapeta Inc.
|
3
|
+
* SPDX-License-Identifier: BUSL-1.1
|
4
|
+
*/
|
5
|
+
import { BlockDefinition, Plan } from '@kapeta/schemas';
|
6
|
+
import { Application } from './types';
|
7
|
+
import { definitionsManager } from '../definitionsManager';
|
8
|
+
import { normalizeKapetaUri, parseKapetaUri } from '@kapeta/nodejs-utils';
|
9
|
+
import uuid from 'node-uuid';
|
10
|
+
|
11
|
+
export type PlanContext = {
|
12
|
+
plan: Plan;
|
13
|
+
blocks: BlockDefinition[];
|
14
|
+
};
|
15
|
+
|
16
|
+
async function getFreeName(name: string) {
|
17
|
+
let currentName = name;
|
18
|
+
let iteration = 1;
|
19
|
+
do {
|
20
|
+
const found = await definitionsManager.getLatestDefinition(currentName);
|
21
|
+
if (!found) {
|
22
|
+
return currentName;
|
23
|
+
}
|
24
|
+
currentName = name + '_' + iteration++;
|
25
|
+
} while (true);
|
26
|
+
}
|
27
|
+
|
28
|
+
export const transformToPlan = async (handle: string, application: Application): Promise<PlanContext> => {
|
29
|
+
const blockTypeService = await definitionsManager.getLatestDefinition('kapeta/block-type-service');
|
30
|
+
const blockTypeFrontend = await definitionsManager.getLatestDefinition('kapeta/block-type-frontend');
|
31
|
+
const mongoDbResource = await definitionsManager.getLatestDefinition('kapeta/resource-type-mongodb');
|
32
|
+
const postgresResource = await definitionsManager.getLatestDefinition('kapeta/resource-type-postgresql');
|
33
|
+
const webPageResource = await definitionsManager.getLatestDefinition('kapeta/resource-type-web-page');
|
34
|
+
const webFragmentResource = await definitionsManager.getLatestDefinition('kapeta/resource-type-web-fragment');
|
35
|
+
const restApiResource = await definitionsManager.getLatestDefinition('kapeta/resource-type-rest-api');
|
36
|
+
const restClientResource = await definitionsManager.getLatestDefinition('kapeta/resource-type-rest-client');
|
37
|
+
const javaLanguage = await definitionsManager.getLatestDefinition('kapeta/language-target-java-spring-boot');
|
38
|
+
const nodejsLanguage = await definitionsManager.getLatestDefinition('kapeta/language-target-nodejs');
|
39
|
+
const reactLanguage = await definitionsManager.getLatestDefinition('kapeta/language-target-react-ts');
|
40
|
+
|
41
|
+
if (
|
42
|
+
!blockTypeService ||
|
43
|
+
!blockTypeFrontend ||
|
44
|
+
!mongoDbResource ||
|
45
|
+
!postgresResource ||
|
46
|
+
!javaLanguage ||
|
47
|
+
!nodejsLanguage ||
|
48
|
+
!reactLanguage ||
|
49
|
+
!webPageResource ||
|
50
|
+
!restApiResource ||
|
51
|
+
!restClientResource ||
|
52
|
+
!webFragmentResource
|
53
|
+
) {
|
54
|
+
throw new Error('Missing definitions');
|
55
|
+
}
|
56
|
+
|
57
|
+
const plan: Plan = {
|
58
|
+
kind: 'core/plan',
|
59
|
+
metadata: {
|
60
|
+
name: await getFreeName(`${handle}/${application.name}`),
|
61
|
+
title: application.title,
|
62
|
+
description: application.description,
|
63
|
+
visibility: 'private',
|
64
|
+
},
|
65
|
+
spec: {
|
66
|
+
blocks: [],
|
67
|
+
connections: [],
|
68
|
+
},
|
69
|
+
};
|
70
|
+
|
71
|
+
const blocks: BlockDefinition[] = [];
|
72
|
+
|
73
|
+
const addToPlan = (ref: string, name: string) => {
|
74
|
+
const top = 100 + Math.floor(plan.spec.blocks.length / 3) * 300;
|
75
|
+
const left = 200 + (plan.spec.blocks.length % 3) * 450;
|
76
|
+
|
77
|
+
plan.spec.blocks.push({
|
78
|
+
block: {
|
79
|
+
ref,
|
80
|
+
},
|
81
|
+
name,
|
82
|
+
id: uuid.v4(),
|
83
|
+
dimensions: {
|
84
|
+
top,
|
85
|
+
left,
|
86
|
+
width: -1,
|
87
|
+
height: -1,
|
88
|
+
},
|
89
|
+
});
|
90
|
+
};
|
91
|
+
|
92
|
+
const nameMapper = new Map<string, string>();
|
93
|
+
|
94
|
+
for (const backend of application.backends) {
|
95
|
+
const blockName = `${handle}/${backend.name}`;
|
96
|
+
const blockRealName = await getFreeName(blockName);
|
97
|
+
nameMapper.set(blockName, blockRealName);
|
98
|
+
|
99
|
+
const language = backend.targetLanguage === 'java' ? javaLanguage : nodejsLanguage;
|
100
|
+
const databaseInfo = backend.databases?.[0];
|
101
|
+
const database = databaseInfo?.type === 'mongodb' ? mongoDbResource : postgresResource;
|
102
|
+
|
103
|
+
const blockRef = normalizeKapetaUri(blockRealName + ':local');
|
104
|
+
let targetOptions = {};
|
105
|
+
if (backend.targetLanguage === 'java') {
|
106
|
+
targetOptions = {
|
107
|
+
basePackage: `${handle}.${application.name}`.toLowerCase().replace(/-/g, '_'),
|
108
|
+
groupId: `${handle}.${application.name}`.toLowerCase().replace(/-/g, '_'),
|
109
|
+
artifactId: backend.name.toLowerCase().replace(/-/g, '_'),
|
110
|
+
};
|
111
|
+
}
|
112
|
+
blocks.push({
|
113
|
+
kind: normalizeKapetaUri(`${blockTypeService.definition.metadata.name}:${blockTypeService.version}`),
|
114
|
+
metadata: {
|
115
|
+
name: blockRealName,
|
116
|
+
title: backend.title,
|
117
|
+
description: backend.description,
|
118
|
+
visibility: 'private',
|
119
|
+
},
|
120
|
+
spec: {
|
121
|
+
target: {
|
122
|
+
kind: normalizeKapetaUri(`${language.definition.metadata.name}:${language.version}`),
|
123
|
+
options: targetOptions,
|
124
|
+
},
|
125
|
+
consumers: [
|
126
|
+
{
|
127
|
+
kind: normalizeKapetaUri(`${database.definition.metadata.name}:${database.version}`),
|
128
|
+
metadata: {
|
129
|
+
name: (databaseInfo?.name ?? 'main').replace(/-/g, ''),
|
130
|
+
},
|
131
|
+
spec: {
|
132
|
+
port: {
|
133
|
+
type: database.definition.spec.ports[0].type,
|
134
|
+
},
|
135
|
+
},
|
136
|
+
},
|
137
|
+
],
|
138
|
+
providers: [
|
139
|
+
{
|
140
|
+
kind: normalizeKapetaUri(
|
141
|
+
`${restApiResource.definition.metadata.name}:${restApiResource.version}`
|
142
|
+
),
|
143
|
+
metadata: {
|
144
|
+
name: 'main',
|
145
|
+
},
|
146
|
+
spec: {
|
147
|
+
port: {
|
148
|
+
type: restApiResource.definition.spec.ports[0].type,
|
149
|
+
},
|
150
|
+
},
|
151
|
+
},
|
152
|
+
],
|
153
|
+
},
|
154
|
+
});
|
155
|
+
|
156
|
+
addToPlan(blockRef, backend.name);
|
157
|
+
}
|
158
|
+
|
159
|
+
for (const frontend of application.frontends) {
|
160
|
+
const blockName = `${handle}/${frontend.name}`;
|
161
|
+
const blockRealName = await getFreeName(blockName);
|
162
|
+
nameMapper.set(blockName, blockRealName);
|
163
|
+
|
164
|
+
const language = reactLanguage;
|
165
|
+
|
166
|
+
const blockRef = normalizeKapetaUri(blockRealName + ':local');
|
167
|
+
blocks.push({
|
168
|
+
kind: normalizeKapetaUri(`${blockTypeFrontend.definition.metadata.name}:${blockTypeFrontend.version}`),
|
169
|
+
metadata: {
|
170
|
+
name: blockRealName,
|
171
|
+
title: frontend.title,
|
172
|
+
description: frontend.description,
|
173
|
+
visibility: 'private',
|
174
|
+
},
|
175
|
+
spec: {
|
176
|
+
target: {
|
177
|
+
kind: normalizeKapetaUri(`${language.definition.metadata.name}:${language.version}`),
|
178
|
+
options: {},
|
179
|
+
},
|
180
|
+
consumers: [],
|
181
|
+
providers: [
|
182
|
+
{
|
183
|
+
kind: normalizeKapetaUri(
|
184
|
+
`${webPageResource.definition.metadata.name}:${webPageResource.version}`
|
185
|
+
),
|
186
|
+
metadata: {
|
187
|
+
name: 'main',
|
188
|
+
},
|
189
|
+
spec: {
|
190
|
+
port: {
|
191
|
+
type: webPageResource.definition.spec.ports[0].type,
|
192
|
+
},
|
193
|
+
},
|
194
|
+
},
|
195
|
+
],
|
196
|
+
},
|
197
|
+
});
|
198
|
+
|
199
|
+
addToPlan(blockRef, frontend.name);
|
200
|
+
}
|
201
|
+
|
202
|
+
application.connections?.forEach((connection) => {
|
203
|
+
const fullProviderName = nameMapper.get(`${handle}/${connection.provider.name}`) as string;
|
204
|
+
const fullConsumerName = nameMapper.get(`${handle}/${connection.consumer.name}`) as string;
|
205
|
+
const consumerResourceName = connection.provider.name;
|
206
|
+
const providerRef = normalizeKapetaUri(`${fullProviderName}:local`);
|
207
|
+
const consumerRef = normalizeKapetaUri(`${fullConsumerName}:local`);
|
208
|
+
|
209
|
+
const instanceProvider = plan.spec.blocks.find((b) => b.block.ref === providerRef)!;
|
210
|
+
const instanceConsumer = plan.spec.blocks.find((b) => b.block.ref === consumerRef)!;
|
211
|
+
const consumerBlock = blocks.find((block) => block.metadata.name === fullConsumerName);
|
212
|
+
const providerBlock = blocks.find((block) => block.metadata.name === fullProviderName);
|
213
|
+
if (!consumerBlock) {
|
214
|
+
throw new Error('Missing consumer block: ' + fullConsumerName);
|
215
|
+
}
|
216
|
+
|
217
|
+
if (!providerBlock) {
|
218
|
+
throw new Error('Missing provider block: ' + fullProviderName);
|
219
|
+
}
|
220
|
+
|
221
|
+
const portType = parseKapetaUri(providerBlock.kind).fullName === 'kapeta/block-type-service' ? 'rest' : 'web';
|
222
|
+
|
223
|
+
if (portType === 'rest') {
|
224
|
+
consumerBlock.spec.consumers!.push({
|
225
|
+
kind: normalizeKapetaUri(
|
226
|
+
`${restClientResource.definition.metadata.name}:${restClientResource.version}`
|
227
|
+
),
|
228
|
+
metadata: {
|
229
|
+
name: consumerResourceName,
|
230
|
+
},
|
231
|
+
spec: {
|
232
|
+
port: {
|
233
|
+
type: 'rest',
|
234
|
+
},
|
235
|
+
},
|
236
|
+
});
|
237
|
+
} else {
|
238
|
+
consumerBlock.spec.consumers!.push({
|
239
|
+
kind: normalizeKapetaUri(
|
240
|
+
`${webFragmentResource.definition.metadata.name}:${webFragmentResource.version}`
|
241
|
+
),
|
242
|
+
metadata: {
|
243
|
+
name: consumerResourceName,
|
244
|
+
},
|
245
|
+
spec: {
|
246
|
+
port: {
|
247
|
+
type: 'web',
|
248
|
+
},
|
249
|
+
},
|
250
|
+
});
|
251
|
+
}
|
252
|
+
|
253
|
+
plan.spec.connections.push({
|
254
|
+
provider: {
|
255
|
+
blockId: instanceProvider.id,
|
256
|
+
resourceName: 'main',
|
257
|
+
port: {
|
258
|
+
type: portType,
|
259
|
+
},
|
260
|
+
},
|
261
|
+
consumer: {
|
262
|
+
blockId: instanceConsumer.id,
|
263
|
+
resourceName: consumerResourceName,
|
264
|
+
port: {
|
265
|
+
type: portType,
|
266
|
+
},
|
267
|
+
},
|
268
|
+
});
|
269
|
+
});
|
270
|
+
|
271
|
+
return {
|
272
|
+
plan,
|
273
|
+
blocks,
|
274
|
+
};
|
275
|
+
};
|
package/src/ai/types.ts
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
/**
|
2
|
+
* Copyright 2023 Kapeta Inc.
|
3
|
+
* SPDX-License-Identifier: BUSL-1.1
|
4
|
+
*/
|
5
|
+
export interface Database {
|
6
|
+
name: string;
|
7
|
+
type: 'mongodb' | 'postgres';
|
8
|
+
}
|
9
|
+
|
10
|
+
export interface BackendService {
|
11
|
+
name: string;
|
12
|
+
title: string;
|
13
|
+
description: string;
|
14
|
+
targetLanguage: 'java' | 'node';
|
15
|
+
databases: Database[] | null;
|
16
|
+
}
|
17
|
+
|
18
|
+
export interface FrontendService {
|
19
|
+
name: string;
|
20
|
+
title: string;
|
21
|
+
description: string;
|
22
|
+
targetLanguage: 'react';
|
23
|
+
}
|
24
|
+
|
25
|
+
export interface Endpoint {
|
26
|
+
type: 'backend' | 'frontend';
|
27
|
+
name: string;
|
28
|
+
}
|
29
|
+
|
30
|
+
export interface Connection {
|
31
|
+
provider: Endpoint;
|
32
|
+
consumer: Endpoint;
|
33
|
+
}
|
34
|
+
|
35
|
+
export interface Application {
|
36
|
+
kind: 'core/plan';
|
37
|
+
name: string;
|
38
|
+
title: string;
|
39
|
+
description: string;
|
40
|
+
backends: BackendService[];
|
41
|
+
frontends: FrontendService[];
|
42
|
+
connections: Connection[];
|
43
|
+
explanation: string;
|
44
|
+
response: string;
|
45
|
+
}
|
package/src/assetManager.ts
CHANGED
@@ -36,6 +36,10 @@ export interface EnrichedAsset {
|
|
36
36
|
ymlPath: string;
|
37
37
|
}
|
38
38
|
|
39
|
+
function filterExists(asset: DefinitionInfo): boolean {
|
40
|
+
return FS.existsSync(asset.path);
|
41
|
+
}
|
42
|
+
|
39
43
|
function enrichAsset(asset: DefinitionInfo): EnrichedAsset {
|
40
44
|
return {
|
41
45
|
ref: `kapeta://${asset.definition.metadata.name}:${asset.version}`,
|
@@ -101,7 +105,7 @@ class AssetManager {
|
|
101
105
|
|
102
106
|
const assets = await definitionsManager.getDefinitions(assetKinds);
|
103
107
|
|
104
|
-
return assets.map(enrichAsset);
|
108
|
+
return assets.filter(filterExists).map(enrichAsset);
|
105
109
|
}
|
106
110
|
|
107
111
|
async getPlans(): Promise<EnrichedAsset[]> {
|
@@ -9,7 +9,7 @@ import { BlockDefinition } from '@kapeta/schemas';
|
|
9
9
|
import { definitionsManager } from './definitionsManager';
|
10
10
|
import { Definition } from '@kapeta/local-cluster-config';
|
11
11
|
import { assetManager } from './assetManager';
|
12
|
-
import { normalizeKapetaUri } from '@kapeta/nodejs-utils';
|
12
|
+
import { normalizeKapetaUri, parseKapetaUri } from '@kapeta/nodejs-utils';
|
13
13
|
import { repositoryManager } from './repositoryManager';
|
14
14
|
|
15
15
|
const TARGET_KIND = 'core/language-target';
|
@@ -61,16 +61,18 @@ class CodeGeneratorManager {
|
|
61
61
|
}
|
62
62
|
|
63
63
|
async canGenerateCode(yamlContent: Definition): Promise<boolean> {
|
64
|
-
if (!yamlContent.spec
|
64
|
+
if (!yamlContent.spec?.target?.kind || !yamlContent.kind) {
|
65
65
|
//Not all block types have targets
|
66
66
|
return false;
|
67
67
|
}
|
68
68
|
|
69
|
+
const kindUri = parseKapetaUri(yamlContent.kind);
|
70
|
+
|
69
71
|
const blockTypes = await definitionsManager.getDefinitions(BLOCK_TYPE_KIND);
|
70
72
|
const blockTypeKinds = blockTypes.map(
|
71
|
-
(blockType) => blockType.definition.metadata.name
|
73
|
+
(blockType) => parseKapetaUri(blockType.definition.metadata.name).fullName
|
72
74
|
);
|
73
|
-
return
|
75
|
+
return blockTypeKinds.includes(kindUri.fullName);
|
74
76
|
}
|
75
77
|
|
76
78
|
async generate(yamlFile: string, yamlContent: Definition) {
|
package/src/containerManager.ts
CHANGED
@@ -166,11 +166,17 @@ class ContainerManager {
|
|
166
166
|
];
|
167
167
|
for (const opts of connectOptions) {
|
168
168
|
try {
|
169
|
+
const testClient = new Docker({
|
170
|
+
...opts,
|
171
|
+
timeout: 1000, // 1 secs should be enough for a ping
|
172
|
+
});
|
173
|
+
await testClient.ping();
|
174
|
+
// If we get here - we have a working connection
|
175
|
+
// Now create a client with a longer timeout for all other operations
|
169
176
|
const client = new Docker({
|
170
177
|
...opts,
|
171
178
|
timeout: 15 * 60 * 1000, //15 minutes should be enough for any operation
|
172
179
|
});
|
173
|
-
await client.ping();
|
174
180
|
this._docker = client;
|
175
181
|
const versionInfo: any = await client.version();
|
176
182
|
this._version = versionInfo.Server?.Version ?? versionInfo.Version;
|
@@ -164,9 +164,9 @@ class DefinitionsManager {
|
|
164
164
|
const definitions = await this.getDefinitions();
|
165
165
|
return definitions.find((d) => {
|
166
166
|
if (!uri.version) {
|
167
|
-
return d.definition.metadata.name === uri.fullName;
|
167
|
+
return d.definition.metadata.name.toLowerCase() === uri.fullName.toLowerCase();
|
168
168
|
}
|
169
|
-
return parseKapetaUri(`${d.definition.metadata.name}:${d.version}`).
|
169
|
+
return parseKapetaUri(`${d.definition.metadata.name}:${d.version}`).equals(uri);
|
170
170
|
});
|
171
171
|
}
|
172
172
|
|
package/src/instanceManager.ts
CHANGED
@@ -27,7 +27,7 @@ import { definitionsManager } from './definitionsManager';
|
|
27
27
|
import { Task, taskManager } from './taskManager';
|
28
28
|
|
29
29
|
const CHECK_INTERVAL = 5000;
|
30
|
-
const DEFAULT_HEALTH_PORT_TYPE = '
|
30
|
+
const DEFAULT_HEALTH_PORT_TYPE = 'http';
|
31
31
|
|
32
32
|
const MIN_TIME_RUNNING = 30000; //If something didnt run for more than 30 secs - it failed
|
33
33
|
|
@@ -264,7 +264,7 @@ export class InstanceManager {
|
|
264
264
|
|
265
265
|
private getHealthUrl(info: Omit<InstanceInfo, 'systemId' | 'instanceId'>, address: string) {
|
266
266
|
let healthUrl = null;
|
267
|
-
let health = info.health;
|
267
|
+
let health = info.health ?? '/.kapeta/health';
|
268
268
|
if (health) {
|
269
269
|
if (health.startsWith('/')) {
|
270
270
|
health = health.substring(1);
|