@aifabrix/builder 2.1.6 ā 2.2.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/lib/app-deploy.js +73 -29
- package/lib/app-list.js +132 -0
- package/lib/app-readme.js +11 -4
- package/lib/app-register.js +435 -0
- package/lib/app-rotate-secret.js +164 -0
- package/lib/app-run.js +98 -84
- package/lib/app.js +13 -0
- package/lib/audit-logger.js +195 -15
- package/lib/build.js +57 -37
- package/lib/cli.js +90 -8
- package/lib/commands/app.js +8 -391
- package/lib/commands/login.js +130 -36
- package/lib/config.js +257 -4
- package/lib/deployer.js +221 -183
- package/lib/infra.js +177 -112
- package/lib/secrets.js +85 -99
- package/lib/utils/api-error-handler.js +465 -0
- package/lib/utils/api.js +165 -16
- package/lib/utils/auth-headers.js +84 -0
- package/lib/utils/build-copy.js +144 -0
- package/lib/utils/cli-utils.js +21 -0
- package/lib/utils/compose-generator.js +43 -14
- package/lib/utils/deployment-errors.js +90 -0
- package/lib/utils/deployment-validation.js +60 -0
- package/lib/utils/dev-config.js +83 -0
- package/lib/utils/env-template.js +30 -10
- package/lib/utils/health-check.js +18 -1
- package/lib/utils/infra-containers.js +101 -0
- package/lib/utils/local-secrets.js +0 -2
- package/lib/utils/secrets-path.js +18 -21
- package/lib/utils/secrets-utils.js +206 -0
- package/lib/utils/token-manager.js +381 -0
- package/package.json +1 -1
- package/templates/applications/README.md.hbs +155 -23
- package/templates/applications/miso-controller/Dockerfile +7 -119
- package/templates/infra/compose.yaml.hbs +93 -0
- package/templates/python/docker-compose.hbs +25 -17
- package/templates/typescript/docker-compose.hbs +25 -17
package/lib/app-deploy.js
CHANGED
|
@@ -15,6 +15,8 @@ const yaml = require('js-yaml');
|
|
|
15
15
|
const chalk = require('chalk');
|
|
16
16
|
const pushUtils = require('./push');
|
|
17
17
|
const logger = require('./utils/logger');
|
|
18
|
+
const config = require('./config');
|
|
19
|
+
const { getDeploymentAuth } = require('./utils/token-manager');
|
|
18
20
|
|
|
19
21
|
/**
|
|
20
22
|
* Validate application name format
|
|
@@ -22,8 +24,8 @@ const logger = require('./utils/logger');
|
|
|
22
24
|
* @throws {Error} If app name is invalid
|
|
23
25
|
*/
|
|
24
26
|
function validateAppName(appName) {
|
|
25
|
-
if (!appName || typeof appName !== 'string') {
|
|
26
|
-
throw new Error('
|
|
27
|
+
if (!appName || typeof appName !== 'string' || appName.trim().length === 0) {
|
|
28
|
+
throw new Error('App name is required');
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
// App name should be lowercase, alphanumeric with dashes, 3-40 characters
|
|
@@ -183,9 +185,7 @@ async function loadVariablesFile(variablesPath) {
|
|
|
183
185
|
function extractDeploymentConfig(options, variables) {
|
|
184
186
|
return {
|
|
185
187
|
controllerUrl: options.controller || variables.deployment?.controllerUrl,
|
|
186
|
-
envKey: options.environment || variables.deployment?.environment
|
|
187
|
-
clientId: options.clientId || variables.deployment?.clientId,
|
|
188
|
-
clientSecret: options.clientSecret || variables.deployment?.clientSecret,
|
|
188
|
+
envKey: options.environment || variables.deployment?.environment,
|
|
189
189
|
poll: options.poll !== false,
|
|
190
190
|
pollInterval: options.pollInterval || 5000,
|
|
191
191
|
pollMaxAttempts: options.pollMaxAttempts || 60
|
|
@@ -194,24 +194,24 @@ function extractDeploymentConfig(options, variables) {
|
|
|
194
194
|
|
|
195
195
|
/**
|
|
196
196
|
* Validates required deployment configuration
|
|
197
|
-
* @param {Object}
|
|
197
|
+
* @param {Object} deploymentConfig - Deployment configuration
|
|
198
198
|
* @throws {Error} If configuration is invalid
|
|
199
199
|
*/
|
|
200
|
-
function validateDeploymentConfig(
|
|
201
|
-
if (!
|
|
200
|
+
function validateDeploymentConfig(deploymentConfig) {
|
|
201
|
+
if (!deploymentConfig.controllerUrl) {
|
|
202
202
|
throw new Error('Controller URL is required. Set it in variables.yaml or use --controller flag');
|
|
203
203
|
}
|
|
204
|
-
if (!
|
|
205
|
-
throw new Error('
|
|
204
|
+
if (!deploymentConfig.auth) {
|
|
205
|
+
throw new Error('Authentication is required. Run "aifabrix login" first or ensure credentials are in secrets.local.yaml');
|
|
206
206
|
}
|
|
207
207
|
}
|
|
208
208
|
|
|
209
209
|
/**
|
|
210
|
-
* Loads deployment configuration from variables.yaml
|
|
210
|
+
* Loads deployment configuration from variables.yaml and gets/refreshes token
|
|
211
211
|
* @async
|
|
212
212
|
* @param {string} appName - Application name
|
|
213
213
|
* @param {Object} options - CLI options
|
|
214
|
-
* @returns {Promise<Object>} Deployment configuration
|
|
214
|
+
* @returns {Promise<Object>} Deployment configuration with token
|
|
215
215
|
* @throws {Error} If configuration is invalid
|
|
216
216
|
*/
|
|
217
217
|
async function loadDeploymentConfig(appName, options) {
|
|
@@ -221,10 +221,44 @@ async function loadDeploymentConfig(appName, options) {
|
|
|
221
221
|
const variablesPath = path.join(builderPath, 'variables.yaml');
|
|
222
222
|
const variables = await loadVariablesFile(variablesPath);
|
|
223
223
|
|
|
224
|
-
const
|
|
225
|
-
validateDeploymentConfig(config);
|
|
224
|
+
const deploymentConfig = extractDeploymentConfig(options, variables);
|
|
226
225
|
|
|
227
|
-
|
|
226
|
+
// Update root-level environment if provided
|
|
227
|
+
if (options.environment) {
|
|
228
|
+
await config.setCurrentEnvironment(options.environment);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Get current environment from root-level config
|
|
232
|
+
const currentEnvironment = await config.getCurrentEnvironment();
|
|
233
|
+
deploymentConfig.envKey = deploymentConfig.envKey || currentEnvironment;
|
|
234
|
+
|
|
235
|
+
// Get controller URL
|
|
236
|
+
if (!deploymentConfig.controllerUrl) {
|
|
237
|
+
throw new Error('Controller URL is required. Set it in variables.yaml or use --controller flag');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Get deployment authentication (device token ā client token ā credentials)
|
|
241
|
+
try {
|
|
242
|
+
const authConfig = await getDeploymentAuth(
|
|
243
|
+
deploymentConfig.controllerUrl,
|
|
244
|
+
deploymentConfig.envKey,
|
|
245
|
+
appName
|
|
246
|
+
);
|
|
247
|
+
if (!authConfig || !authConfig.controller) {
|
|
248
|
+
throw new Error('Invalid authentication configuration: missing controller URL');
|
|
249
|
+
}
|
|
250
|
+
if (!authConfig.token) {
|
|
251
|
+
throw new Error('Authentication is required');
|
|
252
|
+
}
|
|
253
|
+
deploymentConfig.auth = authConfig;
|
|
254
|
+
deploymentConfig.controllerUrl = authConfig.controller;
|
|
255
|
+
} catch (error) {
|
|
256
|
+
throw new Error(`Failed to get authentication: ${error.message}`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
validateDeploymentConfig(deploymentConfig);
|
|
260
|
+
|
|
261
|
+
return deploymentConfig;
|
|
228
262
|
}
|
|
229
263
|
|
|
230
264
|
/**
|
|
@@ -264,22 +298,21 @@ function displayDeploymentInfo(manifest, manifestPath) {
|
|
|
264
298
|
* Executes deployment to controller
|
|
265
299
|
* @async
|
|
266
300
|
* @param {Object} manifest - Deployment manifest
|
|
267
|
-
* @param {Object}
|
|
301
|
+
* @param {Object} deploymentConfig - Deployment configuration
|
|
268
302
|
* @returns {Promise<Object>} Deployment result
|
|
269
303
|
*/
|
|
270
|
-
async function executeDeployment(manifest,
|
|
271
|
-
logger.log(chalk.blue(`\nš Deploying to ${
|
|
304
|
+
async function executeDeployment(manifest, deploymentConfig) {
|
|
305
|
+
logger.log(chalk.blue(`\nš Deploying to ${deploymentConfig.controllerUrl} (environment: ${deploymentConfig.envKey})...`));
|
|
272
306
|
const deployer = require('./deployer');
|
|
273
307
|
return await deployer.deployToController(
|
|
274
308
|
manifest,
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
config.clientSecret,
|
|
309
|
+
deploymentConfig.controllerUrl,
|
|
310
|
+
deploymentConfig.envKey,
|
|
311
|
+
deploymentConfig.auth,
|
|
279
312
|
{
|
|
280
|
-
poll:
|
|
281
|
-
pollInterval:
|
|
282
|
-
pollMaxAttempts:
|
|
313
|
+
poll: deploymentConfig.poll,
|
|
314
|
+
pollInterval: deploymentConfig.pollInterval,
|
|
315
|
+
pollMaxAttempts: deploymentConfig.pollMaxAttempts
|
|
283
316
|
}
|
|
284
317
|
);
|
|
285
318
|
}
|
|
@@ -312,7 +345,7 @@ function displayDeploymentResults(result) {
|
|
|
312
345
|
* @param {string} appName - Name of the application to deploy
|
|
313
346
|
* @param {Object} options - Deployment options
|
|
314
347
|
* @param {string} options.controller - Controller URL (required)
|
|
315
|
-
* @param {string} [options.environment] - Target environment (dev/tst/pro)
|
|
348
|
+
* @param {string} [options.environment] - Target environment (miso/dev/tst/pro)
|
|
316
349
|
* @param {boolean} [options.poll] - Poll for deployment status
|
|
317
350
|
* @param {number} [options.pollInterval] - Polling interval in milliseconds
|
|
318
351
|
* @returns {Promise<Object>} Deployment result
|
|
@@ -322,16 +355,20 @@ function displayDeploymentResults(result) {
|
|
|
322
355
|
* await deployApp('myapp', { controller: 'https://controller.aifabrix.ai', environment: 'dev' });
|
|
323
356
|
*/
|
|
324
357
|
async function deployApp(appName, options = {}) {
|
|
358
|
+
let controllerUrl = null;
|
|
359
|
+
let config = null;
|
|
360
|
+
|
|
325
361
|
try {
|
|
326
362
|
// 1. Input validation
|
|
327
|
-
if (!appName || typeof appName !== 'string') {
|
|
363
|
+
if (!appName || typeof appName !== 'string' || appName.trim().length === 0) {
|
|
328
364
|
throw new Error('App name is required');
|
|
329
365
|
}
|
|
330
366
|
|
|
331
367
|
validateAppName(appName);
|
|
332
368
|
|
|
333
369
|
// 2. Load deployment configuration
|
|
334
|
-
|
|
370
|
+
config = await loadDeploymentConfig(appName, options);
|
|
371
|
+
controllerUrl = config.controllerUrl || options.controller || 'unknown';
|
|
335
372
|
|
|
336
373
|
// 3. Generate and validate manifest
|
|
337
374
|
const { manifest, manifestPath } = await generateAndValidateManifest(appName);
|
|
@@ -348,7 +385,14 @@ async function deployApp(appName, options = {}) {
|
|
|
348
385
|
return result;
|
|
349
386
|
|
|
350
387
|
} catch (error) {
|
|
351
|
-
|
|
388
|
+
// Use unified error handler from deployer
|
|
389
|
+
// Check if error was already logged (from deployer.js)
|
|
390
|
+
const alreadyLogged = error._logged === true;
|
|
391
|
+
const url = controllerUrl || options.controller || 'unknown';
|
|
392
|
+
|
|
393
|
+
const deployer = require('./deployer');
|
|
394
|
+
// handleDeploymentErrors will log, format, and throw the error
|
|
395
|
+
await deployer.handleDeploymentErrors(error, appName, url, alreadyLogged);
|
|
352
396
|
}
|
|
353
397
|
}
|
|
354
398
|
|
package/lib/app-list.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Fabrix Builder - App List Command
|
|
3
|
+
*
|
|
4
|
+
* Handles listing applications in an environment
|
|
5
|
+
*
|
|
6
|
+
* @fileoverview App list command implementation for AI Fabrix Builder
|
|
7
|
+
* @author AI Fabrix Team
|
|
8
|
+
* @version 2.0.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const chalk = require('chalk');
|
|
12
|
+
const { getConfig } = require('./config');
|
|
13
|
+
const { getOrRefreshDeviceToken } = require('./utils/token-manager');
|
|
14
|
+
const { authenticatedApiCall } = require('./utils/api');
|
|
15
|
+
const { formatApiError } = require('./utils/api-error-handler');
|
|
16
|
+
const logger = require('./utils/logger');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Extract applications array from API response
|
|
20
|
+
* Handles multiple response formats:
|
|
21
|
+
* 1. Wrapped format: { success: true, data: { success: true, data: [...] } }
|
|
22
|
+
* 2. Direct array: { success: true, data: [...] }
|
|
23
|
+
* 3. Paginated format: { success: true, data: { items: [...] } }
|
|
24
|
+
* 4. Wrapped paginated: { success: true, data: { success: true, data: { items: [...] } } }
|
|
25
|
+
* @param {Object} response - API response from authenticatedApiCall
|
|
26
|
+
* @returns {Array} Array of applications
|
|
27
|
+
* @throws {Error} If response format is invalid
|
|
28
|
+
*/
|
|
29
|
+
function extractApplications(response) {
|
|
30
|
+
const apiResponse = response.data;
|
|
31
|
+
let applications;
|
|
32
|
+
|
|
33
|
+
// Check if apiResponse.data is an array (wrapped format)
|
|
34
|
+
if (apiResponse && apiResponse.data && Array.isArray(apiResponse.data)) {
|
|
35
|
+
applications = apiResponse.data;
|
|
36
|
+
} else if (Array.isArray(apiResponse)) {
|
|
37
|
+
// Check if apiResponse is directly an array
|
|
38
|
+
applications = apiResponse;
|
|
39
|
+
} else if (apiResponse && Array.isArray(apiResponse.items)) {
|
|
40
|
+
// Check if apiResponse.items is an array (paginated format)
|
|
41
|
+
applications = apiResponse.items;
|
|
42
|
+
} else if (apiResponse && apiResponse.data && apiResponse.data.items && Array.isArray(apiResponse.data.items)) {
|
|
43
|
+
// Check if apiResponse.data.items is an array (wrapped paginated format)
|
|
44
|
+
applications = apiResponse.data.items;
|
|
45
|
+
} else {
|
|
46
|
+
logger.error(chalk.red('ā Invalid response: expected data array or items array'));
|
|
47
|
+
logger.error(chalk.gray('\nAPI response type:'), typeof apiResponse);
|
|
48
|
+
logger.error(chalk.gray('API response:'), JSON.stringify(apiResponse, null, 2));
|
|
49
|
+
logger.error(chalk.gray('\nFull response for debugging:'));
|
|
50
|
+
logger.error(chalk.gray(JSON.stringify(response, null, 2)));
|
|
51
|
+
throw new Error('Invalid response format');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return applications;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Display applications list
|
|
59
|
+
* @param {Array} applications - Array of application objects
|
|
60
|
+
* @param {string} environment - Environment name or key
|
|
61
|
+
*/
|
|
62
|
+
function displayApplications(applications, environment) {
|
|
63
|
+
const environmentName = environment || 'miso';
|
|
64
|
+
const header = `Applications in ${environmentName} environment`;
|
|
65
|
+
|
|
66
|
+
if (applications.length === 0) {
|
|
67
|
+
logger.log(chalk.bold(`\nš± ${header}:\n`));
|
|
68
|
+
logger.log(chalk.gray(' No applications found in this environment.\n'));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
logger.log(chalk.bold(`\nš± ${header}:\n`));
|
|
73
|
+
applications.forEach((app) => {
|
|
74
|
+
const hasPipeline = app.configuration?.pipeline?.isActive ? 'ā' : 'ā';
|
|
75
|
+
logger.log(`${hasPipeline} ${chalk.cyan(app.key)} - ${app.displayName} (${app.status || 'unknown'})`);
|
|
76
|
+
});
|
|
77
|
+
logger.log('');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* List applications in an environment
|
|
82
|
+
* @async
|
|
83
|
+
* @param {Object} options - Command options
|
|
84
|
+
* @param {string} options.environment - Environment ID or key
|
|
85
|
+
* @throws {Error} If listing fails
|
|
86
|
+
*/
|
|
87
|
+
async function listApplications(options) {
|
|
88
|
+
const config = await getConfig();
|
|
89
|
+
|
|
90
|
+
// Try to get device token
|
|
91
|
+
let controllerUrl = null;
|
|
92
|
+
let token = null;
|
|
93
|
+
|
|
94
|
+
if (config.device) {
|
|
95
|
+
const deviceUrls = Object.keys(config.device);
|
|
96
|
+
if (deviceUrls.length > 0) {
|
|
97
|
+
controllerUrl = deviceUrls[0];
|
|
98
|
+
const deviceToken = await getOrRefreshDeviceToken(controllerUrl);
|
|
99
|
+
if (deviceToken && deviceToken.token) {
|
|
100
|
+
token = deviceToken.token;
|
|
101
|
+
controllerUrl = deviceToken.controller;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!token || !controllerUrl) {
|
|
107
|
+
logger.error(chalk.red('ā Not logged in. Run: aifabrix login'));
|
|
108
|
+
logger.error(chalk.gray(' Use device code flow: aifabrix login --method device --controller <url>'));
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const response = await authenticatedApiCall(
|
|
113
|
+
`${controllerUrl}/api/v1/environments/${options.environment}/applications`,
|
|
114
|
+
{},
|
|
115
|
+
token
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
if (!response.success || !response.data) {
|
|
119
|
+
const formattedError = response.formattedError || formatApiError(response);
|
|
120
|
+
logger.error(formattedError);
|
|
121
|
+
// Log full response for debugging
|
|
122
|
+
logger.error(chalk.gray('\nFull response for debugging:'));
|
|
123
|
+
logger.error(chalk.gray(JSON.stringify(response, null, 2)));
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const applications = extractApplications(response);
|
|
128
|
+
displayApplications(applications, options.environment);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module.exports = { listApplications };
|
|
132
|
+
|
package/lib/app-readme.js
CHANGED
|
@@ -69,16 +69,23 @@ function generateReadmeMd(appName, config) {
|
|
|
69
69
|
// Extract registry from nested structure (config.image.registry) or flattened (config.registry)
|
|
70
70
|
const registry = config.image?.registry || config.registry || 'myacr.azurecr.io';
|
|
71
71
|
|
|
72
|
+
const hasDatabase = config.database || config.requires?.database || false;
|
|
73
|
+
const hasRedis = config.redis || config.requires?.redis || false;
|
|
74
|
+
const hasStorage = config.storage || config.requires?.storage || false;
|
|
75
|
+
const hasAuthentication = config.authentication || false;
|
|
76
|
+
const hasAnyService = hasDatabase || hasRedis || hasStorage || hasAuthentication;
|
|
77
|
+
|
|
72
78
|
const context = {
|
|
73
79
|
appName,
|
|
74
80
|
displayName,
|
|
75
81
|
imageName,
|
|
76
82
|
port,
|
|
77
83
|
registry,
|
|
78
|
-
hasDatabase
|
|
79
|
-
hasRedis
|
|
80
|
-
hasStorage
|
|
81
|
-
hasAuthentication
|
|
84
|
+
hasDatabase,
|
|
85
|
+
hasRedis,
|
|
86
|
+
hasStorage,
|
|
87
|
+
hasAuthentication,
|
|
88
|
+
hasAnyService
|
|
82
89
|
};
|
|
83
90
|
|
|
84
91
|
return template(context);
|