@aifabrix/builder 2.37.0 → 2.37.9
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 +19 -0
- package/integration/hubspot/test.js +1 -1
- package/lib/api/wizard.api.js +24 -1
- package/lib/app/deploy.js +43 -7
- package/lib/app/list.js +3 -1
- package/lib/build/index.js +3 -4
- package/lib/cli/setup-app.js +1 -0
- package/lib/cli/setup-external-system.js +1 -0
- package/lib/cli/setup-utility.js +1 -1
- package/lib/commands/up-common.js +31 -1
- package/lib/commands/up-miso.js +7 -3
- package/lib/commands/wizard-core.js +44 -7
- package/lib/core/config.js +16 -1
- package/lib/core/secrets.js +42 -50
- package/lib/deployment/deployer-status.js +101 -0
- package/lib/deployment/deployer.js +62 -110
- package/lib/deployment/environment.js +146 -36
- package/lib/external-system/deploy.js +5 -1
- package/lib/external-system/test-auth.js +14 -7
- package/lib/generator/wizard.js +27 -16
- package/lib/schema/environment-deploy-request.schema.json +64 -0
- package/lib/utils/paths.js +28 -7
- package/lib/utils/secrets-generator.js +23 -8
- package/lib/utils/secrets-helpers.js +46 -21
- package/package.json +1 -1
- package/scripts/install-local.js +11 -2
- package/templates/external-system/deploy.js.hbs +11 -0
- package/templates/infra/environment-dev.json +10 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deployment status and polling helpers for deployer.
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Deployment status checks and polling utilities
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 2.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const chalk = require('chalk');
|
|
10
|
+
const logger = require('../utils/logger');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Checks if deployment status is terminal
|
|
14
|
+
* @param {string} status - Deployment status
|
|
15
|
+
* @returns {boolean} True if status is terminal
|
|
16
|
+
*/
|
|
17
|
+
function isTerminalStatus(status) {
|
|
18
|
+
return status === 'completed' || status === 'failed' || status === 'cancelled';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Convert authConfig to pipeline auth config format
|
|
23
|
+
* @param {Object} authConfig - Authentication configuration
|
|
24
|
+
* @returns {Object} Pipeline auth config
|
|
25
|
+
*/
|
|
26
|
+
function convertToPipelineAuthConfig(authConfig) {
|
|
27
|
+
return authConfig.type === 'bearer'
|
|
28
|
+
? { type: 'bearer', token: authConfig.token }
|
|
29
|
+
: { type: 'client-credentials', clientId: authConfig.clientId, clientSecret: authConfig.clientSecret };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Handles error response from deployment status check
|
|
34
|
+
* @param {Object} response - API response
|
|
35
|
+
* @param {string} deploymentId - Deployment ID
|
|
36
|
+
* @throws {Error} Appropriate error message
|
|
37
|
+
*/
|
|
38
|
+
function handleDeploymentStatusError(response, deploymentId) {
|
|
39
|
+
if (response.status === 404) {
|
|
40
|
+
throw new Error(`Deployment ${deploymentId || response.deploymentId || 'unknown'} not found`);
|
|
41
|
+
}
|
|
42
|
+
throw new Error(`Status check failed: ${response.formattedError || response.error || 'Unknown error'}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Extracts deployment data from response
|
|
47
|
+
* @param {Object} response - API response
|
|
48
|
+
* @returns {Object} Deployment data
|
|
49
|
+
*/
|
|
50
|
+
function extractDeploymentData(response) {
|
|
51
|
+
const responseData = response.data;
|
|
52
|
+
return responseData.data || responseData;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Logs deployment progress
|
|
57
|
+
* @param {Object} deploymentData - Deployment data
|
|
58
|
+
* @param {number} attempt - Current attempt
|
|
59
|
+
* @param {number} maxAttempts - Maximum attempts
|
|
60
|
+
*/
|
|
61
|
+
function logDeploymentProgress(deploymentData, attempt, maxAttempts) {
|
|
62
|
+
const status = deploymentData.status;
|
|
63
|
+
const progress = deploymentData.progress || 0;
|
|
64
|
+
logger.log(chalk.blue(` Status: ${status} (${progress}%) (attempt ${attempt + 1}/${maxAttempts})`));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Process deployment status response
|
|
69
|
+
* @param {Object} response - API response
|
|
70
|
+
* @param {number} attempt - Current attempt number
|
|
71
|
+
* @param {number} maxAttempts - Maximum attempts
|
|
72
|
+
* @param {number} interval - Polling interval
|
|
73
|
+
* @param {string} deploymentId - Deployment ID for error messages
|
|
74
|
+
* @returns {Promise<Object|null>} Deployment data if terminal, null if needs to continue polling
|
|
75
|
+
*/
|
|
76
|
+
async function processDeploymentStatusResponse(response, attempt, maxAttempts, interval, deploymentId) {
|
|
77
|
+
if (!response.success || !response.data) {
|
|
78
|
+
handleDeploymentStatusError(response, deploymentId);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const deploymentData = extractDeploymentData(response);
|
|
82
|
+
if (isTerminalStatus(deploymentData.status)) {
|
|
83
|
+
return deploymentData;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
logDeploymentProgress(deploymentData, attempt, maxAttempts);
|
|
87
|
+
if (attempt < maxAttempts - 1) {
|
|
88
|
+
await new Promise(resolve => setTimeout(resolve, interval));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = {
|
|
95
|
+
isTerminalStatus,
|
|
96
|
+
convertToPipelineAuthConfig,
|
|
97
|
+
handleDeploymentStatusError,
|
|
98
|
+
extractDeploymentData,
|
|
99
|
+
logDeploymentProgress,
|
|
100
|
+
processDeploymentStatusResponse
|
|
101
|
+
};
|
|
@@ -16,6 +16,23 @@ const { validateControllerUrl, validateEnvironmentKey } = require('../utils/depl
|
|
|
16
16
|
const { handleDeploymentError, handleDeploymentErrors } = require('../utils/deployment-errors');
|
|
17
17
|
const { validatePipeline, deployPipeline, getPipelineDeployment } = require('../api/pipeline.api');
|
|
18
18
|
const { handleValidationResponse } = require('../utils/deployment-validation-helpers');
|
|
19
|
+
const {
|
|
20
|
+
convertToPipelineAuthConfig,
|
|
21
|
+
processDeploymentStatusResponse
|
|
22
|
+
} = require('./deployer-status');
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* For external systems, send full manifest (application + inline system + full dataSources).
|
|
26
|
+
* No transform - controller receives complete application, external system, and data sources.
|
|
27
|
+
* @param {Object} manifest - Full manifest (type 'external', system, dataSources as full objects)
|
|
28
|
+
* @returns {Object} Manifest to send to pipeline (external sent as-is)
|
|
29
|
+
*/
|
|
30
|
+
function transformExternalManifestForPipeline(manifest) {
|
|
31
|
+
if (!manifest) {
|
|
32
|
+
return manifest;
|
|
33
|
+
}
|
|
34
|
+
return manifest;
|
|
35
|
+
}
|
|
19
36
|
|
|
20
37
|
/**
|
|
21
38
|
* Build validation data for deployment
|
|
@@ -28,27 +45,42 @@ const { handleValidationResponse } = require('../utils/deployment-validation-hel
|
|
|
28
45
|
*/
|
|
29
46
|
async function buildValidationData(manifest, validatedEnvKey, authConfig, options) {
|
|
30
47
|
const tokenManager = require('../utils/token-manager');
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
48
|
+
let clientId;
|
|
49
|
+
let clientSecret;
|
|
50
|
+
let pipelineAuthConfig;
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const credentials = await tokenManager.extractClientCredentials(
|
|
54
|
+
authConfig,
|
|
55
|
+
manifest.key,
|
|
56
|
+
validatedEnvKey,
|
|
57
|
+
options
|
|
58
|
+
);
|
|
59
|
+
clientId = credentials.clientId;
|
|
60
|
+
clientSecret = credentials.clientSecret;
|
|
61
|
+
pipelineAuthConfig = {
|
|
62
|
+
type: 'client-credentials',
|
|
63
|
+
clientId,
|
|
64
|
+
clientSecret
|
|
65
|
+
};
|
|
66
|
+
} catch (credError) {
|
|
67
|
+
if (authConfig.type === 'bearer' && authConfig.token) {
|
|
68
|
+
pipelineAuthConfig = { type: 'bearer', token: authConfig.token };
|
|
69
|
+
clientId = manifest.key;
|
|
70
|
+
clientSecret = '';
|
|
71
|
+
} else {
|
|
72
|
+
throw credError;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
37
75
|
|
|
38
76
|
const repositoryUrl = options.repositoryUrl || `https://github.com/aifabrix/${manifest.key}`;
|
|
39
77
|
const validationData = {
|
|
40
|
-
clientId: clientId,
|
|
41
|
-
clientSecret: clientSecret,
|
|
78
|
+
clientId: clientId || '',
|
|
79
|
+
clientSecret: clientSecret || '',
|
|
42
80
|
repositoryUrl: repositoryUrl,
|
|
43
81
|
applicationConfig: manifest
|
|
44
82
|
};
|
|
45
83
|
|
|
46
|
-
const pipelineAuthConfig = {
|
|
47
|
-
type: 'client-credentials',
|
|
48
|
-
clientId: clientId,
|
|
49
|
-
clientSecret: clientSecret
|
|
50
|
-
};
|
|
51
|
-
|
|
52
84
|
return { validationData, pipelineAuthConfig };
|
|
53
85
|
}
|
|
54
86
|
|
|
@@ -130,7 +162,7 @@ function validateDeploymentCredentials(authConfig) {
|
|
|
130
162
|
}
|
|
131
163
|
|
|
132
164
|
/**
|
|
133
|
-
* Build deployment data and auth config
|
|
165
|
+
* Build deployment data and auth config (supports bearer-only when no client credentials)
|
|
134
166
|
* @param {string} validateToken - Validation token
|
|
135
167
|
* @param {Object} authConfig - Authentication configuration
|
|
136
168
|
* @param {Object} options - Deployment options
|
|
@@ -143,11 +175,13 @@ function buildDeploymentData(validateToken, authConfig, options) {
|
|
|
143
175
|
imageTag: imageTag
|
|
144
176
|
};
|
|
145
177
|
|
|
146
|
-
const pipelineAuthConfig =
|
|
147
|
-
type: '
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
178
|
+
const pipelineAuthConfig = authConfig.type === 'bearer' && authConfig.token && !authConfig.clientId
|
|
179
|
+
? { type: 'bearer', token: authConfig.token }
|
|
180
|
+
: {
|
|
181
|
+
type: 'client-credentials',
|
|
182
|
+
clientId: authConfig.clientId,
|
|
183
|
+
clientSecret: authConfig.clientSecret
|
|
184
|
+
};
|
|
151
185
|
|
|
152
186
|
return { deployData, pipelineAuthConfig };
|
|
153
187
|
}
|
|
@@ -212,10 +246,12 @@ async function sendDeploymentRequest(url, envKey, validateToken, authConfig, opt
|
|
|
212
246
|
const validatedEnvKey = validateEnvironmentKey(envKey);
|
|
213
247
|
const maxRetries = options.maxRetries || 3;
|
|
214
248
|
|
|
215
|
-
|
|
216
|
-
|
|
249
|
+
const useBearerOnly = authConfig.type === 'bearer' && authConfig.token && !authConfig.clientId;
|
|
250
|
+
if (!useBearerOnly) {
|
|
251
|
+
validateDeploymentCredentials(authConfig);
|
|
252
|
+
}
|
|
217
253
|
|
|
218
|
-
// Build deployment data
|
|
254
|
+
// Build deployment data (supports bearer-only when no client credentials)
|
|
219
255
|
const { deployData, pipelineAuthConfig } = buildDeploymentData(validateToken, authConfig, options);
|
|
220
256
|
|
|
221
257
|
// Wrap API call with retry logic
|
|
@@ -237,92 +273,6 @@ async function sendDeploymentRequest(url, envKey, validateToken, authConfig, opt
|
|
|
237
273
|
throwDeploymentError(lastError, maxRetries);
|
|
238
274
|
}
|
|
239
275
|
|
|
240
|
-
/**
|
|
241
|
-
* Checks if deployment status is terminal
|
|
242
|
-
* @function isTerminalStatus
|
|
243
|
-
* @param {string} status - Deployment status
|
|
244
|
-
* @returns {boolean} True if status is terminal
|
|
245
|
-
*/
|
|
246
|
-
function isTerminalStatus(status) {
|
|
247
|
-
return status === 'completed' || status === 'failed' || status === 'cancelled';
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* Convert authConfig to pipeline auth config format
|
|
252
|
-
* @param {Object} authConfig - Authentication configuration
|
|
253
|
-
* @returns {Object} Pipeline auth config
|
|
254
|
-
*/
|
|
255
|
-
function convertToPipelineAuthConfig(authConfig) {
|
|
256
|
-
return authConfig.type === 'bearer'
|
|
257
|
-
? { type: 'bearer', token: authConfig.token }
|
|
258
|
-
: { type: 'client-credentials', clientId: authConfig.clientId, clientSecret: authConfig.clientSecret };
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* Process deployment status response
|
|
263
|
-
* @param {Object} response - API response
|
|
264
|
-
* @param {number} attempt - Current attempt number
|
|
265
|
-
* @param {number} maxAttempts - Maximum attempts
|
|
266
|
-
* @param {number} interval - Polling interval
|
|
267
|
-
* @param {string} deploymentId - Deployment ID for error messages
|
|
268
|
-
* @returns {Object|null} Deployment data if terminal, null if needs to continue polling
|
|
269
|
-
*/
|
|
270
|
-
/**
|
|
271
|
-
* Handles error response from deployment status check
|
|
272
|
-
* @function handleDeploymentStatusError
|
|
273
|
-
* @param {Object} response - API response
|
|
274
|
-
* @param {string} deploymentId - Deployment ID
|
|
275
|
-
* @throws {Error} Appropriate error message
|
|
276
|
-
*/
|
|
277
|
-
function handleDeploymentStatusError(response, deploymentId) {
|
|
278
|
-
if (response.status === 404) {
|
|
279
|
-
throw new Error(`Deployment ${deploymentId || response.deploymentId || 'unknown'} not found`);
|
|
280
|
-
}
|
|
281
|
-
throw new Error(`Status check failed: ${response.formattedError || response.error || 'Unknown error'}`);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/**
|
|
285
|
-
* Extracts deployment data from response
|
|
286
|
-
* @function extractDeploymentData
|
|
287
|
-
* @param {Object} response - API response
|
|
288
|
-
* @returns {Object} Deployment data
|
|
289
|
-
*/
|
|
290
|
-
function extractDeploymentData(response) {
|
|
291
|
-
const responseData = response.data;
|
|
292
|
-
return responseData.data || responseData;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* Logs deployment progress
|
|
297
|
-
* @function logDeploymentProgress
|
|
298
|
-
* @param {Object} deploymentData - Deployment data
|
|
299
|
-
* @param {number} attempt - Current attempt
|
|
300
|
-
* @param {number} maxAttempts - Maximum attempts
|
|
301
|
-
*/
|
|
302
|
-
function logDeploymentProgress(deploymentData, attempt, maxAttempts) {
|
|
303
|
-
const status = deploymentData.status;
|
|
304
|
-
const progress = deploymentData.progress || 0;
|
|
305
|
-
logger.log(chalk.blue(` Status: ${status} (${progress}%) (attempt ${attempt + 1}/${maxAttempts})`));
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
async function processDeploymentStatusResponse(response, attempt, maxAttempts, interval, deploymentId) {
|
|
309
|
-
if (!response.success || !response.data) {
|
|
310
|
-
handleDeploymentStatusError(response, deploymentId);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
const deploymentData = extractDeploymentData(response);
|
|
314
|
-
if (isTerminalStatus(deploymentData.status)) {
|
|
315
|
-
return deploymentData;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
logDeploymentProgress(deploymentData, attempt, maxAttempts);
|
|
319
|
-
if (attempt < maxAttempts - 1) {
|
|
320
|
-
await new Promise(resolve => setTimeout(resolve, interval));
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
return null;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
276
|
/**
|
|
327
277
|
* Polls deployment status from controller
|
|
328
278
|
* Uses pipeline endpoint for CI/CD monitoring with minimal deployment info
|
|
@@ -470,9 +420,11 @@ async function deployToController(manifest, controllerUrl, envKey, authConfig, o
|
|
|
470
420
|
// Log deployment attempt for audit
|
|
471
421
|
await auditLogger.logDeploymentAttempt(manifest.key, url, options);
|
|
472
422
|
|
|
423
|
+
const pipelineManifest = transformExternalManifestForPipeline(manifest);
|
|
424
|
+
|
|
473
425
|
try {
|
|
474
426
|
// Send deployment request
|
|
475
|
-
const result = await sendDeployment(url, validatedEnvKey,
|
|
427
|
+
const result = await sendDeployment(url, validatedEnvKey, pipelineManifest, authConfig, options);
|
|
476
428
|
|
|
477
429
|
// Poll for deployment status if enabled
|
|
478
430
|
return await pollDeployment(result, url, validatedEnvKey, authConfig, options);
|
|
@@ -9,16 +9,21 @@
|
|
|
9
9
|
* @version 2.0.0
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
12
14
|
const chalk = require('chalk');
|
|
15
|
+
const Ajv = require('ajv');
|
|
13
16
|
const logger = require('../utils/logger');
|
|
14
17
|
const config = require('../core/config');
|
|
15
18
|
const { resolveControllerUrl } = require('../utils/controller-url');
|
|
16
19
|
const { validateControllerUrl, validateEnvironmentKey } = require('../utils/deployment-validation');
|
|
17
20
|
const { getOrRefreshDeviceToken } = require('../utils/token-manager');
|
|
18
|
-
const {
|
|
19
|
-
const { deployEnvironment: deployEnvironmentInfra } = require('../api/deployments.api');
|
|
21
|
+
const { getPipelineDeployment } = require('../api/pipeline.api');
|
|
22
|
+
const { deployEnvironment: deployEnvironmentInfra, getDeployment } = require('../api/deployments.api');
|
|
20
23
|
const { handleDeploymentErrors } = require('../utils/deployment-errors');
|
|
24
|
+
const { formatValidationErrors } = require('../utils/error-formatter');
|
|
21
25
|
const auditLogger = require('../core/audit-logger');
|
|
26
|
+
const environmentDeployRequestSchema = require('../schema/environment-deploy-request.schema.json');
|
|
22
27
|
|
|
23
28
|
/**
|
|
24
29
|
* Validates environment deployment prerequisites
|
|
@@ -77,26 +82,102 @@ async function getEnvironmentAuth(controllerUrl) {
|
|
|
77
82
|
* @returns {Promise<Object>} Deployment result
|
|
78
83
|
* @throws {Error} If deployment fails
|
|
79
84
|
*/
|
|
85
|
+
/** Reads and parses config file; throws if missing, unreadable, or invalid structure. */
|
|
86
|
+
function parseEnvironmentConfigFile(resolvedPath) {
|
|
87
|
+
let raw;
|
|
88
|
+
try {
|
|
89
|
+
raw = fs.readFileSync(resolvedPath, 'utf8');
|
|
90
|
+
} catch (e) {
|
|
91
|
+
throw new Error(`Cannot read config file: ${resolvedPath}. ${e.message}`);
|
|
92
|
+
}
|
|
93
|
+
let parsed;
|
|
94
|
+
try {
|
|
95
|
+
parsed = JSON.parse(raw);
|
|
96
|
+
} catch (e) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
`Invalid JSON in config file: ${resolvedPath}\n${e.message}\n` +
|
|
99
|
+
'Expected format: { "environmentConfig": { "key", "environment", "preset", "serviceName", "location" }, "dryRun": false }'
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
if (parsed === null || typeof parsed !== 'object') {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`Config file must be a JSON object with "environmentConfig". File: ${resolvedPath}\n` +
|
|
105
|
+
'Example: { "environmentConfig": { "key": "dev", "environment": "dev", "preset": "s", "serviceName": "aifabrix", "location": "swedencentral" }, "dryRun": false }'
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
if (parsed.environmentConfig === undefined) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
`Config file must contain "environmentConfig" (object). File: ${resolvedPath}\n` +
|
|
111
|
+
'Example: { "environmentConfig": { "key": "dev", "environment": "dev", "preset": "s", "serviceName": "aifabrix", "location": "swedencentral" } }'
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
if (typeof parsed.environmentConfig !== 'object' || parsed.environmentConfig === null) {
|
|
115
|
+
throw new Error(`"environmentConfig" must be an object. File: ${resolvedPath}`);
|
|
116
|
+
}
|
|
117
|
+
return parsed;
|
|
118
|
+
}
|
|
119
|
+
|
|
80
120
|
/**
|
|
81
|
-
*
|
|
82
|
-
* @
|
|
83
|
-
* @param {string}
|
|
84
|
-
* @
|
|
85
|
-
* @returns {Object} Deployment request object
|
|
121
|
+
* Validates parsed config against schema and returns deploy request.
|
|
122
|
+
* @param {Object} parsed - Parsed config object
|
|
123
|
+
* @param {string} resolvedPath - Path for error messages
|
|
124
|
+
* @returns {Object} { environmentConfig, dryRun? }
|
|
86
125
|
*/
|
|
87
|
-
function
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
126
|
+
function validateEnvironmentDeployParsed(parsed, resolvedPath) {
|
|
127
|
+
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
128
|
+
const validate = ajv.compile(environmentDeployRequestSchema);
|
|
129
|
+
if (!validate(parsed)) {
|
|
130
|
+
const messages = formatValidationErrors(validate.errors);
|
|
131
|
+
throw new Error(
|
|
132
|
+
`Environment config validation failed (${resolvedPath}):\n • ${messages.join('\n • ')}\n` +
|
|
133
|
+
'Fix the config file and run the command again. See templates/infra/environment-dev.json for a valid example.'
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
environmentConfig: parsed.environmentConfig,
|
|
138
|
+
dryRun: Boolean(parsed.dryRun)
|
|
93
139
|
};
|
|
140
|
+
}
|
|
94
141
|
|
|
95
|
-
|
|
96
|
-
|
|
142
|
+
/**
|
|
143
|
+
* Loads and validates environment deploy config from a JSON file
|
|
144
|
+
* @param {string} configPath - Absolute or relative path to config JSON
|
|
145
|
+
* @returns {Object} Valid deploy request { environmentConfig, dryRun? }
|
|
146
|
+
* @throws {Error} If file missing, invalid JSON, or validation fails
|
|
147
|
+
*/
|
|
148
|
+
function loadAndValidateEnvironmentDeployConfig(configPath) {
|
|
149
|
+
const resolvedPath = path.isAbsolute(configPath) ? configPath : path.resolve(process.cwd(), configPath);
|
|
150
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
151
|
+
throw new Error(
|
|
152
|
+
`Environment config file not found: ${resolvedPath}\n` +
|
|
153
|
+
'Use --config <file> with a JSON file containing "environmentConfig" (e.g. templates/infra/environment-dev.json).'
|
|
154
|
+
);
|
|
97
155
|
}
|
|
156
|
+
const parsed = parseEnvironmentConfigFile(resolvedPath);
|
|
157
|
+
return validateEnvironmentDeployParsed(parsed, resolvedPath);
|
|
158
|
+
}
|
|
98
159
|
|
|
99
|
-
|
|
160
|
+
/**
|
|
161
|
+
* Builds environment deployment request from options (config file required)
|
|
162
|
+
* @function buildEnvironmentDeploymentRequest
|
|
163
|
+
* @param {string} validatedEnvKey - Validated environment key
|
|
164
|
+
* @param {Object} options - Deployment options (must include options.config)
|
|
165
|
+
* @returns {Object} Deployment request object for API
|
|
166
|
+
*/
|
|
167
|
+
function buildEnvironmentDeploymentRequest(validatedEnvKey, options) {
|
|
168
|
+
if (!options.config || typeof options.config !== 'string') {
|
|
169
|
+
throw new Error(
|
|
170
|
+
'Environment deploy requires a config file with "environmentConfig". Use --config <file>.\n' +
|
|
171
|
+
'Example: aifabrix environment deploy dev --config templates/infra/environment-dev.json'
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
const deployRequest = loadAndValidateEnvironmentDeployConfig(options.config);
|
|
175
|
+
if (deployRequest.environmentConfig.key && deployRequest.environmentConfig.key !== validatedEnvKey) {
|
|
176
|
+
logger.log(chalk.yellow(
|
|
177
|
+
`⚠ Config key "${deployRequest.environmentConfig.key}" does not match deploy target "${validatedEnvKey}"; using target "${validatedEnvKey}".`
|
|
178
|
+
));
|
|
179
|
+
}
|
|
180
|
+
return deployRequest;
|
|
100
181
|
}
|
|
101
182
|
|
|
102
183
|
/**
|
|
@@ -155,22 +236,47 @@ async function sendEnvironmentDeployment(controllerUrl, envKey, authConfig, opti
|
|
|
155
236
|
}
|
|
156
237
|
|
|
157
238
|
/**
|
|
158
|
-
*
|
|
159
|
-
*
|
|
239
|
+
* Fetches deployment status by ID (pipeline endpoint first, then environments)
|
|
240
|
+
* Mirrors miso-controller manual test: GET pipeline/deployments/:id then GET environments/deployments/:id
|
|
241
|
+
* @async
|
|
242
|
+
* @param {string} controllerUrl - Controller URL
|
|
243
|
+
* @param {string} envKey - Environment key
|
|
244
|
+
* @param {string} deploymentId - Deployment ID
|
|
245
|
+
* @param {Object} apiAuthConfig - Auth config { type: 'bearer', token }
|
|
246
|
+
* @returns {Promise<Object|null>} Deployment record (status, progress, etc.) or null
|
|
247
|
+
*/
|
|
248
|
+
async function getDeploymentStatusById(controllerUrl, envKey, deploymentId, apiAuthConfig) {
|
|
249
|
+
try {
|
|
250
|
+
const pipelineRes = await getPipelineDeployment(controllerUrl, envKey, deploymentId, apiAuthConfig);
|
|
251
|
+
if (pipelineRes?.data?.data) return pipelineRes.data.data;
|
|
252
|
+
if (pipelineRes?.data && typeof pipelineRes.data === 'object' && pipelineRes.data.status) return pipelineRes.data;
|
|
253
|
+
} catch {
|
|
254
|
+
// Fallback to environments endpoint
|
|
255
|
+
}
|
|
256
|
+
try {
|
|
257
|
+
const envRes = await getDeployment(controllerUrl, envKey, deploymentId, apiAuthConfig);
|
|
258
|
+
if (envRes?.data?.data) return envRes.data.data;
|
|
259
|
+
if (envRes?.data && typeof envRes.data === 'object' && envRes.data.status) return envRes.data;
|
|
260
|
+
} catch {
|
|
261
|
+
// Ignore
|
|
262
|
+
}
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Process deployment record from GET .../deployments/:deploymentId
|
|
268
|
+
* @param {Object|null} deployment - Deployment record (status, progress, message, error)
|
|
160
269
|
* @param {string} validatedEnvKey - Validated environment key
|
|
161
|
-
* @returns {Object|null} Status result if
|
|
270
|
+
* @returns {Object|null} Status result if completed, null if still in progress
|
|
162
271
|
* @throws {Error} If deployment failed
|
|
163
272
|
*/
|
|
164
|
-
function
|
|
165
|
-
if (!
|
|
273
|
+
function processDeploymentStatusResponse(deployment, validatedEnvKey) {
|
|
274
|
+
if (!deployment || typeof deployment !== 'object') {
|
|
166
275
|
return null;
|
|
167
276
|
}
|
|
168
277
|
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
const isReady = status === 'ready' || status === 'completed' || responseData.ready === true;
|
|
172
|
-
|
|
173
|
-
if (isReady) {
|
|
278
|
+
const status = deployment.status;
|
|
279
|
+
if (status === 'completed') {
|
|
174
280
|
return {
|
|
175
281
|
success: true,
|
|
176
282
|
environment: validatedEnvKey,
|
|
@@ -179,9 +285,9 @@ function processEnvironmentStatusResponse(response, validatedEnvKey) {
|
|
|
179
285
|
};
|
|
180
286
|
}
|
|
181
287
|
|
|
182
|
-
// Check for terminal failure states
|
|
183
288
|
if (status === 'failed' || status === 'error') {
|
|
184
|
-
|
|
289
|
+
const msg = deployment.message || deployment.error || 'Unknown error';
|
|
290
|
+
throw new Error(`Environment deployment failed: ${msg}`);
|
|
185
291
|
}
|
|
186
292
|
|
|
187
293
|
return null;
|
|
@@ -213,8 +319,18 @@ async function pollEnvironmentStatus(deploymentId, controllerUrl, envKey, authCo
|
|
|
213
319
|
try {
|
|
214
320
|
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
|
215
321
|
|
|
216
|
-
const
|
|
217
|
-
|
|
322
|
+
const deployment = await getDeploymentStatusById(
|
|
323
|
+
validatedUrl,
|
|
324
|
+
validatedEnvKey,
|
|
325
|
+
deploymentId,
|
|
326
|
+
apiAuthConfig
|
|
327
|
+
);
|
|
328
|
+
const progress = deployment?.progress ?? 0;
|
|
329
|
+
const statusLabel = deployment?.status ?? 'pending';
|
|
330
|
+
if (attempt <= maxAttempts) {
|
|
331
|
+
logger.log(chalk.gray(` Attempt ${attempt}/${maxAttempts}... Status: ${statusLabel} (${progress}%)`));
|
|
332
|
+
}
|
|
333
|
+
const statusResult = processDeploymentStatusResponse(deployment, validatedEnvKey);
|
|
218
334
|
if (statusResult) {
|
|
219
335
|
return statusResult;
|
|
220
336
|
}
|
|
@@ -225,10 +341,6 @@ async function pollEnvironmentStatus(deploymentId, controllerUrl, envKey, authCo
|
|
|
225
341
|
}
|
|
226
342
|
// Otherwise, continue polling
|
|
227
343
|
}
|
|
228
|
-
|
|
229
|
-
if (attempt < maxAttempts) {
|
|
230
|
-
logger.log(chalk.gray(` Attempt ${attempt}/${maxAttempts}...`));
|
|
231
|
-
}
|
|
232
344
|
}
|
|
233
345
|
|
|
234
346
|
// Timeout
|
|
@@ -380,8 +492,6 @@ async function deployEnvironment(envKey, options = {}) {
|
|
|
380
492
|
throw error;
|
|
381
493
|
}
|
|
382
494
|
}
|
|
383
|
-
|
|
384
495
|
module.exports = {
|
|
385
496
|
deployEnvironment
|
|
386
497
|
};
|
|
387
|
-
|
|
@@ -97,7 +97,11 @@ async function deployExternalSystem(appName, options = {}) {
|
|
|
97
97
|
|
|
98
98
|
return result;
|
|
99
99
|
} catch (error) {
|
|
100
|
-
|
|
100
|
+
let message = `Failed to deploy external system: ${error.message}`;
|
|
101
|
+
if (error.message && error.message.includes('Application not found')) {
|
|
102
|
+
message += `\n\n💡 Register the app in the controller first: aifabrix app register ${appName}`;
|
|
103
|
+
}
|
|
104
|
+
throw new Error(message);
|
|
101
105
|
}
|
|
102
106
|
}
|
|
103
107
|
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* External System Test Authentication Helpers
|
|
3
3
|
*
|
|
4
|
-
* Authentication setup for integration tests
|
|
4
|
+
* Authentication setup for integration tests. Uses the dataplane service URL
|
|
5
|
+
* (discovered from the controller), not the external system app's config—external
|
|
6
|
+
* apps do not store a dataplane URL.
|
|
5
7
|
*
|
|
6
8
|
* @fileoverview Authentication helpers for external system testing
|
|
7
9
|
* @author AI Fabrix Team
|
|
@@ -9,19 +11,19 @@
|
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
13
|
const { getDeploymentAuth } = require('../utils/token-manager');
|
|
12
|
-
const {
|
|
14
|
+
const { resolveDataplaneUrl } = require('../utils/dataplane-resolver');
|
|
13
15
|
const { resolveControllerUrl } = require('../utils/controller-url');
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
18
|
* Setup authentication and get dataplane URL for integration tests
|
|
17
19
|
* @async
|
|
18
|
-
* @param {string} appName - Application name
|
|
19
|
-
* @param {Object} options - Test options
|
|
20
|
-
* @param {Object}
|
|
20
|
+
* @param {string} appName - Application name (used for auth scope; dataplane URL is discovered from controller)
|
|
21
|
+
* @param {Object} options - Test options; options.dataplane overrides discovered URL
|
|
22
|
+
* @param {Object} _config - Configuration object
|
|
21
23
|
* @returns {Promise<Object>} Object with authConfig and dataplaneUrl
|
|
22
24
|
* @throws {Error} If authentication fails
|
|
23
25
|
*/
|
|
24
|
-
async function setupIntegrationTestAuth(appName,
|
|
26
|
+
async function setupIntegrationTestAuth(appName, options, _config) {
|
|
25
27
|
const { resolveEnvironment } = require('../core/config');
|
|
26
28
|
const environment = await resolveEnvironment();
|
|
27
29
|
const controllerUrl = await resolveControllerUrl();
|
|
@@ -31,7 +33,12 @@ async function setupIntegrationTestAuth(appName, _options, _config) {
|
|
|
31
33
|
throw new Error('Authentication required. Run "aifabrix login" or "aifabrix app register" first.');
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
|
|
36
|
+
let dataplaneUrl;
|
|
37
|
+
if (options && options.dataplane && typeof options.dataplane === 'string' && options.dataplane.trim()) {
|
|
38
|
+
dataplaneUrl = options.dataplane.trim();
|
|
39
|
+
} else {
|
|
40
|
+
dataplaneUrl = await resolveDataplaneUrl(controllerUrl, environment, authConfig);
|
|
41
|
+
}
|
|
35
42
|
|
|
36
43
|
return { authConfig, dataplaneUrl };
|
|
37
44
|
}
|