@aifabrix/builder 2.11.0 → 2.21.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/.cursor/rules/project-rules.mdc +194 -0
- package/README.md +12 -0
- package/lib/api/applications.api.js +164 -0
- package/lib/api/auth.api.js +303 -0
- package/lib/api/datasources-core.api.js +87 -0
- package/lib/api/datasources-extended.api.js +117 -0
- package/lib/api/datasources.api.js +13 -0
- package/lib/api/deployments.api.js +126 -0
- package/lib/api/environments.api.js +245 -0
- package/lib/api/external-systems.api.js +251 -0
- package/lib/api/index.js +236 -0
- package/lib/api/pipeline.api.js +234 -0
- package/lib/api/types/applications.types.js +136 -0
- package/lib/api/types/auth.types.js +218 -0
- package/lib/api/types/datasources.types.js +272 -0
- package/lib/api/types/deployments.types.js +184 -0
- package/lib/api/types/environments.types.js +197 -0
- package/lib/api/types/external-systems.types.js +244 -0
- package/lib/api/types/pipeline.types.js +125 -0
- package/lib/app-list.js +5 -7
- package/lib/app-rotate-secret.js +4 -10
- package/lib/cli.js +30 -0
- package/lib/commands/login.js +41 -12
- package/lib/datasource-deploy.js +7 -30
- package/lib/datasource-list.js +9 -6
- package/lib/deployer.js +103 -135
- package/lib/environment-deploy.js +15 -26
- package/lib/external-system-deploy.js +12 -39
- package/lib/external-system-download.js +5 -13
- package/lib/external-system-test.js +9 -12
- package/lib/generator-split.js +342 -0
- package/lib/generator.js +94 -5
- package/lib/utils/app-register-api.js +5 -10
- package/lib/utils/deployment-errors.js +88 -6
- package/package.json +1 -1
- package/tatus +0 -181
|
@@ -14,7 +14,13 @@ const fsSync = require('fs');
|
|
|
14
14
|
const path = require('path');
|
|
15
15
|
const yaml = require('js-yaml');
|
|
16
16
|
const chalk = require('chalk');
|
|
17
|
-
const {
|
|
17
|
+
const {
|
|
18
|
+
deployExternalSystemViaPipeline,
|
|
19
|
+
deployDatasourceViaPipeline,
|
|
20
|
+
uploadApplicationViaPipeline,
|
|
21
|
+
validateUploadViaPipeline,
|
|
22
|
+
publishUploadViaPipeline
|
|
23
|
+
} = require('./api/pipeline.api');
|
|
18
24
|
const { getDeploymentAuth } = require('./utils/token-manager');
|
|
19
25
|
const { getConfig } = require('./config');
|
|
20
26
|
const logger = require('./utils/logger');
|
|
@@ -155,14 +161,7 @@ async function buildExternalSystem(appName, options = {}) {
|
|
|
155
161
|
const systemContent = await fs.readFile(systemFiles[0], 'utf8');
|
|
156
162
|
const systemJson = JSON.parse(systemContent);
|
|
157
163
|
|
|
158
|
-
const systemResponse = await
|
|
159
|
-
`${dataplaneUrl}/api/v1/pipeline/deploy`,
|
|
160
|
-
{
|
|
161
|
-
method: 'POST',
|
|
162
|
-
body: JSON.stringify(systemJson)
|
|
163
|
-
},
|
|
164
|
-
authConfig.token
|
|
165
|
-
);
|
|
164
|
+
const systemResponse = await deployExternalSystemViaPipeline(dataplaneUrl, authConfig, systemJson);
|
|
166
165
|
|
|
167
166
|
if (!systemResponse.success) {
|
|
168
167
|
throw new Error(`Failed to deploy external system: ${systemResponse.error || systemResponse.formattedError}`);
|
|
@@ -178,14 +177,7 @@ async function buildExternalSystem(appName, options = {}) {
|
|
|
178
177
|
const datasourceContent = await fs.readFile(datasourceFile, 'utf8');
|
|
179
178
|
const datasourceJson = JSON.parse(datasourceContent);
|
|
180
179
|
|
|
181
|
-
const datasourceResponse = await
|
|
182
|
-
`${dataplaneUrl}/api/v1/pipeline/${systemKey}/deploy`,
|
|
183
|
-
{
|
|
184
|
-
method: 'POST',
|
|
185
|
-
body: JSON.stringify(datasourceJson)
|
|
186
|
-
},
|
|
187
|
-
authConfig.token
|
|
188
|
-
);
|
|
180
|
+
const datasourceResponse = await deployDatasourceViaPipeline(dataplaneUrl, systemKey, authConfig, datasourceJson);
|
|
189
181
|
|
|
190
182
|
if (!datasourceResponse.success) {
|
|
191
183
|
throw new Error(`Failed to deploy datasource ${datasourceName}: ${datasourceResponse.error || datasourceResponse.formattedError}`);
|
|
@@ -245,14 +237,7 @@ async function deployExternalSystem(appName, options = {}) {
|
|
|
245
237
|
|
|
246
238
|
// Step 1: Upload application
|
|
247
239
|
logger.log(chalk.blue('📤 Uploading application configuration...'));
|
|
248
|
-
const uploadResponse = await
|
|
249
|
-
`${dataplaneUrl}/api/v1/pipeline/upload`,
|
|
250
|
-
{
|
|
251
|
-
method: 'POST',
|
|
252
|
-
body: JSON.stringify(applicationSchema)
|
|
253
|
-
},
|
|
254
|
-
authConfig.token
|
|
255
|
-
);
|
|
240
|
+
const uploadResponse = await uploadApplicationViaPipeline(dataplaneUrl, authConfig, applicationSchema);
|
|
256
241
|
|
|
257
242
|
if (!uploadResponse.success || !uploadResponse.data) {
|
|
258
243
|
throw new Error(`Failed to upload application: ${uploadResponse.error || uploadResponse.formattedError || 'Unknown error'}`);
|
|
@@ -270,13 +255,7 @@ async function deployExternalSystem(appName, options = {}) {
|
|
|
270
255
|
// Step 2: Validate upload (optional, can be skipped)
|
|
271
256
|
if (!options.skipValidation) {
|
|
272
257
|
logger.log(chalk.blue('🔍 Validating upload...'));
|
|
273
|
-
const validateResponse = await
|
|
274
|
-
`${dataplaneUrl}/api/v1/pipeline/upload/${uploadId}/validate`,
|
|
275
|
-
{
|
|
276
|
-
method: 'POST'
|
|
277
|
-
},
|
|
278
|
-
authConfig.token
|
|
279
|
-
);
|
|
258
|
+
const validateResponse = await validateUploadViaPipeline(dataplaneUrl, uploadId, authConfig);
|
|
280
259
|
|
|
281
260
|
if (!validateResponse.success || !validateResponse.data) {
|
|
282
261
|
throw new Error(`Validation failed: ${validateResponse.error || validateResponse.formattedError || 'Unknown error'}`);
|
|
@@ -308,13 +287,7 @@ async function deployExternalSystem(appName, options = {}) {
|
|
|
308
287
|
const generateMcpContract = options.generateMcpContract !== false; // Default to true
|
|
309
288
|
logger.log(chalk.blue(`📢 Publishing application (MCP contract: ${generateMcpContract ? 'enabled' : 'disabled'})...`));
|
|
310
289
|
|
|
311
|
-
const publishResponse = await
|
|
312
|
-
`${dataplaneUrl}/api/v1/pipeline/upload/${uploadId}/publish?generateMcpContract=${generateMcpContract}`,
|
|
313
|
-
{
|
|
314
|
-
method: 'POST'
|
|
315
|
-
},
|
|
316
|
-
authConfig.token
|
|
317
|
-
);
|
|
290
|
+
const publishResponse = await publishUploadViaPipeline(dataplaneUrl, uploadId, authConfig, { generateMcpContract });
|
|
318
291
|
|
|
319
292
|
if (!publishResponse.success || !publishResponse.data) {
|
|
320
293
|
throw new Error(`Failed to publish application: ${publishResponse.error || publishResponse.formattedError || 'Unknown error'}`);
|
|
@@ -14,7 +14,7 @@ const path = require('path');
|
|
|
14
14
|
const os = require('os');
|
|
15
15
|
const yaml = require('js-yaml');
|
|
16
16
|
const chalk = require('chalk');
|
|
17
|
-
const {
|
|
17
|
+
const { getExternalSystemConfig } = require('./api/external-systems.api');
|
|
18
18
|
const { getDeploymentAuth } = require('./utils/token-manager');
|
|
19
19
|
const { getDataplaneUrl } = require('./datasource-deploy');
|
|
20
20
|
const { getConfig } = require('./config');
|
|
@@ -284,14 +284,12 @@ async function downloadExternalSystem(systemKey, options = {}) {
|
|
|
284
284
|
const dataplaneUrl = await getDataplaneUrl(controllerUrl, systemKey, environment, authConfig);
|
|
285
285
|
logger.log(chalk.green(`✓ Dataplane URL: ${dataplaneUrl}`));
|
|
286
286
|
|
|
287
|
-
// Download system configuration
|
|
288
|
-
|
|
289
|
-
const downloadEndpoint = `${dataplaneUrl}/api/v1/external/systems/${systemKey}/config`;
|
|
290
|
-
logger.log(chalk.blue(`📡 Downloading from: ${downloadEndpoint}`));
|
|
287
|
+
// Download system configuration using centralized API client
|
|
288
|
+
logger.log(chalk.blue(`📡 Downloading system configuration: ${systemKey}`));
|
|
291
289
|
|
|
292
290
|
if (options.dryRun) {
|
|
293
291
|
logger.log(chalk.yellow('🔍 Dry run mode - would download from:'));
|
|
294
|
-
logger.log(chalk.gray(` ${
|
|
292
|
+
logger.log(chalk.gray(` ${dataplaneUrl}/api/v1/external/systems/${systemKey}/config`));
|
|
295
293
|
logger.log(chalk.yellow('\nWould create:'));
|
|
296
294
|
logger.log(chalk.gray(` integration/${systemKey}/`));
|
|
297
295
|
logger.log(chalk.gray(` integration/${systemKey}/variables.yaml`));
|
|
@@ -301,13 +299,7 @@ async function downloadExternalSystem(systemKey, options = {}) {
|
|
|
301
299
|
return;
|
|
302
300
|
}
|
|
303
301
|
|
|
304
|
-
const response = await
|
|
305
|
-
downloadEndpoint,
|
|
306
|
-
{
|
|
307
|
-
method: 'GET'
|
|
308
|
-
},
|
|
309
|
-
authConfig.token
|
|
310
|
-
);
|
|
302
|
+
const response = await getExternalSystemConfig(dataplaneUrl, systemKey, authConfig);
|
|
311
303
|
|
|
312
304
|
if (!response.success || !response.data) {
|
|
313
305
|
throw new Error(`Failed to download system configuration: ${response.error || response.formattedError || 'Unknown error'}`);
|
|
@@ -14,7 +14,7 @@ const fsSync = require('fs');
|
|
|
14
14
|
const path = require('path');
|
|
15
15
|
const yaml = require('js-yaml');
|
|
16
16
|
const chalk = require('chalk');
|
|
17
|
-
const {
|
|
17
|
+
const { testDatasourceViaPipeline } = require('./api/pipeline.api');
|
|
18
18
|
const { getDeploymentAuth } = require('./utils/token-manager');
|
|
19
19
|
const { getDataplaneUrl } = require('./datasource-deploy');
|
|
20
20
|
const { getConfig } = require('./config');
|
|
@@ -270,7 +270,7 @@ async function retryApiCall(fn, maxRetries = 3, backoffMs = 1000) {
|
|
|
270
270
|
}
|
|
271
271
|
|
|
272
272
|
/**
|
|
273
|
-
* Calls pipeline test endpoint
|
|
273
|
+
* Calls pipeline test endpoint using centralized API client
|
|
274
274
|
* @async
|
|
275
275
|
* @param {string} systemKey - System key
|
|
276
276
|
* @param {string} datasourceKey - Datasource key
|
|
@@ -281,17 +281,14 @@ async function retryApiCall(fn, maxRetries = 3, backoffMs = 1000) {
|
|
|
281
281
|
* @returns {Promise<Object>} Test response
|
|
282
282
|
*/
|
|
283
283
|
async function callPipelineTestEndpoint(systemKey, datasourceKey, payloadTemplate, dataplaneUrl, authConfig, timeout = 30000) {
|
|
284
|
-
const endpoint = `${dataplaneUrl}/api/v1/pipeline/${systemKey}/${datasourceKey}/test`;
|
|
285
|
-
|
|
286
284
|
const response = await retryApiCall(async() => {
|
|
287
|
-
return await
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
}
|
|
294
|
-
authConfig.token
|
|
285
|
+
return await testDatasourceViaPipeline(
|
|
286
|
+
dataplaneUrl,
|
|
287
|
+
systemKey,
|
|
288
|
+
datasourceKey,
|
|
289
|
+
authConfig,
|
|
290
|
+
{ payloadTemplate },
|
|
291
|
+
{ timeout }
|
|
295
292
|
);
|
|
296
293
|
});
|
|
297
294
|
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Fabrix Builder Deployment JSON Split Functions
|
|
3
|
+
*
|
|
4
|
+
* Helper functions for splitting deployment JSON files into component files
|
|
5
|
+
*
|
|
6
|
+
* @fileoverview Split functions for deployment JSON reverse conversion
|
|
7
|
+
* @author AI Fabrix Team
|
|
8
|
+
* @version 2.0.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs').promises;
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const yaml = require('js-yaml');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Converts configuration array back to env.template format
|
|
17
|
+
* @function extractEnvTemplate
|
|
18
|
+
* @param {Array} configuration - Configuration array from deployment JSON
|
|
19
|
+
* @returns {string} env.template content
|
|
20
|
+
*/
|
|
21
|
+
function extractEnvTemplate(configuration) {
|
|
22
|
+
if (!Array.isArray(configuration) || configuration.length === 0) {
|
|
23
|
+
return '';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const lines = [];
|
|
27
|
+
|
|
28
|
+
// Generate env.template lines
|
|
29
|
+
for (const config of configuration) {
|
|
30
|
+
if (!config.name || !config.value) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let value = config.value;
|
|
35
|
+
// Add kv:// prefix if location is keyvault
|
|
36
|
+
if (config.location === 'keyvault') {
|
|
37
|
+
value = `kv://${value}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
lines.push(`${config.name}=${value}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return lines.join('\n');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Parses image reference string into components
|
|
48
|
+
* @function parseImageReference
|
|
49
|
+
* @param {string} imageString - Full image string (e.g., "registry/name:tag")
|
|
50
|
+
* @returns {Object} Object with registry, name, and tag
|
|
51
|
+
*/
|
|
52
|
+
function parseImageReference(imageString) {
|
|
53
|
+
if (!imageString || typeof imageString !== 'string') {
|
|
54
|
+
return { registry: null, name: null, tag: 'latest' };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Handle format: registry/name:tag or name:tag or registry/name
|
|
58
|
+
const parts = imageString.split('/');
|
|
59
|
+
let registry = null;
|
|
60
|
+
let nameAndTag = imageString;
|
|
61
|
+
|
|
62
|
+
if (parts.length > 1) {
|
|
63
|
+
// Check if first part looks like a registry (contains .)
|
|
64
|
+
if (parts[0].includes('.')) {
|
|
65
|
+
registry = parts[0];
|
|
66
|
+
nameAndTag = parts.slice(1).join('/');
|
|
67
|
+
} else {
|
|
68
|
+
// No registry, just name:tag
|
|
69
|
+
nameAndTag = imageString;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Split name and tag
|
|
74
|
+
const tagIndex = nameAndTag.lastIndexOf(':');
|
|
75
|
+
let name = nameAndTag;
|
|
76
|
+
let tag = 'latest';
|
|
77
|
+
|
|
78
|
+
if (tagIndex !== -1) {
|
|
79
|
+
name = nameAndTag.substring(0, tagIndex);
|
|
80
|
+
tag = nameAndTag.substring(tagIndex + 1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { registry, name, tag };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Extracts deployment JSON into variables.yaml structure
|
|
88
|
+
* @function extractVariablesYaml
|
|
89
|
+
* @param {Object} deployment - Deployment JSON object
|
|
90
|
+
* @returns {Object} Variables YAML structure
|
|
91
|
+
*/
|
|
92
|
+
function extractVariablesYaml(deployment) {
|
|
93
|
+
if (!deployment || typeof deployment !== 'object') {
|
|
94
|
+
throw new Error('Deployment object is required');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const variables = {};
|
|
98
|
+
|
|
99
|
+
// Extract app information
|
|
100
|
+
if (deployment.key || deployment.displayName || deployment.description || deployment.type) {
|
|
101
|
+
variables.app = {};
|
|
102
|
+
if (deployment.key) variables.app.key = deployment.key;
|
|
103
|
+
if (deployment.displayName) variables.app.displayName = deployment.displayName;
|
|
104
|
+
if (deployment.description) variables.app.description = deployment.description;
|
|
105
|
+
if (deployment.type) variables.app.type = deployment.type;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Extract image information
|
|
109
|
+
if (deployment.image) {
|
|
110
|
+
const imageParts = parseImageReference(deployment.image);
|
|
111
|
+
variables.image = {};
|
|
112
|
+
if (imageParts.name) variables.image.name = imageParts.name;
|
|
113
|
+
if (imageParts.registry) variables.image.registry = imageParts.registry;
|
|
114
|
+
if (imageParts.tag) variables.image.tag = imageParts.tag;
|
|
115
|
+
if (deployment.registryMode) variables.image.registryMode = deployment.registryMode;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Extract port
|
|
119
|
+
if (deployment.port !== undefined) {
|
|
120
|
+
variables.port = deployment.port;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Extract requirements
|
|
124
|
+
if (deployment.requiresDatabase || deployment.requiresRedis || deployment.requiresStorage || deployment.databases) {
|
|
125
|
+
variables.requires = {};
|
|
126
|
+
if (deployment.requiresDatabase !== undefined) variables.requires.database = deployment.requiresDatabase;
|
|
127
|
+
if (deployment.requiresRedis !== undefined) variables.requires.redis = deployment.requiresRedis;
|
|
128
|
+
if (deployment.requiresStorage !== undefined) variables.requires.storage = deployment.requiresStorage;
|
|
129
|
+
if (deployment.databases) variables.requires.databases = deployment.databases;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Extract healthCheck
|
|
133
|
+
if (deployment.healthCheck) {
|
|
134
|
+
variables.healthCheck = deployment.healthCheck;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Extract authentication
|
|
138
|
+
if (deployment.authentication) {
|
|
139
|
+
variables.authentication = { ...deployment.authentication };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Extract build
|
|
143
|
+
if (deployment.build) {
|
|
144
|
+
variables.build = deployment.build;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Extract repository
|
|
148
|
+
if (deployment.repository) {
|
|
149
|
+
variables.repository = deployment.repository;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Extract deployment config
|
|
153
|
+
if (deployment.deployment) {
|
|
154
|
+
variables.deployment = deployment.deployment;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Extract other optional fields
|
|
158
|
+
if (deployment.startupCommand) {
|
|
159
|
+
variables.startupCommand = deployment.startupCommand;
|
|
160
|
+
}
|
|
161
|
+
if (deployment.runtimeVersion) {
|
|
162
|
+
variables.runtimeVersion = deployment.runtimeVersion;
|
|
163
|
+
}
|
|
164
|
+
if (deployment.scaling) {
|
|
165
|
+
variables.scaling = deployment.scaling;
|
|
166
|
+
}
|
|
167
|
+
if (deployment.frontDoorRouting) {
|
|
168
|
+
variables.frontDoorRouting = deployment.frontDoorRouting;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Extract roles and permissions (if present)
|
|
172
|
+
if (deployment.roles) {
|
|
173
|
+
variables.roles = deployment.roles;
|
|
174
|
+
}
|
|
175
|
+
if (deployment.permissions) {
|
|
176
|
+
variables.permissions = deployment.permissions;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return variables;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Extracts roles and permissions from deployment JSON
|
|
184
|
+
* @function extractRbacYaml
|
|
185
|
+
* @param {Object} deployment - Deployment JSON object
|
|
186
|
+
* @returns {Object|null} RBAC YAML structure or null if no roles/permissions
|
|
187
|
+
*/
|
|
188
|
+
function extractRbacYaml(deployment) {
|
|
189
|
+
if (!deployment || typeof deployment !== 'object') {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const hasRoles = deployment.roles && Array.isArray(deployment.roles) && deployment.roles.length > 0;
|
|
194
|
+
const hasPermissions = deployment.permissions && Array.isArray(deployment.permissions) && deployment.permissions.length > 0;
|
|
195
|
+
|
|
196
|
+
if (!hasRoles && !hasPermissions) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const rbac = {};
|
|
201
|
+
if (hasRoles) {
|
|
202
|
+
rbac.roles = deployment.roles;
|
|
203
|
+
}
|
|
204
|
+
if (hasPermissions) {
|
|
205
|
+
rbac.permissions = deployment.permissions;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return rbac;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Generates README.md content from deployment JSON
|
|
213
|
+
* @function generateReadmeFromDeployJson
|
|
214
|
+
* @param {Object} deployment - Deployment JSON object
|
|
215
|
+
* @returns {string} README.md content
|
|
216
|
+
*/
|
|
217
|
+
function generateReadmeFromDeployJson(deployment) {
|
|
218
|
+
if (!deployment || typeof deployment !== 'object') {
|
|
219
|
+
throw new Error('Deployment object is required');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const appName = deployment.key || 'application';
|
|
223
|
+
const displayName = deployment.displayName || appName;
|
|
224
|
+
const description = deployment.description || 'Application deployment configuration';
|
|
225
|
+
const port = deployment.port || 3000;
|
|
226
|
+
const image = deployment.image || 'unknown';
|
|
227
|
+
|
|
228
|
+
const lines = [
|
|
229
|
+
`# ${displayName}`,
|
|
230
|
+
'',
|
|
231
|
+
description,
|
|
232
|
+
'',
|
|
233
|
+
'## Quick Start',
|
|
234
|
+
'',
|
|
235
|
+
'This application is configured via deployment JSON and component files.',
|
|
236
|
+
'',
|
|
237
|
+
'## Configuration',
|
|
238
|
+
'',
|
|
239
|
+
`- **Application Key**: \`${appName}\``,
|
|
240
|
+
`- **Port**: \`${port}\``,
|
|
241
|
+
`- **Image**: \`${image}\``,
|
|
242
|
+
'',
|
|
243
|
+
'## Files',
|
|
244
|
+
'',
|
|
245
|
+
'- `variables.yaml` - Application configuration',
|
|
246
|
+
'- `env.template` - Environment variables template',
|
|
247
|
+
'- `rbac.yml` - Roles and permissions (if applicable)',
|
|
248
|
+
'- `README.md` - This file',
|
|
249
|
+
'',
|
|
250
|
+
'## Documentation',
|
|
251
|
+
'',
|
|
252
|
+
'For more information, see the [AI Fabrix Builder documentation](../../docs/README.md).'
|
|
253
|
+
];
|
|
254
|
+
|
|
255
|
+
return lines.join('\n');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Splits a deployment JSON file into component files
|
|
260
|
+
* @async
|
|
261
|
+
* @function splitDeployJson
|
|
262
|
+
* @param {string} deployJsonPath - Path to deployment JSON file
|
|
263
|
+
* @param {string} [outputDir] - Directory to write component files (defaults to same directory as JSON)
|
|
264
|
+
* @returns {Promise<Object>} Object with paths to generated files
|
|
265
|
+
* @throws {Error} If JSON file not found or invalid
|
|
266
|
+
*/
|
|
267
|
+
async function splitDeployJson(deployJsonPath, outputDir = null) {
|
|
268
|
+
if (!deployJsonPath || typeof deployJsonPath !== 'string') {
|
|
269
|
+
throw new Error('Deployment JSON path is required and must be a string');
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Validate file exists
|
|
273
|
+
const fsSync = require('fs');
|
|
274
|
+
if (!fsSync.existsSync(deployJsonPath)) {
|
|
275
|
+
throw new Error(`Deployment JSON file not found: ${deployJsonPath}`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Determine output directory
|
|
279
|
+
const finalOutputDir = outputDir || path.dirname(deployJsonPath);
|
|
280
|
+
|
|
281
|
+
// Ensure output directory exists
|
|
282
|
+
if (!fsSync.existsSync(finalOutputDir)) {
|
|
283
|
+
await fs.mkdir(finalOutputDir, { recursive: true });
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Load and parse deployment JSON
|
|
287
|
+
let deployment;
|
|
288
|
+
try {
|
|
289
|
+
const jsonContent = await fs.readFile(deployJsonPath, 'utf8');
|
|
290
|
+
deployment = JSON.parse(jsonContent);
|
|
291
|
+
} catch (error) {
|
|
292
|
+
if (error.code === 'ENOENT') {
|
|
293
|
+
throw new Error(`Deployment JSON file not found: ${deployJsonPath}`);
|
|
294
|
+
}
|
|
295
|
+
throw new Error(`Invalid JSON syntax in deployment file: ${error.message}`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Extract components
|
|
299
|
+
const envTemplate = extractEnvTemplate(deployment.configuration || []);
|
|
300
|
+
const variables = extractVariablesYaml(deployment);
|
|
301
|
+
const rbac = extractRbacYaml(deployment);
|
|
302
|
+
const readme = generateReadmeFromDeployJson(deployment);
|
|
303
|
+
|
|
304
|
+
// Write component files
|
|
305
|
+
const results = {};
|
|
306
|
+
|
|
307
|
+
// Write env.template
|
|
308
|
+
const envTemplatePath = path.join(finalOutputDir, 'env.template');
|
|
309
|
+
await fs.writeFile(envTemplatePath, envTemplate, { mode: 0o644, encoding: 'utf8' });
|
|
310
|
+
results.envTemplate = envTemplatePath;
|
|
311
|
+
|
|
312
|
+
// Write variables.yaml
|
|
313
|
+
const variablesPath = path.join(finalOutputDir, 'variables.yaml');
|
|
314
|
+
const variablesYaml = yaml.dump(variables, { indent: 2, lineWidth: -1 });
|
|
315
|
+
await fs.writeFile(variablesPath, variablesYaml, { mode: 0o644, encoding: 'utf8' });
|
|
316
|
+
results.variables = variablesPath;
|
|
317
|
+
|
|
318
|
+
// Write rbac.yml (only if roles/permissions exist)
|
|
319
|
+
if (rbac) {
|
|
320
|
+
const rbacPath = path.join(finalOutputDir, 'rbac.yml');
|
|
321
|
+
const rbacYaml = yaml.dump(rbac, { indent: 2, lineWidth: -1 });
|
|
322
|
+
await fs.writeFile(rbacPath, rbacYaml, { mode: 0o644, encoding: 'utf8' });
|
|
323
|
+
results.rbac = rbacPath;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Write README.md
|
|
327
|
+
const readmePath = path.join(finalOutputDir, 'README.md');
|
|
328
|
+
await fs.writeFile(readmePath, readme, { mode: 0o644, encoding: 'utf8' });
|
|
329
|
+
results.readme = readmePath;
|
|
330
|
+
|
|
331
|
+
return results;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
module.exports = {
|
|
335
|
+
splitDeployJson,
|
|
336
|
+
extractEnvTemplate,
|
|
337
|
+
extractVariablesYaml,
|
|
338
|
+
extractRbacYaml,
|
|
339
|
+
parseImageReference,
|
|
340
|
+
generateReadmeFromDeployJson
|
|
341
|
+
};
|
|
342
|
+
|
package/lib/generator.js
CHANGED
|
@@ -18,6 +18,7 @@ const _keyGenerator = require('./key-generator');
|
|
|
18
18
|
const _validator = require('./validator');
|
|
19
19
|
const builders = require('./generator-builders');
|
|
20
20
|
const { detectAppType, getDeployJsonPath } = require('./utils/paths');
|
|
21
|
+
const splitFunctions = require('./generator-split');
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Loads variables.yaml file
|
|
@@ -161,8 +162,8 @@ async function generateDeployJson(appName) {
|
|
|
161
162
|
const envTemplate = loadEnvTemplate(templatePath);
|
|
162
163
|
const rbac = loadRbac(rbacPath);
|
|
163
164
|
|
|
164
|
-
// Parse environment variables from template
|
|
165
|
-
const configuration = parseEnvironmentVariables(envTemplate);
|
|
165
|
+
// Parse environment variables from template and merge portalInput from variables.yaml
|
|
166
|
+
const configuration = parseEnvironmentVariables(envTemplate, variables);
|
|
166
167
|
|
|
167
168
|
// Build deployment manifest WITHOUT deploymentKey initially
|
|
168
169
|
const deployment = builders.buildManifestStructure(appName, variables, null, configuration, rbac);
|
|
@@ -187,10 +188,85 @@ async function generateDeployJson(appName) {
|
|
|
187
188
|
return jsonPath;
|
|
188
189
|
}
|
|
189
190
|
|
|
190
|
-
|
|
191
|
+
/**
|
|
192
|
+
* Validates portalInput structure against schema requirements
|
|
193
|
+
* @param {Object} portalInput - Portal input configuration to validate
|
|
194
|
+
* @param {string} variableName - Variable name for error messages
|
|
195
|
+
* @throws {Error} If portalInput structure is invalid
|
|
196
|
+
*/
|
|
197
|
+
function validatePortalInput(portalInput, variableName) {
|
|
198
|
+
if (!portalInput || typeof portalInput !== 'object') {
|
|
199
|
+
throw new Error(`Invalid portalInput for variable '${variableName}': must be an object`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Check required fields
|
|
203
|
+
if (!portalInput.field || typeof portalInput.field !== 'string') {
|
|
204
|
+
throw new Error(`Invalid portalInput for variable '${variableName}': field is required and must be a string`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (!portalInput.label || typeof portalInput.label !== 'string') {
|
|
208
|
+
throw new Error(`Invalid portalInput for variable '${variableName}': label is required and must be a string`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Validate field type
|
|
212
|
+
const validFieldTypes = ['password', 'text', 'textarea', 'select'];
|
|
213
|
+
if (!validFieldTypes.includes(portalInput.field)) {
|
|
214
|
+
throw new Error(`Invalid portalInput for variable '${variableName}': field must be one of: ${validFieldTypes.join(', ')}`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Validate select field requires options
|
|
218
|
+
if (portalInput.field === 'select') {
|
|
219
|
+
if (!portalInput.options || !Array.isArray(portalInput.options) || portalInput.options.length === 0) {
|
|
220
|
+
throw new Error(`Invalid portalInput for variable '${variableName}': select field requires a non-empty options array`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Validate optional fields
|
|
225
|
+
if (portalInput.placeholder !== undefined && typeof portalInput.placeholder !== 'string') {
|
|
226
|
+
throw new Error(`Invalid portalInput for variable '${variableName}': placeholder must be a string`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (portalInput.masked !== undefined && typeof portalInput.masked !== 'boolean') {
|
|
230
|
+
throw new Error(`Invalid portalInput for variable '${variableName}': masked must be a boolean`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (portalInput.validation !== undefined) {
|
|
234
|
+
if (typeof portalInput.validation !== 'object' || Array.isArray(portalInput.validation)) {
|
|
235
|
+
throw new Error(`Invalid portalInput for variable '${variableName}': validation must be an object`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (portalInput.options !== undefined && portalInput.field !== 'select') {
|
|
240
|
+
// Options should only be present for select fields
|
|
241
|
+
if (Array.isArray(portalInput.options) && portalInput.options.length > 0) {
|
|
242
|
+
throw new Error(`Invalid portalInput for variable '${variableName}': options can only be used with select field type`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Parses environment variables from env.template and merges portalInput from variables.yaml
|
|
249
|
+
* @param {string} envTemplate - Content of env.template file
|
|
250
|
+
* @param {Object|null} [variablesConfig=null] - Optional configuration from variables.yaml
|
|
251
|
+
* @returns {Array<Object>} Configuration array with merged portalInput
|
|
252
|
+
* @throws {Error} If portalInput structure is invalid
|
|
253
|
+
*/
|
|
254
|
+
function parseEnvironmentVariables(envTemplate, variablesConfig = null) {
|
|
191
255
|
const configuration = [];
|
|
192
256
|
const lines = envTemplate.split('\n');
|
|
193
257
|
|
|
258
|
+
// Create a map of portalInput configurations by variable name
|
|
259
|
+
const portalInputMap = new Map();
|
|
260
|
+
if (variablesConfig && variablesConfig.configuration && Array.isArray(variablesConfig.configuration)) {
|
|
261
|
+
for (const configItem of variablesConfig.configuration) {
|
|
262
|
+
if (configItem.name && configItem.portalInput) {
|
|
263
|
+
// Validate portalInput before adding to map
|
|
264
|
+
validatePortalInput(configItem.portalInput, configItem.name);
|
|
265
|
+
portalInputMap.set(configItem.name, configItem.portalInput);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
194
270
|
for (const line of lines) {
|
|
195
271
|
const trimmed = line.trim();
|
|
196
272
|
|
|
@@ -227,12 +303,19 @@ function parseEnvironmentVariables(envTemplate) {
|
|
|
227
303
|
required = true;
|
|
228
304
|
}
|
|
229
305
|
|
|
230
|
-
|
|
306
|
+
const configItem = {
|
|
231
307
|
name: key,
|
|
232
308
|
value: value.replace('kv://', ''), // Remove kv:// prefix for KeyVault
|
|
233
309
|
location,
|
|
234
310
|
required
|
|
235
|
-
}
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
// Merge portalInput if it exists in variables.yaml
|
|
314
|
+
if (portalInputMap.has(key)) {
|
|
315
|
+
configItem.portalInput = portalInputMap.get(key);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
configuration.push(configItem);
|
|
236
319
|
}
|
|
237
320
|
|
|
238
321
|
return configuration;
|
|
@@ -399,6 +482,12 @@ module.exports = {
|
|
|
399
482
|
generateDeployJsonWithValidation,
|
|
400
483
|
generateExternalSystemApplicationSchema,
|
|
401
484
|
parseEnvironmentVariables,
|
|
485
|
+
splitDeployJson: splitFunctions.splitDeployJson,
|
|
486
|
+
extractEnvTemplate: splitFunctions.extractEnvTemplate,
|
|
487
|
+
extractVariablesYaml: splitFunctions.extractVariablesYaml,
|
|
488
|
+
extractRbacYaml: splitFunctions.extractRbacYaml,
|
|
489
|
+
parseImageReference: splitFunctions.parseImageReference,
|
|
490
|
+
generateReadmeFromDeployJson: splitFunctions.generateReadmeFromDeployJson,
|
|
402
491
|
buildImageReference: builders.buildImageReference,
|
|
403
492
|
buildHealthCheck: builders.buildHealthCheck,
|
|
404
493
|
buildRequirements: builders.buildRequirements,
|