@aifabrix/builder 2.22.2 → 2.31.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/jest.config.coverage.js +37 -0
- package/lib/api/pipeline.api.js +10 -9
- package/lib/app-deploy.js +36 -14
- package/lib/app-list.js +191 -71
- package/lib/app-prompts.js +77 -26
- package/lib/app-readme.js +123 -5
- package/lib/app-rotate-secret.js +101 -57
- package/lib/app-run-helpers.js +200 -172
- package/lib/app-run.js +137 -68
- package/lib/audit-logger.js +8 -7
- package/lib/build.js +161 -250
- package/lib/cli.js +73 -65
- package/lib/commands/login.js +45 -31
- package/lib/commands/logout.js +181 -0
- package/lib/commands/secure.js +59 -24
- package/lib/config.js +79 -45
- package/lib/datasource-deploy.js +89 -29
- package/lib/deployer.js +164 -129
- package/lib/diff.js +63 -21
- package/lib/environment-deploy.js +36 -19
- package/lib/external-system-deploy.js +134 -66
- package/lib/external-system-download.js +244 -171
- package/lib/external-system-test.js +199 -164
- package/lib/generator-external.js +145 -72
- package/lib/generator-helpers.js +49 -17
- package/lib/generator-split.js +105 -58
- package/lib/infra.js +101 -131
- package/lib/schema/application-schema.json +895 -896
- package/lib/schema/env-config.yaml +11 -4
- package/lib/template-validator.js +13 -4
- package/lib/utils/api.js +8 -8
- package/lib/utils/app-register-auth.js +36 -18
- package/lib/utils/app-run-containers.js +140 -0
- package/lib/utils/auth-headers.js +6 -6
- package/lib/utils/build-copy.js +60 -2
- package/lib/utils/build-helpers.js +94 -0
- package/lib/utils/cli-utils.js +177 -76
- package/lib/utils/compose-generator.js +12 -2
- package/lib/utils/config-tokens.js +151 -9
- package/lib/utils/deployment-errors.js +137 -69
- package/lib/utils/deployment-validation-helpers.js +103 -0
- package/lib/utils/docker-build.js +57 -0
- package/lib/utils/dockerfile-utils.js +13 -3
- package/lib/utils/env-copy.js +163 -94
- package/lib/utils/env-map.js +226 -86
- package/lib/utils/error-formatters/network-errors.js +0 -1
- package/lib/utils/external-system-display.js +14 -19
- package/lib/utils/external-system-env-helpers.js +107 -0
- package/lib/utils/external-system-test-helpers.js +144 -0
- package/lib/utils/health-check.js +10 -8
- package/lib/utils/infra-status.js +123 -0
- package/lib/utils/paths.js +228 -49
- package/lib/utils/schema-loader.js +125 -57
- package/lib/utils/token-manager.js +3 -3
- package/lib/utils/yaml-preserve.js +55 -16
- package/lib/validate.js +87 -89
- package/package.json +4 -4
- package/scripts/ci-fix.sh +19 -0
- package/scripts/ci-simulate.sh +19 -0
- package/templates/applications/miso-controller/test.yaml +1 -0
- package/templates/python/Dockerfile.hbs +8 -45
- package/templates/typescript/Dockerfile.hbs +8 -42
package/lib/datasource-deploy.js
CHANGED
|
@@ -55,19 +55,13 @@ async function getDataplaneUrl(controllerUrl, appKey, environment, authConfig) {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* @async
|
|
61
|
-
* @function deployDatasource
|
|
58
|
+
* Validate deployment inputs
|
|
62
59
|
* @param {string} appKey - Application key
|
|
63
|
-
* @param {string} filePath -
|
|
64
|
-
* @param {Object} options -
|
|
65
|
-
* @
|
|
66
|
-
* @param {string} options.environment - Environment key
|
|
67
|
-
* @returns {Promise<Object>} Deployment result
|
|
68
|
-
* @throws {Error} If deployment fails
|
|
60
|
+
* @param {string} filePath - File path
|
|
61
|
+
* @param {Object} options - Options
|
|
62
|
+
* @throws {Error} If validation fails
|
|
69
63
|
*/
|
|
70
|
-
|
|
64
|
+
function validateDeploymentInputs(appKey, filePath, options) {
|
|
71
65
|
if (!appKey || typeof appKey !== 'string') {
|
|
72
66
|
throw new Error('Application key is required');
|
|
73
67
|
}
|
|
@@ -80,10 +74,16 @@ async function deployDatasource(appKey, filePath, options) {
|
|
|
80
74
|
if (!options.environment) {
|
|
81
75
|
throw new Error('Environment is required (-e, --environment)');
|
|
82
76
|
}
|
|
77
|
+
}
|
|
83
78
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
79
|
+
/**
|
|
80
|
+
* Validate and load datasource file
|
|
81
|
+
* @async
|
|
82
|
+
* @param {string} filePath - Path to datasource file
|
|
83
|
+
* @returns {Promise<Object>} Datasource configuration
|
|
84
|
+
* @throws {Error} If validation or loading fails
|
|
85
|
+
*/
|
|
86
|
+
async function validateAndLoadDatasourceFile(filePath) {
|
|
87
87
|
logger.log(chalk.blue('🔍 Validating datasource file...'));
|
|
88
88
|
const validation = await validateDatasourceFile(filePath);
|
|
89
89
|
if (!validation.valid) {
|
|
@@ -95,32 +95,45 @@ async function deployDatasource(appKey, filePath, options) {
|
|
|
95
95
|
}
|
|
96
96
|
logger.log(chalk.green('✓ Datasource file is valid'));
|
|
97
97
|
|
|
98
|
-
// Load datasource configuration
|
|
99
98
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
100
|
-
let datasourceConfig;
|
|
101
99
|
try {
|
|
102
|
-
|
|
100
|
+
return JSON.parse(content);
|
|
103
101
|
} catch (error) {
|
|
104
102
|
throw new Error(`Failed to parse datasource file: ${error.message}`);
|
|
105
103
|
}
|
|
104
|
+
}
|
|
106
105
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
106
|
+
/**
|
|
107
|
+
* Setup authentication and get dataplane URL
|
|
108
|
+
* @async
|
|
109
|
+
* @param {string} controllerUrl - Controller URL
|
|
110
|
+
* @param {string} environment - Environment key
|
|
111
|
+
* @param {string} appKey - Application key
|
|
112
|
+
* @returns {Promise<Object>} Object with authConfig and dataplaneUrl
|
|
113
|
+
*/
|
|
114
|
+
async function setupDeploymentAuth(controllerUrl, environment, appKey) {
|
|
114
115
|
logger.log(chalk.blue('🔐 Getting authentication...'));
|
|
115
|
-
const authConfig = await getDeploymentAuth(
|
|
116
|
+
const authConfig = await getDeploymentAuth(controllerUrl, environment, appKey);
|
|
116
117
|
logger.log(chalk.green('✓ Authentication successful'));
|
|
117
118
|
|
|
118
|
-
// Get dataplane URL from controller
|
|
119
119
|
logger.log(chalk.blue('🌐 Getting dataplane URL from controller...'));
|
|
120
|
-
const dataplaneUrl = await getDataplaneUrl(
|
|
120
|
+
const dataplaneUrl = await getDataplaneUrl(controllerUrl, appKey, environment, authConfig);
|
|
121
121
|
logger.log(chalk.green(`✓ Dataplane URL: ${dataplaneUrl}`));
|
|
122
122
|
|
|
123
|
-
|
|
123
|
+
return { authConfig, dataplaneUrl };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Publish datasource to dataplane
|
|
128
|
+
* @async
|
|
129
|
+
* @param {string} dataplaneUrl - Dataplane URL
|
|
130
|
+
* @param {string} systemKey - System key
|
|
131
|
+
* @param {Object} authConfig - Authentication configuration
|
|
132
|
+
* @param {Object} datasourceConfig - Datasource configuration
|
|
133
|
+
* @returns {Promise<Object>} Publish response
|
|
134
|
+
* @throws {Error} If publish fails
|
|
135
|
+
*/
|
|
136
|
+
async function publishDatasourceToDataplane(dataplaneUrl, systemKey, authConfig, datasourceConfig) {
|
|
124
137
|
logger.log(chalk.blue('\n🚀 Publishing datasource to dataplane...'));
|
|
125
138
|
|
|
126
139
|
const publishResponse = await publishDatasourceViaPipeline(dataplaneUrl, systemKey, authConfig, datasourceConfig);
|
|
@@ -133,9 +146,56 @@ async function deployDatasource(appKey, filePath, options) {
|
|
|
133
146
|
}
|
|
134
147
|
|
|
135
148
|
logger.log(chalk.green('\n✓ Datasource published successfully!'));
|
|
149
|
+
return publishResponse;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Display deployment results
|
|
154
|
+
* @param {Object} datasourceConfig - Datasource configuration
|
|
155
|
+
* @param {string} systemKey - System key
|
|
156
|
+
* @param {string} environment - Environment key
|
|
157
|
+
*/
|
|
158
|
+
function displayDeploymentResults(datasourceConfig, systemKey, environment) {
|
|
136
159
|
logger.log(chalk.blue(`\nDatasource: ${datasourceConfig.key || datasourceConfig.displayName}`));
|
|
137
160
|
logger.log(chalk.blue(`System: ${systemKey}`));
|
|
138
|
-
logger.log(chalk.blue(`Environment: ${
|
|
161
|
+
logger.log(chalk.blue(`Environment: ${environment}`));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Deploys datasource to dataplane
|
|
166
|
+
*
|
|
167
|
+
* @async
|
|
168
|
+
* @function deployDatasource
|
|
169
|
+
* @param {string} appKey - Application key
|
|
170
|
+
* @param {string} filePath - Path to datasource JSON file
|
|
171
|
+
* @param {Object} options - Deployment options
|
|
172
|
+
* @param {string} options.controller - Controller URL
|
|
173
|
+
* @param {string} options.environment - Environment key
|
|
174
|
+
* @returns {Promise<Object>} Deployment result
|
|
175
|
+
* @throws {Error} If deployment fails
|
|
176
|
+
*/
|
|
177
|
+
async function deployDatasource(appKey, filePath, options) {
|
|
178
|
+
validateDeploymentInputs(appKey, filePath, options);
|
|
179
|
+
|
|
180
|
+
logger.log(chalk.blue('📋 Deploying datasource...\n'));
|
|
181
|
+
|
|
182
|
+
// Validate and load datasource file
|
|
183
|
+
const datasourceConfig = await validateAndLoadDatasourceFile(filePath);
|
|
184
|
+
|
|
185
|
+
// Extract systemKey
|
|
186
|
+
const systemKey = datasourceConfig.systemKey;
|
|
187
|
+
if (!systemKey) {
|
|
188
|
+
throw new Error('systemKey is required in datasource configuration');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Setup authentication and get dataplane URL
|
|
192
|
+
const { authConfig, dataplaneUrl } = await setupDeploymentAuth(options.controller, options.environment, appKey);
|
|
193
|
+
|
|
194
|
+
// Publish to dataplane
|
|
195
|
+
await publishDatasourceToDataplane(dataplaneUrl, systemKey, authConfig, datasourceConfig);
|
|
196
|
+
|
|
197
|
+
// Display results
|
|
198
|
+
displayDeploymentResults(datasourceConfig, systemKey, options.environment);
|
|
139
199
|
|
|
140
200
|
return {
|
|
141
201
|
success: true,
|
package/lib/deployer.js
CHANGED
|
@@ -15,25 +15,18 @@ const logger = require('./utils/logger');
|
|
|
15
15
|
const { validateControllerUrl, validateEnvironmentKey } = require('./utils/deployment-validation');
|
|
16
16
|
const { handleDeploymentError, handleDeploymentErrors } = require('./utils/deployment-errors');
|
|
17
17
|
const { validatePipeline, deployPipeline, getPipelineDeployment } = require('./api/pipeline.api');
|
|
18
|
+
const { handleValidationResponse } = require('./utils/deployment-validation-helpers');
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
|
-
*
|
|
21
|
-
* This is the first step in the deployment process
|
|
22
|
-
*
|
|
21
|
+
* Build validation data for deployment
|
|
23
22
|
* @async
|
|
24
|
-
* @param {
|
|
25
|
-
* @param {string}
|
|
26
|
-
* @param {Object} manifest - Deployment manifest (applicationConfig)
|
|
23
|
+
* @param {Object} manifest - Application manifest/config
|
|
24
|
+
* @param {string} validatedEnvKey - Validated environment key
|
|
27
25
|
* @param {Object} authConfig - Authentication configuration
|
|
28
|
-
* @param {Object} options -
|
|
29
|
-
* @returns {Promise<Object>}
|
|
30
|
-
* @throws {Error} If validation fails
|
|
26
|
+
* @param {Object} options - Additional options
|
|
27
|
+
* @returns {Promise<Object>} Object with validationData and pipelineAuthConfig
|
|
31
28
|
*/
|
|
32
|
-
async function
|
|
33
|
-
const validatedEnvKey = validateEnvironmentKey(envKey);
|
|
34
|
-
const maxRetries = options.maxRetries || 3;
|
|
35
|
-
|
|
36
|
-
// Extract clientId and clientSecret
|
|
29
|
+
async function buildValidationData(manifest, validatedEnvKey, authConfig, options) {
|
|
37
30
|
const tokenManager = require('./utils/token-manager');
|
|
38
31
|
const { clientId, clientSecret } = await tokenManager.extractClientCredentials(
|
|
39
32
|
authConfig,
|
|
@@ -42,7 +35,6 @@ async function validateDeployment(url, envKey, manifest, authConfig, options = {
|
|
|
42
35
|
options
|
|
43
36
|
);
|
|
44
37
|
|
|
45
|
-
// Build validation request
|
|
46
38
|
const repositoryUrl = options.repositoryUrl || `https://github.com/aifabrix/${manifest.key}`;
|
|
47
39
|
const validationData = {
|
|
48
40
|
clientId: clientId,
|
|
@@ -51,58 +43,40 @@ async function validateDeployment(url, envKey, manifest, authConfig, options = {
|
|
|
51
43
|
applicationConfig: manifest
|
|
52
44
|
};
|
|
53
45
|
|
|
54
|
-
// Use centralized API client with retry logic
|
|
55
46
|
const pipelineAuthConfig = {
|
|
56
47
|
type: 'client-credentials',
|
|
57
48
|
clientId: clientId,
|
|
58
49
|
clientSecret: clientSecret
|
|
59
50
|
};
|
|
60
51
|
|
|
52
|
+
return { validationData, pipelineAuthConfig };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Validates deployment configuration via validate endpoint
|
|
57
|
+
* This is the first step in the deployment process
|
|
58
|
+
*
|
|
59
|
+
* @async
|
|
60
|
+
* @param {string} url - Controller URL
|
|
61
|
+
* @param {string} envKey - Environment key (miso, dev, tst, pro)
|
|
62
|
+
* @param {Object} manifest - Deployment manifest (applicationConfig)
|
|
63
|
+
* @param {Object} authConfig - Authentication configuration
|
|
64
|
+
* @param {Object} options - Validation options (repositoryUrl, timeout, retries, etc.)
|
|
65
|
+
* @returns {Promise<Object>} Validation result with validateToken
|
|
66
|
+
* @throws {Error} If validation fails
|
|
67
|
+
*/
|
|
68
|
+
async function validateDeployment(url, envKey, manifest, authConfig, options = {}) {
|
|
69
|
+
const validatedEnvKey = validateEnvironmentKey(envKey);
|
|
70
|
+
const maxRetries = options.maxRetries || 3;
|
|
71
|
+
|
|
72
|
+
// Build validation data
|
|
73
|
+
const { validationData, pipelineAuthConfig } = await buildValidationData(manifest, validatedEnvKey, authConfig, options);
|
|
74
|
+
|
|
61
75
|
let lastError;
|
|
62
76
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
63
77
|
try {
|
|
64
78
|
const response = await validatePipeline(url, validatedEnvKey, pipelineAuthConfig, validationData);
|
|
65
|
-
|
|
66
|
-
// Handle successful validation (200 OK with valid: true)
|
|
67
|
-
if (response.success && response.data) {
|
|
68
|
-
const responseData = response.data.data || response.data;
|
|
69
|
-
if (responseData.valid === true) {
|
|
70
|
-
return {
|
|
71
|
-
success: true,
|
|
72
|
-
validateToken: responseData.validateToken,
|
|
73
|
-
draftDeploymentId: responseData.draftDeploymentId,
|
|
74
|
-
imageServer: responseData.imageServer,
|
|
75
|
-
imageUsername: responseData.imageUsername,
|
|
76
|
-
imagePassword: responseData.imagePassword,
|
|
77
|
-
expiresAt: responseData.expiresAt
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
// Handle validation failure (valid: false)
|
|
81
|
-
if (responseData.valid === false) {
|
|
82
|
-
const errorMessage = responseData.errors && responseData.errors.length > 0
|
|
83
|
-
? `Validation failed: ${responseData.errors.join(', ')}`
|
|
84
|
-
: 'Validation failed: Invalid configuration';
|
|
85
|
-
const error = new Error(errorMessage);
|
|
86
|
-
error.status = 400;
|
|
87
|
-
error.data = responseData;
|
|
88
|
-
throw error;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Handle validation errors (non-success responses)
|
|
93
|
-
if (!response.success) {
|
|
94
|
-
const error = new Error(`Validation request failed: ${response.formattedError || response.error || 'Unknown error'}`);
|
|
95
|
-
error.status = response.status || 400;
|
|
96
|
-
error.data = response.data;
|
|
97
|
-
throw error;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// If we get here, response.success is true but valid is not true and not false
|
|
101
|
-
// This is an unexpected state, throw an error
|
|
102
|
-
const error = new Error('Validation response is in an unexpected state');
|
|
103
|
-
error.status = 400;
|
|
104
|
-
error.data = response.data;
|
|
105
|
-
throw error;
|
|
79
|
+
return handleValidationResponse(response);
|
|
106
80
|
} catch (error) {
|
|
107
81
|
lastError = error;
|
|
108
82
|
const shouldRetry = attempt < maxRetries && error.status && error.status >= 500;
|
|
@@ -119,6 +93,83 @@ async function validateDeployment(url, envKey, manifest, authConfig, options = {
|
|
|
119
93
|
throw lastError;
|
|
120
94
|
}
|
|
121
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Validate client credentials for deployment
|
|
98
|
+
* @param {Object} authConfig - Authentication configuration
|
|
99
|
+
* @throws {Error} If credentials are missing
|
|
100
|
+
*/
|
|
101
|
+
function validateDeploymentCredentials(authConfig) {
|
|
102
|
+
if (!authConfig.clientId || !authConfig.clientSecret) {
|
|
103
|
+
throw new Error('Client ID and Client Secret are required for deployment. These should have been loaded during validation.');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Build deployment data and auth config
|
|
109
|
+
* @param {string} validateToken - Validation token
|
|
110
|
+
* @param {Object} authConfig - Authentication configuration
|
|
111
|
+
* @param {Object} options - Deployment options
|
|
112
|
+
* @returns {Object} Object with deployData and pipelineAuthConfig
|
|
113
|
+
*/
|
|
114
|
+
function buildDeploymentData(validateToken, authConfig, options) {
|
|
115
|
+
const imageTag = options.imageTag || 'latest';
|
|
116
|
+
const deployData = {
|
|
117
|
+
validateToken: validateToken,
|
|
118
|
+
imageTag: imageTag
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const pipelineAuthConfig = {
|
|
122
|
+
type: 'client-credentials',
|
|
123
|
+
clientId: authConfig.clientId,
|
|
124
|
+
clientSecret: authConfig.clientSecret
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
return { deployData, pipelineAuthConfig };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Handle deployment response
|
|
132
|
+
* @param {Object} response - API response
|
|
133
|
+
* @returns {Object} Deployment result
|
|
134
|
+
* @throws {Error} If deployment failed
|
|
135
|
+
*/
|
|
136
|
+
function handleDeploymentResponse(response) {
|
|
137
|
+
if (response.success) {
|
|
138
|
+
return response.data.data || response.data;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Handle deployment errors
|
|
142
|
+
if (response.status >= 400) {
|
|
143
|
+
const error = new Error(`Deployment request failed: ${response.formattedError || response.error || 'Unknown error'}`);
|
|
144
|
+
error.status = response.status;
|
|
145
|
+
error.data = response.data;
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
throw new Error('Deployment request failed: Unknown error');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Preserve error information from last error
|
|
154
|
+
* @param {Error} lastError - Last error encountered
|
|
155
|
+
* @param {number} maxRetries - Maximum retry attempts
|
|
156
|
+
* @throws {Error} Formatted error with preserved information
|
|
157
|
+
*/
|
|
158
|
+
function throwDeploymentError(lastError, maxRetries) {
|
|
159
|
+
const errorMessage = lastError.formatted || lastError.message;
|
|
160
|
+
const error = new Error(`Deployment failed after ${maxRetries} attempts: ${errorMessage}`);
|
|
161
|
+
if (lastError.formatted) {
|
|
162
|
+
error.formatted = lastError.formatted;
|
|
163
|
+
}
|
|
164
|
+
if (lastError.status) {
|
|
165
|
+
error.status = lastError.status;
|
|
166
|
+
}
|
|
167
|
+
if (lastError.data) {
|
|
168
|
+
error.data = lastError.data;
|
|
169
|
+
}
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
|
|
122
173
|
/**
|
|
123
174
|
* Sends deployment request using validateToken from validation step
|
|
124
175
|
* This is the second step in the deployment process
|
|
@@ -136,45 +187,18 @@ async function sendDeploymentRequest(url, envKey, validateToken, authConfig, opt
|
|
|
136
187
|
const validatedEnvKey = validateEnvironmentKey(envKey);
|
|
137
188
|
const maxRetries = options.maxRetries || 3;
|
|
138
189
|
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
if (!authConfig.clientId || !authConfig.clientSecret) {
|
|
142
|
-
throw new Error('Client ID and Client Secret are required for deployment. These should have been loaded during validation.');
|
|
143
|
-
}
|
|
144
|
-
const clientId = authConfig.clientId;
|
|
145
|
-
const clientSecret = authConfig.clientSecret;
|
|
190
|
+
// Validate credentials
|
|
191
|
+
validateDeploymentCredentials(authConfig);
|
|
146
192
|
|
|
147
|
-
// Build deployment
|
|
148
|
-
const
|
|
149
|
-
const deployData = {
|
|
150
|
-
validateToken: validateToken,
|
|
151
|
-
imageTag: imageTag
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
// Use centralized API client with retry logic
|
|
155
|
-
const pipelineAuthConfig = {
|
|
156
|
-
type: 'client-credentials',
|
|
157
|
-
clientId: clientId,
|
|
158
|
-
clientSecret: clientSecret
|
|
159
|
-
};
|
|
193
|
+
// Build deployment data
|
|
194
|
+
const { deployData, pipelineAuthConfig } = buildDeploymentData(validateToken, authConfig, options);
|
|
160
195
|
|
|
161
196
|
// Wrap API call with retry logic
|
|
162
197
|
let lastError;
|
|
163
198
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
164
199
|
try {
|
|
165
200
|
const response = await deployPipeline(url, validatedEnvKey, pipelineAuthConfig, deployData);
|
|
166
|
-
|
|
167
|
-
if (response.success) {
|
|
168
|
-
return response.data.data || response.data;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Handle deployment errors
|
|
172
|
-
if (response.status >= 400) {
|
|
173
|
-
const error = new Error(`Deployment request failed: ${response.formattedError || response.error || 'Unknown error'}`);
|
|
174
|
-
error.status = response.status;
|
|
175
|
-
error.data = response.data;
|
|
176
|
-
throw error;
|
|
177
|
-
}
|
|
201
|
+
return handleDeploymentResponse(response);
|
|
178
202
|
} catch (error) {
|
|
179
203
|
lastError = error;
|
|
180
204
|
if (attempt < maxRetries) {
|
|
@@ -185,19 +209,7 @@ async function sendDeploymentRequest(url, envKey, validateToken, authConfig, opt
|
|
|
185
209
|
}
|
|
186
210
|
}
|
|
187
211
|
|
|
188
|
-
|
|
189
|
-
const errorMessage = lastError.formatted || lastError.message;
|
|
190
|
-
const error = new Error(`Deployment failed after ${maxRetries} attempts: ${errorMessage}`);
|
|
191
|
-
if (lastError.formatted) {
|
|
192
|
-
error.formatted = lastError.formatted;
|
|
193
|
-
}
|
|
194
|
-
if (lastError.status) {
|
|
195
|
-
error.status = lastError.status;
|
|
196
|
-
}
|
|
197
|
-
if (lastError.data) {
|
|
198
|
-
error.data = lastError.data;
|
|
199
|
-
}
|
|
200
|
-
throw error;
|
|
212
|
+
throwDeploymentError(lastError, maxRetries);
|
|
201
213
|
}
|
|
202
214
|
|
|
203
215
|
/**
|
|
@@ -210,6 +222,52 @@ function isTerminalStatus(status) {
|
|
|
210
222
|
return status === 'completed' || status === 'failed' || status === 'cancelled';
|
|
211
223
|
}
|
|
212
224
|
|
|
225
|
+
/**
|
|
226
|
+
* Convert authConfig to pipeline auth config format
|
|
227
|
+
* @param {Object} authConfig - Authentication configuration
|
|
228
|
+
* @returns {Object} Pipeline auth config
|
|
229
|
+
*/
|
|
230
|
+
function convertToPipelineAuthConfig(authConfig) {
|
|
231
|
+
return authConfig.type === 'bearer'
|
|
232
|
+
? { type: 'bearer', token: authConfig.token }
|
|
233
|
+
: { type: 'client-credentials', clientId: authConfig.clientId, clientSecret: authConfig.clientSecret };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Process deployment status response
|
|
238
|
+
* @param {Object} response - API response
|
|
239
|
+
* @param {number} attempt - Current attempt number
|
|
240
|
+
* @param {number} maxAttempts - Maximum attempts
|
|
241
|
+
* @param {number} interval - Polling interval
|
|
242
|
+
* @param {string} deploymentId - Deployment ID for error messages
|
|
243
|
+
* @returns {Object|null} Deployment data if terminal, null if needs to continue polling
|
|
244
|
+
*/
|
|
245
|
+
async function processDeploymentStatusResponse(response, attempt, maxAttempts, interval, deploymentId) {
|
|
246
|
+
if (!response.success || !response.data) {
|
|
247
|
+
if (response.status === 404) {
|
|
248
|
+
throw new Error(`Deployment ${deploymentId || response.deploymentId || 'unknown'} not found`);
|
|
249
|
+
}
|
|
250
|
+
throw new Error(`Status check failed: ${response.formattedError || response.error || 'Unknown error'}`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const responseData = response.data;
|
|
254
|
+
const deploymentData = responseData.data || responseData;
|
|
255
|
+
const status = deploymentData.status;
|
|
256
|
+
|
|
257
|
+
if (isTerminalStatus(status)) {
|
|
258
|
+
return deploymentData;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const progress = deploymentData.progress || 0;
|
|
262
|
+
logger.log(chalk.blue(` Status: ${status} (${progress}%) (attempt ${attempt + 1}/${maxAttempts})`));
|
|
263
|
+
|
|
264
|
+
if (attempt < maxAttempts - 1) {
|
|
265
|
+
await new Promise(resolve => setTimeout(resolve, interval));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
|
|
213
271
|
/**
|
|
214
272
|
* Polls deployment status from controller
|
|
215
273
|
* Uses pipeline endpoint for CI/CD monitoring with minimal deployment info
|
|
@@ -227,37 +285,14 @@ async function pollDeploymentStatus(deploymentId, controllerUrl, envKey, authCon
|
|
|
227
285
|
const maxAttempts = options.maxAttempts || 60;
|
|
228
286
|
|
|
229
287
|
const validatedEnvKey = validateEnvironmentKey(envKey);
|
|
230
|
-
|
|
231
|
-
// Convert authConfig to format expected by API client
|
|
232
|
-
const pipelineAuthConfig = authConfig.type === 'bearer'
|
|
233
|
-
? { type: 'bearer', token: authConfig.token }
|
|
234
|
-
: { type: 'client-credentials', clientId: authConfig.clientId, clientSecret: authConfig.clientSecret };
|
|
288
|
+
const pipelineAuthConfig = convertToPipelineAuthConfig(authConfig);
|
|
235
289
|
|
|
236
290
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
237
291
|
try {
|
|
238
292
|
const response = await getPipelineDeployment(controllerUrl, validatedEnvKey, deploymentId, pipelineAuthConfig);
|
|
239
|
-
|
|
240
|
-
if (
|
|
241
|
-
|
|
242
|
-
const responseData = response.data;
|
|
243
|
-
const deploymentData = responseData.data || responseData;
|
|
244
|
-
const status = deploymentData.status;
|
|
245
|
-
|
|
246
|
-
if (isTerminalStatus(status)) {
|
|
247
|
-
return deploymentData;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const progress = deploymentData.progress || 0;
|
|
251
|
-
logger.log(chalk.blue(` Status: ${status} (${progress}%) (attempt ${attempt + 1}/${maxAttempts})`));
|
|
252
|
-
|
|
253
|
-
if (attempt < maxAttempts - 1) {
|
|
254
|
-
await new Promise(resolve => setTimeout(resolve, interval));
|
|
255
|
-
}
|
|
256
|
-
} else {
|
|
257
|
-
if (response.status === 404) {
|
|
258
|
-
throw new Error(`Deployment ${deploymentId} not found`);
|
|
259
|
-
}
|
|
260
|
-
throw new Error(`Status check failed: ${response.formattedError || response.error || 'Unknown error'}`);
|
|
293
|
+
const deploymentData = await processDeploymentStatusResponse(response, attempt, maxAttempts, interval, deploymentId);
|
|
294
|
+
if (deploymentData) {
|
|
295
|
+
return deploymentData;
|
|
261
296
|
}
|
|
262
297
|
} catch (error) {
|
|
263
298
|
if (error.message && error.message.includes('not found')) {
|
package/lib/diff.js
CHANGED
|
@@ -14,6 +14,65 @@ const path = require('path');
|
|
|
14
14
|
const chalk = require('chalk');
|
|
15
15
|
const logger = require('./utils/logger');
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Handle added field in comparison
|
|
19
|
+
* @param {string} key - Field key
|
|
20
|
+
* @param {*} value - Field value
|
|
21
|
+
* @param {string} path - Field path
|
|
22
|
+
* @param {Object} result - Result object to update
|
|
23
|
+
*/
|
|
24
|
+
function handleAddedField(key, value, path, result) {
|
|
25
|
+
result.added.push({
|
|
26
|
+
path: path,
|
|
27
|
+
value: value,
|
|
28
|
+
type: typeof value
|
|
29
|
+
});
|
|
30
|
+
result.identical = false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Handle removed field in comparison
|
|
35
|
+
* @param {string} key - Field key
|
|
36
|
+
* @param {*} value - Field value
|
|
37
|
+
* @param {string} path - Field path
|
|
38
|
+
* @param {Object} result - Result object to update
|
|
39
|
+
*/
|
|
40
|
+
function handleRemovedField(key, value, path, result) {
|
|
41
|
+
result.removed.push({
|
|
42
|
+
path: path,
|
|
43
|
+
value: value,
|
|
44
|
+
type: typeof value
|
|
45
|
+
});
|
|
46
|
+
result.identical = false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Handle changed field in comparison
|
|
51
|
+
* @param {*} oldValue - Old value
|
|
52
|
+
* @param {*} newValue - New value
|
|
53
|
+
* @param {string} path - Field path
|
|
54
|
+
* @param {Object} result - Result object to update
|
|
55
|
+
*/
|
|
56
|
+
function handleChangedField(oldValue, newValue, path, result) {
|
|
57
|
+
result.changed.push({
|
|
58
|
+
path: path,
|
|
59
|
+
oldValue: oldValue,
|
|
60
|
+
newValue: newValue,
|
|
61
|
+
oldType: typeof oldValue,
|
|
62
|
+
newType: typeof newValue
|
|
63
|
+
});
|
|
64
|
+
result.identical = false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Check if value is a nested object (not array, not null)
|
|
69
|
+
* @param {*} value - Value to check
|
|
70
|
+
* @returns {boolean} True if nested object
|
|
71
|
+
*/
|
|
72
|
+
function isNestedObject(value) {
|
|
73
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
74
|
+
}
|
|
75
|
+
|
|
17
76
|
/**
|
|
18
77
|
* Performs deep comparison of two objects
|
|
19
78
|
* Returns differences as structured result
|
|
@@ -40,20 +99,10 @@ function compareObjects(obj1, obj2, currentPath = '') {
|
|
|
40
99
|
const val2 = obj2 && obj2[key];
|
|
41
100
|
|
|
42
101
|
if (!(key in obj1)) {
|
|
43
|
-
result
|
|
44
|
-
path: newPath,
|
|
45
|
-
value: val2,
|
|
46
|
-
type: typeof val2
|
|
47
|
-
});
|
|
48
|
-
result.identical = false;
|
|
102
|
+
handleAddedField(key, val2, newPath, result);
|
|
49
103
|
} else if (!(key in obj2)) {
|
|
50
|
-
result
|
|
51
|
-
|
|
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)) {
|
|
104
|
+
handleRemovedField(key, val1, newPath, result);
|
|
105
|
+
} else if (isNestedObject(val1) && isNestedObject(val2)) {
|
|
57
106
|
// Recursively compare nested objects
|
|
58
107
|
const nestedResult = compareObjects(val1, val2, newPath);
|
|
59
108
|
result.added.push(...nestedResult.added);
|
|
@@ -63,14 +112,7 @@ function compareObjects(obj1, obj2, currentPath = '') {
|
|
|
63
112
|
result.identical = false;
|
|
64
113
|
}
|
|
65
114
|
} else if (JSON.stringify(val1) !== JSON.stringify(val2)) {
|
|
66
|
-
result
|
|
67
|
-
path: newPath,
|
|
68
|
-
oldValue: val1,
|
|
69
|
-
newValue: val2,
|
|
70
|
-
oldType: typeof val1,
|
|
71
|
-
newType: typeof val2
|
|
72
|
-
});
|
|
73
|
-
result.identical = false;
|
|
115
|
+
handleChangedField(val1, val2, newPath, result);
|
|
74
116
|
}
|
|
75
117
|
}
|
|
76
118
|
|