@aifabrix/builder 2.20.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/README.md +2 -2
- package/lib/api/auth.api.js +13 -14
- package/lib/api/index.js +16 -1
- package/lib/cli.js +30 -0
- package/lib/commands/login.js +22 -0
- package/lib/generator-split.js +342 -0
- package/lib/generator.js +94 -5
- package/package.json +1 -1
- package/tatus +0 -181
package/README.md
CHANGED
package/lib/api/auth.api.js
CHANGED
|
@@ -95,29 +95,28 @@ async function getAuthLogin(controllerUrl, redirect, state, authConfig) {
|
|
|
95
95
|
async function initiateDeviceCodeFlow(controllerUrl, environment, scope) {
|
|
96
96
|
const client = new ApiClient(controllerUrl);
|
|
97
97
|
const body = {};
|
|
98
|
+
const params = {};
|
|
98
99
|
|
|
100
|
+
// Environment goes in query params (per OpenAPI spec)
|
|
99
101
|
if (environment) {
|
|
100
|
-
|
|
102
|
+
params.environment = environment;
|
|
101
103
|
}
|
|
104
|
+
|
|
105
|
+
// Scope goes in request body
|
|
102
106
|
if (scope) {
|
|
103
107
|
body.scope = scope;
|
|
104
108
|
}
|
|
105
109
|
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
params
|
|
114
|
-
});
|
|
110
|
+
// Build request options
|
|
111
|
+
const requestOptions = {};
|
|
112
|
+
if (Object.keys(params).length > 0) {
|
|
113
|
+
requestOptions.params = params;
|
|
114
|
+
}
|
|
115
|
+
if (Object.keys(body).length > 0) {
|
|
116
|
+
requestOptions.body = body;
|
|
115
117
|
}
|
|
116
118
|
|
|
117
|
-
|
|
118
|
-
return await client.post('/api/v1/auth/login', {
|
|
119
|
-
body: Object.keys(body).length > 0 ? body : undefined
|
|
120
|
-
});
|
|
119
|
+
return await client.post('/api/v1/auth/login', requestOptions);
|
|
121
120
|
}
|
|
122
121
|
|
|
123
122
|
/**
|
package/lib/api/index.js
CHANGED
|
@@ -110,12 +110,27 @@ class ApiClient {
|
|
|
110
110
|
* @param {Object} [options] - Request options
|
|
111
111
|
* @param {Object} [options.body] - Request body (will be JSON stringified)
|
|
112
112
|
* @param {Object} [options.headers] - Additional headers
|
|
113
|
+
* @param {Object} [options.params] - Query parameters (will be converted to query string)
|
|
113
114
|
* @returns {Promise<Object>} API response
|
|
114
115
|
*/
|
|
115
116
|
async post(endpoint, options = {}) {
|
|
116
|
-
|
|
117
|
+
let url = this._buildUrl(endpoint);
|
|
117
118
|
const headers = this._buildHeaders(options.headers);
|
|
118
119
|
|
|
120
|
+
// Add query parameters if provided
|
|
121
|
+
if (options.params) {
|
|
122
|
+
const params = new URLSearchParams();
|
|
123
|
+
Object.entries(options.params).forEach(([key, value]) => {
|
|
124
|
+
if (value !== undefined && value !== null) {
|
|
125
|
+
params.append(key, String(value));
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
const queryString = params.toString();
|
|
129
|
+
if (queryString) {
|
|
130
|
+
url += `?${queryString}`;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
119
134
|
const requestOptions = {
|
|
120
135
|
method: 'POST',
|
|
121
136
|
headers
|
package/lib/cli.js
CHANGED
|
@@ -340,6 +340,36 @@ function setupCommands(program) {
|
|
|
340
340
|
}
|
|
341
341
|
});
|
|
342
342
|
|
|
343
|
+
program.command('app split-json <app-name>')
|
|
344
|
+
.description('Split deployment JSON into component files (env.template, variables.yaml, rbac.yml, README.md)')
|
|
345
|
+
.option('-o, --output <dir>', 'Output directory for component files (defaults to same directory as JSON)')
|
|
346
|
+
.action(async(appName, options) => {
|
|
347
|
+
try {
|
|
348
|
+
const fs = require('fs');
|
|
349
|
+
const { detectAppType, getDeployJsonPath } = require('./utils/paths');
|
|
350
|
+
const { appPath, appType } = await detectAppType(appName);
|
|
351
|
+
const deployJsonPath = getDeployJsonPath(appName, appType, true);
|
|
352
|
+
|
|
353
|
+
if (!fs.existsSync(deployJsonPath)) {
|
|
354
|
+
throw new Error(`Deployment JSON file not found: ${deployJsonPath}`);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const outputDir = options.output || appPath;
|
|
358
|
+
const result = await generator.splitDeployJson(deployJsonPath, outputDir);
|
|
359
|
+
|
|
360
|
+
logger.log(chalk.green('\n✓ Successfully split deployment JSON into component files:'));
|
|
361
|
+
logger.log(` • env.template: ${result.envTemplate}`);
|
|
362
|
+
logger.log(` • variables.yaml: ${result.variables}`);
|
|
363
|
+
if (result.rbac) {
|
|
364
|
+
logger.log(` • rbac.yml: ${result.rbac}`);
|
|
365
|
+
}
|
|
366
|
+
logger.log(` • README.md: ${result.readme}`);
|
|
367
|
+
} catch (error) {
|
|
368
|
+
handleCommandError(error, 'app split-json');
|
|
369
|
+
process.exit(1);
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
|
|
343
373
|
program.command('json <app>')
|
|
344
374
|
.description('Generate deployment JSON (aifabrix-deploy.json for normal apps, application-schema.json for external systems)')
|
|
345
375
|
.action(async(appName) => {
|
package/lib/commands/login.js
CHANGED
|
@@ -342,7 +342,29 @@ async function handleDeviceCodeLogin(controllerUrl, environment, offline, scope)
|
|
|
342
342
|
// Use centralized API client for device code flow initiation
|
|
343
343
|
const deviceCodeApiResponse = await initiateDeviceCodeFlow(controllerUrl, envKey, requestScope);
|
|
344
344
|
|
|
345
|
+
// Validate response structure
|
|
346
|
+
if (!deviceCodeApiResponse) {
|
|
347
|
+
throw new Error('Device code flow initiation returned no response');
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Check if API call was successful
|
|
351
|
+
if (!deviceCodeApiResponse.success) {
|
|
352
|
+
const errorMessage = deviceCodeApiResponse.formattedError ||
|
|
353
|
+
deviceCodeApiResponse.error ||
|
|
354
|
+
'Device code flow initiation failed';
|
|
355
|
+
const error = new Error(errorMessage);
|
|
356
|
+
// Preserve formattedError for better error display
|
|
357
|
+
if (deviceCodeApiResponse.formattedError) {
|
|
358
|
+
error.formattedError = deviceCodeApiResponse.formattedError;
|
|
359
|
+
}
|
|
360
|
+
throw error;
|
|
361
|
+
}
|
|
362
|
+
|
|
345
363
|
// Handle API response format: { success: boolean, data: DeviceCodeResponse }
|
|
364
|
+
if (!deviceCodeApiResponse.data) {
|
|
365
|
+
throw new Error('Device code flow initiation returned no data');
|
|
366
|
+
}
|
|
367
|
+
|
|
346
368
|
const apiResponse = deviceCodeApiResponse.data;
|
|
347
369
|
const deviceCodeData = apiResponse.data || apiResponse;
|
|
348
370
|
|
|
@@ -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,
|
package/package.json
CHANGED
package/tatus
DELETED
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
SSUUMMMMAARRYY OOFF LLEESSSS CCOOMMMMAANNDDSS
|
|
3
|
-
|
|
4
|
-
Commands marked with * may be preceded by a number, _N.
|
|
5
|
-
Notes in parentheses indicate the behavior if _N is given.
|
|
6
|
-
A key preceded by a caret indicates the Ctrl key; thus ^K is ctrl-K.
|
|
7
|
-
|
|
8
|
-
h H Display this help.
|
|
9
|
-
q :q Q :Q ZZ Exit.
|
|
10
|
-
---------------------------------------------------------------------------
|
|
11
|
-
|
|
12
|
-
MMOOVVIINNGG
|
|
13
|
-
|
|
14
|
-
e ^E j ^N CR * Forward one line (or _N lines).
|
|
15
|
-
y ^Y k ^K ^P * Backward one line (or _N lines).
|
|
16
|
-
f ^F ^V SPACE * Forward one window (or _N lines).
|
|
17
|
-
b ^B ESC-v * Backward one window (or _N lines).
|
|
18
|
-
z * Forward one window (and set window to _N).
|
|
19
|
-
w * Backward one window (and set window to _N).
|
|
20
|
-
ESC-SPACE * Forward one window, but don't stop at end-of-file.
|
|
21
|
-
d ^D * Forward one half-window (and set half-window to _N).
|
|
22
|
-
u ^U * Backward one half-window (and set half-window to _N).
|
|
23
|
-
ESC-) RightArrow * Right one half screen width (or _N positions).
|
|
24
|
-
ESC-( LeftArrow * Left one half screen width (or _N positions).
|
|
25
|
-
ESC-} ^RightArrow Right to last column displayed.
|
|
26
|
-
ESC-{ ^LeftArrow Left to first column.
|
|
27
|
-
F Forward forever; like "tail -f".
|
|
28
|
-
ESC-F Like F but stop when search pattern is found.
|
|
29
|
-
r ^R ^L Repaint screen.
|
|
30
|
-
R Repaint screen, discarding buffered input.
|
|
31
|
-
---------------------------------------------------
|
|
32
|
-
Default "window" is the screen height.
|
|
33
|
-
Default "half-window" is half of the screen height.
|
|
34
|
-
---------------------------------------------------------------------------
|
|
35
|
-
|
|
36
|
-
SSEEAARRCCHHIINNGG
|
|
37
|
-
|
|
38
|
-
/_p_a_t_t_e_r_n * Search forward for (_N-th) matching line.
|
|
39
|
-
?_p_a_t_t_e_r_n * Search backward for (_N-th) matching line.
|
|
40
|
-
n * Repeat previous search (for _N-th occurrence).
|
|
41
|
-
N * Repeat previous search in reverse direction.
|
|
42
|
-
ESC-n * Repeat previous search, spanning files.
|
|
43
|
-
ESC-N * Repeat previous search, reverse dir. & spanning files.
|
|
44
|
-
^O^N ^On * Search forward for (_N-th) OSC8 hyperlink.
|
|
45
|
-
^O^P ^Op * Search backward for (_N-th) OSC8 hyperlink.
|
|
46
|
-
^O^L ^Ol Jump to the currently selected OSC8 hyperlink.
|
|
47
|
-
ESC-u Undo (toggle) search highlighting.
|
|
48
|
-
ESC-U Clear search highlighting.
|
|
49
|
-
&_p_a_t_t_e_r_n * Display only matching lines.
|
|
50
|
-
---------------------------------------------------
|
|
51
|
-
A search pattern may begin with one or more of:
|
|
52
|
-
^N or ! Search for NON-matching lines.
|
|
53
|
-
^E or * Search multiple files (pass thru END OF FILE).
|
|
54
|
-
^F or @ Start search at FIRST file (for /) or last file (for ?).
|
|
55
|
-
^K Highlight matches, but don't move (KEEP position).
|
|
56
|
-
^R Don't use REGULAR EXPRESSIONS.
|
|
57
|
-
^S _n Search for match in _n-th parenthesized subpattern.
|
|
58
|
-
^W WRAP search if no match found.
|
|
59
|
-
^L Enter next character literally into pattern.
|
|
60
|
-
---------------------------------------------------------------------------
|
|
61
|
-
|
|
62
|
-
JJUUMMPPIINNGG
|
|
63
|
-
|
|
64
|
-
g < ESC-< * Go to first line in file (or line _N).
|
|
65
|
-
G > ESC-> * Go to last line in file (or line _N).
|
|
66
|
-
p % * Go to beginning of file (or _N percent into file).
|
|
67
|
-
t * Go to the (_N-th) next tag.
|
|
68
|
-
T * Go to the (_N-th) previous tag.
|
|
69
|
-
{ ( [ * Find close bracket } ) ].
|
|
70
|
-
} ) ] * Find open bracket { ( [.
|
|
71
|
-
ESC-^F _<_c_1_> _<_c_2_> * Find close bracket _<_c_2_>.
|
|
72
|
-
ESC-^B _<_c_1_> _<_c_2_> * Find open bracket _<_c_1_>.
|
|
73
|
-
---------------------------------------------------
|
|
74
|
-
Each "find close bracket" command goes forward to the close bracket
|
|
75
|
-
matching the (_N-th) open bracket in the top line.
|
|
76
|
-
Each "find open bracket" command goes backward to the open bracket
|
|
77
|
-
matching the (_N-th) close bracket in the bottom line.
|
|
78
|
-
|
|
79
|
-
m_<_l_e_t_t_e_r_> Mark the current top line with <letter>.
|
|
80
|
-
M_<_l_e_t_t_e_r_> Mark the current bottom line with <letter>.
|
|
81
|
-
'_<_l_e_t_t_e_r_> Go to a previously marked position.
|
|
82
|
-
'' Go to the previous position.
|
|
83
|
-
^X^X Same as '.
|
|
84
|
-
ESC-m_<_l_e_t_t_e_r_> Clear a mark.
|
|
85
|
-
---------------------------------------------------
|
|
86
|
-
A mark is any upper-case or lower-case letter.
|
|
87
|
-
Certain marks are predefined:
|
|
88
|
-
^ means beginning of the file
|
|
89
|
-
$ means end of the file
|
|
90
|
-
---------------------------------------------------------------------------
|
|
91
|
-
|
|
92
|
-
CCHHAANNGGIINNGG FFIILLEESS
|
|
93
|
-
|
|
94
|
-
:e [_f_i_l_e] Examine a new file.
|
|
95
|
-
^X^V Same as :e.
|
|
96
|
-
:n * Examine the (_N-th) next file from the command line.
|
|
97
|
-
:p * Examine the (_N-th) previous file from the command line.
|
|
98
|
-
:x * Examine the first (or _N-th) file from the command line.
|
|
99
|
-
^O^O Open the currently selected OSC8 hyperlink.
|
|
100
|
-
:d Delete the current file from the command line list.
|
|
101
|
-
= ^G :f Print current file name.
|
|
102
|
-
---------------------------------------------------------------------------
|
|
103
|
-
|
|
104
|
-
MMIISSCCEELLLLAANNEEOOUUSS CCOOMMMMAANNDDSS
|
|
105
|
-
|
|
106
|
-
-_<_f_l_a_g_> Toggle a command line option [see OPTIONS below].
|
|
107
|
-
--_<_n_a_m_e_> Toggle a command line option, by name.
|
|
108
|
-
__<_f_l_a_g_> Display the setting of a command line option.
|
|
109
|
-
___<_n_a_m_e_> Display the setting of an option, by name.
|
|
110
|
-
+_c_m_d Execute the less cmd each time a new file is examined.
|
|
111
|
-
|
|
112
|
-
!_c_o_m_m_a_n_d Execute the shell command with $SHELL.
|
|
113
|
-
#_c_o_m_m_a_n_d Execute the shell command, expanded like a prompt.
|
|
114
|
-
|XX_c_o_m_m_a_n_d Pipe file between current pos & mark XX to shell command.
|
|
115
|
-
s _f_i_l_e Save input to a file.
|
|
116
|
-
v Edit the current file with $VISUAL or $EDITOR.
|
|
117
|
-
V Print version number of "less".
|
|
118
|
-
---------------------------------------------------------------------------
|
|
119
|
-
|
|
120
|
-
OOPPTTIIOONNSS
|
|
121
|
-
|
|
122
|
-
Most options may be changed either on the command line,
|
|
123
|
-
or from within less by using the - or -- command.
|
|
124
|
-
Options may be given in one of two forms: either a single
|
|
125
|
-
character preceded by a -, or a name preceded by --.
|
|
126
|
-
|
|
127
|
-
-? ........ --help
|
|
128
|
-
Display help (from command line).
|
|
129
|
-
-a ........ --search-skip-screen
|
|
130
|
-
Search skips current screen.
|
|
131
|
-
-A ........ --SEARCH-SKIP-SCREEN
|
|
132
|
-
Search starts just after target line.
|
|
133
|
-
-b [_N] .... --buffers=[_N]
|
|
134
|
-
Number of buffers.
|
|
135
|
-
-B ........ --auto-buffers
|
|
136
|
-
Don't automatically allocate buffers for pipes.
|
|
137
|
-
-c ........ --clear-screen
|
|
138
|
-
Repaint by clearing rather than scrolling.
|
|
139
|
-
-d ........ --dumb
|
|
140
|
-
Dumb terminal.
|
|
141
|
-
-D xx_c_o_l_o_r . --color=xx_c_o_l_o_r
|
|
142
|
-
Set screen colors.
|
|
143
|
-
-e -E .... --quit-at-eof --QUIT-AT-EOF
|
|
144
|
-
Quit at end of file.
|
|
145
|
-
-f ........ --force
|
|
146
|
-
Force open non-regular files.
|
|
147
|
-
-F ........ --quit-if-one-screen
|
|
148
|
-
Quit if entire file fits on first screen.
|
|
149
|
-
-g ........ --hilite-search
|
|
150
|
-
Highlight only last match for searches.
|
|
151
|
-
-G ........ --HILITE-SEARCH
|
|
152
|
-
Don't highlight any matches for searches.
|
|
153
|
-
-h [_N] .... --max-back-scroll=[_N]
|
|
154
|
-
Backward scroll limit.
|
|
155
|
-
-i ........ --ignore-case
|
|
156
|
-
Ignore case in searches that do not contain uppercase.
|
|
157
|
-
-I ........ --IGNORE-CASE
|
|
158
|
-
Ignore case in all searches.
|
|
159
|
-
-j [_N] .... --jump-target=[_N]
|
|
160
|
-
Screen position of target lines.
|
|
161
|
-
-J ........ --status-column
|
|
162
|
-
Display a status column at left edge of screen.
|
|
163
|
-
-k _f_i_l_e ... --lesskey-file=_f_i_l_e
|
|
164
|
-
Use a compiled lesskey file.
|
|
165
|
-
-K ........ --quit-on-intr
|
|
166
|
-
Exit less in response to ctrl-C.
|
|
167
|
-
-L ........ --no-lessopen
|
|
168
|
-
Ignore the LESSOPEN environment variable.
|
|
169
|
-
-m -M .... --long-prompt --LONG-PROMPT
|
|
170
|
-
Set prompt style.
|
|
171
|
-
-n ......... --line-numbers
|
|
172
|
-
Suppress line numbers in prompts and messages.
|
|
173
|
-
-N ......... --LINE-NUMBERS
|
|
174
|
-
Display line number at start of each line.
|
|
175
|
-
-o [_f_i_l_e] .. --log-file=[_f_i_l_e]
|
|
176
|
-
Copy to log file (standard input only).
|
|
177
|
-
-O [_f_i_l_e] .. --LOG-FILE=[_f_i_l_e]
|
|
178
|
-
Copy to log file (unconditionally overwrite).
|
|
179
|
-
-p _p_a_t_t_e_r_n . --pattern=[_p_a_t_t_e_r_n]
|
|
180
|
-
Start at pattern (from command line).
|
|
181
|
-
-P [_p_r_o_m_p_t] --prompt=[_p_r_o_m_p_t]
|