@aifabrix/builder 2.0.0 → 2.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/bin/aifabrix.js +9 -3
- package/jest.config.integration.js +30 -0
- package/lib/app-config.js +157 -0
- package/lib/app-deploy.js +233 -82
- package/lib/app-dockerfile.js +112 -0
- package/lib/app-prompts.js +244 -0
- package/lib/app-push.js +172 -0
- package/lib/app-run.js +235 -144
- package/lib/app.js +208 -274
- package/lib/audit-logger.js +2 -0
- package/lib/build.js +177 -125
- package/lib/cli.js +76 -86
- package/lib/commands/app.js +414 -0
- package/lib/commands/login.js +304 -0
- package/lib/config.js +78 -0
- package/lib/deployer.js +225 -81
- package/lib/env-reader.js +45 -30
- package/lib/generator.js +308 -191
- package/lib/github-generator.js +67 -7
- package/lib/infra.js +156 -61
- package/lib/push.js +105 -10
- package/lib/schema/application-schema.json +30 -2
- package/lib/schema/env-config.yaml +9 -1
- package/lib/schema/infrastructure-schema.json +589 -0
- package/lib/secrets.js +229 -24
- package/lib/template-validator.js +205 -0
- package/lib/templates.js +305 -170
- package/lib/utils/api.js +329 -0
- package/lib/utils/cli-utils.js +97 -0
- package/lib/utils/compose-generator.js +185 -0
- package/lib/utils/docker-build.js +173 -0
- package/lib/utils/dockerfile-utils.js +131 -0
- package/lib/utils/environment-checker.js +125 -0
- package/lib/utils/error-formatter.js +61 -0
- package/lib/utils/health-check.js +187 -0
- package/lib/utils/logger.js +53 -0
- package/lib/utils/template-helpers.js +223 -0
- package/lib/utils/variable-transformer.js +271 -0
- package/lib/validator.js +27 -112
- package/package.json +14 -10
- package/templates/README.md +75 -3
- package/templates/applications/keycloak/Dockerfile +36 -0
- package/templates/applications/keycloak/env.template +32 -0
- package/templates/applications/keycloak/rbac.yaml +37 -0
- package/templates/applications/keycloak/variables.yaml +56 -0
- package/templates/applications/miso-controller/Dockerfile +125 -0
- package/templates/applications/miso-controller/env.template +129 -0
- package/templates/applications/miso-controller/rbac.yaml +214 -0
- package/templates/applications/miso-controller/variables.yaml +56 -0
- package/templates/github/release.yaml.hbs +5 -26
- package/templates/github/steps/npm.hbs +24 -0
- package/templates/infra/compose.yaml +6 -6
- package/templates/python/docker-compose.hbs +19 -12
- package/templates/python/main.py +80 -0
- package/templates/python/requirements.txt +4 -0
- package/templates/typescript/Dockerfile.hbs +2 -2
- package/templates/typescript/docker-compose.hbs +19 -12
- package/templates/typescript/index.ts +116 -0
- package/templates/typescript/package.json +26 -0
- package/templates/typescript/tsconfig.json +24 -0
package/lib/deployer.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
const axios = require('axios');
|
|
13
13
|
const chalk = require('chalk');
|
|
14
14
|
const auditLogger = require('./audit-logger');
|
|
15
|
+
const logger = require('./utils/logger');
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Validates and sanitizes controller URL
|
|
@@ -25,13 +26,13 @@ function validateControllerUrl(url) {
|
|
|
25
26
|
throw new Error('Controller URL is required and must be a string');
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
// Must use HTTPS for security
|
|
29
|
-
if (!url.startsWith('https://')) {
|
|
30
|
-
throw new Error('Controller URL must use HTTPS (https://)');
|
|
29
|
+
// Must use HTTPS for security (allow http://localhost for local development)
|
|
30
|
+
if (!url.startsWith('https://') && !url.startsWith('http://localhost')) {
|
|
31
|
+
throw new Error('Controller URL must use HTTPS (https://) or http://localhost');
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
// Basic URL format validation
|
|
34
|
-
const urlPattern = /^https:\/\/[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*(:[0-9]+)?(\/.*)?$/;
|
|
35
|
+
const urlPattern = /^(https?):\/\/[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*(localhost)?(:[0-9]+)?(\/.*)?$/;
|
|
35
36
|
if (!urlPattern.test(url)) {
|
|
36
37
|
throw new Error('Invalid controller URL format');
|
|
37
38
|
}
|
|
@@ -41,36 +42,79 @@ function validateControllerUrl(url) {
|
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
/**
|
|
44
|
-
*
|
|
45
|
+
* Creates authentication headers for Client Credentials flow
|
|
45
46
|
*
|
|
46
|
-
* @
|
|
47
|
-
* @param {string}
|
|
48
|
-
* @
|
|
49
|
-
* @
|
|
50
|
-
* @returns {Promise<Object>} Deployment result from controller
|
|
51
|
-
* @throws {Error} If deployment fails
|
|
47
|
+
* @param {string} clientId - Application client ID
|
|
48
|
+
* @param {string} clientSecret - Application client secret
|
|
49
|
+
* @returns {Object} Headers object with authentication
|
|
50
|
+
* @throws {Error} If credentials are missing
|
|
52
51
|
*/
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
function createClientCredentialsHeaders(clientId, clientSecret) {
|
|
53
|
+
if (!clientId || !clientSecret) {
|
|
54
|
+
throw new Error('Client ID and Client Secret are required for authentication');
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
'x-client-id': clientId,
|
|
58
|
+
'x-client-secret': clientSecret
|
|
59
|
+
};
|
|
60
|
+
}
|
|
57
61
|
|
|
58
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Validates environment key
|
|
64
|
+
* @param {string} envKey - Environment key to validate
|
|
65
|
+
* @throws {Error} If environment key is invalid
|
|
66
|
+
*/
|
|
67
|
+
function validateEnvironmentKey(envKey) {
|
|
68
|
+
if (!envKey || typeof envKey !== 'string') {
|
|
69
|
+
throw new Error('Environment key is required and must be a string');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const validEnvironments = ['miso', 'dev', 'tst', 'pro'];
|
|
73
|
+
if (!validEnvironments.includes(envKey.toLowerCase())) {
|
|
74
|
+
throw new Error(`Invalid environment key: ${envKey}. Must be one of: ${validEnvironments.join(', ')}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return envKey.toLowerCase();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Creates deployment request configuration
|
|
82
|
+
* @function createDeploymentRequestConfig
|
|
83
|
+
* @param {string} clientId - Client ID
|
|
84
|
+
* @param {string} clientSecret - Client Secret
|
|
85
|
+
* @param {number} timeout - Request timeout
|
|
86
|
+
* @returns {Object} Request configuration
|
|
87
|
+
*/
|
|
88
|
+
function createDeploymentRequestConfig(clientId, clientSecret, timeout) {
|
|
89
|
+
return {
|
|
59
90
|
headers: {
|
|
60
91
|
'Content-Type': 'application/json',
|
|
61
|
-
'User-Agent': 'aifabrix-builder/2.0.0'
|
|
92
|
+
'User-Agent': 'aifabrix-builder/2.0.0',
|
|
93
|
+
...createClientCredentialsHeaders(clientId, clientSecret)
|
|
62
94
|
},
|
|
63
95
|
timeout,
|
|
64
|
-
validateStatus: (status) => status < 500
|
|
96
|
+
validateStatus: (status) => status < 500
|
|
65
97
|
};
|
|
98
|
+
}
|
|
66
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Handles deployment request retry logic
|
|
102
|
+
* @async
|
|
103
|
+
* @function handleDeploymentRetry
|
|
104
|
+
* @param {string} endpoint - API endpoint
|
|
105
|
+
* @param {Object} manifest - Deployment manifest
|
|
106
|
+
* @param {Object} requestConfig - Request configuration
|
|
107
|
+
* @param {number} maxRetries - Maximum retry attempts
|
|
108
|
+
* @returns {Promise<Object>} Deployment result
|
|
109
|
+
* @throws {Error} If deployment fails after retries
|
|
110
|
+
*/
|
|
111
|
+
async function handleDeploymentRetry(endpoint, manifest, requestConfig, maxRetries) {
|
|
67
112
|
let lastError;
|
|
68
113
|
|
|
69
114
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
70
115
|
try {
|
|
71
116
|
const response = await axios.post(endpoint, manifest, requestConfig);
|
|
72
117
|
|
|
73
|
-
// Check for HTTP errors
|
|
74
118
|
if (response.status >= 400) {
|
|
75
119
|
const error = new Error(`Controller returned error: ${response.status} ${response.statusText}`);
|
|
76
120
|
error.status = response.status;
|
|
@@ -84,41 +128,76 @@ async function sendDeploymentRequest(url, manifest, options = {}) {
|
|
|
84
128
|
}
|
|
85
129
|
|
|
86
130
|
return response.data;
|
|
87
|
-
|
|
88
131
|
} catch (error) {
|
|
89
132
|
lastError = error;
|
|
90
133
|
|
|
91
|
-
// Log retry attempt
|
|
92
134
|
if (attempt < maxRetries) {
|
|
93
|
-
const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000);
|
|
94
|
-
|
|
135
|
+
const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000);
|
|
136
|
+
logger.log(chalk.yellow(`⚠️ Deployment attempt ${attempt} failed, retrying in ${delay}ms...`));
|
|
95
137
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
96
138
|
}
|
|
97
139
|
}
|
|
98
140
|
}
|
|
99
141
|
|
|
100
|
-
// All retries failed
|
|
101
142
|
throw new Error(`Deployment failed after ${maxRetries} attempts: ${lastError.message}`);
|
|
102
143
|
}
|
|
103
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Sends deployment manifest to Miso Controller with Client Credentials authentication
|
|
147
|
+
*
|
|
148
|
+
* @async
|
|
149
|
+
* @param {string} url - Controller URL
|
|
150
|
+
* @param {string} envKey - Environment key (dev, tst, pro)
|
|
151
|
+
* @param {Object} manifest - Deployment manifest
|
|
152
|
+
* @param {string} clientId - Client ID for authentication
|
|
153
|
+
* @param {string} clientSecret - Client Secret for authentication
|
|
154
|
+
* @param {Object} options - Deployment options (timeout, retries, etc.)
|
|
155
|
+
* @returns {Promise<Object>} Deployment result from controller
|
|
156
|
+
* @throws {Error} If deployment fails
|
|
157
|
+
*/
|
|
158
|
+
async function sendDeploymentRequest(url, envKey, manifest, clientId, clientSecret, options = {}) {
|
|
159
|
+
const validatedEnvKey = validateEnvironmentKey(envKey);
|
|
160
|
+
const endpoint = `${url}/api/v1/pipeline/${validatedEnvKey}/deploy`;
|
|
161
|
+
const timeout = options.timeout || 30000;
|
|
162
|
+
const maxRetries = options.maxRetries || 3;
|
|
163
|
+
const requestConfig = createDeploymentRequestConfig(clientId, clientSecret, timeout);
|
|
164
|
+
|
|
165
|
+
return handleDeploymentRetry(endpoint, manifest, requestConfig, maxRetries);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Checks if deployment status is terminal
|
|
170
|
+
* @function isTerminalStatus
|
|
171
|
+
* @param {string} status - Deployment status
|
|
172
|
+
* @returns {boolean} True if status is terminal
|
|
173
|
+
*/
|
|
174
|
+
function isTerminalStatus(status) {
|
|
175
|
+
return status === 'completed' || status === 'failed' || status === 'cancelled';
|
|
176
|
+
}
|
|
177
|
+
|
|
104
178
|
/**
|
|
105
179
|
* Polls deployment status from controller
|
|
106
180
|
*
|
|
107
181
|
* @async
|
|
108
182
|
* @param {string} deploymentId - Deployment ID to poll
|
|
109
183
|
* @param {string} controllerUrl - Controller URL
|
|
184
|
+
* @param {string} envKey - Environment key
|
|
185
|
+
* @param {string} clientId - Client ID for authentication
|
|
186
|
+
* @param {string} clientSecret - Client Secret for authentication
|
|
110
187
|
* @param {Object} options - Polling options (interval, maxAttempts, etc.)
|
|
111
188
|
* @returns {Promise<Object>} Deployment status
|
|
112
189
|
*/
|
|
113
|
-
async function pollDeploymentStatus(deploymentId, controllerUrl, options = {}) {
|
|
190
|
+
async function pollDeploymentStatus(deploymentId, controllerUrl, envKey, clientId, clientSecret, options = {}) {
|
|
114
191
|
const interval = options.interval || 5000;
|
|
115
|
-
const maxAttempts = options.maxAttempts || 60;
|
|
192
|
+
const maxAttempts = options.maxAttempts || 60;
|
|
116
193
|
|
|
117
|
-
const
|
|
194
|
+
const validatedEnvKey = validateEnvironmentKey(envKey);
|
|
195
|
+
const statusEndpoint = `${controllerUrl}/api/v1/environments/${validatedEnvKey}/deployments/${deploymentId}`;
|
|
118
196
|
|
|
119
197
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
120
198
|
try {
|
|
121
199
|
const response = await axios.get(statusEndpoint, {
|
|
200
|
+
headers: createClientCredentialsHeaders(clientId, clientSecret),
|
|
122
201
|
timeout: 10000,
|
|
123
202
|
validateStatus: (status) => status < 500
|
|
124
203
|
});
|
|
@@ -126,15 +205,12 @@ async function pollDeploymentStatus(deploymentId, controllerUrl, options = {}) {
|
|
|
126
205
|
if (response.status === 200) {
|
|
127
206
|
const status = response.data.status;
|
|
128
207
|
|
|
129
|
-
|
|
130
|
-
if (status === 'completed' || status === 'failed' || status === 'cancelled') {
|
|
208
|
+
if (isTerminalStatus(status)) {
|
|
131
209
|
return response.data;
|
|
132
210
|
}
|
|
133
211
|
|
|
134
|
-
|
|
135
|
-
console.log(chalk.blue(` Status: ${status} (attempt ${attempt + 1}/${maxAttempts})`));
|
|
212
|
+
logger.log(chalk.blue(` Status: ${status} (attempt ${attempt + 1}/${maxAttempts})`));
|
|
136
213
|
|
|
137
|
-
// Wait before next poll
|
|
138
214
|
if (attempt < maxAttempts - 1) {
|
|
139
215
|
await new Promise(resolve => setTimeout(resolve, interval));
|
|
140
216
|
}
|
|
@@ -174,75 +250,141 @@ function handleDeploymentError(error) {
|
|
|
174
250
|
}
|
|
175
251
|
|
|
176
252
|
/**
|
|
177
|
-
*
|
|
253
|
+
* Sends deployment request to controller
|
|
254
|
+
* @async
|
|
255
|
+
* @param {string} url - Controller URL
|
|
256
|
+
* @param {string} validatedEnvKey - Validated environment key
|
|
257
|
+
* @param {Object} manifest - Deployment manifest
|
|
258
|
+
* @param {string} clientId - Client ID
|
|
259
|
+
* @param {string} clientSecret - Client Secret
|
|
260
|
+
* @param {Object} options - Deployment options
|
|
261
|
+
* @returns {Promise<Object>} Deployment result
|
|
262
|
+
*/
|
|
263
|
+
async function sendDeployment(url, validatedEnvKey, manifest, clientId, clientSecret, options) {
|
|
264
|
+
logger.log(chalk.blue(`📤 Sending deployment request to ${url}/api/v1/pipeline/${validatedEnvKey}/deploy...`));
|
|
265
|
+
const result = await sendDeploymentRequest(url, validatedEnvKey, manifest, clientId, clientSecret, {
|
|
266
|
+
timeout: options.timeout || 30000,
|
|
267
|
+
maxRetries: options.maxRetries || 3
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
if (result.deploymentId) {
|
|
271
|
+
auditLogger.logDeploymentSuccess(manifest.key, result.deploymentId, url);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return result;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Polls deployment status if enabled
|
|
279
|
+
* @async
|
|
280
|
+
* @param {Object} result - Deployment result
|
|
281
|
+
* @param {string} url - Controller URL
|
|
282
|
+
* @param {string} validatedEnvKey - Validated environment key
|
|
283
|
+
* @param {string} clientId - Client ID
|
|
284
|
+
* @param {string} clientSecret - Client Secret
|
|
285
|
+
* @param {Object} options - Deployment options
|
|
286
|
+
* @returns {Promise<Object>} Deployment result with status
|
|
287
|
+
*/
|
|
288
|
+
async function pollDeployment(result, url, validatedEnvKey, clientId, clientSecret, options) {
|
|
289
|
+
if (!options.poll || !result.deploymentId) {
|
|
290
|
+
return result;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
logger.log(chalk.blue(`\n⏳ Polling deployment status (${options.pollInterval || 5000}ms intervals)...`));
|
|
294
|
+
const status = await pollDeploymentStatus(
|
|
295
|
+
result.deploymentId,
|
|
296
|
+
url,
|
|
297
|
+
validatedEnvKey,
|
|
298
|
+
clientId,
|
|
299
|
+
clientSecret,
|
|
300
|
+
{
|
|
301
|
+
interval: options.pollInterval || 5000,
|
|
302
|
+
maxAttempts: options.pollMaxAttempts || 60
|
|
303
|
+
}
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
result.status = status;
|
|
307
|
+
return result;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Handles deployment errors and maps them to user-friendly messages
|
|
312
|
+
* @param {Error} error - Error object
|
|
313
|
+
* @param {string} manifestKey - Manifest key for audit logging
|
|
314
|
+
* @param {string} url - Controller URL for audit logging
|
|
315
|
+
* @throws {Error} User-friendly error message
|
|
316
|
+
*/
|
|
317
|
+
function handleDeploymentErrors(error, manifestKey, url) {
|
|
318
|
+
auditLogger.logDeploymentFailure(manifestKey, url, error);
|
|
319
|
+
|
|
320
|
+
const safeError = handleDeploymentError(error);
|
|
321
|
+
|
|
322
|
+
if (safeError.status === 401 || safeError.status === 403) {
|
|
323
|
+
throw new Error('Authentication failed. Check your client ID and client secret.');
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (safeError.status === 400) {
|
|
327
|
+
throw new Error('Invalid deployment manifest. Please check your configuration.');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (safeError.status === 404) {
|
|
331
|
+
throw new Error('Controller endpoint not found. Check the controller URL and environment.');
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (safeError.code === 'ECONNREFUSED') {
|
|
335
|
+
throw new Error('Cannot connect to controller. Check if the controller is running.');
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (safeError.code === 'ENOTFOUND') {
|
|
339
|
+
throw new Error('Controller hostname not found. Check your controller URL.');
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (safeError.timeout) {
|
|
343
|
+
throw new Error('Request timed out. The controller may be overloaded.');
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
throw new Error(safeError.message);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Deploys application to Miso Controller with Client Credentials authentication
|
|
178
351
|
* Main orchestrator for the deployment process
|
|
179
352
|
*
|
|
180
353
|
* @async
|
|
181
354
|
* @param {Object} manifest - Deployment manifest
|
|
182
355
|
* @param {string} controllerUrl - Controller URL
|
|
356
|
+
* @param {string} envKey - Environment key (dev, tst, pro)
|
|
357
|
+
* @param {string} clientId - Client ID for authentication
|
|
358
|
+
* @param {string} clientSecret - Client Secret for authentication
|
|
183
359
|
* @param {Object} options - Deployment options
|
|
184
360
|
* @returns {Promise<Object>} Deployment result
|
|
185
361
|
* @throws {Error} If deployment fails
|
|
186
362
|
*/
|
|
187
|
-
async function deployToController(manifest, controllerUrl, options = {}) {
|
|
363
|
+
async function deployToController(manifest, controllerUrl, envKey, clientId, clientSecret, options = {}) {
|
|
364
|
+
// Validate inputs
|
|
365
|
+
if (!envKey) {
|
|
366
|
+
throw new Error('Environment key is required');
|
|
367
|
+
}
|
|
368
|
+
if (!clientId || !clientSecret) {
|
|
369
|
+
throw new Error('Client ID and Client Secret are required for authentication');
|
|
370
|
+
}
|
|
371
|
+
|
|
188
372
|
// Validate and sanitize controller URL
|
|
189
373
|
const url = validateControllerUrl(controllerUrl);
|
|
374
|
+
const validatedEnvKey = validateEnvironmentKey(envKey);
|
|
190
375
|
|
|
191
376
|
// Log deployment attempt for audit
|
|
192
377
|
auditLogger.logDeploymentAttempt(manifest.key, url, options);
|
|
193
378
|
|
|
194
379
|
try {
|
|
195
380
|
// Send deployment request
|
|
196
|
-
|
|
197
|
-
const result = await sendDeploymentRequest(url, manifest, {
|
|
198
|
-
timeout: options.timeout || 30000,
|
|
199
|
-
maxRetries: options.maxRetries || 3
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
// Log success
|
|
203
|
-
if (result.deploymentId) {
|
|
204
|
-
auditLogger.logDeploymentSuccess(manifest.key, result.deploymentId, url);
|
|
205
|
-
}
|
|
381
|
+
const result = await sendDeployment(url, validatedEnvKey, manifest, clientId, clientSecret, options);
|
|
206
382
|
|
|
207
383
|
// Poll for deployment status if enabled
|
|
208
|
-
|
|
209
|
-
console.log(chalk.blue(`\n⏳ Polling deployment status (${options.pollInterval || 5000}ms intervals)...`));
|
|
210
|
-
const status = await pollDeploymentStatus(
|
|
211
|
-
result.deploymentId,
|
|
212
|
-
url,
|
|
213
|
-
{
|
|
214
|
-
interval: options.pollInterval || 5000,
|
|
215
|
-
maxAttempts: options.pollMaxAttempts || 60
|
|
216
|
-
}
|
|
217
|
-
);
|
|
218
|
-
result.status = status;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
return result;
|
|
384
|
+
return await pollDeployment(result, url, validatedEnvKey, clientId, clientSecret, options);
|
|
222
385
|
|
|
223
386
|
} catch (error) {
|
|
224
|
-
|
|
225
|
-
auditLogger.logDeploymentFailure(manifest.key, url, error);
|
|
226
|
-
|
|
227
|
-
// Handle and re-throw with safe error
|
|
228
|
-
const safeError = handleDeploymentError(error);
|
|
229
|
-
|
|
230
|
-
// Provide user-friendly error messages
|
|
231
|
-
if (safeError.status === 401 || safeError.status === 403) {
|
|
232
|
-
throw new Error('Authentication failed. Check your deployment key.');
|
|
233
|
-
} else if (safeError.status === 400) {
|
|
234
|
-
throw new Error('Invalid deployment manifest. Please check your configuration.');
|
|
235
|
-
} else if (safeError.status === 404) {
|
|
236
|
-
throw new Error('Controller endpoint not found. Check the controller URL.');
|
|
237
|
-
} else if (safeError.code === 'ECONNREFUSED') {
|
|
238
|
-
throw new Error('Cannot connect to controller. Check if the controller is running.');
|
|
239
|
-
} else if (safeError.code === 'ENOTFOUND') {
|
|
240
|
-
throw new Error('Controller hostname not found. Check your controller URL.');
|
|
241
|
-
} else if (safeError.timeout) {
|
|
242
|
-
throw new Error('Request timed out. The controller may be overloaded.');
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
throw new Error(safeError.message);
|
|
387
|
+
handleDeploymentErrors(error, manifest.key, url);
|
|
246
388
|
}
|
|
247
389
|
}
|
|
248
390
|
|
|
@@ -251,6 +393,8 @@ module.exports = {
|
|
|
251
393
|
sendDeploymentRequest,
|
|
252
394
|
pollDeploymentStatus,
|
|
253
395
|
validateControllerUrl,
|
|
254
|
-
handleDeploymentError
|
|
396
|
+
handleDeploymentError,
|
|
397
|
+
createClientCredentialsHeaders,
|
|
398
|
+
validateEnvironmentKey
|
|
255
399
|
};
|
|
256
400
|
|
package/lib/env-reader.js
CHANGED
|
@@ -177,7 +177,48 @@ function sanitizeEnvValue(value) {
|
|
|
177
177
|
}
|
|
178
178
|
|
|
179
179
|
/**
|
|
180
|
-
*
|
|
180
|
+
* Filters existing environment variables for template
|
|
181
|
+
* @function filterExistingEnvVars
|
|
182
|
+
* @param {Object} convertedEnv - Converted environment variables
|
|
183
|
+
* @returns {string[]} Filtered environment variable lines
|
|
184
|
+
*/
|
|
185
|
+
function filterExistingEnvVars(convertedEnv) {
|
|
186
|
+
const excludedPrefixes = [
|
|
187
|
+
'NODE_ENV', 'PORT', 'APP_NAME', 'LOG_LEVEL',
|
|
188
|
+
'DB_', 'DATABASE_', 'REDIS_', 'STORAGE_',
|
|
189
|
+
'JWT_', 'AUTH_', 'SESSION_'
|
|
190
|
+
];
|
|
191
|
+
|
|
192
|
+
return Object.entries(convertedEnv)
|
|
193
|
+
.filter(([key]) => !excludedPrefixes.some(prefix => key.startsWith(prefix)))
|
|
194
|
+
.map(([key, value]) => `${key}=${value}`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Validates and processes environment variables
|
|
199
|
+
* @function validateAndProcessEnvVars
|
|
200
|
+
* @param {Object} existingEnv - Existing environment variables
|
|
201
|
+
* @returns {Object} Validation result with warnings
|
|
202
|
+
*/
|
|
203
|
+
function validateAndProcessEnvVars(existingEnv) {
|
|
204
|
+
const warnings = [];
|
|
205
|
+
|
|
206
|
+
Object.entries(existingEnv).forEach(([key, value]) => {
|
|
207
|
+
if (!validateEnvKey(key)) {
|
|
208
|
+
warnings.push(`Invalid environment variable name: ${key}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const sanitizedValue = sanitizeEnvValue(value);
|
|
212
|
+
if (sanitizedValue !== value) {
|
|
213
|
+
warnings.push(`Sanitized value for ${key} (removed special characters)`);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
return { warnings };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Generates environment template with security considerations
|
|
181
222
|
* @param {Object} config - Application configuration
|
|
182
223
|
* @param {Object} existingEnv - Existing environment variables
|
|
183
224
|
* @returns {Promise<Object>} Template generation result
|
|
@@ -190,41 +231,15 @@ async function generateEnvTemplate(config, existingEnv = {}) {
|
|
|
190
231
|
};
|
|
191
232
|
|
|
192
233
|
try {
|
|
193
|
-
// Convert existing environment variables
|
|
194
234
|
const convertedEnv = convertToEnvTemplate(existingEnv, {});
|
|
195
|
-
|
|
196
|
-
// Extract secrets for secrets.yaml
|
|
197
235
|
result.secrets = generateSecretsFromEnv(existingEnv);
|
|
236
|
+
const validation = validateAndProcessEnvVars(existingEnv);
|
|
237
|
+
result.warnings = validation.warnings;
|
|
198
238
|
|
|
199
|
-
// Validate environment variables
|
|
200
|
-
Object.entries(existingEnv).forEach(([key, value]) => {
|
|
201
|
-
if (!validateEnvKey(key)) {
|
|
202
|
-
result.warnings.push(`Invalid environment variable name: ${key}`);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const sanitizedValue = sanitizeEnvValue(value);
|
|
206
|
-
if (sanitizedValue !== value) {
|
|
207
|
-
result.warnings.push(`Sanitized value for ${key} (removed special characters)`);
|
|
208
|
-
}
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
// Generate template content
|
|
212
239
|
const { generateEnvTemplate: generateTemplate } = require('./templates');
|
|
213
240
|
const baseTemplate = generateTemplate(config);
|
|
214
241
|
|
|
215
|
-
|
|
216
|
-
const existingEnvSection = [];
|
|
217
|
-
Object.entries(convertedEnv).forEach(([key, value]) => {
|
|
218
|
-
if (!key.startsWith('NODE_ENV') && !key.startsWith('PORT') &&
|
|
219
|
-
!key.startsWith('APP_NAME') && !key.startsWith('LOG_LEVEL') &&
|
|
220
|
-
!key.startsWith('DB_') && !key.startsWith('DATABASE_') &&
|
|
221
|
-
!key.startsWith('REDIS_') && !key.startsWith('STORAGE_') &&
|
|
222
|
-
!key.startsWith('JWT_') && !key.startsWith('AUTH_') &&
|
|
223
|
-
!key.startsWith('SESSION_')) {
|
|
224
|
-
existingEnvSection.push(`${key}=${value}`);
|
|
225
|
-
}
|
|
226
|
-
});
|
|
227
|
-
|
|
242
|
+
const existingEnvSection = filterExistingEnvVars(convertedEnv);
|
|
228
243
|
if (existingEnvSection.length > 0) {
|
|
229
244
|
result.template = baseTemplate + '\n\n# Existing Environment Variables\n' + existingEnvSection.join('\n');
|
|
230
245
|
} else {
|