@aifabrix/builder 2.1.7 → 2.3.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 +155 -42
- package/lib/cli.js +104 -8
- package/lib/commands/app.js +8 -391
- package/lib/commands/login.js +130 -36
- package/lib/commands/secure.js +260 -0
- package/lib/config.js +315 -4
- package/lib/deployer.js +221 -183
- package/lib/infra.js +177 -112
- package/lib/push.js +34 -7
- package/lib/secrets.js +89 -23
- package/lib/templates.js +1 -1
- 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 +162 -0
- package/lib/utils/cli-utils.js +49 -3
- package/lib/utils/compose-generator.js +57 -16
- 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/docker-build.js +24 -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-encryption.js +203 -0
- package/lib/utils/secrets-path.js +22 -3
- package/lib/utils/token-manager.js +381 -0
- package/package.json +2 -2
- 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/test-output.txt +0 -5431
|
@@ -13,6 +13,8 @@ const fsSync = require('fs');
|
|
|
13
13
|
const fs = require('fs').promises;
|
|
14
14
|
const path = require('path');
|
|
15
15
|
const handlebars = require('handlebars');
|
|
16
|
+
const config = require('../config');
|
|
17
|
+
const buildCopy = require('./build-copy');
|
|
16
18
|
|
|
17
19
|
// Register Handlebars helper for quoting PostgreSQL identifiers
|
|
18
20
|
// PostgreSQL requires identifiers with hyphens or special characters to be quoted
|
|
@@ -39,14 +41,27 @@ handlebars.registerHelper('pgUser', (dbName) => {
|
|
|
39
41
|
|
|
40
42
|
// Helper to generate old user name format (for migration - drops old users with hyphens)
|
|
41
43
|
// This is used to drop legacy users that were created with hyphens before the fix
|
|
44
|
+
// Returns unquoted name (quotes should be added in template where needed)
|
|
42
45
|
handlebars.registerHelper('pgUserOld', (dbName) => {
|
|
43
46
|
if (!dbName) {
|
|
44
47
|
return '';
|
|
45
48
|
}
|
|
46
49
|
// Old format: database name + _user (preserving hyphens)
|
|
47
50
|
const userName = `${String(dbName)}_user`;
|
|
48
|
-
// Return
|
|
49
|
-
return new handlebars.SafeString(
|
|
51
|
+
// Return unquoted name - template will add quotes where needed
|
|
52
|
+
return new handlebars.SafeString(userName);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Helper to generate unquoted PostgreSQL user name (for SQL WHERE clauses)
|
|
56
|
+
// Returns the user name without quotes for use in SQL queries
|
|
57
|
+
handlebars.registerHelper('pgUserName', (dbName) => {
|
|
58
|
+
if (!dbName) {
|
|
59
|
+
return '';
|
|
60
|
+
}
|
|
61
|
+
// Replace hyphens with underscores in user name
|
|
62
|
+
const userName = `${String(dbName).replace(/-/g, '_')}_user`;
|
|
63
|
+
// Return unquoted name for SQL queries
|
|
64
|
+
return new handlebars.SafeString(userName);
|
|
50
65
|
});
|
|
51
66
|
|
|
52
67
|
/**
|
|
@@ -144,8 +159,9 @@ function buildRequiresConfig(config) {
|
|
|
144
159
|
* @returns {Object} Service configuration
|
|
145
160
|
*/
|
|
146
161
|
function buildServiceConfig(appName, config, port) {
|
|
147
|
-
// Container port: build.containerPort > config.port
|
|
148
|
-
|
|
162
|
+
// Container port: build.containerPort > config.port (NEVER use host port parameter)
|
|
163
|
+
// Container port should remain unchanged regardless of developer ID
|
|
164
|
+
const containerPortValue = config.build?.containerPort || config.port || 3000;
|
|
149
165
|
|
|
150
166
|
// Host port: use port parameter (already calculated from CLI --port or config.port in generateDockerCompose)
|
|
151
167
|
// Note: build.localPort is ONLY used for .env file PORT variable (for local PC dev), NOT for Docker Compose
|
|
@@ -277,36 +293,61 @@ async function readDatabasePasswords(envPath, databases, appKey) {
|
|
|
277
293
|
/**
|
|
278
294
|
* Generates Docker Compose configuration from template
|
|
279
295
|
* @param {string} appName - Application name
|
|
280
|
-
* @param {Object}
|
|
296
|
+
* @param {Object} appConfig - Application configuration
|
|
281
297
|
* @param {Object} options - Run options
|
|
282
298
|
* @returns {Promise<string>} Generated compose content
|
|
283
299
|
*/
|
|
284
|
-
async function generateDockerCompose(appName,
|
|
285
|
-
const language =
|
|
300
|
+
async function generateDockerCompose(appName, appConfig, options) {
|
|
301
|
+
const language = appConfig.build?.language || appConfig.language || 'typescript';
|
|
286
302
|
const template = loadDockerComposeTemplate(language);
|
|
287
303
|
|
|
288
304
|
// Use options.port if provided, otherwise use config.port
|
|
289
305
|
// (localPort will be handled in buildServiceConfig)
|
|
290
|
-
const port = options.port ||
|
|
291
|
-
|
|
292
|
-
|
|
306
|
+
const port = options.port || appConfig.port || 3000;
|
|
307
|
+
|
|
308
|
+
// Get developer ID and network name
|
|
309
|
+
const devId = await config.getDeveloperId();
|
|
310
|
+
// Dev 0: infra-aifabrix-network (no dev-0 suffix)
|
|
311
|
+
// Dev > 0: infra-dev{id}-aifabrix-network
|
|
312
|
+
const networkName = devId === 0 ? 'infra-aifabrix-network' : `infra-dev${devId}-aifabrix-network`;
|
|
313
|
+
// Dev 0: aifabrix-{appName} (no dev-0 suffix)
|
|
314
|
+
// Dev > 0: aifabrix-dev{id}-{appName}
|
|
315
|
+
const containerName = devId === 0 ? `aifabrix-${appName}` : `aifabrix-dev${devId}-${appName}`;
|
|
316
|
+
|
|
317
|
+
const serviceConfig = buildServiceConfig(appName, appConfig, port);
|
|
293
318
|
const volumesConfig = buildVolumesConfig(appName);
|
|
294
|
-
const networksConfig = buildNetworksConfig(
|
|
319
|
+
const networksConfig = buildNetworksConfig(appConfig);
|
|
295
320
|
|
|
296
|
-
// Get absolute path to .env file for docker-compose
|
|
297
|
-
const
|
|
321
|
+
// Get absolute path to .env file for docker-compose (use dev-specific directory)
|
|
322
|
+
const devDir = buildCopy.getDevDirectory(appName, devId);
|
|
323
|
+
const envFilePath = path.join(devDir, '.env');
|
|
298
324
|
const envFileAbsolutePath = envFilePath.replace(/\\/g, '/'); // Use forward slashes for Docker
|
|
299
325
|
|
|
300
|
-
// Read database passwords from .env file
|
|
326
|
+
// Read database passwords from .env file only if database is required
|
|
301
327
|
const databases = networksConfig.databases || [];
|
|
302
|
-
const
|
|
328
|
+
const requiresDatabase = serviceConfig.requiresDatabase || false;
|
|
329
|
+
let databasePasswords;
|
|
330
|
+
|
|
331
|
+
if (requiresDatabase || databases.length > 0) {
|
|
332
|
+
// Only read passwords if database is actually required
|
|
333
|
+
databasePasswords = await readDatabasePasswords(envFilePath, databases, appName);
|
|
334
|
+
} else {
|
|
335
|
+
// Return empty passwords when database is not required
|
|
336
|
+
databasePasswords = {
|
|
337
|
+
map: {},
|
|
338
|
+
array: []
|
|
339
|
+
};
|
|
340
|
+
}
|
|
303
341
|
|
|
304
342
|
const templateData = {
|
|
305
343
|
...serviceConfig,
|
|
306
344
|
...volumesConfig,
|
|
307
345
|
...networksConfig,
|
|
308
346
|
envFile: envFileAbsolutePath,
|
|
309
|
-
databasePasswords: databasePasswords
|
|
347
|
+
databasePasswords: databasePasswords,
|
|
348
|
+
devId: devId,
|
|
349
|
+
networkName: networkName,
|
|
350
|
+
containerName: containerName
|
|
310
351
|
};
|
|
311
352
|
|
|
312
353
|
return template(templateData);
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deployment Error Handling Utilities
|
|
3
|
+
*
|
|
4
|
+
* Handles deployment errors with security-aware messages and audit logging.
|
|
5
|
+
*
|
|
6
|
+
* @fileoverview Deployment error handling functions
|
|
7
|
+
* @author AI Fabrix Team
|
|
8
|
+
* @version 2.0.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const auditLogger = require('../audit-logger');
|
|
12
|
+
const { parseErrorResponse } = require('./api-error-handler');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Handles deployment errors with security-aware messages
|
|
16
|
+
*
|
|
17
|
+
* @param {Error} error - Error to handle
|
|
18
|
+
* @returns {Object} Structured error information
|
|
19
|
+
*/
|
|
20
|
+
function handleDeploymentError(error) {
|
|
21
|
+
const safeError = {
|
|
22
|
+
message: error.message,
|
|
23
|
+
code: error.code || 'UNKNOWN',
|
|
24
|
+
timeout: error.code === 'ECONNABORTED',
|
|
25
|
+
status: error.status || error.response?.status,
|
|
26
|
+
data: error.data || error.response?.data
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Mask sensitive information in error messages
|
|
30
|
+
safeError.message = auditLogger.maskSensitiveData(safeError.message);
|
|
31
|
+
|
|
32
|
+
return safeError;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Unified error handler for deployment errors
|
|
37
|
+
* Handles audit logging, error formatting, and user-friendly messages
|
|
38
|
+
* @param {Error} error - Error object
|
|
39
|
+
* @param {string} appName - Application name for audit logging
|
|
40
|
+
* @param {string} url - Controller URL for audit logging
|
|
41
|
+
* @param {boolean} [alreadyLogged=false] - Whether error has already been logged
|
|
42
|
+
* @throws {Error} User-friendly error message
|
|
43
|
+
*/
|
|
44
|
+
async function handleDeploymentErrors(error, appName, url, alreadyLogged = false) {
|
|
45
|
+
// Log to audit log if not already logged
|
|
46
|
+
if (!alreadyLogged) {
|
|
47
|
+
try {
|
|
48
|
+
await auditLogger.logDeploymentFailure(appName, url, error);
|
|
49
|
+
} catch (logError) {
|
|
50
|
+
// Don't fail if audit logging fails, but log to console
|
|
51
|
+
// eslint-disable-next-line no-console
|
|
52
|
+
console.error(`[AUDIT LOG ERROR] Failed to log deployment failure: ${logError.message}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const safeError = handleDeploymentError(error);
|
|
57
|
+
|
|
58
|
+
// Extract error data from axios response
|
|
59
|
+
let errorData = safeError.data;
|
|
60
|
+
if (error.response && error.response.data !== undefined) {
|
|
61
|
+
errorData = error.response.data;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Ensure errorData is not undefined before parsing
|
|
65
|
+
// If errorData is undefined, use the error message instead
|
|
66
|
+
const errorResponse = errorData !== undefined ? errorData : safeError.message;
|
|
67
|
+
|
|
68
|
+
// Determine if this is a network error
|
|
69
|
+
const isNetworkError = safeError.code === 'ECONNREFUSED' ||
|
|
70
|
+
safeError.code === 'ENOTFOUND' ||
|
|
71
|
+
safeError.code === 'ECONNABORTED' ||
|
|
72
|
+
safeError.timeout;
|
|
73
|
+
|
|
74
|
+
// Parse error using error handler
|
|
75
|
+
const parsedError = parseErrorResponse(errorResponse, safeError.status || 0, isNetworkError);
|
|
76
|
+
|
|
77
|
+
// Throw clean error message (without emoji) - CLI will format it
|
|
78
|
+
const formattedError = new Error(parsedError.message);
|
|
79
|
+
formattedError.formatted = parsedError.formatted;
|
|
80
|
+
formattedError.status = safeError.status;
|
|
81
|
+
formattedError.data = parsedError.data;
|
|
82
|
+
formattedError._logged = true; // Mark as logged to prevent double-logging
|
|
83
|
+
throw formattedError;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = {
|
|
87
|
+
handleDeploymentError,
|
|
88
|
+
handleDeploymentErrors
|
|
89
|
+
};
|
|
90
|
+
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deployment Validation Utilities
|
|
3
|
+
*
|
|
4
|
+
* Validates deployment configuration inputs with ISO 27001 security measures.
|
|
5
|
+
*
|
|
6
|
+
* @fileoverview Deployment validation functions
|
|
7
|
+
* @author AI Fabrix Team
|
|
8
|
+
* @version 2.0.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Validates and sanitizes controller URL
|
|
13
|
+
* Enforces HTTPS-only communication for security
|
|
14
|
+
*
|
|
15
|
+
* @param {string} url - Controller URL to validate
|
|
16
|
+
* @throws {Error} If URL is invalid or uses HTTP
|
|
17
|
+
*/
|
|
18
|
+
function validateControllerUrl(url) {
|
|
19
|
+
if (!url || typeof url !== 'string') {
|
|
20
|
+
throw new Error('Controller URL is required and must be a string');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Must use HTTPS for security (allow http://localhost for local development)
|
|
24
|
+
if (!url.startsWith('https://') && !url.startsWith('http://localhost')) {
|
|
25
|
+
throw new Error('Controller URL must use HTTPS (https://) or http://localhost');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Basic URL format validation
|
|
29
|
+
const urlPattern = /^(https?):\/\/[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*(localhost)?(:[0-9]+)?(\/.*)?$/;
|
|
30
|
+
if (!urlPattern.test(url)) {
|
|
31
|
+
throw new Error('Invalid controller URL format');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Remove trailing slash if present
|
|
35
|
+
return url.replace(/\/$/, '');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Validates environment key
|
|
40
|
+
* @param {string} envKey - Environment key to validate
|
|
41
|
+
* @throws {Error} If environment key is invalid
|
|
42
|
+
*/
|
|
43
|
+
function validateEnvironmentKey(envKey) {
|
|
44
|
+
if (!envKey || typeof envKey !== 'string') {
|
|
45
|
+
throw new Error('Environment key is required and must be a string');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const validEnvironments = ['miso', 'dev', 'tst', 'pro'];
|
|
49
|
+
if (!validEnvironments.includes(envKey.toLowerCase())) {
|
|
50
|
+
throw new Error(`Invalid environment key: ${envKey}. Must be one of: ${validEnvironments.join(', ')}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return envKey.toLowerCase();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = {
|
|
57
|
+
validateControllerUrl,
|
|
58
|
+
validateEnvironmentKey
|
|
59
|
+
};
|
|
60
|
+
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Developer Configuration Utilities
|
|
3
|
+
*
|
|
4
|
+
* This module provides utilities for calculating developer-specific ports
|
|
5
|
+
* based on developer ID. Ports are offset by (developer-id * 100).
|
|
6
|
+
*
|
|
7
|
+
* @fileoverview Developer configuration and port calculation utilities
|
|
8
|
+
* @author AI Fabrix Team
|
|
9
|
+
* @version 2.0.0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Base ports for infrastructure and applications
|
|
14
|
+
* These are the default ports before any developer offset
|
|
15
|
+
*/
|
|
16
|
+
const BASE_PORTS = {
|
|
17
|
+
app: 3000,
|
|
18
|
+
postgres: 5432,
|
|
19
|
+
redis: 6379,
|
|
20
|
+
pgadmin: 5050,
|
|
21
|
+
redisCommander: 8081
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Calculates developer-specific ports based on developer ID
|
|
26
|
+
* Formula: basePort + (developerId * 100)
|
|
27
|
+
* Developer ID: 0 = default infra (base ports), > 0 = developer-specific (offset ports)
|
|
28
|
+
*
|
|
29
|
+
* @function getDevPorts
|
|
30
|
+
* @param {number} developerId - Developer ID (0 = default infra, 1, 2, 3, etc. = developer-specific)
|
|
31
|
+
* @returns {Object} Object with calculated ports for all services
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* const ports = getDevPorts(0); // Default infra
|
|
35
|
+
* // Returns: { app: 3000, postgres: 5432, redis: 6379, pgadmin: 5050, redisCommander: 8081 }
|
|
36
|
+
* const ports = getDevPorts(1); // Developer-specific
|
|
37
|
+
* // Returns: { app: 3100, postgres: 5532, redis: 6479, pgadmin: 5150, redisCommander: 8181 }
|
|
38
|
+
*/
|
|
39
|
+
function getDevPorts(developerId) {
|
|
40
|
+
// Validate type first - must be a number
|
|
41
|
+
if (typeof developerId !== 'number') {
|
|
42
|
+
throw new Error('Developer ID must be a positive number');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Handle NaN, undefined, null - throw error (don't default)
|
|
46
|
+
if (isNaN(developerId) || developerId === undefined || developerId === null) {
|
|
47
|
+
throw new Error('Developer ID must be a positive number');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (developerId < 0 || !Number.isInteger(developerId)) {
|
|
51
|
+
throw new Error('Developer ID must be a positive number');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Developer ID 0 = default infra (base ports, no offset)
|
|
55
|
+
if (developerId === 0) {
|
|
56
|
+
return { ...BASE_PORTS };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Developer ID > 0 = developer-specific (add offset)
|
|
60
|
+
const offset = developerId * 100;
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
app: BASE_PORTS.app + offset,
|
|
64
|
+
postgres: BASE_PORTS.postgres + offset,
|
|
65
|
+
redis: BASE_PORTS.redis + offset,
|
|
66
|
+
pgadmin: BASE_PORTS.pgadmin + offset,
|
|
67
|
+
redisCommander: BASE_PORTS.redisCommander + offset
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Gets base ports (for reference/documentation)
|
|
73
|
+
* @returns {Object} Base ports object
|
|
74
|
+
*/
|
|
75
|
+
function getBasePorts() {
|
|
76
|
+
return { ...BASE_PORTS };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = {
|
|
80
|
+
getDevPorts,
|
|
81
|
+
getBasePorts
|
|
82
|
+
};
|
|
83
|
+
|
|
@@ -78,6 +78,30 @@ async function executeDockerBuild(imageName, dockerfilePath, contextPath, tag) {
|
|
|
78
78
|
spinner: 'dots'
|
|
79
79
|
}).start();
|
|
80
80
|
|
|
81
|
+
// Ensure paths are absolute and normalized
|
|
82
|
+
const fsSync = require('fs');
|
|
83
|
+
const path = require('path');
|
|
84
|
+
|
|
85
|
+
dockerfilePath = path.resolve(dockerfilePath);
|
|
86
|
+
contextPath = path.resolve(contextPath);
|
|
87
|
+
|
|
88
|
+
// Validate paths exist (skip in test environments)
|
|
89
|
+
const isTestEnv = process.env.NODE_ENV === 'test' ||
|
|
90
|
+
process.env.JEST_WORKER_ID !== undefined ||
|
|
91
|
+
typeof jest !== 'undefined';
|
|
92
|
+
|
|
93
|
+
if (!isTestEnv) {
|
|
94
|
+
if (!fsSync.existsSync(dockerfilePath)) {
|
|
95
|
+
spinner.fail('Build failed');
|
|
96
|
+
throw new Error(`Dockerfile not found: ${dockerfilePath}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!fsSync.existsSync(contextPath)) {
|
|
100
|
+
spinner.fail('Build failed');
|
|
101
|
+
throw new Error(`Build context path does not exist: ${contextPath}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
81
105
|
return new Promise((resolve, reject) => {
|
|
82
106
|
// Use spawn for streaming output
|
|
83
107
|
const dockerProcess = spawn('docker', [
|
|
@@ -15,14 +15,15 @@ const chalk = require('chalk');
|
|
|
15
15
|
const logger = require('../utils/logger');
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
* Updates env.template to add MISO_CLIENTID and
|
|
18
|
+
* Updates env.template to add MISO_CLIENTID, MISO_CLIENTSECRET, and MISO_CONTROLLER_URL entries
|
|
19
19
|
* @async
|
|
20
20
|
* @param {string} appKey - Application key
|
|
21
21
|
* @param {string} clientIdKey - Secret key for client ID (e.g., 'myapp-client-idKeyVault')
|
|
22
22
|
* @param {string} clientSecretKey - Secret key for client secret (e.g., 'myapp-client-secretKeyVault')
|
|
23
|
+
* @param {string} controllerUrl - Controller URL (e.g., 'http://localhost:3010' or 'https://controller.aifabrix.ai')
|
|
23
24
|
* @returns {Promise<void>} Resolves when template is updated
|
|
24
25
|
*/
|
|
25
|
-
async function updateEnvTemplate(appKey, clientIdKey, clientSecretKey) {
|
|
26
|
+
async function updateEnvTemplate(appKey, clientIdKey, clientSecretKey, controllerUrl) {
|
|
26
27
|
const envTemplatePath = path.join(process.cwd(), 'builder', appKey, 'env.template');
|
|
27
28
|
|
|
28
29
|
if (!fsSync.existsSync(envTemplatePath)) {
|
|
@@ -33,19 +34,39 @@ async function updateEnvTemplate(appKey, clientIdKey, clientSecretKey) {
|
|
|
33
34
|
try {
|
|
34
35
|
let content = await fs.readFile(envTemplatePath, 'utf8');
|
|
35
36
|
|
|
36
|
-
// Check if
|
|
37
|
+
// Check if entries already exist
|
|
37
38
|
const hasClientId = /^MISO_CLIENTID\s*=/m.test(content);
|
|
38
39
|
const hasClientSecret = /^MISO_CLIENTSECRET\s*=/m.test(content);
|
|
40
|
+
const hasControllerUrl = /^MISO_CONTROLLER_URL\s*=/m.test(content);
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
// Update existing entries
|
|
43
|
+
if (hasClientId) {
|
|
42
44
|
content = content.replace(/^MISO_CLIENTID\s*=.*$/m, `MISO_CLIENTID=kv://${clientIdKey}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (hasClientSecret) {
|
|
43
48
|
content = content.replace(/^MISO_CLIENTSECRET\s*=.*$/m, `MISO_CLIENTSECRET=kv://${clientSecretKey}`);
|
|
44
|
-
}
|
|
45
|
-
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (hasControllerUrl) {
|
|
52
|
+
content = content.replace(/^MISO_CONTROLLER_URL\s*=.*$/m, `MISO_CONTROLLER_URL=${controllerUrl}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Add missing entries
|
|
56
|
+
if (!hasClientId || !hasClientSecret || !hasControllerUrl) {
|
|
57
|
+
const missingEntries = [];
|
|
58
|
+
if (!hasClientId) {
|
|
59
|
+
missingEntries.push(`MISO_CLIENTID=kv://${clientIdKey}`);
|
|
60
|
+
}
|
|
61
|
+
if (!hasClientSecret) {
|
|
62
|
+
missingEntries.push(`MISO_CLIENTSECRET=kv://${clientSecretKey}`);
|
|
63
|
+
}
|
|
64
|
+
if (!hasControllerUrl) {
|
|
65
|
+
missingEntries.push(`MISO_CONTROLLER_URL=${controllerUrl}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
46
68
|
const misoSection = `# MISO Application Client Credentials (per application)
|
|
47
|
-
|
|
48
|
-
MISO_CLIENTSECRET=kv://${clientSecretKey}
|
|
69
|
+
${missingEntries.join('\n')}
|
|
49
70
|
`;
|
|
50
71
|
|
|
51
72
|
// Try to find a good place to insert (after last section or at end)
|
|
@@ -60,7 +81,6 @@ MISO_CLIENTSECRET=kv://${clientSecretKey}
|
|
|
60
81
|
}
|
|
61
82
|
|
|
62
83
|
await fs.writeFile(envTemplatePath, content, 'utf8');
|
|
63
|
-
logger.log(chalk.green(`✓ Updated env.template for ${appKey}`));
|
|
64
84
|
} catch (error) {
|
|
65
85
|
logger.warn(chalk.yellow(`⚠️ Could not update env.template: ${error.message}`));
|
|
66
86
|
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
const http = require('http');
|
|
12
|
+
const net = require('net');
|
|
12
13
|
const chalk = require('chalk');
|
|
13
14
|
const { exec } = require('child_process');
|
|
14
15
|
const { promisify } = require('util');
|
|
@@ -300,8 +301,24 @@ async function waitForHealthCheck(appName, timeout = 90, port = null, config = n
|
|
|
300
301
|
throw new Error(`Health check timeout after ${timeout} seconds`);
|
|
301
302
|
}
|
|
302
303
|
|
|
304
|
+
/**
|
|
305
|
+
* Checks if port is available
|
|
306
|
+
* @param {number} port - Port number to check
|
|
307
|
+
* @returns {Promise<boolean>} True if port is available
|
|
308
|
+
*/
|
|
309
|
+
async function checkPortAvailable(port) {
|
|
310
|
+
return new Promise((resolve) => {
|
|
311
|
+
const server = net.createServer();
|
|
312
|
+
server.listen(port, () => {
|
|
313
|
+
server.close(() => resolve(true));
|
|
314
|
+
});
|
|
315
|
+
server.on('error', () => resolve(false));
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
303
319
|
module.exports = {
|
|
304
320
|
waitForHealthCheck,
|
|
305
|
-
checkHealthEndpoint
|
|
321
|
+
checkHealthEndpoint,
|
|
322
|
+
checkPortAvailable
|
|
306
323
|
};
|
|
307
324
|
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Infrastructure Container Utilities
|
|
3
|
+
*
|
|
4
|
+
* This module provides helper functions for finding and checking
|
|
5
|
+
* infrastructure containers. Used by the main infra.js module.
|
|
6
|
+
*
|
|
7
|
+
* @fileoverview Container utilities for infrastructure management
|
|
8
|
+
* @author AI Fabrix Team
|
|
9
|
+
* @version 2.0.0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { exec } = require('child_process');
|
|
13
|
+
const { promisify } = require('util');
|
|
14
|
+
const config = require('../config');
|
|
15
|
+
|
|
16
|
+
const execAsync = promisify(exec);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Finds container by name pattern
|
|
20
|
+
* @private
|
|
21
|
+
* @async
|
|
22
|
+
* @param {string} serviceName - Service name
|
|
23
|
+
* @param {number} [devId] - Developer ID (optional, will be loaded from config if not provided)
|
|
24
|
+
* @returns {Promise<string|null>} Container name or null if not found
|
|
25
|
+
*/
|
|
26
|
+
async function findContainer(serviceName, devId = null) {
|
|
27
|
+
try {
|
|
28
|
+
const developerId = devId || await config.getDeveloperId();
|
|
29
|
+
// Dev 0: aifabrix-{serviceName}, Dev > 0: aifabrix-dev{id}-{serviceName}
|
|
30
|
+
const containerNamePattern = developerId === 0
|
|
31
|
+
? `aifabrix-${serviceName}`
|
|
32
|
+
: `aifabrix-dev${developerId}-${serviceName}`;
|
|
33
|
+
let { stdout } = await execAsync(`docker ps --filter "name=${containerNamePattern}" --format "{{.Names}}"`);
|
|
34
|
+
let containerName = stdout.trim();
|
|
35
|
+
if (!containerName) {
|
|
36
|
+
// Fallback to old naming patterns for backward compatibility
|
|
37
|
+
({ stdout } = await execAsync(`docker ps --filter "name=infra-${serviceName}" --format "{{.Names}}"`));
|
|
38
|
+
containerName = stdout.trim();
|
|
39
|
+
if (!containerName) {
|
|
40
|
+
({ stdout } = await execAsync(`docker ps --filter "name=aifabrix-${serviceName}" --format "{{.Names}}"`));
|
|
41
|
+
containerName = stdout.trim();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return containerName;
|
|
45
|
+
} catch (error) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Checks health status for a service with health checks
|
|
52
|
+
* @private
|
|
53
|
+
* @async
|
|
54
|
+
* @param {string} serviceName - Service name
|
|
55
|
+
* @param {number} [devId] - Developer ID (optional, will be loaded from config if not provided)
|
|
56
|
+
* @returns {Promise<string>} Health status
|
|
57
|
+
*/
|
|
58
|
+
async function checkServiceWithHealthCheck(serviceName, devId = null) {
|
|
59
|
+
try {
|
|
60
|
+
const containerName = await findContainer(serviceName, devId);
|
|
61
|
+
if (!containerName) {
|
|
62
|
+
return 'unknown';
|
|
63
|
+
}
|
|
64
|
+
const { stdout } = await execAsync(`docker inspect --format='{{.State.Health.Status}}' ${containerName}`);
|
|
65
|
+
const status = stdout.trim().replace(/['"]/g, '');
|
|
66
|
+
// Accept both 'healthy' and 'starting' as healthy (starting means it's initializing)
|
|
67
|
+
return (status === 'healthy' || status === 'starting') ? 'healthy' : status;
|
|
68
|
+
} catch (error) {
|
|
69
|
+
return 'unknown';
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Checks health status for a service without health checks
|
|
75
|
+
* @private
|
|
76
|
+
* @async
|
|
77
|
+
* @param {string} serviceName - Service name
|
|
78
|
+
* @param {number} [devId] - Developer ID (optional, will be loaded from config if not provided)
|
|
79
|
+
* @returns {Promise<string>} Health status
|
|
80
|
+
*/
|
|
81
|
+
async function checkServiceWithoutHealthCheck(serviceName, devId = null) {
|
|
82
|
+
try {
|
|
83
|
+
const containerName = await findContainer(serviceName, devId);
|
|
84
|
+
if (!containerName) {
|
|
85
|
+
return 'unknown';
|
|
86
|
+
}
|
|
87
|
+
const { stdout } = await execAsync(`docker inspect --format='{{.State.Status}}' ${containerName}`);
|
|
88
|
+
const status = stdout.trim().replace(/['"]/g, '');
|
|
89
|
+
// Treat 'running' or 'healthy' as 'healthy' for services without health checks
|
|
90
|
+
return (status === 'running' || status === 'healthy') ? 'healthy' : 'unhealthy';
|
|
91
|
+
} catch (error) {
|
|
92
|
+
return 'unknown';
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = {
|
|
97
|
+
findContainer,
|
|
98
|
+
checkServiceWithHealthCheck,
|
|
99
|
+
checkServiceWithoutHealthCheck
|
|
100
|
+
};
|
|
101
|
+
|
|
@@ -12,7 +12,6 @@ const fs = require('fs');
|
|
|
12
12
|
const path = require('path');
|
|
13
13
|
const yaml = require('js-yaml');
|
|
14
14
|
const os = require('os');
|
|
15
|
-
const chalk = require('chalk');
|
|
16
15
|
const logger = require('../utils/logger');
|
|
17
16
|
|
|
18
17
|
/**
|
|
@@ -76,7 +75,6 @@ async function saveLocalSecret(key, value) {
|
|
|
76
75
|
});
|
|
77
76
|
|
|
78
77
|
fs.writeFileSync(secretsPath, yamlContent, { mode: 0o600 });
|
|
79
|
-
logger.log(chalk.green(`✓ Saved secret ${key} to ${secretsPath}`));
|
|
80
78
|
}
|
|
81
79
|
|
|
82
80
|
/**
|