@aifabrix/builder 2.5.2 → 2.6.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/.markdownlint.json +14 -0
- package/lib/cli.js +16 -2
- package/lib/generator.js +28 -8
- package/lib/infra.js +55 -56
- package/lib/key-generator.js +58 -0
- package/lib/schema/application-schema.json +6 -1
- package/lib/secrets.js +15 -3
- package/lib/utils/cli-utils.js +5 -1
- package/lib/utils/env-endpoints.js +135 -80
- package/lib/utils/env-map.js +48 -1
- package/lib/utils/secrets-helpers.js +80 -75
- package/lib/utils/variable-transformer.js +15 -1
- package/package.json +1 -1
- package/templates/applications/keycloak/variables.yaml +0 -6
- package/templates/applications/miso-controller/env.template +3 -5
- package/templates/applications/miso-controller/rbac.yaml +6 -6
- package/templates/applications/miso-controller/variables.yaml +1 -7
- package/templates/infra/compose.yaml.hbs +14 -0
- package/templates/infra/servers.json.hbs +21 -0
package/lib/cli.js
CHANGED
|
@@ -14,7 +14,6 @@ const app = require('./app');
|
|
|
14
14
|
const secrets = require('./secrets');
|
|
15
15
|
const generator = require('./generator');
|
|
16
16
|
const validator = require('./validator');
|
|
17
|
-
const keyGenerator = require('./key-generator');
|
|
18
17
|
const config = require('./config');
|
|
19
18
|
const devConfig = require('./utils/dev-config');
|
|
20
19
|
const chalk = require('chalk');
|
|
@@ -306,9 +305,24 @@ function setupCommands(program) {
|
|
|
306
305
|
.description('Generate deployment key')
|
|
307
306
|
.action(async(appName) => {
|
|
308
307
|
try {
|
|
309
|
-
|
|
308
|
+
// Generate JSON first, then extract key from it
|
|
309
|
+
const jsonPath = await generator.generateDeployJson(appName);
|
|
310
|
+
|
|
311
|
+
// Read the generated JSON file
|
|
312
|
+
const fs = require('fs');
|
|
313
|
+
const jsonContent = fs.readFileSync(jsonPath, 'utf8');
|
|
314
|
+
const deployment = JSON.parse(jsonContent);
|
|
315
|
+
|
|
316
|
+
// Extract deploymentKey from JSON
|
|
317
|
+
const key = deployment.deploymentKey;
|
|
318
|
+
|
|
319
|
+
if (!key) {
|
|
320
|
+
throw new Error('deploymentKey not found in generated JSON');
|
|
321
|
+
}
|
|
322
|
+
|
|
310
323
|
logger.log(`\nDeployment key for ${appName}:`);
|
|
311
324
|
logger.log(key);
|
|
325
|
+
logger.log(chalk.gray(`\nGenerated from: ${jsonPath}`));
|
|
312
326
|
} catch (error) {
|
|
313
327
|
handleCommandError(error, 'genkey');
|
|
314
328
|
process.exit(1);
|
package/lib/generator.js
CHANGED
|
@@ -16,6 +16,22 @@ const _secrets = require('./secrets');
|
|
|
16
16
|
const _keyGenerator = require('./key-generator');
|
|
17
17
|
const _validator = require('./validator');
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Sanitizes authentication type - map keycloak to azure (schema allows: azure, local, none)
|
|
21
|
+
* @function sanitizeAuthType
|
|
22
|
+
* @param {string} authType - Authentication type
|
|
23
|
+
* @returns {string} Sanitized authentication type
|
|
24
|
+
*/
|
|
25
|
+
function sanitizeAuthType(authType) {
|
|
26
|
+
if (authType === 'keycloak') {
|
|
27
|
+
return 'azure';
|
|
28
|
+
}
|
|
29
|
+
if (authType && !['azure', 'local', 'none'].includes(authType)) {
|
|
30
|
+
return 'azure'; // Default to azure if invalid type
|
|
31
|
+
}
|
|
32
|
+
return authType;
|
|
33
|
+
}
|
|
34
|
+
|
|
19
35
|
/**
|
|
20
36
|
* Loads variables.yaml file
|
|
21
37
|
* @param {string} variablesPath - Path to variables.yaml
|
|
@@ -129,11 +145,12 @@ function buildAuthenticationConfig(variables, rbac) {
|
|
|
129
145
|
|
|
130
146
|
// When enableSSO is false, default type to 'none' and requiredRoles to []
|
|
131
147
|
// When enableSSO is true, require type and requiredRoles
|
|
148
|
+
// Sanitize auth type (e.g., map keycloak to azure)
|
|
132
149
|
if (auth.enableSSO === false) {
|
|
133
|
-
auth.type = variables.authentication.type || 'none';
|
|
150
|
+
auth.type = sanitizeAuthType(variables.authentication.type || 'none');
|
|
134
151
|
auth.requiredRoles = variables.authentication.requiredRoles || [];
|
|
135
152
|
} else {
|
|
136
|
-
auth.type = variables.authentication.type || 'azure';
|
|
153
|
+
auth.type = sanitizeAuthType(variables.authentication.type || 'azure');
|
|
137
154
|
auth.requiredRoles = variables.authentication.requiredRoles || [];
|
|
138
155
|
}
|
|
139
156
|
|
|
@@ -313,18 +330,21 @@ async function generateDeployJson(appName) {
|
|
|
313
330
|
const jsonPath = path.join(builderPath, 'aifabrix-deploy.json');
|
|
314
331
|
|
|
315
332
|
// Load configuration files
|
|
316
|
-
const {
|
|
333
|
+
const { parsed: variables } = loadVariables(variablesPath);
|
|
317
334
|
const envTemplate = loadEnvTemplate(templatePath);
|
|
318
335
|
const rbac = loadRbac(rbacPath);
|
|
319
336
|
|
|
320
|
-
// Generate deployment key
|
|
321
|
-
const deploymentKey = _keyGenerator.generateDeploymentKeyFromContent(variablesContent);
|
|
322
|
-
|
|
323
337
|
// Parse environment variables from template
|
|
324
338
|
const configuration = parseEnvironmentVariables(envTemplate);
|
|
325
339
|
|
|
326
|
-
// Build deployment manifest
|
|
327
|
-
const deployment = buildManifestStructure(appName, variables,
|
|
340
|
+
// Build deployment manifest WITHOUT deploymentKey initially
|
|
341
|
+
const deployment = buildManifestStructure(appName, variables, null, configuration, rbac);
|
|
342
|
+
|
|
343
|
+
// Generate deploymentKey from the manifest object (excluding deploymentKey field)
|
|
344
|
+
const deploymentKey = _keyGenerator.generateDeploymentKeyFromJson(deployment);
|
|
345
|
+
|
|
346
|
+
// Add deploymentKey to manifest
|
|
347
|
+
deployment.deploymentKey = deploymentKey;
|
|
328
348
|
|
|
329
349
|
// Validate deployment JSON against schema
|
|
330
350
|
const validation = _validator.validateDeploymentJson(deployment);
|
package/lib/infra.js
CHANGED
|
@@ -107,24 +107,42 @@ async function ensureAdminSecrets() {
|
|
|
107
107
|
return adminSecretsPath;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Generates pgAdmin4 configuration files (servers.json and pgpass)
|
|
112
|
+
* @param {string} infraDir - Infrastructure directory path
|
|
113
|
+
* @param {string} postgresPassword - PostgreSQL password
|
|
114
|
+
*/
|
|
115
|
+
function generatePgAdminConfig(infraDir, postgresPassword) {
|
|
116
|
+
const serversJsonTemplatePath = path.join(__dirname, '..', 'templates', 'infra', 'servers.json.hbs');
|
|
117
|
+
if (!fs.existsSync(serversJsonTemplatePath)) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const serversJsonTemplateContent = fs.readFileSync(serversJsonTemplatePath, 'utf8');
|
|
122
|
+
const serversJsonTemplate = handlebars.compile(serversJsonTemplateContent);
|
|
123
|
+
const serversJsonContent = serversJsonTemplate({ postgresPassword });
|
|
124
|
+
const serversJsonPath = path.join(infraDir, 'servers.json');
|
|
125
|
+
fs.writeFileSync(serversJsonPath, serversJsonContent, { mode: 0o644 });
|
|
126
|
+
|
|
127
|
+
const pgpassContent = `postgres:5432:postgres:pgadmin:${postgresPassword}\n`;
|
|
128
|
+
const pgpassPath = path.join(infraDir, 'pgpass');
|
|
129
|
+
fs.writeFileSync(pgpassPath, pgpassContent, { mode: 0o600 });
|
|
130
|
+
}
|
|
131
|
+
|
|
110
132
|
async function startInfra(developerId = null) {
|
|
111
133
|
await checkDockerAvailability();
|
|
112
134
|
const adminSecretsPath = await ensureAdminSecrets();
|
|
113
135
|
|
|
114
|
-
// Get developer ID from parameter or config
|
|
115
136
|
const devId = developerId || await config.getDeveloperId();
|
|
116
|
-
// Convert to number for getDevPorts (it expects numbers)
|
|
117
137
|
const devIdNum = typeof devId === 'string' ? parseInt(devId, 10) : devId;
|
|
118
138
|
const ports = devConfig.getDevPorts(devIdNum);
|
|
119
139
|
const idNum = devIdNum;
|
|
120
140
|
|
|
121
|
-
// Load compose template (Handlebars)
|
|
122
141
|
const templatePath = path.join(__dirname, '..', 'templates', 'infra', 'compose.yaml.hbs');
|
|
123
142
|
if (!fs.existsSync(templatePath)) {
|
|
124
143
|
throw new Error(`Compose template not found: ${templatePath}`);
|
|
125
144
|
}
|
|
126
145
|
|
|
127
|
-
// Create infra directory in AIFABRIX_HOME with dev ID
|
|
128
146
|
const aifabrixDir = paths.getAifabrixHome();
|
|
129
147
|
const infraDirName = getInfraDirName(devId);
|
|
130
148
|
const infraDir = path.join(aifabrixDir, infraDirName);
|
|
@@ -132,8 +150,11 @@ async function startInfra(developerId = null) {
|
|
|
132
150
|
fs.mkdirSync(infraDir, { recursive: true });
|
|
133
151
|
}
|
|
134
152
|
|
|
135
|
-
|
|
136
|
-
|
|
153
|
+
const adminSecretsContent = fs.readFileSync(adminSecretsPath, 'utf8');
|
|
154
|
+
const postgresPasswordMatch = adminSecretsContent.match(/^POSTGRES_PASSWORD=(.+)$/m);
|
|
155
|
+
const postgresPassword = postgresPasswordMatch ? postgresPasswordMatch[1] : '';
|
|
156
|
+
generatePgAdminConfig(infraDir, postgresPassword);
|
|
157
|
+
|
|
137
158
|
handlebars.registerHelper('eq', (a, b) => {
|
|
138
159
|
if (a === null || a === undefined) a = '0';
|
|
139
160
|
if (b === null || b === undefined) b = '0';
|
|
@@ -146,19 +167,20 @@ async function startInfra(developerId = null) {
|
|
|
146
167
|
});
|
|
147
168
|
const templateContent = fs.readFileSync(templatePath, 'utf8');
|
|
148
169
|
const template = handlebars.compile(templateContent);
|
|
149
|
-
// Dev 0: infra-aifabrix-network, Dev > 0: infra-dev{id}-aifabrix-network
|
|
150
170
|
const networkName = idNum === 0 ? 'infra-aifabrix-network' : `infra-dev${devId}-aifabrix-network`;
|
|
151
|
-
|
|
152
|
-
|
|
171
|
+
const serversJsonPath = path.join(infraDir, 'servers.json');
|
|
172
|
+
const pgpassPath = path.join(infraDir, 'pgpass');
|
|
153
173
|
const composeContent = template({
|
|
154
174
|
devId: devId,
|
|
155
175
|
postgresPort: ports.postgres,
|
|
156
176
|
redisPort: ports.redis,
|
|
157
177
|
pgadminPort: ports.pgadmin,
|
|
158
178
|
redisCommanderPort: ports.redisCommander,
|
|
159
|
-
networkName: networkName
|
|
179
|
+
networkName: networkName,
|
|
180
|
+
serversJsonPath: serversJsonPath,
|
|
181
|
+
pgpassPath: pgpassPath,
|
|
182
|
+
infraDir: infraDir
|
|
160
183
|
});
|
|
161
|
-
|
|
162
184
|
const composePath = path.join(infraDir, 'compose.yaml');
|
|
163
185
|
fs.writeFileSync(composePath, composeContent);
|
|
164
186
|
|
|
@@ -170,6 +192,22 @@ async function startInfra(developerId = null) {
|
|
|
170
192
|
await execAsyncWithCwd(`${composeCmd} -f "${composePath}" -p ${projectName} --env-file "${adminSecretsPath}" up -d`, { cwd: infraDir });
|
|
171
193
|
logger.log('Infrastructure services started successfully');
|
|
172
194
|
|
|
195
|
+
// Copy pgAdmin4 config files into container after it starts
|
|
196
|
+
const pgadminContainerName = idNum === 0 ? 'aifabrix-pgadmin' : `aifabrix-dev${devId}-pgadmin`;
|
|
197
|
+
try {
|
|
198
|
+
await new Promise(resolve => setTimeout(resolve, 2000)); // Wait for container to be ready
|
|
199
|
+
if (fs.existsSync(serversJsonPath)) {
|
|
200
|
+
await execAsync(`docker cp "${serversJsonPath}" ${pgadminContainerName}:/pgadmin4/servers.json`);
|
|
201
|
+
}
|
|
202
|
+
if (fs.existsSync(pgpassPath)) {
|
|
203
|
+
await execAsync(`docker cp "${pgpassPath}" ${pgadminContainerName}:/pgpass`);
|
|
204
|
+
await execAsync(`docker exec ${pgadminContainerName} chmod 600 /pgpass`);
|
|
205
|
+
}
|
|
206
|
+
} catch (error) {
|
|
207
|
+
// Ignore copy errors - files might already be there or container not ready
|
|
208
|
+
logger.log('Note: Could not copy pgAdmin4 config files (this is OK if container was just restarted)');
|
|
209
|
+
}
|
|
210
|
+
|
|
173
211
|
await waitForServices(devId);
|
|
174
212
|
logger.log('All services are healthy and ready');
|
|
175
213
|
} finally {
|
|
@@ -431,64 +469,25 @@ async function getAppStatus() {
|
|
|
431
469
|
const apps = [];
|
|
432
470
|
|
|
433
471
|
try {
|
|
434
|
-
// Find all containers with pattern
|
|
435
|
-
// Dev 0: aifabrix-* (but exclude infrastructure containers)
|
|
436
|
-
// Dev > 0: aifabrix-dev{id}-*
|
|
437
472
|
const filterPattern = devId === 0 ? 'aifabrix-' : `aifabrix-dev${devId}-`;
|
|
438
473
|
const { stdout } = await execAsync(`docker ps --filter "name=${filterPattern}" --format "{{.Names}}\t{{.Ports}}\t{{.Status}}"`);
|
|
439
474
|
const lines = stdout.trim().split('\n').filter(line => line.trim() !== '');
|
|
440
|
-
|
|
441
|
-
// Infrastructure container names to exclude
|
|
442
|
-
// Dev 0: aifabrix-{serviceName}, Dev > 0: aifabrix-dev{id}-{serviceName}
|
|
443
475
|
const infraContainers = devId === 0
|
|
444
|
-
? [
|
|
445
|
-
|
|
446
|
-
'aifabrix-redis',
|
|
447
|
-
'aifabrix-pgadmin',
|
|
448
|
-
'aifabrix-redis-commander'
|
|
449
|
-
]
|
|
450
|
-
: [
|
|
451
|
-
`aifabrix-dev${devId}-postgres`,
|
|
452
|
-
`aifabrix-dev${devId}-redis`,
|
|
453
|
-
`aifabrix-dev${devId}-pgadmin`,
|
|
454
|
-
`aifabrix-dev${devId}-redis-commander`
|
|
455
|
-
];
|
|
456
|
-
|
|
476
|
+
? ['aifabrix-postgres', 'aifabrix-redis', 'aifabrix-pgadmin', 'aifabrix-redis-commander']
|
|
477
|
+
: [`aifabrix-dev${devId}-postgres`, `aifabrix-dev${devId}-redis`, `aifabrix-dev${devId}-pgadmin`, `aifabrix-dev${devId}-redis-commander`];
|
|
457
478
|
for (const line of lines) {
|
|
458
479
|
const [containerName, ports, status] = line.split('\t');
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
if (infraContainers.includes(containerName)) {
|
|
462
|
-
continue;
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
// Extract app name from container name
|
|
466
|
-
// Dev 0: aifabrix-{appName}, Dev > 0: aifabrix-dev{id}-{appName}
|
|
467
|
-
const pattern = devId === 0
|
|
468
|
-
? /^aifabrix-(.+)$/
|
|
469
|
-
: new RegExp(`^aifabrix-dev${devId}-(.+)$`);
|
|
480
|
+
if (infraContainers.includes(containerName)) continue;
|
|
481
|
+
const pattern = devId === 0 ? /^aifabrix-(.+)$/ : new RegExp(`^aifabrix-dev${devId}-(.+)$`);
|
|
470
482
|
const appNameMatch = containerName.match(pattern);
|
|
471
|
-
if (!appNameMatch)
|
|
472
|
-
continue;
|
|
473
|
-
}
|
|
474
|
-
|
|
483
|
+
if (!appNameMatch) continue;
|
|
475
484
|
const appName = appNameMatch[1];
|
|
476
|
-
|
|
477
|
-
// Extract host port from ports string (e.g., "0.0.0.0:3100->3000/tcp")
|
|
478
485
|
const portMatch = ports.match(/:(\d+)->\d+\//);
|
|
479
486
|
const hostPort = portMatch ? portMatch[1] : 'unknown';
|
|
480
487
|
const url = hostPort !== 'unknown' ? `http://localhost:${hostPort}` : 'unknown';
|
|
481
|
-
|
|
482
|
-
apps.push({
|
|
483
|
-
name: appName,
|
|
484
|
-
container: containerName,
|
|
485
|
-
port: ports,
|
|
486
|
-
status: status.trim(),
|
|
487
|
-
url: url
|
|
488
|
-
});
|
|
488
|
+
apps.push({ name: appName, container: containerName, port: ports, status: status.trim(), url: url });
|
|
489
489
|
}
|
|
490
490
|
} catch (error) {
|
|
491
|
-
// If no containers found, return empty array
|
|
492
491
|
return [];
|
|
493
492
|
}
|
|
494
493
|
|
package/lib/key-generator.js
CHANGED
|
@@ -64,6 +64,63 @@ function generateDeploymentKeyFromContent(content) {
|
|
|
64
64
|
return hash.digest('hex');
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Recursively sorts object keys for deterministic JSON stringification
|
|
69
|
+
* @param {*} obj - Object to sort
|
|
70
|
+
* @returns {*} Object with sorted keys
|
|
71
|
+
*/
|
|
72
|
+
function sortObjectKeys(obj) {
|
|
73
|
+
if (obj === null || typeof obj !== 'object') {
|
|
74
|
+
return obj;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (Array.isArray(obj)) {
|
|
78
|
+
return obj.map(item => sortObjectKeys(item));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const sorted = {};
|
|
82
|
+
const keys = Object.keys(obj).sort();
|
|
83
|
+
for (const key of keys) {
|
|
84
|
+
sorted[key] = sortObjectKeys(obj[key]);
|
|
85
|
+
}
|
|
86
|
+
return sorted;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Generates deployment key from deployment manifest object
|
|
91
|
+
* Creates SHA256 hash of manifest (excluding deploymentKey field) for integrity verification
|
|
92
|
+
* Uses deterministic JSON stringification (sorted keys, no whitespace)
|
|
93
|
+
*
|
|
94
|
+
* @function generateDeploymentKeyFromJson
|
|
95
|
+
* @param {Object} deploymentObject - Deployment manifest object
|
|
96
|
+
* @returns {string} SHA256 hash of manifest (64-character hex string)
|
|
97
|
+
* @throws {Error} If deploymentObject is invalid
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* const key = generateDeploymentKeyFromJson(deploymentManifest);
|
|
101
|
+
* // Returns: 'a1b2c3d4e5f6...' (64-character SHA256 hash)
|
|
102
|
+
*/
|
|
103
|
+
function generateDeploymentKeyFromJson(deploymentObject) {
|
|
104
|
+
if (!deploymentObject || typeof deploymentObject !== 'object') {
|
|
105
|
+
throw new Error('Deployment object is required and must be an object');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Create a copy and remove deploymentKey field if present
|
|
109
|
+
const manifestCopy = { ...deploymentObject };
|
|
110
|
+
delete manifestCopy.deploymentKey;
|
|
111
|
+
|
|
112
|
+
// Sort all keys recursively for deterministic JSON stringification
|
|
113
|
+
const sortedManifest = sortObjectKeys(manifestCopy);
|
|
114
|
+
|
|
115
|
+
// Deterministic JSON stringification: sorted keys, no whitespace
|
|
116
|
+
const jsonString = JSON.stringify(sortedManifest);
|
|
117
|
+
|
|
118
|
+
// Generate SHA256 hash
|
|
119
|
+
const hash = crypto.createHash('sha256');
|
|
120
|
+
hash.update(jsonString, 'utf8');
|
|
121
|
+
return hash.digest('hex');
|
|
122
|
+
}
|
|
123
|
+
|
|
67
124
|
/**
|
|
68
125
|
* Validates deployment key format
|
|
69
126
|
* Ensures key is a valid SHA256 hash
|
|
@@ -89,5 +146,6 @@ function validateDeploymentKey(key) {
|
|
|
89
146
|
module.exports = {
|
|
90
147
|
generateDeploymentKey,
|
|
91
148
|
generateDeploymentKeyFromContent,
|
|
149
|
+
generateDeploymentKeyFromJson,
|
|
92
150
|
validateDeploymentKey
|
|
93
151
|
};
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
]
|
|
35
35
|
},
|
|
36
36
|
"type": "object",
|
|
37
|
-
"required": ["key", "displayName", "description", "type", "image", "registryMode", "port"],
|
|
37
|
+
"required": ["key", "displayName", "description", "type", "image", "registryMode", "port", "deploymentKey"],
|
|
38
38
|
"properties": {
|
|
39
39
|
"key": {
|
|
40
40
|
"type": "string",
|
|
@@ -76,6 +76,11 @@
|
|
|
76
76
|
"minimum": 1,
|
|
77
77
|
"maximum": 65535
|
|
78
78
|
},
|
|
79
|
+
"deploymentKey": {
|
|
80
|
+
"type": "string",
|
|
81
|
+
"description": "SHA256 hash of deployment manifest (excluding deploymentKey field)",
|
|
82
|
+
"pattern": "^[a-f0-9]{64}$"
|
|
83
|
+
},
|
|
79
84
|
"requiresDatabase": {
|
|
80
85
|
"type": "boolean",
|
|
81
86
|
"description": "Whether application requires database"
|
package/lib/secrets.js
CHANGED
|
@@ -169,10 +169,21 @@ async function loadSecrets(secretsPath, _appName) {
|
|
|
169
169
|
*/
|
|
170
170
|
async function resolveKvReferences(envTemplate, secrets, environment = 'local', secretsFilePaths = null, appName = null) {
|
|
171
171
|
const os = require('os');
|
|
172
|
-
|
|
172
|
+
|
|
173
|
+
// Get developer-id for local environment to adjust port variables
|
|
174
|
+
let developerId = null;
|
|
175
|
+
if (environment === 'local') {
|
|
176
|
+
try {
|
|
177
|
+
developerId = await config.getDeveloperId();
|
|
178
|
+
} catch {
|
|
179
|
+
// ignore, will use null (buildEnvVarMap will fetch it)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
let envVars = await buildEnvVarMap(environment, os, developerId);
|
|
173
184
|
if (!envVars || Object.keys(envVars).length === 0) {
|
|
174
185
|
// Fallback to local environment variables if requested environment does not exist
|
|
175
|
-
envVars = await buildEnvVarMap('local', os);
|
|
186
|
+
envVars = await buildEnvVarMap('local', os, developerId);
|
|
176
187
|
}
|
|
177
188
|
const resolved = interpolateEnvVars(envTemplate, envVars);
|
|
178
189
|
const missing = collectMissingSecrets(resolved, secrets);
|
|
@@ -282,6 +293,7 @@ async function applyEnvironmentTransformations(resolved, environment, variablesP
|
|
|
282
293
|
resolved = await rewriteInfraEndpoints(resolved, 'docker');
|
|
283
294
|
resolved = await updatePortForDocker(resolved, variablesPath);
|
|
284
295
|
} else if (environment === 'local') {
|
|
296
|
+
// adjustLocalEnvPortsInContent handles both PORT and infra endpoints
|
|
285
297
|
resolved = await adjustLocalEnvPortsInContent(resolved, variablesPath);
|
|
286
298
|
}
|
|
287
299
|
return resolved;
|
|
@@ -379,7 +391,7 @@ async function generateAdminSecretsEnv(secretsPath) {
|
|
|
379
391
|
POSTGRES_PASSWORD=${postgresPassword}
|
|
380
392
|
PGADMIN_DEFAULT_EMAIL=admin@aifabrix.ai
|
|
381
393
|
PGADMIN_DEFAULT_PASSWORD=${postgresPassword}
|
|
382
|
-
REDIS_HOST=local:redis:6379
|
|
394
|
+
REDIS_HOST=local:redis:6379:0:
|
|
383
395
|
REDIS_COMMANDER_USER=admin
|
|
384
396
|
REDIS_COMMANDER_PASSWORD=${postgresPassword}
|
|
385
397
|
`;
|
package/lib/utils/cli-utils.js
CHANGED
|
@@ -48,6 +48,9 @@ function formatError(error) {
|
|
|
48
48
|
// Check for specific error patterns first (most specific to least specific)
|
|
49
49
|
if (errorMsg.includes('Configuration not found')) {
|
|
50
50
|
messages.push(` ${errorMsg}`);
|
|
51
|
+
} else if (errorMsg.includes('does not match schema') || errorMsg.includes('Validation failed') || errorMsg.includes('Field "') || errorMsg.includes('Invalid format')) {
|
|
52
|
+
// Schema validation errors - show the actual error message
|
|
53
|
+
messages.push(` ${errorMsg}`);
|
|
51
54
|
} else if (errorMsg.includes('not found locally') || (errorMsg.includes('Docker image') && errorMsg.includes('not found'))) {
|
|
52
55
|
messages.push(' Docker image not found.');
|
|
53
56
|
messages.push(' Run: aifabrix build <app> first');
|
|
@@ -57,7 +60,8 @@ function formatError(error) {
|
|
|
57
60
|
} else if (errorMsg.includes('port')) {
|
|
58
61
|
messages.push(' Port conflict detected.');
|
|
59
62
|
messages.push(' Run "aifabrix doctor" to check which ports are in use.');
|
|
60
|
-
} else if (errorMsg.includes('permission')) {
|
|
63
|
+
} else if ((errorMsg.includes('permission denied') || errorMsg.includes('EACCES') || errorMsg.includes('Permission denied')) && !errorMsg.includes('permissions/') && !errorMsg.includes('Field "permissions')) {
|
|
64
|
+
// Only match actual permission denied errors, not validation errors about permissions fields
|
|
61
65
|
messages.push(' Permission denied.');
|
|
62
66
|
messages.push(' Make sure you have the necessary permissions to run Docker commands.');
|
|
63
67
|
} else if (errorMsg.includes('Azure CLI is not installed') || errorMsg.includes('az --version failed') || (errorMsg.includes('az') && errorMsg.includes('failed'))) {
|
|
@@ -28,83 +28,76 @@ async function getEnvHosts(context) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* @
|
|
34
|
-
* @
|
|
35
|
-
* @param {string} envContent - .env file content
|
|
36
|
-
* @param {'docker'|'local'} context - Environment context
|
|
37
|
-
* @param {{redis:number, postgres:number}} [devPorts] - Ports object with developer-id adjusted ports (optional)
|
|
38
|
-
* @returns {Promise<string>} Updated content
|
|
31
|
+
* Split host:port value into host and port
|
|
32
|
+
* @function splitHost
|
|
33
|
+
* @param {string|number} value - Host:port string or plain value
|
|
34
|
+
* @returns {{host: string|undefined, port: number|undefined}} Split host and port
|
|
39
35
|
*/
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
36
|
+
function splitHost(value) {
|
|
37
|
+
if (typeof value !== 'string') return { host: undefined, port: undefined };
|
|
38
|
+
const m = value.match(/^([^:]+):(\d+)$/);
|
|
39
|
+
if (m) return { host: m[1], port: parseInt(m[2], 10) };
|
|
40
|
+
return { host: value, port: undefined };
|
|
41
|
+
}
|
|
43
42
|
|
|
44
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Get aifabrix-localhost override from config.yaml for local context
|
|
45
|
+
* @function getLocalhostOverride
|
|
46
|
+
* @param {'docker'|'local'} context - Environment context
|
|
47
|
+
* @returns {string|null} Localhost override value or null
|
|
48
|
+
*/
|
|
49
|
+
function getLocalhostOverride(context) {
|
|
50
|
+
if (context !== 'local') return null;
|
|
45
51
|
try {
|
|
46
52
|
const os = require('os');
|
|
47
53
|
const cfgPath = path.join(os.homedir(), '.aifabrix', 'config.yaml');
|
|
48
54
|
if (fs.existsSync(cfgPath)) {
|
|
49
55
|
const cfgContent = fs.readFileSync(cfgPath, 'utf8');
|
|
50
56
|
const cfg = yaml.load(cfgContent) || {};
|
|
51
|
-
if (cfg
|
|
52
|
-
|
|
57
|
+
if (typeof cfg['aifabrix-localhost'] === 'string' && cfg['aifabrix-localhost'].trim().length > 0) {
|
|
58
|
+
return cfg['aifabrix-localhost'].trim();
|
|
53
59
|
}
|
|
54
60
|
}
|
|
55
61
|
} catch {
|
|
56
|
-
//
|
|
62
|
+
// ignore override errors
|
|
57
63
|
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
58
66
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const cfgContent = fs.readFileSync(cfgPath, 'utf8');
|
|
75
|
-
const cfg = yaml.load(cfgContent) || {};
|
|
76
|
-
if (typeof cfg['aifabrix-localhost'] === 'string' && cfg['aifabrix-localhost'].trim().length > 0) {
|
|
77
|
-
localhostOverride = cfg['aifabrix-localhost'].trim();
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
} catch {
|
|
81
|
-
// ignore override errors
|
|
82
|
-
}
|
|
67
|
+
/**
|
|
68
|
+
* Get service port with developer-id adjustment (only for local context)
|
|
69
|
+
* @async
|
|
70
|
+
* @function getServicePort
|
|
71
|
+
* @param {string} portKey - Port key (e.g., 'REDIS_PORT', 'DB_PORT')
|
|
72
|
+
* @param {string} serviceName - Service name (e.g., 'redis', 'postgres')
|
|
73
|
+
* @param {Object} hosts - Hosts configuration object
|
|
74
|
+
* @param {'docker'|'local'} context - Environment context
|
|
75
|
+
* @param {Object} [devPorts] - Optional devPorts object with pre-adjusted ports
|
|
76
|
+
* @returns {Promise<number>} Service port with developer-id adjustment (for local context only)
|
|
77
|
+
*/
|
|
78
|
+
async function getServicePort(portKey, serviceName, hosts, context, devPorts) {
|
|
79
|
+
// If devPorts provided, use it (already has developer-id adjustment)
|
|
80
|
+
if (devPorts && typeof devPorts[serviceName] === 'number') {
|
|
81
|
+
return devPorts[serviceName];
|
|
83
82
|
}
|
|
84
83
|
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
// Get base port from config
|
|
93
|
-
let basePort = null;
|
|
94
|
-
if (hosts[portKey] !== undefined && hosts[portKey] !== null) {
|
|
95
|
-
const portVal = typeof hosts[portKey] === 'number' ? hosts[portKey] : parseInt(hosts[portKey], 10);
|
|
96
|
-
if (!Number.isNaN(portVal)) {
|
|
97
|
-
basePort = portVal;
|
|
98
|
-
}
|
|
84
|
+
// Get base port from config
|
|
85
|
+
let basePort = null;
|
|
86
|
+
if (hosts[portKey] !== undefined && hosts[portKey] !== null) {
|
|
87
|
+
const portVal = typeof hosts[portKey] === 'number' ? hosts[portKey] : parseInt(hosts[portKey], 10);
|
|
88
|
+
if (!Number.isNaN(portVal)) {
|
|
89
|
+
basePort = portVal;
|
|
99
90
|
}
|
|
91
|
+
}
|
|
100
92
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
93
|
+
// Last resort fallback to devConfig (only if not in config)
|
|
94
|
+
if (basePort === null || basePort === undefined) {
|
|
95
|
+
const basePorts = devConfig.getBasePorts();
|
|
96
|
+
basePort = basePorts[serviceName];
|
|
97
|
+
}
|
|
106
98
|
|
|
107
|
-
|
|
99
|
+
// Apply developer-id adjustment only for local context
|
|
100
|
+
if (context === 'local') {
|
|
108
101
|
try {
|
|
109
102
|
const devId = await config.getDeveloperId();
|
|
110
103
|
let devIdNum = 0;
|
|
@@ -118,29 +111,40 @@ async function rewriteInfraEndpoints(envContent, context, devPorts) {
|
|
|
118
111
|
} catch {
|
|
119
112
|
return basePort;
|
|
120
113
|
}
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
// Get Redis configuration
|
|
124
|
-
const redisParts = splitHost(hosts.REDIS_HOST);
|
|
125
|
-
let redisHost = redisParts.host || hosts.REDIS_HOST;
|
|
126
|
-
// Fallback to default if not in config
|
|
127
|
-
if (!redisHost) {
|
|
128
|
-
redisHost = context === 'docker' ? 'redis' : 'localhost';
|
|
129
|
-
}
|
|
130
|
-
if (context === 'local' && localhostOverride && redisHost === 'localhost') {
|
|
131
|
-
redisHost = localhostOverride;
|
|
132
114
|
}
|
|
133
|
-
const redisPort = await getServicePort('REDIS_PORT', 'redis');
|
|
134
115
|
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
116
|
+
// For docker context, return base port without adjustment
|
|
117
|
+
return basePort;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Get service host with localhost override applied
|
|
122
|
+
* @function getServiceHost
|
|
123
|
+
* @param {string|undefined} host - Host value from config
|
|
124
|
+
* @param {'docker'|'local'} context - Environment context
|
|
125
|
+
* @param {string} defaultHost - Default host if not in config
|
|
126
|
+
* @param {string|null} localhostOverride - Localhost override value
|
|
127
|
+
* @returns {string} Final host value
|
|
128
|
+
*/
|
|
129
|
+
function getServiceHost(host, context, defaultHost, localhostOverride) {
|
|
130
|
+
const finalHost = host || defaultHost;
|
|
131
|
+
if (context === 'local' && localhostOverride && finalHost === 'localhost') {
|
|
132
|
+
return localhostOverride;
|
|
140
133
|
}
|
|
141
|
-
|
|
142
|
-
|
|
134
|
+
return finalHost;
|
|
135
|
+
}
|
|
143
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Update endpoint variables in env content
|
|
139
|
+
* @function updateEndpointVariables
|
|
140
|
+
* @param {string} envContent - Environment content
|
|
141
|
+
* @param {string} redisHost - Redis host
|
|
142
|
+
* @param {number} redisPort - Redis port
|
|
143
|
+
* @param {string} dbHost - Database host
|
|
144
|
+
* @param {number} dbPort - Database port
|
|
145
|
+
* @returns {string} Updated content
|
|
146
|
+
*/
|
|
147
|
+
function updateEndpointVariables(envContent, redisHost, redisPort, dbHost, dbPort) {
|
|
144
148
|
let updated = envContent;
|
|
145
149
|
|
|
146
150
|
// Update REDIS_URL if present
|
|
@@ -180,7 +184,7 @@ async function rewriteInfraEndpoints(envContent, context, devPorts) {
|
|
|
180
184
|
|
|
181
185
|
// Update DB_HOST if present
|
|
182
186
|
if (/^DB_HOST\s*=.*$/m.test(updated)) {
|
|
183
|
-
updated = updated.replace(/^DB_HOST\s*=\s*.*$/m, `DB_HOST=${
|
|
187
|
+
updated = updated.replace(/^DB_HOST\s*=\s*.*$/m, `DB_HOST=${dbHost}`);
|
|
184
188
|
}
|
|
185
189
|
|
|
186
190
|
// Update DB_PORT if present
|
|
@@ -202,8 +206,59 @@ async function rewriteInfraEndpoints(envContent, context, devPorts) {
|
|
|
202
206
|
return updated;
|
|
203
207
|
}
|
|
204
208
|
|
|
209
|
+
/**
|
|
210
|
+
* Rewrites infra endpoints (REDIS_URL/REDIS_HOST/REDIS_PORT, DB_HOST/DB_PORT, etc.) based on env-config and context
|
|
211
|
+
* Uses getEnvHosts() to get all service values dynamically, avoiding hardcoded values
|
|
212
|
+
* @async
|
|
213
|
+
* @function rewriteInfraEndpoints
|
|
214
|
+
* @param {string} envContent - .env file content
|
|
215
|
+
* @param {'docker'|'local'} context - Environment context
|
|
216
|
+
* @param {{redis:number, postgres:number}} [devPorts] - Ports object with developer-id adjusted ports (optional)
|
|
217
|
+
* @param {Object} [adjustedHosts] - Optional adjusted hosts object (with developer-id adjusted ports) to use instead of loading from config
|
|
218
|
+
* @returns {Promise<string>} Updated content
|
|
219
|
+
*/
|
|
220
|
+
async function rewriteInfraEndpoints(envContent, context, devPorts, adjustedHosts) {
|
|
221
|
+
// Get all service values from config system (includes env-config.yaml + user env-config file)
|
|
222
|
+
// Use adjustedHosts if provided, otherwise load from config
|
|
223
|
+
let hosts = adjustedHosts || await getEnvHosts(context);
|
|
224
|
+
|
|
225
|
+
// Apply config.yaml → environments.{context} override (if exists)
|
|
226
|
+
try {
|
|
227
|
+
const os = require('os');
|
|
228
|
+
const cfgPath = path.join(os.homedir(), '.aifabrix', 'config.yaml');
|
|
229
|
+
if (fs.existsSync(cfgPath)) {
|
|
230
|
+
const cfgContent = fs.readFileSync(cfgPath, 'utf8');
|
|
231
|
+
const cfg = yaml.load(cfgContent) || {};
|
|
232
|
+
if (cfg && cfg.environments && cfg.environments[context]) {
|
|
233
|
+
hosts = { ...hosts, ...cfg.environments[context] };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
} catch {
|
|
237
|
+
// Ignore config.yaml read errors, continue with env-config values
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Get localhost override for local context
|
|
241
|
+
const localhostOverride = getLocalhostOverride(context);
|
|
242
|
+
|
|
243
|
+
// Get Redis configuration
|
|
244
|
+
const redisParts = splitHost(hosts.REDIS_HOST);
|
|
245
|
+
const redisHost = getServiceHost(redisParts.host || hosts.REDIS_HOST, context, context === 'docker' ? 'redis' : 'localhost', localhostOverride);
|
|
246
|
+
const redisPort = await getServicePort('REDIS_PORT', 'redis', hosts, context, devPorts);
|
|
247
|
+
|
|
248
|
+
// Get DB configuration
|
|
249
|
+
const dbHost = getServiceHost(hosts.DB_HOST, context, context === 'docker' ? 'postgres' : 'localhost', localhostOverride);
|
|
250
|
+
const dbPort = await getServicePort('DB_PORT', 'postgres', hosts, context, devPorts);
|
|
251
|
+
|
|
252
|
+
// Update endpoint variables
|
|
253
|
+
return updateEndpointVariables(envContent, redisHost, redisPort, dbHost, dbPort);
|
|
254
|
+
}
|
|
255
|
+
|
|
205
256
|
module.exports = {
|
|
206
257
|
rewriteInfraEndpoints,
|
|
207
|
-
getEnvHosts
|
|
258
|
+
getEnvHosts,
|
|
259
|
+
splitHost,
|
|
260
|
+
getServicePort,
|
|
261
|
+
getServiceHost,
|
|
262
|
+
updateEndpointVariables
|
|
208
263
|
};
|
|
209
264
|
|
package/lib/utils/env-map.js
CHANGED
|
@@ -12,19 +12,22 @@ const fs = require('fs');
|
|
|
12
12
|
const path = require('path');
|
|
13
13
|
const yaml = require('js-yaml');
|
|
14
14
|
const { loadEnvConfig } = require('./env-config-loader');
|
|
15
|
+
const config = require('../config');
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Build environment variable map for interpolation based on env-config.yaml
|
|
18
19
|
* - Supports values like "host:port" by splitting into *_HOST (host) and *_PORT (port)
|
|
19
20
|
* - Merges overrides from ~/.aifabrix/config.yaml under environments.{env}
|
|
20
21
|
* - Applies aifabrix-localhost override for local context if configured
|
|
22
|
+
* - Applies developer-id adjustment to port variables for local context
|
|
21
23
|
* @async
|
|
22
24
|
* @function buildEnvVarMap
|
|
23
25
|
* @param {'docker'|'local'} context - Environment context
|
|
24
26
|
* @param {Object} [osModule] - Optional os module (for testing). If not provided, requires 'os'
|
|
27
|
+
* @param {number|null} [developerId] - Optional developer ID for port adjustment. If not provided, will be fetched from config for local context.
|
|
25
28
|
* @returns {Promise<Object>} Map of variables for interpolation
|
|
26
29
|
*/
|
|
27
|
-
async function buildEnvVarMap(context, osModule = null) {
|
|
30
|
+
async function buildEnvVarMap(context, osModule = null, developerId = null) {
|
|
28
31
|
// Load env-config (base + user override if configured)
|
|
29
32
|
let baseVars = {};
|
|
30
33
|
try {
|
|
@@ -107,6 +110,50 @@ async function buildEnvVarMap(context, osModule = null) {
|
|
|
107
110
|
result[key] = val;
|
|
108
111
|
}
|
|
109
112
|
}
|
|
113
|
+
|
|
114
|
+
// Apply developer-id adjustment to port variables for local context
|
|
115
|
+
if (context === 'local') {
|
|
116
|
+
let devIdNum = 0;
|
|
117
|
+
if (developerId !== null && developerId !== undefined) {
|
|
118
|
+
const parsed = typeof developerId === 'number' ? developerId : parseInt(developerId, 10);
|
|
119
|
+
if (!Number.isNaN(parsed)) {
|
|
120
|
+
devIdNum = parsed;
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
// Get developer-id from config if not provided
|
|
124
|
+
try {
|
|
125
|
+
const devId = await config.getDeveloperId();
|
|
126
|
+
if (devId !== null && devId !== undefined) {
|
|
127
|
+
const parsed = parseInt(devId, 10);
|
|
128
|
+
if (!Number.isNaN(parsed)) {
|
|
129
|
+
devIdNum = parsed;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
} catch {
|
|
133
|
+
// ignore, will use 0
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Apply adjustment to all *_PORT variables
|
|
138
|
+
if (devIdNum !== 0) {
|
|
139
|
+
for (const [key, value] of Object.entries(result)) {
|
|
140
|
+
if (/_PORT$/.test(key)) {
|
|
141
|
+
let portVal;
|
|
142
|
+
if (typeof value === 'string') {
|
|
143
|
+
portVal = parseInt(value, 10);
|
|
144
|
+
} else if (typeof value === 'number') {
|
|
145
|
+
portVal = value;
|
|
146
|
+
} else {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (!Number.isNaN(portVal)) {
|
|
150
|
+
result[key] = String(portVal + (devIdNum * 100));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
110
157
|
return result;
|
|
111
158
|
}
|
|
112
159
|
|
|
@@ -13,7 +13,6 @@ const path = require('path');
|
|
|
13
13
|
const yaml = require('js-yaml');
|
|
14
14
|
const config = require('../config');
|
|
15
15
|
const { buildHostnameToServiceMap, resolveUrlPort } = require('./secrets-utils');
|
|
16
|
-
const devConfig = require('../utils/dev-config');
|
|
17
16
|
const { rewriteInfraEndpoints, getEnvHosts } = require('./env-endpoints');
|
|
18
17
|
const { loadEnvConfig } = require('./env-config-loader');
|
|
19
18
|
const { processEnvVariables } = require('./env-copy');
|
|
@@ -132,45 +131,18 @@ function loadEnvTemplate(templatePath) {
|
|
|
132
131
|
}
|
|
133
132
|
|
|
134
133
|
/**
|
|
135
|
-
*
|
|
136
|
-
*
|
|
134
|
+
* Calculate application port following override chain and developer-id adjustment
|
|
135
|
+
* Override chain: env-config.yaml → config.yaml → variables.yaml build.localPort → variables.yaml port
|
|
137
136
|
* @async
|
|
138
|
-
* @function
|
|
139
|
-
* @param {string}
|
|
140
|
-
* @param {
|
|
141
|
-
* @
|
|
137
|
+
* @function calculateAppPort
|
|
138
|
+
* @param {string} [variablesPath] - Path to variables.yaml
|
|
139
|
+
* @param {Object} localEnv - Local environment config from env-config.yaml and config.yaml
|
|
140
|
+
* @param {string} envContent - Environment content for fallback
|
|
141
|
+
* @param {number} devIdNum - Developer ID number
|
|
142
|
+
* @returns {Promise<number>} Final application port with developer-id adjustment
|
|
142
143
|
*/
|
|
143
|
-
async function
|
|
144
|
-
//
|
|
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
|
|
144
|
+
async function calculateAppPort(variablesPath, localEnv, envContent, devIdNum) {
|
|
145
|
+
// Start with env-config value
|
|
174
146
|
let baseAppPort = null;
|
|
175
147
|
if (localEnv.PORT !== undefined && localEnv.PORT !== null) {
|
|
176
148
|
const portVal = typeof localEnv.PORT === 'number' ? localEnv.PORT : parseInt(localEnv.PORT, 10);
|
|
@@ -179,7 +151,7 @@ async function adjustLocalEnvPortsInContent(envContent, variablesPath) {
|
|
|
179
151
|
}
|
|
180
152
|
}
|
|
181
153
|
|
|
182
|
-
// Override with variables.yaml → build.localPort (
|
|
154
|
+
// Override with variables.yaml → build.localPort (strongest)
|
|
183
155
|
if (variablesPath && fs.existsSync(variablesPath)) {
|
|
184
156
|
try {
|
|
185
157
|
const variablesContent = fs.readFileSync(variablesPath, 'utf8');
|
|
@@ -188,7 +160,7 @@ async function adjustLocalEnvPortsInContent(envContent, variablesPath) {
|
|
|
188
160
|
if (typeof localPort === 'number' && localPort > 0) {
|
|
189
161
|
baseAppPort = localPort;
|
|
190
162
|
} else if (baseAppPort === null || baseAppPort === undefined) {
|
|
191
|
-
// Fallback to variables.yaml → port
|
|
163
|
+
// Fallback to variables.yaml → port
|
|
192
164
|
baseAppPort = variables?.port || 3000;
|
|
193
165
|
}
|
|
194
166
|
} catch {
|
|
@@ -206,29 +178,75 @@ async function adjustLocalEnvPortsInContent(envContent, variablesPath) {
|
|
|
206
178
|
}
|
|
207
179
|
}
|
|
208
180
|
|
|
209
|
-
//
|
|
210
|
-
|
|
181
|
+
// Apply developer-id adjustment
|
|
182
|
+
return devIdNum === 0 ? baseAppPort : (baseAppPort + (devIdNum * 100));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Update localhost URLs that point to base app port to use developer-specific app port
|
|
187
|
+
* @function updateLocalhostUrls
|
|
188
|
+
* @param {string} content - Environment content
|
|
189
|
+
* @param {number} baseAppPort - Base application port
|
|
190
|
+
* @param {number} appPort - Developer-specific application port
|
|
191
|
+
* @returns {string} Updated content with adjusted localhost URLs
|
|
192
|
+
*/
|
|
193
|
+
function updateLocalhostUrls(content, baseAppPort, appPort) {
|
|
194
|
+
const localhostUrlPattern = /(https?:\/\/localhost:)(\d+)(\b[^ \n]*)?/g;
|
|
195
|
+
return content.replace(localhostUrlPattern, (match, prefix, portNum, rest = '') => {
|
|
196
|
+
const num = parseInt(portNum, 10);
|
|
197
|
+
if (num === baseAppPort) {
|
|
198
|
+
return `${prefix}${appPort}${rest || ''}`;
|
|
199
|
+
}
|
|
200
|
+
return match;
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Adjust infra-related ports in resolved .env content for local environment
|
|
206
|
+
* Only handles PORT variable (other ports handled by interpolation)
|
|
207
|
+
* Follows flow: getEnvHosts() → config.yaml override → variables.yaml override → developer-id adjustment
|
|
208
|
+
* @async
|
|
209
|
+
* @function adjustLocalEnvPortsInContent
|
|
210
|
+
* @param {string} envContent - Resolved .env content
|
|
211
|
+
* @param {string} [variablesPath] - Path to variables.yaml (to read build.localPort)
|
|
212
|
+
* @returns {Promise<string>} Updated content with local ports
|
|
213
|
+
*/
|
|
214
|
+
async function adjustLocalEnvPortsInContent(envContent, variablesPath) {
|
|
215
|
+
// Get developer-id for port adjustment
|
|
216
|
+
const devId = await config.getDeveloperId();
|
|
217
|
+
let devIdNum = 0;
|
|
218
|
+
if (devId !== null && devId !== undefined) {
|
|
219
|
+
const parsed = parseInt(devId, 10);
|
|
220
|
+
if (!Number.isNaN(parsed)) {
|
|
221
|
+
devIdNum = parsed;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Get base config from env-config.yaml (includes user env-config file if configured)
|
|
226
|
+
let localEnv = await getEnvHosts('local');
|
|
211
227
|
|
|
212
|
-
//
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
if (
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
228
|
+
// Apply config.yaml → environments.local override (if exists)
|
|
229
|
+
try {
|
|
230
|
+
const os = require('os');
|
|
231
|
+
const cfgPath = path.join(os.homedir(), '.aifabrix', 'config.yaml');
|
|
232
|
+
if (fs.existsSync(cfgPath)) {
|
|
233
|
+
const cfgContent = fs.readFileSync(cfgPath, 'utf8');
|
|
234
|
+
const cfg = yaml.load(cfgContent) || {};
|
|
235
|
+
if (cfg && cfg.environments && cfg.environments.local) {
|
|
236
|
+
localEnv = { ...localEnv, ...cfg.environments.local };
|
|
220
237
|
}
|
|
221
238
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
239
|
+
} catch {
|
|
240
|
+
// Ignore config.yaml read errors, continue with env-config values
|
|
241
|
+
}
|
|
225
242
|
|
|
226
|
-
//
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
const
|
|
243
|
+
// Calculate base port (without developer-id adjustment) for URL matching
|
|
244
|
+
const baseAppPort = await calculateAppPort(variablesPath, localEnv, envContent, 0);
|
|
245
|
+
// Calculate final port with developer-id adjustment
|
|
246
|
+
const appPort = await calculateAppPort(variablesPath, localEnv, envContent, devIdNum);
|
|
230
247
|
|
|
231
|
-
// Update .env content
|
|
248
|
+
// Update .env content - only handle PORT variable
|
|
249
|
+
// Other port variables (DB_PORT, REDIS_PORT, etc.) are handled by interpolation
|
|
232
250
|
let updated = envContent;
|
|
233
251
|
|
|
234
252
|
// Update PORT
|
|
@@ -238,24 +256,11 @@ async function adjustLocalEnvPortsInContent(envContent, variablesPath) {
|
|
|
238
256
|
updated = `${updated}\nPORT=${appPort}\n`;
|
|
239
257
|
}
|
|
240
258
|
|
|
241
|
-
// Update
|
|
242
|
-
|
|
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
|
-
});
|
|
259
|
+
// Update localhost URLs
|
|
260
|
+
updated = updateLocalhostUrls(updated, baseAppPort, appPort);
|
|
255
261
|
|
|
256
|
-
//
|
|
257
|
-
|
|
258
|
-
updated = await rewriteInfraEndpoints(updated, 'local', { redis: redisPort, postgres: dbPort });
|
|
262
|
+
// Update infra endpoints with developer-id adjusted ports for local context
|
|
263
|
+
updated = await rewriteInfraEndpoints(updated, 'local');
|
|
259
264
|
|
|
260
265
|
return updated;
|
|
261
266
|
}
|
|
@@ -73,6 +73,12 @@ function transformFlatStructure(variables, appName) {
|
|
|
73
73
|
result.authentication = auth;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
// Add placeholder deploymentKey for validation (will be generated from JSON later)
|
|
77
|
+
// This is a 64-character hex string matching the SHA256 pattern
|
|
78
|
+
if (!result.deploymentKey) {
|
|
79
|
+
result.deploymentKey = '0000000000000000000000000000000000000000000000000000000000000000';
|
|
80
|
+
}
|
|
81
|
+
|
|
76
82
|
return result;
|
|
77
83
|
}
|
|
78
84
|
|
|
@@ -284,7 +290,15 @@ function transformVariablesForValidation(variables, appName) {
|
|
|
284
290
|
databases: requires.databases || (requires.database ? [{ name: variables.app?.key || appName }] : [])
|
|
285
291
|
};
|
|
286
292
|
|
|
287
|
-
|
|
293
|
+
const result = transformOptionalFields(variables, transformed);
|
|
294
|
+
|
|
295
|
+
// Add placeholder deploymentKey for validation (will be generated from JSON later)
|
|
296
|
+
// This is a 64-character hex string matching the SHA256 pattern
|
|
297
|
+
if (!result.deploymentKey) {
|
|
298
|
+
result.deploymentKey = '0000000000000000000000000000000000000000000000000000000000000000';
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return result;
|
|
288
302
|
}
|
|
289
303
|
|
|
290
304
|
module.exports = {
|
package/package.json
CHANGED
|
@@ -44,9 +44,3 @@ build:
|
|
|
44
44
|
localPort: 8082 # Port for local development (different from Docker port)
|
|
45
45
|
containerPort: 8080 # Container port (different from local port)
|
|
46
46
|
language: typescript # Runtime language for template selection
|
|
47
|
-
secrets: # Path to secrets file (optional)
|
|
48
|
-
|
|
49
|
-
# Docker Compose
|
|
50
|
-
compose:
|
|
51
|
-
file: docker-compose.yaml
|
|
52
|
-
service: keycloak
|
|
@@ -71,7 +71,7 @@ REDIS_PERMISSIONS_TTL=900
|
|
|
71
71
|
# Connects to external keycloak from aifabrix-setup
|
|
72
72
|
|
|
73
73
|
KEYCLOAK_REALM=aifabrix
|
|
74
|
-
|
|
74
|
+
KEYCLOAK_SERVER_URL=kv://keycloak-server-urlKeyVault
|
|
75
75
|
KEYCLOAK_CLIENT_ID=miso-controller
|
|
76
76
|
KEYCLOAK_CLIENT_SECRET=kv://keycloak-client-secretKeyVault
|
|
77
77
|
KEYCLOAK_ADMIN_USERNAME=admin
|
|
@@ -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-secretKeyVault
|
|
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
|
|
@@ -149,6 +149,4 @@ LOG_FILE_PATH=/mnt/data/logs
|
|
|
149
149
|
# =============================================================================
|
|
150
150
|
|
|
151
151
|
# Mount Volume Configuration
|
|
152
|
-
MOUNT_VOLUME
|
|
153
|
-
|
|
154
|
-
|
|
152
|
+
MOUNT_VOLUME=/mnt/data/
|
|
@@ -134,19 +134,19 @@ permissions:
|
|
|
134
134
|
description: "Delete environments"
|
|
135
135
|
|
|
136
136
|
# Environment Applications
|
|
137
|
-
- name: "
|
|
137
|
+
- name: "environments-applications:create"
|
|
138
138
|
roles: ["aifabrix-platform-admin", "aifabrix-deployment-admin", "aifabrix-developer"]
|
|
139
139
|
description: "Create applications within environments"
|
|
140
140
|
|
|
141
|
-
- name: "
|
|
141
|
+
- name: "environments-applications:read"
|
|
142
142
|
roles: ["aifabrix-platform-admin", "aifabrix-deployment-admin", "aifabrix-developer", "aifabrix-observer"]
|
|
143
143
|
description: "View applications within environments"
|
|
144
144
|
|
|
145
|
-
- name: "
|
|
145
|
+
- name: "environments-applications:update"
|
|
146
146
|
roles: ["aifabrix-platform-admin", "aifabrix-deployment-admin", "aifabrix-developer"]
|
|
147
147
|
description: "Update applications within environments"
|
|
148
148
|
|
|
149
|
-
- name: "
|
|
149
|
+
- name: "environments-applications:delete"
|
|
150
150
|
roles: ["aifabrix-platform-admin", "aifabrix-deployment-admin"]
|
|
151
151
|
description: "Remove applications from environments"
|
|
152
152
|
|
|
@@ -207,11 +207,11 @@ permissions:
|
|
|
207
207
|
description: "Administrative export access to all data"
|
|
208
208
|
|
|
209
209
|
# Admin Operations
|
|
210
|
-
- name: "admin
|
|
210
|
+
- name: "admin:sync"
|
|
211
211
|
roles: ["aifabrix-platform-admin", "aifabrix-infrastructure-admin"]
|
|
212
212
|
description: "Full system synchronization operations"
|
|
213
213
|
|
|
214
|
-
- name: "admin
|
|
214
|
+
- name: "admin:keycloak"
|
|
215
215
|
roles: ["aifabrix-platform-admin", "aifabrix-security-admin"]
|
|
216
216
|
description: "Keycloak administration and configuration"
|
|
217
217
|
|
|
@@ -34,7 +34,7 @@ healthCheck:
|
|
|
34
34
|
|
|
35
35
|
# Authentication
|
|
36
36
|
authentication:
|
|
37
|
-
type:
|
|
37
|
+
type: local
|
|
38
38
|
enableSSO: true
|
|
39
39
|
requiredRoles:
|
|
40
40
|
- aifabrix-user
|
|
@@ -48,10 +48,4 @@ build:
|
|
|
48
48
|
envOutputPath: # Copy .env to repo root for local dev (relative to builder/) (if null, no .env file is copied) (if empty, .env file is copied to repo root)
|
|
49
49
|
localPort: 3010 # Port for local development (different from Docker port)
|
|
50
50
|
language: typescript # Runtime language for template selection (typescript or python)
|
|
51
|
-
secrets: # Path to secrets file
|
|
52
|
-
envFilePath: .env # Generated in builder/
|
|
53
51
|
|
|
54
|
-
# Docker Compose
|
|
55
|
-
compose:
|
|
56
|
-
file: docker-compose.yaml
|
|
57
|
-
service: miso-controller
|
|
@@ -53,10 +53,24 @@ services:
|
|
|
53
53
|
PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL}
|
|
54
54
|
PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD}
|
|
55
55
|
PGADMIN_CONFIG_SERVER_MODE: 'False'
|
|
56
|
+
PGADMIN_SERVER_JSON_FILE: /pgadmin4/servers.json
|
|
57
|
+
PGPASSFILE: /pgpass
|
|
56
58
|
ports:
|
|
57
59
|
- "{{pgadminPort}}:80"
|
|
58
60
|
volumes:
|
|
59
61
|
- {{#if (eq devId 0)}}pgadmin_data{{else}}dev{{devId}}_pgadmin_data{{/if}}:/var/lib/pgadmin
|
|
62
|
+
- {{infraDir}}:/host-config:ro
|
|
63
|
+
command: >
|
|
64
|
+
sh -c "
|
|
65
|
+
if [ -f /host-config/servers.json ]; then
|
|
66
|
+
cp /host-config/servers.json /pgadmin4/servers.json;
|
|
67
|
+
fi &&
|
|
68
|
+
if [ -f /host-config/pgpass ]; then
|
|
69
|
+
cp /host-config/pgpass /pgpass;
|
|
70
|
+
chmod 600 /pgpass;
|
|
71
|
+
fi &&
|
|
72
|
+
/entrypoint.sh
|
|
73
|
+
"
|
|
60
74
|
restart: unless-stopped
|
|
61
75
|
depends_on:
|
|
62
76
|
postgres:
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"Servers": {
|
|
3
|
+
"1": {
|
|
4
|
+
"Name": "PostgreSQL (pgvector)",
|
|
5
|
+
"Group": "Servers",
|
|
6
|
+
"Host": "postgres",
|
|
7
|
+
"Port": 5432,
|
|
8
|
+
"MaintenanceDB": "postgres",
|
|
9
|
+
"Username": "pgadmin",
|
|
10
|
+
"PassFile": "/pgpass",
|
|
11
|
+
"SSLMode": "prefer",
|
|
12
|
+
"Comment": "Auto-registered PostgreSQL server with pgvector extension"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|