@aifabrix/builder 2.0.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/LICENSE +21 -0
- package/README.md +75 -0
- package/bin/aifabrix.js +51 -0
- package/lib/app-deploy.js +209 -0
- package/lib/app-run.js +291 -0
- package/lib/app.js +472 -0
- package/lib/audit-logger.js +162 -0
- package/lib/build.js +313 -0
- package/lib/cli.js +307 -0
- package/lib/deployer.js +256 -0
- package/lib/env-reader.js +250 -0
- package/lib/generator.js +361 -0
- package/lib/github-generator.js +220 -0
- package/lib/infra.js +300 -0
- package/lib/key-generator.js +93 -0
- package/lib/push.js +141 -0
- package/lib/schema/application-schema.json +649 -0
- package/lib/schema/env-config.yaml +15 -0
- package/lib/secrets.js +282 -0
- package/lib/templates.js +301 -0
- package/lib/validator.js +377 -0
- package/package.json +59 -0
- package/templates/README.md +51 -0
- package/templates/github/ci.yaml.hbs +15 -0
- package/templates/github/pr-checks.yaml.hbs +35 -0
- package/templates/github/release.yaml.hbs +79 -0
- package/templates/github/test.hbs +11 -0
- package/templates/github/test.yaml.hbs +11 -0
- package/templates/infra/compose.yaml +93 -0
- package/templates/python/Dockerfile.hbs +49 -0
- package/templates/python/docker-compose.hbs +69 -0
- package/templates/typescript/Dockerfile.hbs +46 -0
- package/templates/typescript/docker-compose.hbs +69 -0
package/lib/deployer.js
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Fabrix Builder Deployment Module
|
|
3
|
+
*
|
|
4
|
+
* Handles deployment to Miso Controller API with ISO 27001 security measures.
|
|
5
|
+
* Manages authentication, validation, and API communication for deployments.
|
|
6
|
+
*
|
|
7
|
+
* @fileoverview Deployment orchestration and API communication
|
|
8
|
+
* @author AI Fabrix Team
|
|
9
|
+
* @version 2.0.0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const axios = require('axios');
|
|
13
|
+
const chalk = require('chalk');
|
|
14
|
+
const auditLogger = require('./audit-logger');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Validates and sanitizes controller URL
|
|
18
|
+
* Enforces HTTPS-only communication for security
|
|
19
|
+
*
|
|
20
|
+
* @param {string} url - Controller URL to validate
|
|
21
|
+
* @throws {Error} If URL is invalid or uses HTTP
|
|
22
|
+
*/
|
|
23
|
+
function validateControllerUrl(url) {
|
|
24
|
+
if (!url || typeof url !== 'string') {
|
|
25
|
+
throw new Error('Controller URL is required and must be a string');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Must use HTTPS for security
|
|
29
|
+
if (!url.startsWith('https://')) {
|
|
30
|
+
throw new Error('Controller URL must use HTTPS (https://)');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 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
|
+
if (!urlPattern.test(url)) {
|
|
36
|
+
throw new Error('Invalid controller URL format');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Remove trailing slash if present
|
|
40
|
+
return url.replace(/\/$/, '');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Sends deployment manifest to Miso Controller
|
|
45
|
+
*
|
|
46
|
+
* @async
|
|
47
|
+
* @param {string} url - Controller URL
|
|
48
|
+
* @param {Object} manifest - Deployment manifest
|
|
49
|
+
* @param {Object} options - Deployment options (timeout, retries, etc.)
|
|
50
|
+
* @returns {Promise<Object>} Deployment result from controller
|
|
51
|
+
* @throws {Error} If deployment fails
|
|
52
|
+
*/
|
|
53
|
+
async function sendDeploymentRequest(url, manifest, options = {}) {
|
|
54
|
+
const endpoint = `${url}/api/pipeline/deploy`;
|
|
55
|
+
const timeout = options.timeout || 30000;
|
|
56
|
+
const maxRetries = options.maxRetries || 3;
|
|
57
|
+
|
|
58
|
+
const requestConfig = {
|
|
59
|
+
headers: {
|
|
60
|
+
'Content-Type': 'application/json',
|
|
61
|
+
'User-Agent': 'aifabrix-builder/2.0.0'
|
|
62
|
+
},
|
|
63
|
+
timeout,
|
|
64
|
+
validateStatus: (status) => status < 500 // Don't throw on 4xx errors
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
let lastError;
|
|
68
|
+
|
|
69
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
70
|
+
try {
|
|
71
|
+
const response = await axios.post(endpoint, manifest, requestConfig);
|
|
72
|
+
|
|
73
|
+
// Check for HTTP errors
|
|
74
|
+
if (response.status >= 400) {
|
|
75
|
+
const error = new Error(`Controller returned error: ${response.status} ${response.statusText}`);
|
|
76
|
+
error.status = response.status;
|
|
77
|
+
error.response = {
|
|
78
|
+
status: response.status,
|
|
79
|
+
statusText: response.statusText,
|
|
80
|
+
data: response.data
|
|
81
|
+
};
|
|
82
|
+
error.data = response.data;
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return response.data;
|
|
87
|
+
|
|
88
|
+
} catch (error) {
|
|
89
|
+
lastError = error;
|
|
90
|
+
|
|
91
|
+
// Log retry attempt
|
|
92
|
+
if (attempt < maxRetries) {
|
|
93
|
+
const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000); // Exponential backoff, max 5s
|
|
94
|
+
console.log(chalk.yellow(`⚠️ Deployment attempt ${attempt} failed, retrying in ${delay}ms...`));
|
|
95
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// All retries failed
|
|
101
|
+
throw new Error(`Deployment failed after ${maxRetries} attempts: ${lastError.message}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Polls deployment status from controller
|
|
106
|
+
*
|
|
107
|
+
* @async
|
|
108
|
+
* @param {string} deploymentId - Deployment ID to poll
|
|
109
|
+
* @param {string} controllerUrl - Controller URL
|
|
110
|
+
* @param {Object} options - Polling options (interval, maxAttempts, etc.)
|
|
111
|
+
* @returns {Promise<Object>} Deployment status
|
|
112
|
+
*/
|
|
113
|
+
async function pollDeploymentStatus(deploymentId, controllerUrl, options = {}) {
|
|
114
|
+
const interval = options.interval || 5000;
|
|
115
|
+
const maxAttempts = options.maxAttempts || 60; // 5 minutes max
|
|
116
|
+
|
|
117
|
+
const statusEndpoint = `${controllerUrl}/api/pipeline/status/${deploymentId}`;
|
|
118
|
+
|
|
119
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
120
|
+
try {
|
|
121
|
+
const response = await axios.get(statusEndpoint, {
|
|
122
|
+
timeout: 10000,
|
|
123
|
+
validateStatus: (status) => status < 500
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
if (response.status === 200) {
|
|
127
|
+
const status = response.data.status;
|
|
128
|
+
|
|
129
|
+
// Terminal states
|
|
130
|
+
if (status === 'completed' || status === 'failed' || status === 'cancelled') {
|
|
131
|
+
return response.data;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Log progress
|
|
135
|
+
console.log(chalk.blue(` Status: ${status} (attempt ${attempt + 1}/${maxAttempts})`));
|
|
136
|
+
|
|
137
|
+
// Wait before next poll
|
|
138
|
+
if (attempt < maxAttempts - 1) {
|
|
139
|
+
await new Promise(resolve => setTimeout(resolve, interval));
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
throw new Error(`Status check failed: ${response.status}`);
|
|
143
|
+
}
|
|
144
|
+
} catch (error) {
|
|
145
|
+
if (error.response && error.response.status === 404) {
|
|
146
|
+
throw new Error(`Deployment ${deploymentId} not found`);
|
|
147
|
+
}
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
throw new Error('Deployment timeout: Maximum polling attempts reached');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Handles deployment errors with security-aware messages
|
|
157
|
+
*
|
|
158
|
+
* @param {Error} error - Error to handle
|
|
159
|
+
* @returns {Object} Structured error information
|
|
160
|
+
*/
|
|
161
|
+
function handleDeploymentError(error) {
|
|
162
|
+
const safeError = {
|
|
163
|
+
message: error.message,
|
|
164
|
+
code: error.code || 'UNKNOWN',
|
|
165
|
+
timeout: error.code === 'ECONNABORTED',
|
|
166
|
+
status: error.status || error.response?.status,
|
|
167
|
+
data: error.data || error.response?.data
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// Mask sensitive information in error messages
|
|
171
|
+
safeError.message = auditLogger.maskSensitiveData(safeError.message);
|
|
172
|
+
|
|
173
|
+
return safeError;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Deploys application to Miso Controller
|
|
178
|
+
* Main orchestrator for the deployment process
|
|
179
|
+
*
|
|
180
|
+
* @async
|
|
181
|
+
* @param {Object} manifest - Deployment manifest
|
|
182
|
+
* @param {string} controllerUrl - Controller URL
|
|
183
|
+
* @param {Object} options - Deployment options
|
|
184
|
+
* @returns {Promise<Object>} Deployment result
|
|
185
|
+
* @throws {Error} If deployment fails
|
|
186
|
+
*/
|
|
187
|
+
async function deployToController(manifest, controllerUrl, options = {}) {
|
|
188
|
+
// Validate and sanitize controller URL
|
|
189
|
+
const url = validateControllerUrl(controllerUrl);
|
|
190
|
+
|
|
191
|
+
// Log deployment attempt for audit
|
|
192
|
+
auditLogger.logDeploymentAttempt(manifest.key, url, options);
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
// Send deployment request
|
|
196
|
+
console.log(chalk.blue(`📤 Sending deployment request to ${url}...`));
|
|
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
|
+
}
|
|
206
|
+
|
|
207
|
+
// Poll for deployment status if enabled
|
|
208
|
+
if (options.poll && result.deploymentId) {
|
|
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;
|
|
222
|
+
|
|
223
|
+
} catch (error) {
|
|
224
|
+
// Log failure for audit
|
|
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);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
module.exports = {
|
|
250
|
+
deployToController,
|
|
251
|
+
sendDeploymentRequest,
|
|
252
|
+
pollDeploymentStatus,
|
|
253
|
+
validateControllerUrl,
|
|
254
|
+
handleDeploymentError
|
|
255
|
+
};
|
|
256
|
+
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment File Reader and Converter Module
|
|
3
|
+
*
|
|
4
|
+
* Handles reading existing .env files and converting them to templates
|
|
5
|
+
* following ISO 27001 security standards for sensitive data handling
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs').promises;
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Read existing .env file from application folder
|
|
13
|
+
* @param {string} appPath - Path to application directory
|
|
14
|
+
* @returns {Promise<Object|null>} Parsed environment variables or null if not found
|
|
15
|
+
*/
|
|
16
|
+
async function readExistingEnv(appPath) {
|
|
17
|
+
const envPath = path.join(appPath, '.env');
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
await fs.access(envPath);
|
|
21
|
+
const content = await fs.readFile(envPath, 'utf8');
|
|
22
|
+
return parseEnvContent(content);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
if (error.code === 'ENOENT') {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
throw new Error(`Failed to read .env file: ${error.message}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Parse .env file content into key-value pairs
|
|
33
|
+
* @param {string} content - Raw .env file content
|
|
34
|
+
* @returns {Object} Parsed environment variables
|
|
35
|
+
*/
|
|
36
|
+
function parseEnvContent(content) {
|
|
37
|
+
const envVars = {};
|
|
38
|
+
const lines = content.split('\n');
|
|
39
|
+
|
|
40
|
+
for (const line of lines) {
|
|
41
|
+
const trimmedLine = line.trim();
|
|
42
|
+
|
|
43
|
+
// Skip empty lines and comments
|
|
44
|
+
if (!trimmedLine || trimmedLine.startsWith('#')) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Parse key=value pairs
|
|
49
|
+
const equalIndex = trimmedLine.indexOf('=');
|
|
50
|
+
if (equalIndex === -1) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const key = trimmedLine.substring(0, equalIndex).trim();
|
|
55
|
+
let value = trimmedLine.substring(equalIndex + 1).trim();
|
|
56
|
+
|
|
57
|
+
// Remove quotes if present
|
|
58
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
59
|
+
(value.startsWith('\'') && value.endsWith('\''))) {
|
|
60
|
+
value = value.slice(1, -1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
envVars[key] = value;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return envVars;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Determine if a value should be converted to kv:// reference
|
|
71
|
+
* @param {string} key - Environment variable key
|
|
72
|
+
* @param {string} value - Environment variable value
|
|
73
|
+
* @returns {boolean} True if value should be treated as sensitive
|
|
74
|
+
*/
|
|
75
|
+
function detectSensitiveValue(key, value) {
|
|
76
|
+
// Check key patterns for sensitive data
|
|
77
|
+
const sensitiveKeyPatterns = [
|
|
78
|
+
/password/i,
|
|
79
|
+
/secret/i,
|
|
80
|
+
/key/i,
|
|
81
|
+
/token/i,
|
|
82
|
+
/api[_-]?key/i,
|
|
83
|
+
/private/i,
|
|
84
|
+
/auth/i,
|
|
85
|
+
/credential/i,
|
|
86
|
+
/passwd/i,
|
|
87
|
+
/pwd/i
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
for (const pattern of sensitiveKeyPatterns) {
|
|
91
|
+
if (pattern.test(key)) {
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Check value patterns for sensitive data
|
|
97
|
+
const sensitiveValuePatterns = [
|
|
98
|
+
// UUIDs (8-4-4-4-12 format)
|
|
99
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
|
|
100
|
+
// Long random strings (>32 characters)
|
|
101
|
+
/^[a-zA-Z0-9+/=]{32,}$/,
|
|
102
|
+
// JWT tokens (three base64 parts separated by dots)
|
|
103
|
+
/^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+$/,
|
|
104
|
+
// Hex strings (>16 characters)
|
|
105
|
+
/^[0-9a-f]{16,}$/i
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
for (const pattern of sensitiveValuePatterns) {
|
|
109
|
+
if (pattern.test(value)) {
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Convert existing .env variables to env.template format
|
|
119
|
+
* @param {Object} existingEnv - Existing environment variables
|
|
120
|
+
* @param {Object} requiredVars - Required variables for the application
|
|
121
|
+
* @returns {Object} Merged environment variables with sensitive values converted
|
|
122
|
+
*/
|
|
123
|
+
function convertToEnvTemplate(existingEnv, requiredVars) {
|
|
124
|
+
const convertedEnv = { ...requiredVars };
|
|
125
|
+
|
|
126
|
+
// Process existing environment variables
|
|
127
|
+
Object.entries(existingEnv).forEach(([key, value]) => {
|
|
128
|
+
if (detectSensitiveValue(key, value)) {
|
|
129
|
+
// Convert sensitive values to kv:// references
|
|
130
|
+
convertedEnv[key] = `kv://${key.toLowerCase().replace(/[^a-z0-9]/g, '-')}`;
|
|
131
|
+
} else {
|
|
132
|
+
// Keep non-sensitive values as-is
|
|
133
|
+
convertedEnv[key] = value;
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
return convertedEnv;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Extract sensitive values for secrets.yaml generation
|
|
142
|
+
* @param {Object} envVars - Environment variables
|
|
143
|
+
* @returns {Object} Object suitable for secrets.yaml
|
|
144
|
+
*/
|
|
145
|
+
function generateSecretsFromEnv(envVars) {
|
|
146
|
+
const secrets = {};
|
|
147
|
+
|
|
148
|
+
Object.entries(envVars).forEach(([key, value]) => {
|
|
149
|
+
if (detectSensitiveValue(key, value)) {
|
|
150
|
+
// Convert key to secret name format
|
|
151
|
+
const secretName = key.toLowerCase().replace(/[^a-z0-9]/g, '-');
|
|
152
|
+
secrets[secretName] = value;
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
return secrets;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Validate environment variable names
|
|
161
|
+
* @param {string} key - Environment variable key
|
|
162
|
+
* @returns {boolean} True if key is valid
|
|
163
|
+
*/
|
|
164
|
+
function validateEnvKey(key) {
|
|
165
|
+
// Environment variable names should be uppercase letters, numbers, and underscores
|
|
166
|
+
return /^[A-Z][A-Z0-9_]*$/.test(key);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Sanitize environment variable value
|
|
171
|
+
* @param {string} value - Environment variable value
|
|
172
|
+
* @returns {string} Sanitized value
|
|
173
|
+
*/
|
|
174
|
+
function sanitizeEnvValue(value) {
|
|
175
|
+
// Remove any potential injection characters
|
|
176
|
+
return value.replace(/[;\r\n]/g, '');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Generate environment template with security considerations
|
|
181
|
+
* @param {Object} config - Application configuration
|
|
182
|
+
* @param {Object} existingEnv - Existing environment variables
|
|
183
|
+
* @returns {Promise<Object>} Template generation result
|
|
184
|
+
*/
|
|
185
|
+
async function generateEnvTemplate(config, existingEnv = {}) {
|
|
186
|
+
const result = {
|
|
187
|
+
template: '',
|
|
188
|
+
secrets: {},
|
|
189
|
+
warnings: []
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
// Convert existing environment variables
|
|
194
|
+
const convertedEnv = convertToEnvTemplate(existingEnv, {});
|
|
195
|
+
|
|
196
|
+
// Extract secrets for secrets.yaml
|
|
197
|
+
result.secrets = generateSecretsFromEnv(existingEnv);
|
|
198
|
+
|
|
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
|
+
const { generateEnvTemplate: generateTemplate } = require('./templates');
|
|
213
|
+
const baseTemplate = generateTemplate(config);
|
|
214
|
+
|
|
215
|
+
// Add existing environment variables to the template
|
|
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
|
+
|
|
228
|
+
if (existingEnvSection.length > 0) {
|
|
229
|
+
result.template = baseTemplate + '\n\n# Existing Environment Variables\n' + existingEnvSection.join('\n');
|
|
230
|
+
} else {
|
|
231
|
+
result.template = baseTemplate;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
} catch (error) {
|
|
235
|
+
throw new Error(`Failed to generate environment template: ${error.message}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return result;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
module.exports = {
|
|
242
|
+
readExistingEnv,
|
|
243
|
+
parseEnvContent,
|
|
244
|
+
detectSensitiveValue,
|
|
245
|
+
convertToEnvTemplate,
|
|
246
|
+
generateSecretsFromEnv,
|
|
247
|
+
validateEnvKey,
|
|
248
|
+
sanitizeEnvValue,
|
|
249
|
+
generateEnvTemplate
|
|
250
|
+
};
|