@aifabrix/builder 2.1.6 → 2.2.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/lib/app-deploy.js +73 -29
- package/lib/app-list.js +132 -0
- package/lib/app-readme.js +11 -4
- package/lib/app-register.js +435 -0
- package/lib/app-rotate-secret.js +164 -0
- package/lib/app-run.js +98 -84
- package/lib/app.js +13 -0
- package/lib/audit-logger.js +195 -15
- package/lib/build.js +57 -37
- package/lib/cli.js +90 -8
- package/lib/commands/app.js +8 -391
- package/lib/commands/login.js +130 -36
- package/lib/config.js +257 -4
- package/lib/deployer.js +221 -183
- package/lib/infra.js +177 -112
- package/lib/secrets.js +85 -99
- package/lib/utils/api-error-handler.js +465 -0
- package/lib/utils/api.js +165 -16
- package/lib/utils/auth-headers.js +84 -0
- package/lib/utils/build-copy.js +144 -0
- package/lib/utils/cli-utils.js +21 -0
- package/lib/utils/compose-generator.js +43 -14
- package/lib/utils/deployment-errors.js +90 -0
- package/lib/utils/deployment-validation.js +60 -0
- package/lib/utils/dev-config.js +83 -0
- package/lib/utils/env-template.js +30 -10
- package/lib/utils/health-check.js +18 -1
- package/lib/utils/infra-containers.js +101 -0
- package/lib/utils/local-secrets.js +0 -2
- package/lib/utils/secrets-path.js +18 -21
- package/lib/utils/secrets-utils.js +206 -0
- package/lib/utils/token-manager.js +381 -0
- package/package.json +1 -1
- package/templates/applications/README.md.hbs +155 -23
- package/templates/applications/miso-controller/Dockerfile +7 -119
- package/templates/infra/compose.yaml.hbs +93 -0
- package/templates/python/docker-compose.hbs +25 -17
- package/templates/typescript/docker-compose.hbs +25 -17
package/lib/secrets.js
CHANGED
|
@@ -15,6 +15,8 @@ const yaml = require('js-yaml');
|
|
|
15
15
|
const os = require('os');
|
|
16
16
|
const chalk = require('chalk');
|
|
17
17
|
const logger = require('./utils/logger');
|
|
18
|
+
const config = require('./config');
|
|
19
|
+
const devConfig = require('./utils/dev-config');
|
|
18
20
|
const {
|
|
19
21
|
generateMissingSecrets,
|
|
20
22
|
createDefaultSecrets
|
|
@@ -23,6 +25,13 @@ const {
|
|
|
23
25
|
resolveSecretsPath,
|
|
24
26
|
getActualSecretsPath
|
|
25
27
|
} = require('./utils/secrets-path');
|
|
28
|
+
const {
|
|
29
|
+
loadUserSecrets,
|
|
30
|
+
loadBuildSecrets,
|
|
31
|
+
loadDefaultSecrets,
|
|
32
|
+
buildHostnameToServiceMap,
|
|
33
|
+
resolveUrlPort
|
|
34
|
+
} = require('./utils/secrets-utils');
|
|
26
35
|
|
|
27
36
|
/**
|
|
28
37
|
* Loads environment configuration for docker/local context
|
|
@@ -34,35 +43,6 @@ function loadEnvConfig() {
|
|
|
34
43
|
return yaml.load(content);
|
|
35
44
|
}
|
|
36
45
|
|
|
37
|
-
/**
|
|
38
|
-
* Loads secrets from file with cascading lookup support
|
|
39
|
-
* First checks ~/.aifabrix/secrets.local.yaml, then build.secrets from variables.yaml
|
|
40
|
-
*
|
|
41
|
-
* @async
|
|
42
|
-
* @function loadSecretsFromFile
|
|
43
|
-
* @param {string} filePath - Path to secrets file
|
|
44
|
-
* @returns {Promise<Object>} Loaded secrets object or empty object if file doesn't exist
|
|
45
|
-
*/
|
|
46
|
-
async function loadSecretsFromFile(filePath) {
|
|
47
|
-
if (!fs.existsSync(filePath)) {
|
|
48
|
-
return {};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
try {
|
|
52
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
53
|
-
const secrets = yaml.load(content);
|
|
54
|
-
|
|
55
|
-
if (!secrets || typeof secrets !== 'object') {
|
|
56
|
-
return {};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return secrets;
|
|
60
|
-
} catch (error) {
|
|
61
|
-
logger.warn(`Warning: Could not read secrets file ${filePath}: ${error.message}`);
|
|
62
|
-
return {};
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
46
|
/**
|
|
67
47
|
* Loads secrets with cascading lookup
|
|
68
48
|
* Supports both user secrets (~/.aifabrix/secrets.local.yaml) and project overrides
|
|
@@ -98,76 +78,16 @@ async function loadSecrets(secretsPath, appName) {
|
|
|
98
78
|
}
|
|
99
79
|
|
|
100
80
|
// Cascading lookup: user's file first
|
|
101
|
-
|
|
102
|
-
let mergedSecrets;
|
|
103
|
-
if (fs.existsSync(userSecretsPath)) {
|
|
104
|
-
try {
|
|
105
|
-
const content = fs.readFileSync(userSecretsPath, 'utf8');
|
|
106
|
-
const secrets = yaml.load(content);
|
|
107
|
-
if (!secrets || typeof secrets !== 'object') {
|
|
108
|
-
throw new Error(`Invalid secrets file format: ${userSecretsPath}`);
|
|
109
|
-
}
|
|
110
|
-
mergedSecrets = secrets;
|
|
111
|
-
} catch (error) {
|
|
112
|
-
// If it's a format error, throw it; otherwise log warning and continue
|
|
113
|
-
if (error.message.includes('Invalid secrets file format')) {
|
|
114
|
-
throw error;
|
|
115
|
-
}
|
|
116
|
-
logger.warn(`Warning: Could not read secrets file ${userSecretsPath}: ${error.message}`);
|
|
117
|
-
mergedSecrets = {};
|
|
118
|
-
}
|
|
119
|
-
} else {
|
|
120
|
-
mergedSecrets = {};
|
|
121
|
-
}
|
|
81
|
+
let mergedSecrets = loadUserSecrets();
|
|
122
82
|
|
|
123
83
|
// Then check build.secrets from variables.yaml if appName provided
|
|
124
84
|
if (appName) {
|
|
125
|
-
|
|
126
|
-
if (fs.existsSync(variablesPath)) {
|
|
127
|
-
try {
|
|
128
|
-
const variablesContent = fs.readFileSync(variablesPath, 'utf8');
|
|
129
|
-
const variables = yaml.load(variablesContent);
|
|
130
|
-
|
|
131
|
-
if (variables?.build?.secrets) {
|
|
132
|
-
const buildSecretsPath = path.resolve(
|
|
133
|
-
path.dirname(variablesPath),
|
|
134
|
-
variables.build.secrets
|
|
135
|
-
);
|
|
136
|
-
|
|
137
|
-
const buildSecrets = await loadSecretsFromFile(buildSecretsPath);
|
|
138
|
-
|
|
139
|
-
// Merge: user's file takes priority, but use build.secrets for missing/empty values
|
|
140
|
-
for (const [key, value] of Object.entries(buildSecrets)) {
|
|
141
|
-
if (!(key in mergedSecrets) || !mergedSecrets[key] || mergedSecrets[key] === '') {
|
|
142
|
-
mergedSecrets[key] = value;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
} catch (error) {
|
|
147
|
-
logger.warn(`Warning: Could not load build.secrets from variables.yaml: ${error.message}`);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
85
|
+
mergedSecrets = await loadBuildSecrets(mergedSecrets, appName);
|
|
150
86
|
}
|
|
151
87
|
|
|
152
88
|
// If still no secrets found, try default location
|
|
153
89
|
if (Object.keys(mergedSecrets).length === 0) {
|
|
154
|
-
|
|
155
|
-
if (fs.existsSync(defaultPath)) {
|
|
156
|
-
try {
|
|
157
|
-
const content = fs.readFileSync(defaultPath, 'utf8');
|
|
158
|
-
const secrets = yaml.load(content);
|
|
159
|
-
if (!secrets || typeof secrets !== 'object') {
|
|
160
|
-
throw new Error(`Invalid secrets file format: ${defaultPath}`);
|
|
161
|
-
}
|
|
162
|
-
mergedSecrets = secrets;
|
|
163
|
-
} catch (error) {
|
|
164
|
-
// If it's a format error, throw it; otherwise log warning
|
|
165
|
-
if (error.message.includes('Invalid secrets file format')) {
|
|
166
|
-
throw error;
|
|
167
|
-
}
|
|
168
|
-
logger.warn(`Warning: Could not read secrets file ${defaultPath}: ${error.message}`);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
90
|
+
mergedSecrets = loadDefaultSecrets();
|
|
171
91
|
}
|
|
172
92
|
|
|
173
93
|
// If still empty, throw error
|
|
@@ -187,7 +107,9 @@ async function loadSecrets(secretsPath, appName) {
|
|
|
187
107
|
* @param {string} envTemplate - Environment template content
|
|
188
108
|
* @param {Object} secrets - Secrets object from loadSecrets()
|
|
189
109
|
* @param {string} [environment='local'] - Environment context (docker/local)
|
|
190
|
-
* @param {string} [
|
|
110
|
+
* @param {Object|string|null} [secretsFilePaths] - Paths object with userPath and buildPath, or string path (for backward compatibility)
|
|
111
|
+
* @param {string} [secretsFilePaths.userPath] - User's secrets file path
|
|
112
|
+
* @param {string|null} [secretsFilePaths.buildPath] - App's build.secrets file path (if configured)
|
|
191
113
|
* @returns {Promise<string>} Resolved environment content
|
|
192
114
|
* @throws {Error} If kv:// reference cannot be resolved
|
|
193
115
|
*
|
|
@@ -195,7 +117,7 @@ async function loadSecrets(secretsPath, appName) {
|
|
|
195
117
|
* const resolved = await resolveKvReferences(template, secrets, 'local');
|
|
196
118
|
* // Returns: 'DATABASE_URL=postgresql://user:pass@localhost:5432/db'
|
|
197
119
|
*/
|
|
198
|
-
async function resolveKvReferences(envTemplate, secrets, environment = 'local',
|
|
120
|
+
async function resolveKvReferences(envTemplate, secrets, environment = 'local', secretsFilePaths = null) {
|
|
199
121
|
const envConfig = loadEnvConfig();
|
|
200
122
|
const envVars = envConfig.environments[environment] || envConfig.environments.local;
|
|
201
123
|
|
|
@@ -216,7 +138,20 @@ async function resolveKvReferences(envTemplate, secrets, environment = 'local',
|
|
|
216
138
|
}
|
|
217
139
|
|
|
218
140
|
if (missingSecrets.length > 0) {
|
|
219
|
-
|
|
141
|
+
let fileInfo = '';
|
|
142
|
+
if (secretsFilePaths) {
|
|
143
|
+
// Handle backward compatibility: if it's a string, use it as-is
|
|
144
|
+
if (typeof secretsFilePaths === 'string') {
|
|
145
|
+
fileInfo = `\n\nSecrets file location: ${secretsFilePaths}`;
|
|
146
|
+
} else if (typeof secretsFilePaths === 'object' && secretsFilePaths.userPath) {
|
|
147
|
+
// New format: show both paths if buildPath is configured
|
|
148
|
+
const paths = [secretsFilePaths.userPath];
|
|
149
|
+
if (secretsFilePaths.buildPath) {
|
|
150
|
+
paths.push(secretsFilePaths.buildPath);
|
|
151
|
+
}
|
|
152
|
+
fileInfo = `\n\nSecrets file location: ${paths.join(' and ')}`;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
220
155
|
throw new Error(`Missing secrets: ${missingSecrets.join(', ')}${fileInfo}`);
|
|
221
156
|
}
|
|
222
157
|
|
|
@@ -235,6 +170,36 @@ async function resolveKvReferences(envTemplate, secrets, environment = 'local',
|
|
|
235
170
|
return resolved;
|
|
236
171
|
}
|
|
237
172
|
|
|
173
|
+
/**
|
|
174
|
+
* Resolves service ports in URLs within .env content for Docker environment
|
|
175
|
+
* Replaces ports in URLs with containerPort from service's variables.yaml
|
|
176
|
+
*
|
|
177
|
+
* @function resolveServicePortsInEnvContent
|
|
178
|
+
* @param {string} envContent - Resolved .env file content
|
|
179
|
+
* @param {string} environment - Environment context (docker/local)
|
|
180
|
+
* @returns {string} Content with resolved ports
|
|
181
|
+
*/
|
|
182
|
+
function resolveServicePortsInEnvContent(envContent, environment) {
|
|
183
|
+
// Only process docker environment
|
|
184
|
+
if (environment !== 'docker') {
|
|
185
|
+
return envContent;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const envConfig = loadEnvConfig();
|
|
189
|
+
const dockerHosts = envConfig.environments.docker || {};
|
|
190
|
+
const hostnameToService = buildHostnameToServiceMap(dockerHosts);
|
|
191
|
+
|
|
192
|
+
// Pattern to match URLs: http://hostname:port or https://hostname:port
|
|
193
|
+
// Matches: protocol://hostname:port/path?query
|
|
194
|
+
// Captures: protocol, hostname, port, and optional path/query
|
|
195
|
+
// Note: [^\s\n]* matches any non-whitespace characters except newline (stops at end of line)
|
|
196
|
+
const urlPattern = /(https?:\/\/)([a-zA-Z0-9-]+):(\d+)([^\s\n]*)?/g;
|
|
197
|
+
|
|
198
|
+
return envContent.replace(urlPattern, (match, protocol, hostname, port, urlPath = '') => {
|
|
199
|
+
return resolveUrlPort(protocol, hostname, port, urlPath || '', hostnameToService);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
238
203
|
/**
|
|
239
204
|
* Loads environment template from file
|
|
240
205
|
* @function loadEnvTemplate
|
|
@@ -324,15 +289,36 @@ async function generateEnvFile(appName, secretsPath, environment = 'local', forc
|
|
|
324
289
|
|
|
325
290
|
const template = loadEnvTemplate(templatePath);
|
|
326
291
|
|
|
327
|
-
// Resolve secrets
|
|
328
|
-
const
|
|
292
|
+
// Resolve secrets paths to show in error messages (use actual paths that loadSecrets would use)
|
|
293
|
+
const secretsPaths = getActualSecretsPath(secretsPath, appName);
|
|
329
294
|
|
|
330
295
|
if (force) {
|
|
331
|
-
|
|
296
|
+
// Use userPath for generating missing secrets (priority file)
|
|
297
|
+
await generateMissingSecrets(template, secretsPaths.userPath);
|
|
332
298
|
}
|
|
333
299
|
|
|
334
300
|
const secrets = await loadSecrets(secretsPath, appName);
|
|
335
|
-
|
|
301
|
+
let resolved = await resolveKvReferences(template, secrets, environment, secretsPaths);
|
|
302
|
+
|
|
303
|
+
// Resolve service ports in URLs for docker environment
|
|
304
|
+
if (environment === 'docker') {
|
|
305
|
+
resolved = resolveServicePortsInEnvContent(resolved, environment);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// For local environment, update infrastructure ports to use dev-specific ports
|
|
309
|
+
if (environment === 'local') {
|
|
310
|
+
const devId = await config.getDeveloperId();
|
|
311
|
+
const ports = devConfig.getDevPorts(devId);
|
|
312
|
+
|
|
313
|
+
// Update DATABASE_PORT if present
|
|
314
|
+
resolved = resolved.replace(/^DATABASE_PORT\s*=\s*.*$/m, `DATABASE_PORT=${ports.postgres}`);
|
|
315
|
+
|
|
316
|
+
// Update REDIS_URL if present (format: redis://localhost:port)
|
|
317
|
+
resolved = resolved.replace(/^REDIS_URL\s*=\s*redis:\/\/localhost:\d+/m, `REDIS_URL=redis://localhost:${ports.redis}`);
|
|
318
|
+
|
|
319
|
+
// Update REDIS_HOST if it contains a port
|
|
320
|
+
resolved = resolved.replace(/^REDIS_HOST\s*=\s*localhost:\d+/m, `REDIS_HOST=localhost:${ports.redis}`);
|
|
321
|
+
}
|
|
336
322
|
|
|
337
323
|
fs.writeFileSync(envPath, resolved, { mode: 0o600 });
|
|
338
324
|
|