@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
|
@@ -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
|
+
|
|
@@ -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
|
/**
|
|
@@ -54,26 +54,30 @@ function resolveSecretsPath(secretsPath) {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
/**
|
|
57
|
-
* Determines the actual secrets file
|
|
57
|
+
* Determines the actual secrets file paths that loadSecrets would use
|
|
58
58
|
* Mirrors the cascading lookup logic from loadSecrets
|
|
59
59
|
* @function getActualSecretsPath
|
|
60
60
|
* @param {string} [secretsPath] - Path to secrets file (optional)
|
|
61
61
|
* @param {string} [appName] - Application name (optional, for variables.yaml lookup)
|
|
62
|
-
* @returns {
|
|
62
|
+
* @returns {Object} Object with userPath and buildPath (if configured)
|
|
63
|
+
* @returns {string} returns.userPath - User's secrets file path (~/.aifabrix/secrets.local.yaml)
|
|
64
|
+
* @returns {string|null} returns.buildPath - App's build.secrets file path (if configured in variables.yaml)
|
|
63
65
|
*/
|
|
64
66
|
function getActualSecretsPath(secretsPath, appName) {
|
|
65
67
|
// If explicit path provided, use it (backward compatibility)
|
|
66
68
|
if (secretsPath) {
|
|
67
|
-
|
|
69
|
+
const resolvedPath = resolveSecretsPath(secretsPath);
|
|
70
|
+
return {
|
|
71
|
+
userPath: resolvedPath,
|
|
72
|
+
buildPath: null
|
|
73
|
+
};
|
|
68
74
|
}
|
|
69
75
|
|
|
70
76
|
// Cascading lookup: user's file first
|
|
71
77
|
const userSecretsPath = path.join(os.homedir(), '.aifabrix', 'secrets.local.yaml');
|
|
72
|
-
if (fs.existsSync(userSecretsPath)) {
|
|
73
|
-
return userSecretsPath;
|
|
74
|
-
}
|
|
75
78
|
|
|
76
|
-
//
|
|
79
|
+
// Check build.secrets from variables.yaml if appName provided
|
|
80
|
+
let buildSecretsPath = null;
|
|
77
81
|
if (appName) {
|
|
78
82
|
const variablesPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
|
|
79
83
|
if (fs.existsSync(variablesPath)) {
|
|
@@ -82,29 +86,22 @@ function getActualSecretsPath(secretsPath, appName) {
|
|
|
82
86
|
const variables = yaml.load(variablesContent);
|
|
83
87
|
|
|
84
88
|
if (variables?.build?.secrets) {
|
|
85
|
-
|
|
89
|
+
buildSecretsPath = path.resolve(
|
|
86
90
|
path.dirname(variablesPath),
|
|
87
91
|
variables.build.secrets
|
|
88
92
|
);
|
|
89
|
-
|
|
90
|
-
if (fs.existsSync(buildSecretsPath)) {
|
|
91
|
-
return buildSecretsPath;
|
|
92
|
-
}
|
|
93
93
|
}
|
|
94
94
|
} catch (error) {
|
|
95
|
-
// Ignore errors, continue
|
|
95
|
+
// Ignore errors, continue
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Return user's file path as default (even if it doesn't exist) for error messages
|
|
107
|
-
return userSecretsPath;
|
|
100
|
+
// Return both paths (even if files don't exist) for error messages
|
|
101
|
+
return {
|
|
102
|
+
userPath: userSecretsPath,
|
|
103
|
+
buildPath: buildSecretsPath
|
|
104
|
+
};
|
|
108
105
|
}
|
|
109
106
|
|
|
110
107
|
module.exports = {
|