@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
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secrets helper utilities
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Helper functions for secrets and env processing
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 2.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const yaml = require('js-yaml');
|
|
14
|
+
const config = require('../config');
|
|
15
|
+
const { buildHostnameToServiceMap, resolveUrlPort } = require('./secrets-utils');
|
|
16
|
+
const devConfig = require('../utils/dev-config');
|
|
17
|
+
const { rewriteInfraEndpoints, getEnvHosts } = require('./env-endpoints');
|
|
18
|
+
const { loadEnvConfig } = require('./env-config-loader');
|
|
19
|
+
const { processEnvVariables } = require('./env-copy');
|
|
20
|
+
const { updateContainerPortInEnvFile } = require('./env-ports');
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Interpolate ${VAR} occurrences with values from envVars map
|
|
24
|
+
* @function interpolateEnvVars
|
|
25
|
+
* @param {string} content - Text content
|
|
26
|
+
* @param {Object} envVars - Map of variable name to value
|
|
27
|
+
* @returns {string} Interpolated content
|
|
28
|
+
*/
|
|
29
|
+
function interpolateEnvVars(content, envVars) {
|
|
30
|
+
return content.replace(/\$\{([A-Z_]+)\}/g, (match, envVar) => {
|
|
31
|
+
return envVars[envVar] || match;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Collect missing kv:// secrets referenced in content
|
|
37
|
+
* @function collectMissingSecrets
|
|
38
|
+
* @param {string} content - Text content
|
|
39
|
+
* @param {Object} secrets - Available secrets
|
|
40
|
+
* @returns {string[]} Array of missing kv://<key> references
|
|
41
|
+
*/
|
|
42
|
+
function collectMissingSecrets(content, secrets) {
|
|
43
|
+
const kvPattern = /kv:\/\/([a-zA-Z0-9-_]+)/g;
|
|
44
|
+
const missing = [];
|
|
45
|
+
let match;
|
|
46
|
+
while ((match = kvPattern.exec(content)) !== null) {
|
|
47
|
+
const secretKey = match[1];
|
|
48
|
+
if (!(secretKey in secrets)) {
|
|
49
|
+
missing.push(`kv://${secretKey}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return missing;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Format secrets file info for error message
|
|
57
|
+
* @function formatMissingSecretsFileInfo
|
|
58
|
+
* @param {Object|string|null} secretsFilePaths - Paths or single string path
|
|
59
|
+
* @returns {string} Formatted file info suffix for error message
|
|
60
|
+
*/
|
|
61
|
+
function formatMissingSecretsFileInfo(secretsFilePaths) {
|
|
62
|
+
if (!secretsFilePaths) {
|
|
63
|
+
return '';
|
|
64
|
+
}
|
|
65
|
+
if (typeof secretsFilePaths === 'string') {
|
|
66
|
+
return `\n\nSecrets file location: ${secretsFilePaths}`;
|
|
67
|
+
}
|
|
68
|
+
if (typeof secretsFilePaths === 'object' && secretsFilePaths.userPath) {
|
|
69
|
+
const paths = [secretsFilePaths.userPath];
|
|
70
|
+
if (secretsFilePaths.buildPath) {
|
|
71
|
+
paths.push(secretsFilePaths.buildPath);
|
|
72
|
+
}
|
|
73
|
+
return `\n\nSecrets file location: ${paths.join(' and ')}`;
|
|
74
|
+
}
|
|
75
|
+
return '';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Replace kv:// references with actual values, after also interpolating any ${VAR} within secret values
|
|
80
|
+
* @function replaceKvInContent
|
|
81
|
+
* @param {string} content - Text content containing kv:// references
|
|
82
|
+
* @param {Object} secrets - Secrets map
|
|
83
|
+
* @param {Object} envVars - Environment variables map for nested interpolation
|
|
84
|
+
* @returns {string} Content with kv:// references replaced
|
|
85
|
+
*/
|
|
86
|
+
function replaceKvInContent(content, secrets, envVars) {
|
|
87
|
+
const kvPattern = /kv:\/\/([a-zA-Z0-9-_]+)/g;
|
|
88
|
+
return content.replace(kvPattern, (match, secretKey) => {
|
|
89
|
+
let value = secrets[secretKey];
|
|
90
|
+
if (typeof value === 'string') {
|
|
91
|
+
value = value.replace(/\$\{([A-Z_]+)\}/g, (m, envVar) => {
|
|
92
|
+
return envVars[envVar] || m;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return value;
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Resolve service ports inside URLs for docker environment (.env content)
|
|
101
|
+
* @async
|
|
102
|
+
* @function resolveServicePortsInEnvContent
|
|
103
|
+
* @param {string} envContent - .env content
|
|
104
|
+
* @param {string} environment - Environment name
|
|
105
|
+
* @returns {Promise<string>} Updated content
|
|
106
|
+
*/
|
|
107
|
+
async function resolveServicePortsInEnvContent(envContent, environment) {
|
|
108
|
+
if (environment !== 'docker') {
|
|
109
|
+
return envContent;
|
|
110
|
+
}
|
|
111
|
+
const envConfig = await loadEnvConfig();
|
|
112
|
+
const dockerHosts = envConfig.environments.docker || {};
|
|
113
|
+
const hostnameToService = buildHostnameToServiceMap(dockerHosts);
|
|
114
|
+
const urlPattern = /(https?:\/\/)([a-zA-Z0-9-]+):(\d+)([^\s\n]*)?/g;
|
|
115
|
+
return envContent.replace(urlPattern, (match, protocol, hostname, port, urlPath = '') => {
|
|
116
|
+
return resolveUrlPort(protocol, hostname, port, urlPath || '', hostnameToService);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Load env.template content from disk
|
|
122
|
+
* @function loadEnvTemplate
|
|
123
|
+
* @param {string} templatePath - Path to env.template
|
|
124
|
+
* @returns {string} Template content
|
|
125
|
+
* @throws {Error} If template not found
|
|
126
|
+
*/
|
|
127
|
+
function loadEnvTemplate(templatePath) {
|
|
128
|
+
if (!fs.existsSync(templatePath)) {
|
|
129
|
+
throw new Error(`env.template not found: ${templatePath}`);
|
|
130
|
+
}
|
|
131
|
+
return fs.readFileSync(templatePath, 'utf8');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Adjust infra-related ports in resolved .env content for local environment
|
|
136
|
+
* Follows flow: getEnvHosts() → config.yaml override → variables.yaml override → developer-id adjustment
|
|
137
|
+
* @async
|
|
138
|
+
* @function adjustLocalEnvPortsInContent
|
|
139
|
+
* @param {string} envContent - Resolved .env content
|
|
140
|
+
* @param {string} [variablesPath] - Path to variables.yaml (to read build.localPort)
|
|
141
|
+
* @returns {Promise<string>} Updated content with local ports
|
|
142
|
+
*/
|
|
143
|
+
async function adjustLocalEnvPortsInContent(envContent, variablesPath) {
|
|
144
|
+
// Get developer-id for port adjustment
|
|
145
|
+
const devId = await config.getDeveloperId();
|
|
146
|
+
let devIdNum = 0;
|
|
147
|
+
if (devId !== null && devId !== undefined) {
|
|
148
|
+
const parsed = parseInt(devId, 10);
|
|
149
|
+
if (!Number.isNaN(parsed)) {
|
|
150
|
+
devIdNum = parsed;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Step 1: Get base config from env-config.yaml (includes user env-config file if configured)
|
|
155
|
+
let localEnv = await getEnvHosts('local');
|
|
156
|
+
|
|
157
|
+
// Step 2: Apply config.yaml → environments.local override (if exists)
|
|
158
|
+
try {
|
|
159
|
+
const os = require('os');
|
|
160
|
+
const cfgPath = path.join(os.homedir(), '.aifabrix', 'config.yaml');
|
|
161
|
+
if (fs.existsSync(cfgPath)) {
|
|
162
|
+
const cfgContent = fs.readFileSync(cfgPath, 'utf8');
|
|
163
|
+
const cfg = yaml.load(cfgContent) || {};
|
|
164
|
+
if (cfg && cfg.environments && cfg.environments.local) {
|
|
165
|
+
localEnv = { ...localEnv, ...cfg.environments.local };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
} catch {
|
|
169
|
+
// Ignore config.yaml read errors, continue with env-config values
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Step 3: Get PORT value following override chain
|
|
173
|
+
// Start with env-config value, override with variables.yaml build.localPort, then port
|
|
174
|
+
let baseAppPort = null;
|
|
175
|
+
if (localEnv.PORT !== undefined && localEnv.PORT !== null) {
|
|
176
|
+
const portVal = typeof localEnv.PORT === 'number' ? localEnv.PORT : parseInt(localEnv.PORT, 10);
|
|
177
|
+
if (!Number.isNaN(portVal)) {
|
|
178
|
+
baseAppPort = portVal;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Override with variables.yaml → build.localPort (if exists)
|
|
183
|
+
if (variablesPath && fs.existsSync(variablesPath)) {
|
|
184
|
+
try {
|
|
185
|
+
const variablesContent = fs.readFileSync(variablesPath, 'utf8');
|
|
186
|
+
const variables = yaml.load(variablesContent);
|
|
187
|
+
const localPort = variables?.build?.localPort;
|
|
188
|
+
if (typeof localPort === 'number' && localPort > 0) {
|
|
189
|
+
baseAppPort = localPort;
|
|
190
|
+
} else if (baseAppPort === null || baseAppPort === undefined) {
|
|
191
|
+
// Fallback to variables.yaml → port if baseAppPort still not set
|
|
192
|
+
baseAppPort = variables?.port || 3000;
|
|
193
|
+
}
|
|
194
|
+
} catch {
|
|
195
|
+
// Fallback to reading from env content if variables.yaml read fails
|
|
196
|
+
if (baseAppPort === null || baseAppPort === undefined) {
|
|
197
|
+
const portMatch = envContent.match(/^PORT\s*=\s*(\d+)/m);
|
|
198
|
+
baseAppPort = portMatch ? parseInt(portMatch[1], 10) : 3000;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
} else {
|
|
202
|
+
// Fallback if variablesPath not provided
|
|
203
|
+
if (baseAppPort === null || baseAppPort === undefined) {
|
|
204
|
+
const portMatch = envContent.match(/^PORT\s*=\s*(\d+)/m);
|
|
205
|
+
baseAppPort = portMatch ? parseInt(portMatch[1], 10) : 3000;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Step 4: Apply developer-id adjustment: finalPort = basePort + (developerId * 100)
|
|
210
|
+
const appPort = devIdNum === 0 ? baseAppPort : (baseAppPort + (devIdNum * 100));
|
|
211
|
+
|
|
212
|
+
// Step 5: Get infra service ports from config and apply developer-id adjustment
|
|
213
|
+
// All infra ports (REDIS_PORT, DB_PORT, etc.) come from localEnv and get developer-id adjustment
|
|
214
|
+
const getInfraPort = (portKey, defaultValue) => {
|
|
215
|
+
let port = defaultValue;
|
|
216
|
+
if (localEnv[portKey] !== undefined && localEnv[portKey] !== null) {
|
|
217
|
+
const portVal = typeof localEnv[portKey] === 'number' ? localEnv[portKey] : parseInt(localEnv[portKey], 10);
|
|
218
|
+
if (!Number.isNaN(portVal)) {
|
|
219
|
+
port = portVal;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// Apply developer-id adjustment (infra ports are similar to docker)
|
|
223
|
+
return devIdNum === 0 ? port : (port + (devIdNum * 100));
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// Get default ports from devConfig as last resort fallback
|
|
227
|
+
const basePorts = devConfig.getBasePorts();
|
|
228
|
+
const redisPort = getInfraPort('REDIS_PORT', basePorts.redis);
|
|
229
|
+
const dbPort = getInfraPort('DB_PORT', basePorts.postgres);
|
|
230
|
+
|
|
231
|
+
// Update .env content
|
|
232
|
+
let updated = envContent;
|
|
233
|
+
|
|
234
|
+
// Update PORT
|
|
235
|
+
if (/^PORT\s*=.*$/m.test(updated)) {
|
|
236
|
+
updated = updated.replace(/^PORT\s*=\s*.*$/m, `PORT=${appPort}`);
|
|
237
|
+
} else {
|
|
238
|
+
updated = `${updated}\nPORT=${appPort}\n`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Update DATABASE_PORT
|
|
242
|
+
if (/^DATABASE_PORT\s*=.*$/m.test(updated)) {
|
|
243
|
+
updated = updated.replace(/^DATABASE_PORT\s*=\s*.*$/m, `DATABASE_PORT=${dbPort}`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Update localhost URLs that point to the base app port to the dev-specific app port
|
|
247
|
+
const localhostUrlPattern = /(https?:\/\/localhost:)(\d+)(\b[^ \n]*)?/g;
|
|
248
|
+
updated = updated.replace(localhostUrlPattern, (match, prefix, portNum, rest = '') => {
|
|
249
|
+
const num = parseInt(portNum, 10);
|
|
250
|
+
if (num === baseAppPort) {
|
|
251
|
+
return `${prefix}${appPort}${rest || ''}`;
|
|
252
|
+
}
|
|
253
|
+
return match;
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// Rewrite infra endpoints using env-config mapping for local context
|
|
257
|
+
// This handles REDIS_HOST, REDIS_PORT, REDIS_URL, DB_HOST, etc.
|
|
258
|
+
updated = await rewriteInfraEndpoints(updated, 'local', { redis: redisPort, postgres: dbPort });
|
|
259
|
+
|
|
260
|
+
return updated;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Read a YAML file and return parsed object
|
|
265
|
+
* @function readYamlAtPath
|
|
266
|
+
* @param {string} filePath - Absolute file path
|
|
267
|
+
* @returns {Object} Parsed YAML object
|
|
268
|
+
*/
|
|
269
|
+
function readYamlAtPath(filePath) {
|
|
270
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
271
|
+
return yaml.load(content);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Apply canonical secrets path override if configured and file exists
|
|
276
|
+
* @async
|
|
277
|
+
* @function applyCanonicalSecretsOverride
|
|
278
|
+
* @param {Object} currentSecrets - Current secrets map
|
|
279
|
+
* @returns {Promise<Object>} Possibly overridden secrets
|
|
280
|
+
*/
|
|
281
|
+
async function applyCanonicalSecretsOverride(currentSecrets) {
|
|
282
|
+
let mergedSecrets = currentSecrets || {};
|
|
283
|
+
try {
|
|
284
|
+
const canonicalPath = await config.getSecretsPath();
|
|
285
|
+
if (canonicalPath) {
|
|
286
|
+
const resolvedCanonical = path.isAbsolute(canonicalPath)
|
|
287
|
+
? canonicalPath
|
|
288
|
+
: path.resolve(process.cwd(), canonicalPath);
|
|
289
|
+
if (fs.existsSync(resolvedCanonical)) {
|
|
290
|
+
const configSecrets = readYamlAtPath(resolvedCanonical);
|
|
291
|
+
// Apply canonical secrets as a fallback source:
|
|
292
|
+
// - Do NOT override any existing keys from user/build
|
|
293
|
+
// - Add only missing keys from canonical path
|
|
294
|
+
if (configSecrets && typeof configSecrets === 'object') {
|
|
295
|
+
const result = { ...configSecrets, ...mergedSecrets };
|
|
296
|
+
mergedSecrets = result;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
} catch {
|
|
301
|
+
// ignore and fall through
|
|
302
|
+
}
|
|
303
|
+
return mergedSecrets;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Ensure secrets map is non-empty or throw a friendly guidance error
|
|
308
|
+
* @function ensureNonEmptySecrets
|
|
309
|
+
* @param {Object} secrets - Secrets map
|
|
310
|
+
* @throws {Error} If secrets is empty
|
|
311
|
+
*/
|
|
312
|
+
function ensureNonEmptySecrets(secrets) {
|
|
313
|
+
if (Object.keys(secrets || {}).length === 0) {
|
|
314
|
+
throw new Error('No secrets file found. Please create ~/.aifabrix/secrets.local.yaml or configure build.secrets in variables.yaml');
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Validate secrets against the env template, returning missing refs
|
|
320
|
+
* @function validateSecrets
|
|
321
|
+
* @param {string} envTemplate - Environment template content
|
|
322
|
+
* @param {Object} secrets - Available secrets
|
|
323
|
+
* @returns {Object} Validation result
|
|
324
|
+
*/
|
|
325
|
+
function validateSecrets(envTemplate, secrets) {
|
|
326
|
+
const kvPattern = /kv:\/\/([a-zA-Z0-9-_]+)/g;
|
|
327
|
+
const missing = [];
|
|
328
|
+
let match;
|
|
329
|
+
while ((match = kvPattern.exec(envTemplate)) !== null) {
|
|
330
|
+
const secretKey = match[1];
|
|
331
|
+
if (!(secretKey in secrets)) {
|
|
332
|
+
missing.push(`kv://${secretKey}`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return {
|
|
336
|
+
valid: missing.length === 0,
|
|
337
|
+
missing
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
module.exports = {
|
|
342
|
+
loadEnvConfig,
|
|
343
|
+
interpolateEnvVars,
|
|
344
|
+
collectMissingSecrets,
|
|
345
|
+
formatMissingSecretsFileInfo,
|
|
346
|
+
replaceKvInContent,
|
|
347
|
+
resolveServicePortsInEnvContent,
|
|
348
|
+
loadEnvTemplate,
|
|
349
|
+
processEnvVariables,
|
|
350
|
+
updateContainerPortInEnvFile,
|
|
351
|
+
adjustLocalEnvPortsInContent,
|
|
352
|
+
readYamlAtPath,
|
|
353
|
+
applyCanonicalSecretsOverride,
|
|
354
|
+
ensureNonEmptySecrets,
|
|
355
|
+
validateSecrets,
|
|
356
|
+
rewriteInfraEndpoints,
|
|
357
|
+
getEnvHosts
|
|
358
|
+
};
|
|
359
|
+
|
|
@@ -9,66 +9,42 @@
|
|
|
9
9
|
* @version 2.0.0
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
const fs = require('fs');
|
|
13
12
|
const path = require('path');
|
|
14
|
-
const os = require('os');
|
|
15
|
-
const yaml = require('js-yaml');
|
|
16
13
|
const config = require('../config');
|
|
17
14
|
const paths = require('./paths');
|
|
18
15
|
|
|
19
16
|
/**
|
|
20
|
-
* Resolves secrets file path
|
|
21
|
-
*
|
|
17
|
+
* Resolves secrets file path when an explicit path is provided.
|
|
18
|
+
* If not provided, returns default fallback under <home>/secrets.yaml.
|
|
22
19
|
* @function resolveSecretsPath
|
|
23
20
|
* @param {string} [secretsPath] - Path to secrets file (optional)
|
|
24
21
|
* @returns {string} Resolved secrets file path
|
|
25
22
|
*/
|
|
26
23
|
function resolveSecretsPath(secretsPath) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (!resolvedPath) {
|
|
30
|
-
// Check common locations for secrets.local.yaml
|
|
31
|
-
const commonLocations = [
|
|
32
|
-
path.join(process.cwd(), '..', 'aifabrix-setup', 'secrets.local.yaml'),
|
|
33
|
-
path.join(process.cwd(), '..', '..', 'aifabrix-setup', 'secrets.local.yaml'),
|
|
34
|
-
path.join(process.cwd(), 'secrets.local.yaml'),
|
|
35
|
-
path.join(process.cwd(), '..', 'secrets.local.yaml'),
|
|
36
|
-
path.join(paths.getAifabrixHome(), 'secrets.yaml')
|
|
37
|
-
];
|
|
38
|
-
|
|
39
|
-
// Find first existing file
|
|
40
|
-
for (const location of commonLocations) {
|
|
41
|
-
if (fs.existsSync(location)) {
|
|
42
|
-
resolvedPath = location;
|
|
43
|
-
break;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// If none found, use default location
|
|
48
|
-
if (!resolvedPath) {
|
|
49
|
-
resolvedPath = path.join(paths.getAifabrixHome(), 'secrets.yaml');
|
|
50
|
-
}
|
|
51
|
-
} else if (secretsPath.startsWith('..')) {
|
|
52
|
-
resolvedPath = path.resolve(process.cwd(), secretsPath);
|
|
24
|
+
if (secretsPath && secretsPath.startsWith('..')) {
|
|
25
|
+
return path.resolve(process.cwd(), secretsPath);
|
|
53
26
|
}
|
|
54
|
-
|
|
55
|
-
|
|
27
|
+
if (secretsPath) {
|
|
28
|
+
return secretsPath;
|
|
29
|
+
}
|
|
30
|
+
// Default fallback
|
|
31
|
+
return path.join(paths.getAifabrixHome(), 'secrets.yaml');
|
|
56
32
|
}
|
|
57
33
|
|
|
58
34
|
/**
|
|
59
35
|
* Determines the actual secrets file paths that loadSecrets would use
|
|
60
36
|
* Mirrors the cascading lookup logic from loadSecrets
|
|
61
|
-
*
|
|
37
|
+
* Uses config.yaml for default secrets path as fallback
|
|
62
38
|
*
|
|
63
39
|
* @async
|
|
64
40
|
* @function getActualSecretsPath
|
|
65
41
|
* @param {string} [secretsPath] - Path to secrets file (optional)
|
|
66
|
-
* @param {string} [
|
|
42
|
+
* @param {string} [_appName] - Application name (optional, for backward compatibility, unused)
|
|
67
43
|
* @returns {Promise<Object>} Object with userPath and buildPath (if configured)
|
|
68
44
|
* @returns {string} returns.userPath - User's secrets file path (~/.aifabrix/secrets.local.yaml)
|
|
69
|
-
* @returns {string|null} returns.buildPath - App's
|
|
45
|
+
* @returns {string|null} returns.buildPath - App's secrets file path (if configured in config.yaml)
|
|
70
46
|
*/
|
|
71
|
-
async function getActualSecretsPath(secretsPath,
|
|
47
|
+
async function getActualSecretsPath(secretsPath, _appName) {
|
|
72
48
|
// If explicit path provided, use it (backward compatibility)
|
|
73
49
|
if (secretsPath) {
|
|
74
50
|
const resolvedPath = resolveSecretsPath(secretsPath);
|
|
@@ -78,43 +54,20 @@ async function getActualSecretsPath(secretsPath, appName) {
|
|
|
78
54
|
};
|
|
79
55
|
}
|
|
80
56
|
|
|
81
|
-
// Cascading lookup: user's file first
|
|
82
|
-
const userSecretsPath = path.join(
|
|
57
|
+
// Cascading lookup: user's file first (under configured home)
|
|
58
|
+
const userSecretsPath = path.join(paths.getAifabrixHome(), 'secrets.local.yaml');
|
|
83
59
|
|
|
84
|
-
// Check
|
|
60
|
+
// Check config.yaml for canonical secrets path
|
|
85
61
|
let buildSecretsPath = null;
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
if (
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (variables?.build?.secrets) {
|
|
94
|
-
buildSecretsPath = path.resolve(
|
|
95
|
-
path.dirname(variablesPath),
|
|
96
|
-
variables.build.secrets
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
} catch (error) {
|
|
100
|
-
// Ignore errors, continue
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// If no build.secrets found in variables.yaml, check config.yaml for general secrets-path
|
|
106
|
-
if (!buildSecretsPath) {
|
|
107
|
-
try {
|
|
108
|
-
const generalSecretsPath = await config.getSecretsPath();
|
|
109
|
-
if (generalSecretsPath) {
|
|
110
|
-
// Resolve relative paths from current working directory
|
|
111
|
-
buildSecretsPath = path.isAbsolute(generalSecretsPath)
|
|
112
|
-
? generalSecretsPath
|
|
113
|
-
: path.resolve(process.cwd(), generalSecretsPath);
|
|
114
|
-
}
|
|
115
|
-
} catch (error) {
|
|
116
|
-
// Ignore errors, continue
|
|
62
|
+
try {
|
|
63
|
+
const canonicalSecretsPath = await config.getAifabrixSecretsPath();
|
|
64
|
+
if (canonicalSecretsPath) {
|
|
65
|
+
buildSecretsPath = path.isAbsolute(canonicalSecretsPath)
|
|
66
|
+
? canonicalSecretsPath
|
|
67
|
+
: path.resolve(process.cwd(), canonicalSecretsPath);
|
|
117
68
|
}
|
|
69
|
+
} catch (error) {
|
|
70
|
+
// Ignore errors, continue
|
|
118
71
|
}
|
|
119
72
|
|
|
120
73
|
// Return both paths (even if files don't exist) for error messages
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL and service port resolution utilities
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Resolve ports in URLs using env-config and service maps (docker context)
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 2.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const { buildHostnameToServiceMap, resolveUrlPort } = require('./secrets-utils');
|
|
12
|
+
const { loadEnvConfig } = require('./env-config-loader');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Resolve service ports inside URLs for docker environment (.env content)
|
|
16
|
+
* @async
|
|
17
|
+
* @function resolveServicePortsInEnvContent
|
|
18
|
+
* @param {string} envContent - .env content
|
|
19
|
+
* @param {string} environment - Environment name
|
|
20
|
+
* @returns {Promise<string>} Updated content
|
|
21
|
+
*/
|
|
22
|
+
async function resolveServicePortsInEnvContent(envContent, environment) {
|
|
23
|
+
if (environment !== 'docker') {
|
|
24
|
+
return envContent;
|
|
25
|
+
}
|
|
26
|
+
const envConfig = await loadEnvConfig();
|
|
27
|
+
const dockerHosts = envConfig.environments.docker || {};
|
|
28
|
+
const hostnameToService = buildHostnameToServiceMap(dockerHosts);
|
|
29
|
+
const urlPattern = /(https?:\/\/)([a-zA-Z0-9-]+):(\d+)([^\s\n]*)?/g;
|
|
30
|
+
return envContent.replace(urlPattern, (match, protocol, hostname, port, urlPath = '') => {
|
|
31
|
+
return resolveUrlPort(protocol, hostname, port, urlPath || '', hostnameToService);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = {
|
|
36
|
+
resolveServicePortsInEnvContent
|
|
37
|
+
};
|
|
38
|
+
|
|
@@ -71,46 +71,6 @@ function loadUserSecrets() {
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
/**
|
|
75
|
-
* Loads build secrets from variables.yaml and merges with existing secrets
|
|
76
|
-
* @async
|
|
77
|
-
* @function loadBuildSecrets
|
|
78
|
-
* @param {Object} mergedSecrets - Existing secrets to merge with
|
|
79
|
-
* @param {string} appName - Application name
|
|
80
|
-
* @returns {Promise<Object>} Merged secrets object
|
|
81
|
-
*/
|
|
82
|
-
async function loadBuildSecrets(mergedSecrets, appName) {
|
|
83
|
-
const variablesPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
|
|
84
|
-
if (!fs.existsSync(variablesPath)) {
|
|
85
|
-
return mergedSecrets;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
try {
|
|
89
|
-
const variablesContent = fs.readFileSync(variablesPath, 'utf8');
|
|
90
|
-
const variables = yaml.load(variablesContent);
|
|
91
|
-
|
|
92
|
-
if (variables?.build?.secrets) {
|
|
93
|
-
const buildSecretsPath = path.resolve(
|
|
94
|
-
path.dirname(variablesPath),
|
|
95
|
-
variables.build.secrets
|
|
96
|
-
);
|
|
97
|
-
|
|
98
|
-
const buildSecrets = await loadSecretsFromFile(buildSecretsPath);
|
|
99
|
-
|
|
100
|
-
// Merge: user's file takes priority, but use build.secrets for missing/empty values
|
|
101
|
-
for (const [key, value] of Object.entries(buildSecrets)) {
|
|
102
|
-
if (!(key in mergedSecrets) || !mergedSecrets[key] || mergedSecrets[key] === '') {
|
|
103
|
-
mergedSecrets[key] = value;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
} catch (error) {
|
|
108
|
-
logger.warn(`Warning: Could not load build.secrets from variables.yaml: ${error.message}`);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return mergedSecrets;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
74
|
/**
|
|
115
75
|
* Loads default secrets from ~/.aifabrix/secrets.yaml
|
|
116
76
|
* @function loadDefaultSecrets
|
|
@@ -198,7 +158,6 @@ function resolveUrlPort(protocol, hostname, port, urlPath, hostnameToService) {
|
|
|
198
158
|
module.exports = {
|
|
199
159
|
loadSecretsFromFile,
|
|
200
160
|
loadUserSecrets,
|
|
201
|
-
loadBuildSecrets,
|
|
202
161
|
loadDefaultSecrets,
|
|
203
162
|
buildHostnameToServiceMap,
|
|
204
163
|
resolveUrlPort
|
|
@@ -142,9 +142,6 @@ function validateBuildConfig(build) {
|
|
|
142
142
|
if (build.envOutputPath) {
|
|
143
143
|
buildConfig.envOutputPath = build.envOutputPath;
|
|
144
144
|
}
|
|
145
|
-
if (build.secrets !== null && build.secrets !== undefined && build.secrets !== '') {
|
|
146
|
-
buildConfig.secrets = build.secrets;
|
|
147
|
-
}
|
|
148
145
|
if (build.localPort) {
|
|
149
146
|
buildConfig.localPort = build.localPort;
|
|
150
147
|
}
|
|
@@ -176,12 +173,6 @@ function validateDeploymentConfig(deployment) {
|
|
|
176
173
|
if (deployment.controllerUrl && deployment.controllerUrl.trim() !== '' && /^https:\/\/.*$/.test(deployment.controllerUrl)) {
|
|
177
174
|
deploymentConfig.controllerUrl = deployment.controllerUrl;
|
|
178
175
|
}
|
|
179
|
-
if (deployment.clientId && deployment.clientId.trim() !== '' && /^[a-z0-9-]+$/.test(deployment.clientId)) {
|
|
180
|
-
deploymentConfig.clientId = deployment.clientId;
|
|
181
|
-
}
|
|
182
|
-
if (deployment.clientSecret && deployment.clientSecret.trim() !== '' && /^(kv:\/\/.*|.+)$/.test(deployment.clientSecret)) {
|
|
183
|
-
deploymentConfig.clientSecret = deployment.clientSecret;
|
|
184
|
-
}
|
|
185
176
|
|
|
186
177
|
return Object.keys(deploymentConfig).length > 0 ? deploymentConfig : null;
|
|
187
178
|
}
|
package/package.json
CHANGED
|
@@ -47,7 +47,8 @@ docker logs aifabrix-{{appName}} -f
|
|
|
47
47
|
|
|
48
48
|
**Stop:**
|
|
49
49
|
```bash
|
|
50
|
-
|
|
50
|
+
aifabrix down {{appName}}
|
|
51
|
+
# aifabrix down {{appName}} --volumes # also remove data volume
|
|
51
52
|
```
|
|
52
53
|
|
|
53
54
|
### 4. Deploy to Azure
|
|
@@ -86,6 +87,7 @@ aifabrix app rotate-secret {{appName}} --environment dev
|
|
|
86
87
|
# Development
|
|
87
88
|
aifabrix build {{appName}} # Build app
|
|
88
89
|
aifabrix run {{appName}} # Run locally
|
|
90
|
+
aifabrix down {{appName}} [--volumes] # Stop app (optionally remove volume)
|
|
89
91
|
aifabrix dockerfile {{appName}} --force # Generate Dockerfile
|
|
90
92
|
aifabrix resolve {{appName}} # Generate .env file
|
|
91
93
|
|
|
@@ -148,11 +150,13 @@ aifabrix login --method credentials --app {{appName}} --environment dev
|
|
|
148
150
|
aifabrix login --method credentials --app {{appName}} --client-id $CLIENT_ID --client-secret $CLIENT_SECRET --environment dev
|
|
149
151
|
```
|
|
150
152
|
|
|
151
|
-
###
|
|
153
|
+
### Configuration
|
|
152
154
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
155
|
+
Set overrides in `~/.aifabrix/config.yaml`:
|
|
156
|
+
|
|
157
|
+
```yaml
|
|
158
|
+
aifabrix-home: "/custom/path"
|
|
159
|
+
aifabrix-secrets: "/path/to/secrets.yaml"
|
|
156
160
|
```
|
|
157
161
|
|
|
158
162
|
---
|
|
@@ -107,7 +107,7 @@ MOCK=true
|
|
|
107
107
|
ENCRYPTION_KEY=kv://secrets-encryptionKeyVault
|
|
108
108
|
|
|
109
109
|
# JWT Configuration (for client token generation)
|
|
110
|
-
JWT_SECRET=kv://miso-controller-jwt-
|
|
110
|
+
JWT_SECRET=kv://miso-controller-jwt-secretKeyVaultKeyVault
|
|
111
111
|
|
|
112
112
|
# When API_KEY is set, a matching Bearer token bypasses OAuth2 validation
|
|
113
113
|
API_KEY=kv://miso-controller-api-key-secretKeyVault
|
|
@@ -54,6 +54,8 @@ services:
|
|
|
54
54
|
PGADMIN_CONFIG_SERVER_MODE: 'False'
|
|
55
55
|
ports:
|
|
56
56
|
- "5050:80"
|
|
57
|
+
volumes:
|
|
58
|
+
- pgadmin_data:/var/lib/pgadmin
|
|
57
59
|
restart: unless-stopped
|
|
58
60
|
depends_on:
|
|
59
61
|
postgres:
|
|
@@ -84,6 +86,8 @@ volumes:
|
|
|
84
86
|
driver: local
|
|
85
87
|
redis_data:
|
|
86
88
|
driver: local
|
|
89
|
+
pgadmin_data:
|
|
90
|
+
driver: local
|
|
87
91
|
|
|
88
92
|
networks:
|
|
89
93
|
infra_aifabrix-network:
|