@aifabrix/builder 2.6.3 → 2.8.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 +680 -0
- package/bin/aifabrix.js +4 -0
- package/lib/app-config.js +10 -0
- package/lib/app-deploy.js +18 -0
- package/lib/app-dockerfile.js +15 -0
- package/lib/app-prompts.js +172 -9
- package/lib/app-push.js +15 -0
- package/lib/app-register.js +14 -0
- package/lib/app-run.js +25 -0
- package/lib/app.js +30 -13
- package/lib/audit-logger.js +9 -4
- package/lib/build.js +8 -0
- package/lib/cli.js +99 -2
- package/lib/commands/datasource.js +94 -0
- package/lib/commands/login.js +40 -3
- package/lib/config.js +121 -114
- package/lib/datasource-deploy.js +182 -0
- package/lib/datasource-diff.js +73 -0
- package/lib/datasource-list.js +138 -0
- package/lib/datasource-validate.js +63 -0
- package/lib/diff.js +266 -0
- package/lib/environment-deploy.js +305 -0
- package/lib/external-system-deploy.js +262 -0
- package/lib/external-system-generator.js +187 -0
- package/lib/schema/application-schema.json +869 -698
- package/lib/schema/external-datasource.schema.json +512 -0
- package/lib/schema/external-system.schema.json +262 -0
- package/lib/schema/infrastructure-schema.json +1 -1
- package/lib/secrets.js +20 -1
- package/lib/templates.js +32 -1
- package/lib/utils/device-code.js +10 -2
- package/lib/utils/env-copy.js +24 -0
- package/lib/utils/env-endpoints.js +50 -11
- package/lib/utils/schema-loader.js +220 -0
- package/lib/utils/schema-resolver.js +174 -0
- package/lib/utils/secrets-helpers.js +65 -17
- package/lib/utils/token-encryption.js +68 -0
- package/lib/validate.js +299 -0
- package/lib/validator.js +47 -3
- package/package.json +1 -1
- package/tatus +181 -0
- package/templates/external-system/external-datasource.json.hbs +55 -0
- package/templates/external-system/external-system.json.hbs +37 -0
package/lib/diff.js
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Comparison Utilities
|
|
3
|
+
*
|
|
4
|
+
* Compares two configuration files and identifies differences.
|
|
5
|
+
* Used for deployment pipeline validation and schema migration detection.
|
|
6
|
+
*
|
|
7
|
+
* @fileoverview File comparison utilities for AI Fabrix Builder
|
|
8
|
+
* @author AI Fabrix Team
|
|
9
|
+
* @version 2.0.0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const chalk = require('chalk');
|
|
15
|
+
const logger = require('./utils/logger');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Performs deep comparison of two objects
|
|
19
|
+
* Returns differences as structured result
|
|
20
|
+
*
|
|
21
|
+
* @function compareObjects
|
|
22
|
+
* @param {Object} obj1 - First object
|
|
23
|
+
* @param {Object} obj2 - Second object
|
|
24
|
+
* @param {string} [path=''] - Current path in object (for nested fields)
|
|
25
|
+
* @returns {Object} Comparison result with added, removed, changed fields
|
|
26
|
+
*/
|
|
27
|
+
function compareObjects(obj1, obj2, currentPath = '') {
|
|
28
|
+
const result = {
|
|
29
|
+
added: [],
|
|
30
|
+
removed: [],
|
|
31
|
+
changed: [],
|
|
32
|
+
identical: true
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const allKeys = new Set([...Object.keys(obj1 || {}), ...Object.keys(obj2 || {})]);
|
|
36
|
+
|
|
37
|
+
for (const key of allKeys) {
|
|
38
|
+
const newPath = currentPath ? `${currentPath}.${key}` : key;
|
|
39
|
+
const val1 = obj1 && obj1[key];
|
|
40
|
+
const val2 = obj2 && obj2[key];
|
|
41
|
+
|
|
42
|
+
if (!(key in obj1)) {
|
|
43
|
+
result.added.push({
|
|
44
|
+
path: newPath,
|
|
45
|
+
value: val2,
|
|
46
|
+
type: typeof val2
|
|
47
|
+
});
|
|
48
|
+
result.identical = false;
|
|
49
|
+
} else if (!(key in obj2)) {
|
|
50
|
+
result.removed.push({
|
|
51
|
+
path: newPath,
|
|
52
|
+
value: val1,
|
|
53
|
+
type: typeof val1
|
|
54
|
+
});
|
|
55
|
+
result.identical = false;
|
|
56
|
+
} else if (typeof val1 === 'object' && typeof val2 === 'object' && val1 !== null && val2 !== null && !Array.isArray(val1) && !Array.isArray(val2)) {
|
|
57
|
+
// Recursively compare nested objects
|
|
58
|
+
const nestedResult = compareObjects(val1, val2, newPath);
|
|
59
|
+
result.added.push(...nestedResult.added);
|
|
60
|
+
result.removed.push(...nestedResult.removed);
|
|
61
|
+
result.changed.push(...nestedResult.changed);
|
|
62
|
+
if (!nestedResult.identical) {
|
|
63
|
+
result.identical = false;
|
|
64
|
+
}
|
|
65
|
+
} else if (JSON.stringify(val1) !== JSON.stringify(val2)) {
|
|
66
|
+
result.changed.push({
|
|
67
|
+
path: newPath,
|
|
68
|
+
oldValue: val1,
|
|
69
|
+
newValue: val2,
|
|
70
|
+
oldType: typeof val1,
|
|
71
|
+
newType: typeof val2
|
|
72
|
+
});
|
|
73
|
+
result.identical = false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Identifies breaking changes in comparison result
|
|
82
|
+
* Breaking changes include: removed required fields, type changes
|
|
83
|
+
*
|
|
84
|
+
* @function identifyBreakingChanges
|
|
85
|
+
* @param {Object} comparison - Comparison result from compareObjects
|
|
86
|
+
* @param {Object} schema1 - First file schema (optional, for required fields check)
|
|
87
|
+
* @param {Object} schema2 - Second file schema (optional, for required fields check)
|
|
88
|
+
* @returns {Array} Array of breaking change descriptions
|
|
89
|
+
*/
|
|
90
|
+
function identifyBreakingChanges(comparison) {
|
|
91
|
+
const breaking = [];
|
|
92
|
+
|
|
93
|
+
// Removed fields are potentially breaking
|
|
94
|
+
comparison.removed.forEach(removed => {
|
|
95
|
+
breaking.push({
|
|
96
|
+
type: 'removed_field',
|
|
97
|
+
path: removed.path,
|
|
98
|
+
description: `Field removed: ${removed.path} (${removed.type})`
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Type changes are breaking
|
|
103
|
+
comparison.changed.forEach(change => {
|
|
104
|
+
if (change.oldType !== change.newType) {
|
|
105
|
+
breaking.push({
|
|
106
|
+
type: 'type_change',
|
|
107
|
+
path: change.path,
|
|
108
|
+
description: `Type changed: ${change.path} (${change.oldType} → ${change.newType})`
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return breaking;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Compares two configuration files
|
|
118
|
+
* Loads files, parses JSON, and performs deep comparison
|
|
119
|
+
*
|
|
120
|
+
* @async
|
|
121
|
+
* @function compareFiles
|
|
122
|
+
* @param {string} file1 - Path to first file
|
|
123
|
+
* @param {string} file2 - Path to second file
|
|
124
|
+
* @returns {Promise<Object>} Comparison result with differences
|
|
125
|
+
* @throws {Error} If files cannot be read or parsed
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* const result = await compareFiles('./old.json', './new.json');
|
|
129
|
+
* // Returns: { identical: false, added: [...], removed: [...], changed: [...] }
|
|
130
|
+
*/
|
|
131
|
+
async function compareFiles(file1, file2) {
|
|
132
|
+
if (!file1 || typeof file1 !== 'string') {
|
|
133
|
+
throw new Error('First file path is required');
|
|
134
|
+
}
|
|
135
|
+
if (!file2 || typeof file2 !== 'string') {
|
|
136
|
+
throw new Error('Second file path is required');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Validate files exist
|
|
140
|
+
if (!fs.existsSync(file1)) {
|
|
141
|
+
throw new Error(`File not found: ${file1}`);
|
|
142
|
+
}
|
|
143
|
+
if (!fs.existsSync(file2)) {
|
|
144
|
+
throw new Error(`File not found: ${file2}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Read and parse files
|
|
148
|
+
let content1, content2;
|
|
149
|
+
let parsed1, parsed2;
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
content1 = fs.readFileSync(file1, 'utf8');
|
|
153
|
+
parsed1 = JSON.parse(content1);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
throw new Error(`Failed to parse ${file1}: ${error.message}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
content2 = fs.readFileSync(file2, 'utf8');
|
|
160
|
+
parsed2 = JSON.parse(content2);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
throw new Error(`Failed to parse ${file2}: ${error.message}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Compare objects
|
|
166
|
+
const comparison = compareObjects(parsed1, parsed2);
|
|
167
|
+
|
|
168
|
+
// Check for version changes
|
|
169
|
+
const version1 = parsed1.version || parsed1.metadata?.version || 'unknown';
|
|
170
|
+
const version2 = parsed2.version || parsed2.metadata?.version || 'unknown';
|
|
171
|
+
const versionChanged = version1 !== version2;
|
|
172
|
+
|
|
173
|
+
// Identify breaking changes
|
|
174
|
+
const breakingChanges = identifyBreakingChanges(comparison);
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
identical: comparison.identical && !versionChanged,
|
|
178
|
+
file1: path.basename(file1),
|
|
179
|
+
file2: path.basename(file2),
|
|
180
|
+
version1,
|
|
181
|
+
version2,
|
|
182
|
+
versionChanged,
|
|
183
|
+
added: comparison.added,
|
|
184
|
+
removed: comparison.removed,
|
|
185
|
+
changed: comparison.changed,
|
|
186
|
+
breakingChanges,
|
|
187
|
+
summary: {
|
|
188
|
+
totalAdded: comparison.added.length,
|
|
189
|
+
totalRemoved: comparison.removed.length,
|
|
190
|
+
totalChanged: comparison.changed.length,
|
|
191
|
+
totalBreaking: breakingChanges.length
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Formats and displays diff output
|
|
198
|
+
* Shows differences in a user-friendly format with color coding
|
|
199
|
+
*
|
|
200
|
+
* @function formatDiffOutput
|
|
201
|
+
* @param {Object} diffResult - Comparison result from compareFiles
|
|
202
|
+
*/
|
|
203
|
+
function formatDiffOutput(diffResult) {
|
|
204
|
+
logger.log(chalk.blue(`\nComparing: ${diffResult.file1} ↔ ${diffResult.file2}`));
|
|
205
|
+
|
|
206
|
+
if (diffResult.identical) {
|
|
207
|
+
logger.log(chalk.green('\n✓ Files are identical'));
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
logger.log(chalk.yellow('\nFiles are different'));
|
|
212
|
+
|
|
213
|
+
// Version information
|
|
214
|
+
if (diffResult.versionChanged) {
|
|
215
|
+
logger.log(chalk.blue(`\nVersion: ${diffResult.version1} → ${diffResult.version2}`));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Breaking changes
|
|
219
|
+
if (diffResult.breakingChanges.length > 0) {
|
|
220
|
+
logger.log(chalk.red('\n⚠️ Breaking Changes:'));
|
|
221
|
+
diffResult.breakingChanges.forEach(change => {
|
|
222
|
+
logger.log(chalk.red(` • ${change.description}`));
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Added fields
|
|
227
|
+
if (diffResult.added.length > 0) {
|
|
228
|
+
logger.log(chalk.green('\nAdded Fields:'));
|
|
229
|
+
diffResult.added.forEach(field => {
|
|
230
|
+
logger.log(chalk.green(` + ${field.path}: ${JSON.stringify(field.value)}`));
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Removed fields
|
|
235
|
+
if (diffResult.removed.length > 0) {
|
|
236
|
+
logger.log(chalk.red('\nRemoved Fields:'));
|
|
237
|
+
diffResult.removed.forEach(field => {
|
|
238
|
+
logger.log(chalk.red(` - ${field.path}: ${JSON.stringify(field.value)}`));
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Changed fields
|
|
243
|
+
if (diffResult.changed.length > 0) {
|
|
244
|
+
logger.log(chalk.yellow('\nChanged Fields:'));
|
|
245
|
+
diffResult.changed.forEach(change => {
|
|
246
|
+
logger.log(chalk.yellow(` ~ ${change.path}:`));
|
|
247
|
+
logger.log(chalk.gray(` Old: ${JSON.stringify(change.oldValue)}`));
|
|
248
|
+
logger.log(chalk.gray(` New: ${JSON.stringify(change.newValue)}`));
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Summary
|
|
253
|
+
logger.log(chalk.blue('\nSummary:'));
|
|
254
|
+
logger.log(chalk.blue(` Added: ${diffResult.summary.totalAdded}`));
|
|
255
|
+
logger.log(chalk.blue(` Removed: ${diffResult.summary.totalRemoved}`));
|
|
256
|
+
logger.log(chalk.blue(` Changed: ${diffResult.summary.totalChanged}`));
|
|
257
|
+
logger.log(chalk.blue(` Breaking: ${diffResult.summary.totalBreaking}`));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
module.exports = {
|
|
261
|
+
compareFiles,
|
|
262
|
+
formatDiffOutput,
|
|
263
|
+
compareObjects,
|
|
264
|
+
identifyBreakingChanges
|
|
265
|
+
};
|
|
266
|
+
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Fabrix Builder Environment Deployment Module
|
|
3
|
+
*
|
|
4
|
+
* Handles environment deployment/setup in Miso Controller.
|
|
5
|
+
* Sets up environment infrastructure before applications can be deployed.
|
|
6
|
+
*
|
|
7
|
+
* @fileoverview Environment deployment for AI Fabrix Builder
|
|
8
|
+
* @author AI Fabrix Team
|
|
9
|
+
* @version 2.0.0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const chalk = require('chalk');
|
|
13
|
+
const logger = require('./utils/logger');
|
|
14
|
+
const config = require('./config');
|
|
15
|
+
const { validateControllerUrl, validateEnvironmentKey } = require('./utils/deployment-validation');
|
|
16
|
+
const { getOrRefreshDeviceToken } = require('./utils/token-manager');
|
|
17
|
+
const { authenticatedApiCall } = require('./utils/api');
|
|
18
|
+
const { handleDeploymentErrors } = require('./utils/deployment-errors');
|
|
19
|
+
const auditLogger = require('./audit-logger');
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Validates environment deployment prerequisites
|
|
23
|
+
* @param {string} envKey - Environment key
|
|
24
|
+
* @param {string} controllerUrl - Controller URL
|
|
25
|
+
* @throws {Error} If prerequisites are not met
|
|
26
|
+
*/
|
|
27
|
+
function validateEnvironmentPrerequisites(envKey, controllerUrl) {
|
|
28
|
+
if (!envKey || typeof envKey !== 'string') {
|
|
29
|
+
throw new Error('Environment key is required');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!controllerUrl || typeof controllerUrl !== 'string') {
|
|
33
|
+
throw new Error('Controller URL is required');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Validate environment key format
|
|
37
|
+
validateEnvironmentKey(envKey);
|
|
38
|
+
|
|
39
|
+
// Validate controller URL
|
|
40
|
+
validateControllerUrl(controllerUrl);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Gets authentication for environment deployment
|
|
45
|
+
* Uses device token (not app-specific client credentials)
|
|
46
|
+
* @async
|
|
47
|
+
* @param {string} controllerUrl - Controller URL
|
|
48
|
+
* @returns {Promise<Object>} Authentication configuration
|
|
49
|
+
* @throws {Error} If authentication is not available
|
|
50
|
+
*/
|
|
51
|
+
async function getEnvironmentAuth(controllerUrl) {
|
|
52
|
+
const validatedUrl = validateControllerUrl(controllerUrl);
|
|
53
|
+
|
|
54
|
+
// Get or refresh device token
|
|
55
|
+
const deviceToken = await getOrRefreshDeviceToken(validatedUrl);
|
|
56
|
+
|
|
57
|
+
if (!deviceToken || !deviceToken.token) {
|
|
58
|
+
throw new Error('Device token is required for environment deployment. Run "aifabrix login" first to authenticate.');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
type: 'device',
|
|
63
|
+
token: deviceToken.token,
|
|
64
|
+
controller: deviceToken.controller || validatedUrl
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Sends environment deployment request to controller
|
|
70
|
+
* @async
|
|
71
|
+
* @param {string} controllerUrl - Controller URL
|
|
72
|
+
* @param {string} envKey - Environment key
|
|
73
|
+
* @param {Object} authConfig - Authentication configuration
|
|
74
|
+
* @param {Object} options - Deployment options
|
|
75
|
+
* @returns {Promise<Object>} Deployment result
|
|
76
|
+
* @throws {Error} If deployment fails
|
|
77
|
+
*/
|
|
78
|
+
async function sendEnvironmentDeployment(controllerUrl, envKey, authConfig, options = {}) {
|
|
79
|
+
const validatedUrl = validateControllerUrl(controllerUrl);
|
|
80
|
+
const validatedEnvKey = validateEnvironmentKey(envKey);
|
|
81
|
+
|
|
82
|
+
// Build environment deployment request
|
|
83
|
+
const deploymentRequest = {
|
|
84
|
+
key: validatedEnvKey,
|
|
85
|
+
displayName: `${validatedEnvKey.charAt(0).toUpperCase() + validatedEnvKey.slice(1)} Environment`,
|
|
86
|
+
description: `${validatedEnvKey.charAt(0).toUpperCase() + validatedEnvKey.slice(1)} environment for application deployments`
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Add configuration if provided
|
|
90
|
+
if (options.config) {
|
|
91
|
+
// TODO: Load and parse config file if provided
|
|
92
|
+
// For now, just include the config path in description
|
|
93
|
+
deploymentRequest.description += ` (config: ${options.config})`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// API endpoint: POST /api/v1/environments/{env}/deploy
|
|
97
|
+
// Alternative: POST /api/v1/environments/deploy with environment in body
|
|
98
|
+
const endpoint = `${validatedUrl}/api/v1/environments/${validatedEnvKey}/deploy`;
|
|
99
|
+
|
|
100
|
+
// Log deployment attempt for audit
|
|
101
|
+
await auditLogger.logDeploymentAttempt(validatedEnvKey, validatedUrl, options);
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const response = await authenticatedApiCall(
|
|
105
|
+
endpoint,
|
|
106
|
+
{
|
|
107
|
+
method: 'POST',
|
|
108
|
+
body: JSON.stringify(deploymentRequest)
|
|
109
|
+
},
|
|
110
|
+
authConfig.token
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
if (!response.success) {
|
|
114
|
+
const error = new Error(response.formattedError || response.error || 'Environment deployment failed');
|
|
115
|
+
error.status = response.status;
|
|
116
|
+
error.data = response.errorData;
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Handle response structure
|
|
121
|
+
const responseData = response.data || {};
|
|
122
|
+
return {
|
|
123
|
+
success: true,
|
|
124
|
+
environment: validatedEnvKey,
|
|
125
|
+
deploymentId: responseData.deploymentId || responseData.id,
|
|
126
|
+
status: responseData.status || 'initiated',
|
|
127
|
+
url: responseData.url || `${validatedUrl}/environments/${validatedEnvKey}`,
|
|
128
|
+
message: responseData.message
|
|
129
|
+
};
|
|
130
|
+
} catch (error) {
|
|
131
|
+
// Use unified error handler
|
|
132
|
+
await handleDeploymentErrors(error, validatedEnvKey, validatedUrl, false);
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Polls environment deployment status
|
|
139
|
+
* @async
|
|
140
|
+
* @param {string} deploymentId - Deployment ID
|
|
141
|
+
* @param {string} controllerUrl - Controller URL
|
|
142
|
+
* @param {string} envKey - Environment key
|
|
143
|
+
* @param {Object} authConfig - Authentication configuration
|
|
144
|
+
* @param {Object} options - Polling options
|
|
145
|
+
* @returns {Promise<Object>} Final deployment status
|
|
146
|
+
*/
|
|
147
|
+
async function pollEnvironmentStatus(deploymentId, controllerUrl, envKey, authConfig, options = {}) {
|
|
148
|
+
const validatedUrl = validateControllerUrl(controllerUrl);
|
|
149
|
+
const validatedEnvKey = validateEnvironmentKey(envKey);
|
|
150
|
+
|
|
151
|
+
const pollInterval = options.pollInterval || 5000;
|
|
152
|
+
const maxAttempts = options.maxAttempts || 60;
|
|
153
|
+
const statusEndpoint = `${validatedUrl}/api/v1/environments/${validatedEnvKey}/status`;
|
|
154
|
+
|
|
155
|
+
logger.log(chalk.blue(`⏳ Polling environment status (${pollInterval}ms intervals)...`));
|
|
156
|
+
|
|
157
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
158
|
+
try {
|
|
159
|
+
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
|
160
|
+
|
|
161
|
+
const response = await authenticatedApiCall(
|
|
162
|
+
statusEndpoint,
|
|
163
|
+
{
|
|
164
|
+
method: 'GET'
|
|
165
|
+
},
|
|
166
|
+
authConfig.token
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
if (response.success && response.data) {
|
|
170
|
+
const status = response.data.status || response.data.ready;
|
|
171
|
+
const isReady = status === 'ready' || status === 'completed' || response.data.ready === true;
|
|
172
|
+
|
|
173
|
+
if (isReady) {
|
|
174
|
+
return {
|
|
175
|
+
success: true,
|
|
176
|
+
environment: validatedEnvKey,
|
|
177
|
+
status: 'ready',
|
|
178
|
+
message: 'Environment is ready for application deployments'
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Check for terminal failure states
|
|
183
|
+
if (status === 'failed' || status === 'error') {
|
|
184
|
+
throw new Error(`Environment deployment failed: ${response.data.message || 'Unknown error'}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
} catch (error) {
|
|
188
|
+
// If it's a terminal error (not a timeout), throw it
|
|
189
|
+
if (error.message && error.message.includes('failed')) {
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
// Otherwise, continue polling
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (attempt < maxAttempts) {
|
|
196
|
+
logger.log(chalk.gray(` Attempt ${attempt}/${maxAttempts}...`));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Timeout
|
|
201
|
+
throw new Error(`Environment deployment timeout after ${maxAttempts} attempts. Check controller logs for status.`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Displays environment deployment results
|
|
206
|
+
* @param {Object} result - Deployment result
|
|
207
|
+
*/
|
|
208
|
+
function displayDeploymentResults(result) {
|
|
209
|
+
logger.log(chalk.green('\n✅ Environment deployed successfully'));
|
|
210
|
+
logger.log(chalk.blue(` Environment: ${result.environment}`));
|
|
211
|
+
logger.log(chalk.blue(` Status: ${result.status === 'ready' ? '✅ ready' : result.status}`));
|
|
212
|
+
if (result.url) {
|
|
213
|
+
logger.log(chalk.blue(` URL: ${result.url}`));
|
|
214
|
+
}
|
|
215
|
+
if (result.deploymentId) {
|
|
216
|
+
logger.log(chalk.blue(` Deployment ID: ${result.deploymentId}`));
|
|
217
|
+
}
|
|
218
|
+
logger.log(chalk.green('\n✓ Environment is ready for application deployments'));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Deploys/setups an environment in the controller
|
|
223
|
+
* @async
|
|
224
|
+
* @function deployEnvironment
|
|
225
|
+
* @param {string} envKey - Environment key (miso, dev, tst, pro)
|
|
226
|
+
* @param {Object} options - Deployment options
|
|
227
|
+
* @param {string} options.controller - Controller URL (required)
|
|
228
|
+
* @param {string} [options.config] - Environment configuration file (optional)
|
|
229
|
+
* @param {boolean} [options.skipValidation] - Skip validation checks
|
|
230
|
+
* @param {boolean} [options.poll] - Poll for deployment status (default: true)
|
|
231
|
+
* @param {boolean} [options.noPoll] - Do not poll for status
|
|
232
|
+
* @returns {Promise<Object>} Deployment result
|
|
233
|
+
* @throws {Error} If deployment fails
|
|
234
|
+
*
|
|
235
|
+
* @example
|
|
236
|
+
* await deployEnvironment('dev', { controller: 'https://controller.aifabrix.ai' });
|
|
237
|
+
*/
|
|
238
|
+
async function deployEnvironment(envKey, options = {}) {
|
|
239
|
+
try {
|
|
240
|
+
// 1. Input validation
|
|
241
|
+
if (!envKey || typeof envKey !== 'string' || envKey.trim().length === 0) {
|
|
242
|
+
throw new Error('Environment key is required');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const controllerUrl = options.controller || options['controller-url'];
|
|
246
|
+
if (!controllerUrl) {
|
|
247
|
+
throw new Error('Controller URL is required. Use --controller flag');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// 2. Validate prerequisites
|
|
251
|
+
if (!options.skipValidation) {
|
|
252
|
+
validateEnvironmentPrerequisites(envKey, controllerUrl);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// 3. Update root-level environment in config.yaml
|
|
256
|
+
await config.setCurrentEnvironment(envKey);
|
|
257
|
+
|
|
258
|
+
// 4. Get authentication (device token)
|
|
259
|
+
logger.log(chalk.blue(`\n📋 Deploying environment '${envKey}' to ${controllerUrl}...`));
|
|
260
|
+
const authConfig = await getEnvironmentAuth(controllerUrl);
|
|
261
|
+
logger.log(chalk.green('✓ Environment validated'));
|
|
262
|
+
logger.log(chalk.green('✓ Authentication successful'));
|
|
263
|
+
|
|
264
|
+
// 5. Send environment deployment request
|
|
265
|
+
logger.log(chalk.blue('\n🚀 Deploying environment infrastructure...'));
|
|
266
|
+
const validatedControllerUrl = validateControllerUrl(authConfig.controller);
|
|
267
|
+
const result = await sendEnvironmentDeployment(validatedControllerUrl, envKey, authConfig, options);
|
|
268
|
+
|
|
269
|
+
logger.log(chalk.blue(`📤 Sending deployment request to ${validatedControllerUrl}/api/v1/environments/${envKey}/deploy...`));
|
|
270
|
+
|
|
271
|
+
// 6. Poll for status if enabled
|
|
272
|
+
const shouldPoll = options.poll !== false && !options.noPoll;
|
|
273
|
+
if (shouldPoll && result.deploymentId) {
|
|
274
|
+
const pollResult = await pollEnvironmentStatus(
|
|
275
|
+
result.deploymentId,
|
|
276
|
+
validatedControllerUrl,
|
|
277
|
+
envKey,
|
|
278
|
+
authConfig,
|
|
279
|
+
{
|
|
280
|
+
pollInterval: 5000,
|
|
281
|
+
maxAttempts: 60
|
|
282
|
+
}
|
|
283
|
+
);
|
|
284
|
+
result.status = pollResult.status;
|
|
285
|
+
result.message = pollResult.message;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// 7. Display results
|
|
289
|
+
displayDeploymentResults(result);
|
|
290
|
+
|
|
291
|
+
return result;
|
|
292
|
+
} catch (error) {
|
|
293
|
+
// Error handling is done in sendEnvironmentDeployment and pollEnvironmentStatus
|
|
294
|
+
// Re-throw with context
|
|
295
|
+
if (error._logged !== true) {
|
|
296
|
+
logger.error(chalk.red(`\n❌ Environment deployment failed: ${error.message}`));
|
|
297
|
+
}
|
|
298
|
+
throw error;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
module.exports = {
|
|
303
|
+
deployEnvironment
|
|
304
|
+
};
|
|
305
|
+
|