@aifabrix/builder 2.3.6 → 2.5.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/README.md +3 -0
- package/lib/app-down.js +123 -0
- package/lib/app.js +4 -2
- package/lib/build.js +19 -13
- package/lib/cli.js +52 -9
- package/lib/config.js +83 -8
- package/lib/env-reader.js +3 -2
- package/lib/generator.js +0 -9
- package/lib/infra.js +30 -3
- package/lib/schema/application-schema.json +0 -15
- package/lib/schema/env-config.yaml +8 -8
- package/lib/secrets.js +167 -253
- package/lib/templates.js +10 -18
- package/lib/utils/api-error-handler.js +182 -147
- package/lib/utils/api.js +144 -354
- package/lib/utils/build-copy.js +6 -13
- package/lib/utils/compose-generator.js +2 -1
- package/lib/utils/device-code.js +349 -0
- package/lib/utils/env-config-loader.js +102 -0
- package/lib/utils/env-copy.js +131 -0
- package/lib/utils/env-endpoints.js +209 -0
- package/lib/utils/env-map.js +116 -0
- package/lib/utils/env-ports.js +60 -0
- package/lib/utils/environment-checker.js +39 -6
- package/lib/utils/image-name.js +49 -0
- package/lib/utils/paths.js +40 -18
- package/lib/utils/secrets-generator.js +3 -3
- package/lib/utils/secrets-helpers.js +359 -0
- package/lib/utils/secrets-path.js +24 -71
- package/lib/utils/secrets-url.js +38 -0
- package/lib/utils/secrets-utils.js +0 -41
- package/lib/utils/variable-transformer.js +0 -9
- package/package.json +1 -1
- package/templates/applications/README.md.hbs +9 -5
- package/templates/applications/miso-controller/env.template +1 -1
- package/templates/infra/compose.yaml +4 -0
- package/templates/infra/compose.yaml.hbs +9 -4
package/lib/secrets.js
CHANGED
|
@@ -12,11 +12,24 @@
|
|
|
12
12
|
const fs = require('fs');
|
|
13
13
|
const path = require('path');
|
|
14
14
|
const yaml = require('js-yaml');
|
|
15
|
-
const os = require('os');
|
|
16
|
-
const chalk = require('chalk');
|
|
17
15
|
const logger = require('./utils/logger');
|
|
18
16
|
const config = require('./config');
|
|
19
|
-
const
|
|
17
|
+
const {
|
|
18
|
+
interpolateEnvVars,
|
|
19
|
+
collectMissingSecrets,
|
|
20
|
+
formatMissingSecretsFileInfo,
|
|
21
|
+
replaceKvInContent,
|
|
22
|
+
loadEnvTemplate,
|
|
23
|
+
processEnvVariables,
|
|
24
|
+
adjustLocalEnvPortsInContent,
|
|
25
|
+
rewriteInfraEndpoints,
|
|
26
|
+
readYamlAtPath,
|
|
27
|
+
applyCanonicalSecretsOverride,
|
|
28
|
+
ensureNonEmptySecrets,
|
|
29
|
+
validateSecrets
|
|
30
|
+
} = require('./utils/secrets-helpers');
|
|
31
|
+
const { buildEnvVarMap } = require('./utils/env-map');
|
|
32
|
+
const { resolveServicePortsInEnvContent } = require('./utils/secrets-url');
|
|
20
33
|
const {
|
|
21
34
|
generateMissingSecrets,
|
|
22
35
|
createDefaultSecrets
|
|
@@ -27,21 +40,28 @@ const {
|
|
|
27
40
|
} = require('./utils/secrets-path');
|
|
28
41
|
const {
|
|
29
42
|
loadUserSecrets,
|
|
30
|
-
|
|
31
|
-
loadDefaultSecrets,
|
|
32
|
-
buildHostnameToServiceMap,
|
|
33
|
-
resolveUrlPort
|
|
43
|
+
loadDefaultSecrets
|
|
34
44
|
} = require('./utils/secrets-utils');
|
|
35
45
|
const { decryptSecret, isEncrypted } = require('./utils/secrets-encryption');
|
|
46
|
+
const pathsUtil = require('./utils/paths');
|
|
36
47
|
|
|
37
48
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
49
|
+
* Generates a canonical secret name from an environment variable key.
|
|
50
|
+
* Converts to lowercase, replaces non-alphanumeric characters with hyphens,
|
|
51
|
+
* collapses consecutive hyphens, and trims leading/trailing hyphens.
|
|
52
|
+
*
|
|
53
|
+
* @function getCanonicalSecretName
|
|
54
|
+
* @param {string} key - Environment variable key (e.g., JWT_SECRET)
|
|
55
|
+
* @returns {string} Canonical secret name (e.g., jwt-secret)
|
|
40
56
|
*/
|
|
41
|
-
function
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
57
|
+
function getCanonicalSecretName(key) {
|
|
58
|
+
if (!key || typeof key !== 'string') {
|
|
59
|
+
return '';
|
|
60
|
+
}
|
|
61
|
+
const lower = key.toLowerCase();
|
|
62
|
+
const hyphenated = lower.replace(/[^a-z0-9]/g, '-');
|
|
63
|
+
const collapsed = hyphenated.replace(/-+/g, '-');
|
|
64
|
+
return collapsed.replace(/^-+|-+$/g, '');
|
|
45
65
|
}
|
|
46
66
|
|
|
47
67
|
/**
|
|
@@ -103,46 +123,28 @@ async function decryptSecretsObject(secrets) {
|
|
|
103
123
|
* const secrets = await loadSecrets('../../secrets.local.yaml');
|
|
104
124
|
* // Returns: { 'postgres-passwordKeyVault': 'admin123', ... }
|
|
105
125
|
*/
|
|
106
|
-
async function loadSecrets(secretsPath,
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
// If explicit path provided, use it (backward compatibility)
|
|
126
|
+
async function loadSecrets(secretsPath, _appName) {
|
|
127
|
+
// Explicit path branch
|
|
110
128
|
if (secretsPath) {
|
|
111
129
|
const resolvedPath = resolveSecretsPath(secretsPath);
|
|
112
130
|
if (!fs.existsSync(resolvedPath)) {
|
|
113
131
|
throw new Error(`Secrets file not found: ${resolvedPath}`);
|
|
114
132
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
secrets = yaml.load(content);
|
|
118
|
-
|
|
119
|
-
if (!secrets || typeof secrets !== 'object') {
|
|
133
|
+
const explicitSecrets = readYamlAtPath(resolvedPath);
|
|
134
|
+
if (!explicitSecrets || typeof explicitSecrets !== 'object') {
|
|
120
135
|
throw new Error(`Invalid secrets file format: ${resolvedPath}`);
|
|
121
136
|
}
|
|
122
|
-
|
|
123
|
-
// Cascading lookup: user's file first
|
|
124
|
-
let mergedSecrets = loadUserSecrets();
|
|
125
|
-
|
|
126
|
-
// Then check build.secrets from variables.yaml if appName provided
|
|
127
|
-
if (appName) {
|
|
128
|
-
mergedSecrets = await loadBuildSecrets(mergedSecrets, appName);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// If still no secrets found, try default location
|
|
132
|
-
if (Object.keys(mergedSecrets).length === 0) {
|
|
133
|
-
mergedSecrets = loadDefaultSecrets();
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// If still empty, throw error
|
|
137
|
-
if (Object.keys(mergedSecrets).length === 0) {
|
|
138
|
-
throw new Error('No secrets file found. Please create ~/.aifabrix/secrets.local.yaml or configure build.secrets in variables.yaml');
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
secrets = mergedSecrets;
|
|
137
|
+
return await decryptSecretsObject(explicitSecrets);
|
|
142
138
|
}
|
|
143
139
|
|
|
144
|
-
//
|
|
145
|
-
|
|
140
|
+
// Cascading lookup branch
|
|
141
|
+
let mergedSecrets = loadUserSecrets();
|
|
142
|
+
mergedSecrets = await applyCanonicalSecretsOverride(mergedSecrets);
|
|
143
|
+
if (Object.keys(mergedSecrets).length === 0) {
|
|
144
|
+
mergedSecrets = loadDefaultSecrets();
|
|
145
|
+
}
|
|
146
|
+
ensureNonEmptySecrets(mergedSecrets);
|
|
147
|
+
return await decryptSecretsObject(mergedSecrets);
|
|
146
148
|
}
|
|
147
149
|
|
|
148
150
|
/**
|
|
@@ -166,229 +168,168 @@ async function loadSecrets(secretsPath, appName) {
|
|
|
166
168
|
* // Returns: 'DATABASE_URL=postgresql://user:pass@localhost:5432/db'
|
|
167
169
|
*/
|
|
168
170
|
async function resolveKvReferences(envTemplate, secrets, environment = 'local', secretsFilePaths = null, appName = null) {
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
return envVars[envVar] || match;
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
const kvPattern = /kv:\/\/([a-zA-Z0-9-_]+)/g;
|
|
178
|
-
const missingSecrets = [];
|
|
179
|
-
|
|
180
|
-
let match;
|
|
181
|
-
while ((match = kvPattern.exec(resolved)) !== null) {
|
|
182
|
-
const secretKey = match[1];
|
|
183
|
-
if (!(secretKey in secrets)) {
|
|
184
|
-
missingSecrets.push(`kv://${secretKey}`);
|
|
185
|
-
}
|
|
171
|
+
const os = require('os');
|
|
172
|
+
let envVars = await buildEnvVarMap(environment, os);
|
|
173
|
+
if (!envVars || Object.keys(envVars).length === 0) {
|
|
174
|
+
// Fallback to local environment variables if requested environment does not exist
|
|
175
|
+
envVars = await buildEnvVarMap('local', os);
|
|
186
176
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
// Handle backward compatibility: if it's a string, use it as-is
|
|
192
|
-
if (typeof secretsFilePaths === 'string') {
|
|
193
|
-
fileInfo = `\n\nSecrets file location: ${secretsFilePaths}`;
|
|
194
|
-
} else if (typeof secretsFilePaths === 'object' && secretsFilePaths.userPath) {
|
|
195
|
-
// New format: show both paths if buildPath is configured
|
|
196
|
-
const paths = [secretsFilePaths.userPath];
|
|
197
|
-
if (secretsFilePaths.buildPath) {
|
|
198
|
-
paths.push(secretsFilePaths.buildPath);
|
|
199
|
-
}
|
|
200
|
-
fileInfo = `\n\nSecrets file location: ${paths.join(' and ')}`;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
177
|
+
const resolved = interpolateEnvVars(envTemplate, envVars);
|
|
178
|
+
const missing = collectMissingSecrets(resolved, secrets);
|
|
179
|
+
if (missing.length > 0) {
|
|
180
|
+
const fileInfo = formatMissingSecretsFileInfo(secretsFilePaths);
|
|
203
181
|
const resolveCommand = appName ? `aifabrix resolve ${appName}` : 'aifabrix resolve <app-name>';
|
|
204
|
-
throw new Error(`Missing secrets: ${
|
|
182
|
+
throw new Error(`Missing secrets: ${missing.join(', ')}${fileInfo}\n\nRun "${resolveCommand}" to generate missing secrets.`);
|
|
205
183
|
}
|
|
206
|
-
|
|
207
|
-
// Now replace kv:// references, and handle ${VAR} inside the secret values
|
|
208
|
-
resolved = resolved.replace(kvPattern, (match, secretKey) => {
|
|
209
|
-
let value = secrets[secretKey];
|
|
210
|
-
if (typeof value === 'string') {
|
|
211
|
-
// Replace ${VAR} references inside the secret value
|
|
212
|
-
value = value.replace(/\$\{([A-Z_]+)\}/g, (m, envVar) => {
|
|
213
|
-
return envVars[envVar] || m;
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
return value;
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
return resolved;
|
|
184
|
+
return replaceKvInContent(resolved, secrets, envVars);
|
|
220
185
|
}
|
|
221
186
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
* Replaces ports in URLs with containerPort from service's variables.yaml
|
|
225
|
-
*
|
|
226
|
-
* @function resolveServicePortsInEnvContent
|
|
227
|
-
* @param {string} envContent - Resolved .env file content
|
|
228
|
-
* @param {string} environment - Environment context (docker/local)
|
|
229
|
-
* @returns {string} Content with resolved ports
|
|
230
|
-
*/
|
|
231
|
-
function resolveServicePortsInEnvContent(envContent, environment) {
|
|
232
|
-
// Only process docker environment
|
|
233
|
-
if (environment !== 'docker') {
|
|
234
|
-
return envContent;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
const envConfig = loadEnvConfig();
|
|
238
|
-
const dockerHosts = envConfig.environments.docker || {};
|
|
239
|
-
const hostnameToService = buildHostnameToServiceMap(dockerHosts);
|
|
240
|
-
|
|
241
|
-
// Pattern to match URLs: http://hostname:port or https://hostname:port
|
|
242
|
-
// Matches: protocol://hostname:port/path?query
|
|
243
|
-
// Captures: protocol, hostname, port, and optional path/query
|
|
244
|
-
// Note: [^\s\n]* matches any non-whitespace characters except newline (stops at end of line)
|
|
245
|
-
const urlPattern = /(https?:\/\/)([a-zA-Z0-9-]+):(\d+)([^\s\n]*)?/g;
|
|
246
|
-
|
|
247
|
-
return envContent.replace(urlPattern, (match, protocol, hostname, port, urlPath = '') => {
|
|
248
|
-
return resolveUrlPort(protocol, hostname, port, urlPath || '', hostnameToService);
|
|
249
|
-
});
|
|
250
|
-
}
|
|
187
|
+
// resolveServicePortsInEnvContent, loadEnvTemplate, and processEnvVariables
|
|
188
|
+
// are imported from ./utils/secrets-helpers above.
|
|
251
189
|
|
|
252
190
|
/**
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
* @
|
|
257
|
-
* @
|
|
191
|
+
* Generates .env file from template and secrets
|
|
192
|
+
* Creates environment file for local development
|
|
193
|
+
*
|
|
194
|
+
* @async
|
|
195
|
+
* @function generateEnvFile
|
|
196
|
+
* @param {string} appName - Name of the application
|
|
197
|
+
* @param {string} [secretsPath] - Path to secrets file (optional)
|
|
198
|
+
* @param {string} [environment='local'] - Environment context
|
|
199
|
+
* @param {boolean} [force=false] - Generate missing secret keys in secrets file
|
|
200
|
+
* @param {boolean} [skipOutputPath=false] - Skip processing envOutputPath (to avoid recursion)
|
|
201
|
+
* @returns {Promise<string>} Path to generated .env file
|
|
202
|
+
* @throws {Error} If generation fails
|
|
203
|
+
*
|
|
204
|
+
* @example
|
|
205
|
+
* const envPath = await generateEnvFile('myapp', '../../secrets.local.yaml', 'local', true);
|
|
206
|
+
* // Returns: './builder/myapp/.env'
|
|
258
207
|
*/
|
|
259
|
-
function loadEnvTemplate(templatePath) {
|
|
260
|
-
if (!fs.existsSync(templatePath)) {
|
|
261
|
-
throw new Error(`env.template not found: ${templatePath}`);
|
|
262
|
-
}
|
|
263
|
-
return fs.readFileSync(templatePath, 'utf8');
|
|
264
|
-
}
|
|
265
|
-
|
|
266
208
|
/**
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
* @
|
|
271
|
-
* @
|
|
272
|
-
* @param {string}
|
|
273
|
-
* @
|
|
209
|
+
* Updates PORT in resolved content for docker environment
|
|
210
|
+
* Sets PORT to container port (build.containerPort or port from variables.yaml)
|
|
211
|
+
* NOT the host port (which includes developer-id offset)
|
|
212
|
+
* @async
|
|
213
|
+
* @function updatePortForDocker
|
|
214
|
+
* @param {string} resolved - Resolved environment content
|
|
215
|
+
* @param {string} variablesPath - Path to variables.yaml file
|
|
216
|
+
* @returns {Promise<string>} Updated content with PORT set
|
|
274
217
|
*/
|
|
275
|
-
function
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
const variablesContent = fs.readFileSync(variablesPath, 'utf8');
|
|
281
|
-
const variables = yaml.load(variablesContent);
|
|
218
|
+
async function updatePortForDocker(resolved, variablesPath) {
|
|
219
|
+
// Step 1: Get base config from env-config.yaml (includes user env-config file if configured)
|
|
220
|
+
const { getEnvHosts } = require('./utils/env-endpoints');
|
|
221
|
+
let dockerEnv = await getEnvHosts('docker');
|
|
282
222
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
223
|
+
// Step 2: Apply config.yaml → environments.docker override (if exists)
|
|
224
|
+
try {
|
|
225
|
+
const os = require('os');
|
|
226
|
+
const cfgPath = path.join(os.homedir(), '.aifabrix', 'config.yaml');
|
|
227
|
+
if (fs.existsSync(cfgPath)) {
|
|
228
|
+
const cfgContent = fs.readFileSync(cfgPath, 'utf8');
|
|
229
|
+
const cfg = yaml.load(cfgContent) || {};
|
|
230
|
+
if (cfg && cfg.environments && cfg.environments.docker) {
|
|
231
|
+
dockerEnv = { ...dockerEnv, ...cfg.environments.docker };
|
|
232
|
+
}
|
|
293
233
|
}
|
|
234
|
+
} catch {
|
|
235
|
+
// Ignore config.yaml read errors, continue with env-config values
|
|
294
236
|
}
|
|
295
237
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
238
|
+
// Step 3: Get PORT value for container (should be container port, NOT host port)
|
|
239
|
+
// For Docker containers, PORT should be the container port (build.containerPort or port)
|
|
240
|
+
// NOT the host port (which includes developer-id offset)
|
|
241
|
+
let containerPort = null;
|
|
242
|
+
if (variablesPath && fs.existsSync(variablesPath)) {
|
|
243
|
+
try {
|
|
244
|
+
const variablesContent = fs.readFileSync(variablesPath, 'utf8');
|
|
245
|
+
const variables = yaml.load(variablesContent);
|
|
246
|
+
// Use containerPort if specified, otherwise use base port (no developer-id offset)
|
|
247
|
+
containerPort = variables?.build?.containerPort || variables?.port || 3000;
|
|
248
|
+
} catch {
|
|
249
|
+
// Fallback to default
|
|
250
|
+
containerPort = 3000;
|
|
251
|
+
}
|
|
252
|
+
} else {
|
|
253
|
+
// Fallback: check dockerEnv.PORT (but this should be container port, not host port)
|
|
254
|
+
if (dockerEnv.PORT !== undefined && dockerEnv.PORT !== null) {
|
|
255
|
+
const portVal = typeof dockerEnv.PORT === 'number' ? dockerEnv.PORT : parseInt(dockerEnv.PORT, 10);
|
|
256
|
+
if (!Number.isNaN(portVal)) {
|
|
257
|
+
containerPort = portVal;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (containerPort === null || containerPort === undefined) {
|
|
261
|
+
containerPort = 3000;
|
|
262
|
+
}
|
|
299
263
|
}
|
|
300
264
|
|
|
301
|
-
//
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
// When .env stays in builder folder, it uses port (container port)
|
|
306
|
-
const portToUse = variables.build?.localPort || variables.port || 3000;
|
|
307
|
-
|
|
308
|
-
// Replace PORT line (handles PORT=value format, with or without spaces)
|
|
309
|
-
envContent = envContent.replace(/^PORT\s*=\s*.*$/m, `PORT=${portToUse}`);
|
|
265
|
+
// PORT in container should be the container port (no developer-id adjustment)
|
|
266
|
+
// Docker will map container port to host port via port mapping
|
|
267
|
+
return resolved.replace(/^PORT\s*=\s*.*$/m, `PORT=${containerPort}`);
|
|
268
|
+
}
|
|
310
269
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
270
|
+
/**
|
|
271
|
+
* Applies environment-specific transformations to resolved content
|
|
272
|
+
* @async
|
|
273
|
+
* @function applyEnvironmentTransformations
|
|
274
|
+
* @param {string} resolved - Resolved environment content
|
|
275
|
+
* @param {string} environment - Environment context
|
|
276
|
+
* @param {string} variablesPath - Path to variables.yaml file
|
|
277
|
+
* @returns {Promise<string>} Transformed content
|
|
278
|
+
*/
|
|
279
|
+
async function applyEnvironmentTransformations(resolved, environment, variablesPath) {
|
|
280
|
+
if (environment === 'docker') {
|
|
281
|
+
resolved = await resolveServicePortsInEnvContent(resolved, environment);
|
|
282
|
+
resolved = await rewriteInfraEndpoints(resolved, 'docker');
|
|
283
|
+
resolved = await updatePortForDocker(resolved, variablesPath);
|
|
284
|
+
} else if (environment === 'local') {
|
|
285
|
+
resolved = await adjustLocalEnvPortsInContent(resolved, variablesPath);
|
|
286
|
+
}
|
|
287
|
+
return resolved;
|
|
314
288
|
}
|
|
315
289
|
|
|
316
290
|
/**
|
|
317
|
-
* Generates .env file from template and secrets
|
|
318
|
-
* Creates environment file for local development
|
|
319
|
-
*
|
|
291
|
+
* Generates .env file content from template and secrets (without writing to disk)
|
|
320
292
|
* @async
|
|
321
|
-
* @function
|
|
293
|
+
* @function generateEnvContent
|
|
322
294
|
* @param {string} appName - Name of the application
|
|
323
295
|
* @param {string} [secretsPath] - Path to secrets file (optional)
|
|
324
296
|
* @param {string} [environment='local'] - Environment context
|
|
325
297
|
* @param {boolean} [force=false] - Generate missing secret keys in secrets file
|
|
326
|
-
* @returns {Promise<string>}
|
|
298
|
+
* @returns {Promise<string>} Generated .env file content
|
|
327
299
|
* @throws {Error} If generation fails
|
|
328
|
-
*
|
|
329
|
-
* @example
|
|
330
|
-
* const envPath = await generateEnvFile('myapp', '../../secrets.local.yaml', 'local', true);
|
|
331
|
-
* // Returns: './builder/myapp/.env'
|
|
332
300
|
*/
|
|
333
|
-
async function
|
|
301
|
+
async function generateEnvContent(appName, secretsPath, environment = 'local', force = false) {
|
|
334
302
|
const builderPath = path.join(process.cwd(), 'builder', appName);
|
|
335
303
|
const templatePath = path.join(builderPath, 'env.template');
|
|
336
304
|
const variablesPath = path.join(builderPath, 'variables.yaml');
|
|
337
|
-
const envPath = path.join(builderPath, '.env');
|
|
338
305
|
|
|
339
306
|
const template = loadEnvTemplate(templatePath);
|
|
340
|
-
|
|
341
|
-
// Resolve secrets paths to show in error messages (use actual paths that loadSecrets would use)
|
|
342
307
|
const secretsPaths = await getActualSecretsPath(secretsPath, appName);
|
|
343
308
|
|
|
344
309
|
if (force) {
|
|
345
|
-
// Use userPath for generating missing secrets (priority file)
|
|
346
310
|
await generateMissingSecrets(template, secretsPaths.userPath);
|
|
347
311
|
}
|
|
348
312
|
|
|
349
313
|
const secrets = await loadSecrets(secretsPath, appName);
|
|
350
314
|
let resolved = await resolveKvReferences(template, secrets, environment, secretsPaths, appName);
|
|
315
|
+
resolved = await applyEnvironmentTransformations(resolved, environment, variablesPath);
|
|
351
316
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
resolved = resolveServicePortsInEnvContent(resolved, environment);
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// For local environment, update infrastructure ports to use dev-specific ports
|
|
358
|
-
if (environment === 'local') {
|
|
359
|
-
const devId = await config.getDeveloperId();
|
|
360
|
-
// Convert string developer ID to number for getDevPorts
|
|
361
|
-
const devIdNum = parseInt(devId, 10);
|
|
362
|
-
const ports = devConfig.getDevPorts(devIdNum);
|
|
363
|
-
|
|
364
|
-
// Update DATABASE_PORT if present
|
|
365
|
-
resolved = resolved.replace(/^DATABASE_PORT\s*=\s*.*$/m, `DATABASE_PORT=${ports.postgres}`);
|
|
317
|
+
return resolved;
|
|
318
|
+
}
|
|
366
319
|
|
|
367
|
-
|
|
368
|
-
|
|
320
|
+
async function generateEnvFile(appName, secretsPath, environment = 'local', force = false, skipOutputPath = false) {
|
|
321
|
+
const builderPath = path.join(process.cwd(), 'builder', appName);
|
|
322
|
+
const variablesPath = path.join(builderPath, 'variables.yaml');
|
|
323
|
+
const envPath = path.join(builderPath, '.env');
|
|
369
324
|
|
|
370
|
-
|
|
371
|
-
resolved = resolved.replace(/^REDIS_HOST\s*=\s*localhost:\d+/m, `REDIS_HOST=localhost:${ports.redis}`);
|
|
372
|
-
}
|
|
325
|
+
const resolved = await generateEnvContent(appName, secretsPath, environment, force);
|
|
373
326
|
|
|
374
327
|
fs.writeFileSync(envPath, resolved, { mode: 0o600 });
|
|
375
328
|
|
|
376
|
-
// Update PORT variable in container .env file to use port (from variables.yaml)
|
|
377
|
-
// Note: containerPort is ONLY used for Docker Compose port mapping, NOT for PORT env variable
|
|
378
|
-
// The application inside container listens on PORT env variable, which should be 'port' from variables.yaml
|
|
379
|
-
if (fs.existsSync(variablesPath)) {
|
|
380
|
-
const variablesContent = fs.readFileSync(variablesPath, 'utf8');
|
|
381
|
-
const variables = yaml.load(variablesContent);
|
|
382
|
-
const port = variables.port || 3000;
|
|
383
|
-
|
|
384
|
-
// Update PORT in container .env file to use port (NOT containerPort, NOT localPort)
|
|
385
|
-
let envContent = fs.readFileSync(envPath, 'utf8');
|
|
386
|
-
envContent = envContent.replace(/^PORT\s*=\s*.*$/m, `PORT=${port}`);
|
|
387
|
-
fs.writeFileSync(envPath, envContent, { mode: 0o600 });
|
|
388
|
-
}
|
|
389
|
-
|
|
390
329
|
// Process and copy to envOutputPath if configured (uses localPort for copied file)
|
|
391
|
-
|
|
330
|
+
if (!skipOutputPath) {
|
|
331
|
+
await processEnvVariables(envPath, variablesPath, appName, secretsPath);
|
|
332
|
+
}
|
|
392
333
|
|
|
393
334
|
return envPath;
|
|
394
335
|
}
|
|
@@ -414,7 +355,7 @@ async function generateAdminSecretsEnv(secretsPath) {
|
|
|
414
355
|
secrets = await loadSecrets(secretsPath);
|
|
415
356
|
} catch (error) {
|
|
416
357
|
// If secrets file doesn't exist, create default secrets
|
|
417
|
-
const defaultSecretsPath = secretsPath || path.join(
|
|
358
|
+
const defaultSecretsPath = secretsPath || path.join(pathsUtil.getAifabrixHome(), 'secrets.yaml');
|
|
418
359
|
|
|
419
360
|
if (!fs.existsSync(defaultSecretsPath)) {
|
|
420
361
|
logger.log('Creating default secrets file...');
|
|
@@ -425,7 +366,7 @@ async function generateAdminSecretsEnv(secretsPath) {
|
|
|
425
366
|
}
|
|
426
367
|
}
|
|
427
368
|
|
|
428
|
-
const aifabrixDir =
|
|
369
|
+
const aifabrixDir = pathsUtil.getAifabrixHome();
|
|
429
370
|
const adminEnvPath = path.join(aifabrixDir, 'admin-secrets.env');
|
|
430
371
|
|
|
431
372
|
if (!fs.existsSync(aifabrixDir)) {
|
|
@@ -447,43 +388,16 @@ REDIS_COMMANDER_PASSWORD=${postgresPassword}
|
|
|
447
388
|
return adminEnvPath;
|
|
448
389
|
}
|
|
449
390
|
|
|
450
|
-
|
|
451
|
-
* Validates that all required secrets are present
|
|
452
|
-
* Checks for missing kv:// references and provides helpful error messages
|
|
453
|
-
*
|
|
454
|
-
* @function validateSecrets
|
|
455
|
-
* @param {string} envTemplate - Environment template content
|
|
456
|
-
* @param {Object} secrets - Available secrets
|
|
457
|
-
* @returns {Object} Validation result with missing secrets
|
|
458
|
-
*
|
|
459
|
-
* @example
|
|
460
|
-
* const validation = validateSecrets(template, secrets);
|
|
461
|
-
* // Returns: { valid: false, missing: ['kv://missing-secret'] }
|
|
462
|
-
*/
|
|
463
|
-
function validateSecrets(envTemplate, secrets) {
|
|
464
|
-
const kvPattern = /kv:\/\/([a-zA-Z0-9-_]+)/g;
|
|
465
|
-
const missing = [];
|
|
466
|
-
|
|
467
|
-
let match;
|
|
468
|
-
while ((match = kvPattern.exec(envTemplate)) !== null) {
|
|
469
|
-
const secretKey = match[1];
|
|
470
|
-
if (!(secretKey in secrets)) {
|
|
471
|
-
missing.push(`kv://${secretKey}`);
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
return {
|
|
476
|
-
valid: missing.length === 0,
|
|
477
|
-
missing
|
|
478
|
-
};
|
|
479
|
-
}
|
|
391
|
+
// validateSecrets is imported from ./utils/secrets-helpers
|
|
480
392
|
|
|
481
393
|
module.exports = {
|
|
482
394
|
loadSecrets,
|
|
483
395
|
resolveKvReferences,
|
|
484
396
|
generateEnvFile,
|
|
397
|
+
generateEnvContent,
|
|
485
398
|
generateMissingSecrets,
|
|
486
399
|
generateAdminSecretsEnv,
|
|
487
400
|
validateSecrets,
|
|
488
|
-
createDefaultSecrets
|
|
401
|
+
createDefaultSecrets,
|
|
402
|
+
getCanonicalSecretName
|
|
489
403
|
};
|
package/lib/templates.js
CHANGED
|
@@ -50,8 +50,7 @@ function generateVariablesYaml(appName, config) {
|
|
|
50
50
|
language: config.language || 'typescript',
|
|
51
51
|
envOutputPath: null,
|
|
52
52
|
context: null, // Defaults to dev directory in build process
|
|
53
|
-
dockerfile: ''
|
|
54
|
-
secrets: null
|
|
53
|
+
dockerfile: ''
|
|
55
54
|
},
|
|
56
55
|
repository: {
|
|
57
56
|
enabled: false,
|
|
@@ -59,9 +58,7 @@ function generateVariablesYaml(appName, config) {
|
|
|
59
58
|
},
|
|
60
59
|
deployment: {
|
|
61
60
|
controllerUrl: '',
|
|
62
|
-
environment: 'dev'
|
|
63
|
-
clientId: '',
|
|
64
|
-
clientSecret: ''
|
|
61
|
+
environment: 'dev'
|
|
65
62
|
}
|
|
66
63
|
};
|
|
67
64
|
|
|
@@ -125,7 +122,7 @@ function buildDatabaseEnv(config) {
|
|
|
125
122
|
return {
|
|
126
123
|
'DATABASE_URL': `kv://databases-${appName}-0-urlKeyVault`,
|
|
127
124
|
'DB_HOST': '${DB_HOST}',
|
|
128
|
-
'DB_PORT': '
|
|
125
|
+
'DB_PORT': '${DB_PORT}',
|
|
129
126
|
'DB_NAME': dbName,
|
|
130
127
|
'DB_USER': `${dbName}_user`,
|
|
131
128
|
'DB_PASSWORD': `kv://databases-${appName}-0-passwordKeyVault`,
|
|
@@ -143,8 +140,8 @@ function buildRedisEnv(config) {
|
|
|
143
140
|
return {
|
|
144
141
|
'REDIS_URL': 'kv://redis-url',
|
|
145
142
|
'REDIS_HOST': '${REDIS_HOST}',
|
|
146
|
-
'REDIS_PORT': '
|
|
147
|
-
'REDIS_PASSWORD': 'kv://redis-
|
|
143
|
+
'REDIS_PORT': '${REDIS_PORT}',
|
|
144
|
+
'REDIS_PASSWORD': 'kv://redis-passwordKeyVault'
|
|
148
145
|
};
|
|
149
146
|
}
|
|
150
147
|
|
|
@@ -155,10 +152,7 @@ function buildStorageEnv(config) {
|
|
|
155
152
|
|
|
156
153
|
return {
|
|
157
154
|
'STORAGE_TYPE': 'local',
|
|
158
|
-
'STORAGE_PATH': '/app/storage'
|
|
159
|
-
'STORAGE_URL': 'kv://storage-url',
|
|
160
|
-
'STORAGE_KEY': 'kv://storage-key',
|
|
161
|
-
'STORAGE_SECRET': 'kv://storage-secret'
|
|
155
|
+
'STORAGE_PATH': '/app/storage'
|
|
162
156
|
};
|
|
163
157
|
}
|
|
164
158
|
|
|
@@ -168,10 +162,9 @@ function buildAuthEnv(config) {
|
|
|
168
162
|
}
|
|
169
163
|
|
|
170
164
|
return {
|
|
171
|
-
'JWT_SECRET': 'kv://jwt-
|
|
165
|
+
'JWT_SECRET': 'kv://miso-controller-jwt-secretKeyVault',
|
|
172
166
|
'JWT_EXPIRES_IN': '24h',
|
|
173
|
-
'AUTH_PROVIDER': 'local'
|
|
174
|
-
'SESSION_SECRET': 'kv://session-secret'
|
|
167
|
+
'AUTH_PROVIDER': 'local'
|
|
175
168
|
};
|
|
176
169
|
}
|
|
177
170
|
|
|
@@ -415,15 +408,14 @@ function generateSecretsYaml(config, existingSecrets = {}) {
|
|
|
415
408
|
secrets.data['database-user'] = 'base64-encoded-user';
|
|
416
409
|
}
|
|
417
410
|
if (config.redis) {
|
|
418
|
-
secrets.data['redis-
|
|
411
|
+
secrets.data['redis-passwordKeyVault'] = 'base64-encoded-redis-password';
|
|
419
412
|
}
|
|
420
413
|
if (config.storage) {
|
|
421
414
|
secrets.data['storage-key'] = 'base64-encoded-storage-key';
|
|
422
415
|
secrets.data['storage-secret'] = 'base64-encoded-storage-secret';
|
|
423
416
|
}
|
|
424
417
|
if (config.authentication) {
|
|
425
|
-
secrets.data['jwt-
|
|
426
|
-
secrets.data['session-secret'] = 'base64-encoded-session-secret';
|
|
418
|
+
secrets.data['miso-controller-jwt-secretKeyVault'] = 'base64-encoded-miso-controller-jwt-secretKeyVault';
|
|
427
419
|
}
|
|
428
420
|
Object.entries(existingSecrets).forEach(([key, value]) => {
|
|
429
421
|
secrets.data[key] = Buffer.from(value).toString('base64');
|