@aifabrix/builder 2.1.0 → 2.1.2
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-run.js +121 -18
- package/lib/app.js +1 -0
- package/lib/cli.js +1 -0
- package/lib/generator.js +14 -4
- package/lib/schema/application-schema.json +63 -5
- package/lib/utils/health-check.js +105 -11
- package/lib/utils/variable-transformer.js +33 -2
- package/package.json +1 -1
- package/templates/applications/keycloak/variables.yaml +1 -5
- package/templates/applications/keycloak/rbac.yaml +0 -37
package/lib/app-run.js
CHANGED
|
@@ -30,16 +30,28 @@ const execAsync = promisify(exec);
|
|
|
30
30
|
* Checks if Docker image exists for the application
|
|
31
31
|
* @param {string} imageName - Image name (can include repository prefix)
|
|
32
32
|
* @param {string} tag - Image tag (default: latest)
|
|
33
|
+
* @param {boolean} [debug=false] - Enable debug logging
|
|
33
34
|
* @returns {Promise<boolean>} True if image exists
|
|
34
35
|
*/
|
|
35
|
-
async function checkImageExists(imageName, tag = 'latest') {
|
|
36
|
+
async function checkImageExists(imageName, tag = 'latest', debug = false) {
|
|
36
37
|
try {
|
|
37
38
|
const fullImageName = `${imageName}:${tag}`;
|
|
39
|
+
const cmd = `docker images --format "{{.Repository}}:{{.Tag}}" --filter "reference=${fullImageName}"`;
|
|
40
|
+
if (debug) {
|
|
41
|
+
logger.log(chalk.gray(`[DEBUG] Executing: ${cmd}`));
|
|
42
|
+
}
|
|
38
43
|
// Use Docker's native filtering for cross-platform compatibility (Windows-safe)
|
|
39
|
-
const { stdout } = await execAsync(
|
|
44
|
+
const { stdout } = await execAsync(cmd);
|
|
40
45
|
const lines = stdout.trim().split('\n').filter(line => line.trim() !== '');
|
|
41
|
-
|
|
46
|
+
const exists = lines.some(line => line.trim() === fullImageName);
|
|
47
|
+
if (debug) {
|
|
48
|
+
logger.log(chalk.gray(`[DEBUG] Image ${fullImageName} exists: ${exists}`));
|
|
49
|
+
}
|
|
50
|
+
return exists;
|
|
42
51
|
} catch (error) {
|
|
52
|
+
if (debug) {
|
|
53
|
+
logger.log(chalk.gray(`[DEBUG] Image check failed: ${error.message}`));
|
|
54
|
+
}
|
|
43
55
|
return false;
|
|
44
56
|
}
|
|
45
57
|
}
|
|
@@ -47,13 +59,34 @@ async function checkImageExists(imageName, tag = 'latest') {
|
|
|
47
59
|
/**
|
|
48
60
|
* Checks if container is already running
|
|
49
61
|
* @param {string} appName - Application name
|
|
62
|
+
* @param {boolean} [debug=false] - Enable debug logging
|
|
50
63
|
* @returns {Promise<boolean>} True if container is running
|
|
51
64
|
*/
|
|
52
|
-
async function checkContainerRunning(appName) {
|
|
65
|
+
async function checkContainerRunning(appName, debug = false) {
|
|
53
66
|
try {
|
|
54
|
-
const
|
|
55
|
-
|
|
67
|
+
const cmd = `docker ps --filter "name=aifabrix-${appName}" --format "{{.Names}}"`;
|
|
68
|
+
if (debug) {
|
|
69
|
+
logger.log(chalk.gray(`[DEBUG] Executing: ${cmd}`));
|
|
70
|
+
}
|
|
71
|
+
const { stdout } = await execAsync(cmd);
|
|
72
|
+
const isRunning = stdout.trim() === `aifabrix-${appName}`;
|
|
73
|
+
if (debug) {
|
|
74
|
+
logger.log(chalk.gray(`[DEBUG] Container aifabrix-${appName} running: ${isRunning}`));
|
|
75
|
+
if (isRunning) {
|
|
76
|
+
// Get container status details
|
|
77
|
+
const statusCmd = `docker ps --filter "name=aifabrix-${appName}" --format "{{.Status}}"`;
|
|
78
|
+
const { stdout: status } = await execAsync(statusCmd);
|
|
79
|
+
const portsCmd = `docker ps --filter "name=aifabrix-${appName}" --format "{{.Ports}}"`;
|
|
80
|
+
const { stdout: ports } = await execAsync(portsCmd);
|
|
81
|
+
logger.log(chalk.gray(`[DEBUG] Container status: ${status.trim()}`));
|
|
82
|
+
logger.log(chalk.gray(`[DEBUG] Container ports: ${ports.trim()}`));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return isRunning;
|
|
56
86
|
} catch (error) {
|
|
87
|
+
if (debug) {
|
|
88
|
+
logger.log(chalk.gray(`[DEBUG] Container check failed: ${error.message}`));
|
|
89
|
+
}
|
|
57
90
|
return false;
|
|
58
91
|
}
|
|
59
92
|
}
|
|
@@ -61,14 +94,26 @@ async function checkContainerRunning(appName) {
|
|
|
61
94
|
/**
|
|
62
95
|
* Stops and removes existing container
|
|
63
96
|
* @param {string} appName - Application name
|
|
97
|
+
* @param {boolean} [debug=false] - Enable debug logging
|
|
64
98
|
*/
|
|
65
|
-
async function stopAndRemoveContainer(appName) {
|
|
99
|
+
async function stopAndRemoveContainer(appName, debug = false) {
|
|
66
100
|
try {
|
|
67
101
|
logger.log(chalk.yellow(`Stopping existing container aifabrix-${appName}...`));
|
|
68
|
-
|
|
69
|
-
|
|
102
|
+
const stopCmd = `docker stop aifabrix-${appName}`;
|
|
103
|
+
if (debug) {
|
|
104
|
+
logger.log(chalk.gray(`[DEBUG] Executing: ${stopCmd}`));
|
|
105
|
+
}
|
|
106
|
+
await execAsync(stopCmd);
|
|
107
|
+
const rmCmd = `docker rm aifabrix-${appName}`;
|
|
108
|
+
if (debug) {
|
|
109
|
+
logger.log(chalk.gray(`[DEBUG] Executing: ${rmCmd}`));
|
|
110
|
+
}
|
|
111
|
+
await execAsync(rmCmd);
|
|
70
112
|
logger.log(chalk.green(`✓ Container aifabrix-${appName} stopped and removed`));
|
|
71
113
|
} catch (error) {
|
|
114
|
+
if (debug) {
|
|
115
|
+
logger.log(chalk.gray(`[DEBUG] Stop/remove container error: ${error.message}`));
|
|
116
|
+
}
|
|
72
117
|
// Container might not exist, which is fine
|
|
73
118
|
logger.log(chalk.gray(`Container aifabrix-${appName} was not running`));
|
|
74
119
|
}
|
|
@@ -180,17 +225,22 @@ async function validateAppConfiguration(appName) {
|
|
|
180
225
|
* @async
|
|
181
226
|
* @param {string} appName - Application name
|
|
182
227
|
* @param {Object} config - Application configuration
|
|
228
|
+
* @param {boolean} [debug=false] - Enable debug logging
|
|
183
229
|
* @throws {Error} If prerequisites are not met
|
|
184
230
|
*/
|
|
185
|
-
async function checkPrerequisites(appName, config) {
|
|
231
|
+
async function checkPrerequisites(appName, config, debug = false) {
|
|
186
232
|
// Extract image name from configuration (same logic as build process)
|
|
187
233
|
const imageName = getImageName(config, appName);
|
|
188
234
|
const imageTag = config.image?.tag || 'latest';
|
|
189
235
|
const fullImageName = `${imageName}:${imageTag}`;
|
|
190
236
|
|
|
237
|
+
if (debug) {
|
|
238
|
+
logger.log(chalk.gray(`[DEBUG] Image name: ${imageName}, tag: ${imageTag}`));
|
|
239
|
+
}
|
|
240
|
+
|
|
191
241
|
// Check if Docker image exists
|
|
192
242
|
logger.log(chalk.blue(`Checking if image ${fullImageName} exists...`));
|
|
193
|
-
const imageExists = await checkImageExists(imageName, imageTag);
|
|
243
|
+
const imageExists = await checkImageExists(imageName, imageTag, debug);
|
|
194
244
|
if (!imageExists) {
|
|
195
245
|
throw new Error(`Docker image ${fullImageName} not found\nRun 'aifabrix build ${appName}' first`);
|
|
196
246
|
}
|
|
@@ -199,6 +249,9 @@ async function checkPrerequisites(appName, config) {
|
|
|
199
249
|
// Check infrastructure health
|
|
200
250
|
logger.log(chalk.blue('Checking infrastructure health...'));
|
|
201
251
|
const infraHealth = await infra.checkInfraHealth();
|
|
252
|
+
if (debug) {
|
|
253
|
+
logger.log(chalk.gray(`[DEBUG] Infrastructure health: ${JSON.stringify(infraHealth, null, 2)}`));
|
|
254
|
+
}
|
|
202
255
|
const unhealthyServices = Object.entries(infraHealth)
|
|
203
256
|
.filter(([_, status]) => status !== 'healthy')
|
|
204
257
|
.map(([service, _]) => service);
|
|
@@ -258,13 +311,18 @@ async function prepareEnvironment(appName, config, options) {
|
|
|
258
311
|
* @param {string} appName - Application name
|
|
259
312
|
* @param {string} composePath - Path to Docker Compose file
|
|
260
313
|
* @param {number} port - Application port
|
|
314
|
+
* @param {Object} config - Application configuration
|
|
315
|
+
* @param {boolean} [debug=false] - Enable debug logging
|
|
261
316
|
* @throws {Error} If container fails to start or become healthy
|
|
262
317
|
*/
|
|
263
|
-
async function startContainer(appName, composePath, port, config = null) {
|
|
318
|
+
async function startContainer(appName, composePath, port, config = null, debug = false) {
|
|
264
319
|
logger.log(chalk.blue(`Starting ${appName}...`));
|
|
265
320
|
|
|
266
321
|
// Ensure ADMIN_SECRETS_PATH is set for db-init service
|
|
267
322
|
const adminSecretsPath = await infra.ensureAdminSecrets();
|
|
323
|
+
if (debug) {
|
|
324
|
+
logger.log(chalk.gray(`[DEBUG] Admin secrets path: ${adminSecretsPath}`));
|
|
325
|
+
}
|
|
268
326
|
|
|
269
327
|
// Load POSTGRES_PASSWORD from admin-secrets.env
|
|
270
328
|
const adminSecretsContent = fsSync.readFileSync(adminSecretsPath, 'utf8');
|
|
@@ -278,14 +336,33 @@ async function startContainer(appName, composePath, port, config = null) {
|
|
|
278
336
|
POSTGRES_PASSWORD: postgresPassword
|
|
279
337
|
};
|
|
280
338
|
|
|
281
|
-
|
|
339
|
+
if (debug) {
|
|
340
|
+
logger.log(chalk.gray(`[DEBUG] Environment variables: ADMIN_SECRETS_PATH=${adminSecretsPath}, POSTGRES_PASSWORD=${postgresPassword ? '***' : '(not set)'}`));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const composeCmd = `docker-compose -f "${composePath}" up -d`;
|
|
344
|
+
if (debug) {
|
|
345
|
+
logger.log(chalk.gray(`[DEBUG] Executing: ${composeCmd}`));
|
|
346
|
+
logger.log(chalk.gray(`[DEBUG] Compose file: ${composePath}`));
|
|
347
|
+
}
|
|
348
|
+
await execAsync(composeCmd, { env });
|
|
282
349
|
logger.log(chalk.green(`✓ Container aifabrix-${appName} started`));
|
|
283
350
|
|
|
351
|
+
if (debug) {
|
|
352
|
+
// Get container status after start
|
|
353
|
+
const statusCmd = `docker ps --filter "name=aifabrix-${appName}" --format "{{.Status}}"`;
|
|
354
|
+
const { stdout: status } = await execAsync(statusCmd);
|
|
355
|
+
const portsCmd = `docker ps --filter "name=aifabrix-${appName}" --format "{{.Ports}}"`;
|
|
356
|
+
const { stdout: ports } = await execAsync(portsCmd);
|
|
357
|
+
logger.log(chalk.gray(`[DEBUG] Container status: ${status.trim()}`));
|
|
358
|
+
logger.log(chalk.gray(`[DEBUG] Container ports: ${ports.trim()}`));
|
|
359
|
+
}
|
|
360
|
+
|
|
284
361
|
// Wait for health check using host port
|
|
285
362
|
// Port is the host port (CLI --port or config.port, NOT localPort)
|
|
286
363
|
const healthCheckPath = config?.healthCheck?.path || '/health';
|
|
287
364
|
logger.log(chalk.blue(`Waiting for application to be healthy at http://localhost:${port}${healthCheckPath}...`));
|
|
288
|
-
await waitForHealthCheck(appName, 90, port, config);
|
|
365
|
+
await waitForHealthCheck(appName, 90, port, config, debug);
|
|
289
366
|
}
|
|
290
367
|
|
|
291
368
|
/**
|
|
@@ -320,6 +397,7 @@ function displayRunStatus(appName, port, config) {
|
|
|
320
397
|
* @param {string} appName - Name of the application to run
|
|
321
398
|
* @param {Object} options - Run options
|
|
322
399
|
* @param {number} [options.port] - Override local port
|
|
400
|
+
* @param {boolean} [options.debug] - Enable debug output
|
|
323
401
|
* @returns {Promise<void>} Resolves when app is running
|
|
324
402
|
* @throws {Error} If run fails or app is not built
|
|
325
403
|
*
|
|
@@ -328,34 +406,53 @@ function displayRunStatus(appName, port, config) {
|
|
|
328
406
|
* // Application is now running on localhost:3001
|
|
329
407
|
*/
|
|
330
408
|
async function runApp(appName, options = {}) {
|
|
409
|
+
const debug = options.debug || false;
|
|
410
|
+
|
|
411
|
+
if (debug) {
|
|
412
|
+
logger.log(chalk.gray(`[DEBUG] Starting run process for: ${appName}`));
|
|
413
|
+
logger.log(chalk.gray(`[DEBUG] Options: ${JSON.stringify(options, null, 2)}`));
|
|
414
|
+
}
|
|
415
|
+
|
|
331
416
|
try {
|
|
332
417
|
// Validate app name and load configuration
|
|
333
418
|
const config = await validateAppConfiguration(appName);
|
|
419
|
+
if (debug) {
|
|
420
|
+
logger.log(chalk.gray(`[DEBUG] Configuration loaded: port=${config.port || 'default'}, healthCheck.path=${config.healthCheck?.path || '/health'}`));
|
|
421
|
+
}
|
|
334
422
|
|
|
335
423
|
// Check prerequisites: image and infrastructure
|
|
336
|
-
await checkPrerequisites(appName, config);
|
|
424
|
+
await checkPrerequisites(appName, config, debug);
|
|
337
425
|
|
|
338
426
|
// Check if container is already running
|
|
339
|
-
const containerRunning = await checkContainerRunning(appName);
|
|
427
|
+
const containerRunning = await checkContainerRunning(appName, debug);
|
|
340
428
|
if (containerRunning) {
|
|
341
429
|
logger.log(chalk.yellow(`Container aifabrix-${appName} is already running`));
|
|
342
|
-
await stopAndRemoveContainer(appName);
|
|
430
|
+
await stopAndRemoveContainer(appName, debug);
|
|
343
431
|
}
|
|
344
432
|
|
|
345
433
|
// Check port availability
|
|
346
434
|
// Host port: CLI --port if provided, otherwise port from variables.yaml (NOT localPort)
|
|
347
435
|
const port = options.port || config.port || 3000;
|
|
436
|
+
if (debug) {
|
|
437
|
+
logger.log(chalk.gray(`[DEBUG] Port selection: ${port} (${options.port ? 'CLI override' : config.port ? 'config.port' : 'default'})`));
|
|
438
|
+
}
|
|
348
439
|
const portAvailable = await checkPortAvailable(port);
|
|
440
|
+
if (debug) {
|
|
441
|
+
logger.log(chalk.gray(`[DEBUG] Port ${port} available: ${portAvailable}`));
|
|
442
|
+
}
|
|
349
443
|
if (!portAvailable) {
|
|
350
444
|
throw new Error(`Port ${port} is already in use. Try --port <alternative>`);
|
|
351
445
|
}
|
|
352
446
|
|
|
353
447
|
// Prepare environment: ensure .env file and generate Docker Compose
|
|
354
448
|
const tempComposePath = await prepareEnvironment(appName, config, options);
|
|
449
|
+
if (debug) {
|
|
450
|
+
logger.log(chalk.gray(`[DEBUG] Compose file generated: ${tempComposePath}`));
|
|
451
|
+
}
|
|
355
452
|
|
|
356
453
|
try {
|
|
357
454
|
// Start container and wait for health check
|
|
358
|
-
await startContainer(appName, tempComposePath, port, config);
|
|
455
|
+
await startContainer(appName, tempComposePath, port, config, debug);
|
|
359
456
|
|
|
360
457
|
// Display success message
|
|
361
458
|
displayRunStatus(appName, port, config);
|
|
@@ -364,10 +461,16 @@ async function runApp(appName, options = {}) {
|
|
|
364
461
|
// Keep the compose file for debugging - don't delete on error
|
|
365
462
|
logger.log(chalk.yellow(`\n⚠️ Compose file preserved at: ${tempComposePath}`));
|
|
366
463
|
logger.log(chalk.yellow(' Review the file to debug issues'));
|
|
464
|
+
if (debug) {
|
|
465
|
+
logger.log(chalk.gray(`[DEBUG] Error during container start: ${error.message}`));
|
|
466
|
+
}
|
|
367
467
|
throw error;
|
|
368
468
|
}
|
|
369
469
|
|
|
370
470
|
} catch (error) {
|
|
471
|
+
if (debug) {
|
|
472
|
+
logger.log(chalk.gray(`[DEBUG] Run failed: ${error.message}`));
|
|
473
|
+
}
|
|
371
474
|
throw new Error(`Failed to run application: ${error.message}`);
|
|
372
475
|
}
|
|
373
476
|
}
|
package/lib/app.js
CHANGED
|
@@ -361,6 +361,7 @@ async function generateDockerfile(appPath, language, config) {
|
|
|
361
361
|
* @param {string} appName - Name of the application to run
|
|
362
362
|
* @param {Object} options - Run options
|
|
363
363
|
* @param {number} [options.port] - Override local port
|
|
364
|
+
* @param {boolean} [options.debug] - Enable debug output
|
|
364
365
|
* @returns {Promise<void>} Resolves when app is running
|
|
365
366
|
* @throws {Error} If run fails or app is not built
|
|
366
367
|
*
|
package/lib/cli.js
CHANGED
|
@@ -111,6 +111,7 @@ function setupCommands(program) {
|
|
|
111
111
|
program.command('run <app>')
|
|
112
112
|
.description('Run application locally')
|
|
113
113
|
.option('-p, --port <port>', 'Override local port')
|
|
114
|
+
.option('-d, --debug', 'Enable debug output with detailed container information')
|
|
114
115
|
.action(async(appName, options) => {
|
|
115
116
|
try {
|
|
116
117
|
await app.runApp(appName, options);
|
package/lib/generator.js
CHANGED
|
@@ -124,10 +124,19 @@ function buildBaseDeployment(appName, variables, filteredConfiguration) {
|
|
|
124
124
|
function buildAuthenticationConfig(variables, rbac) {
|
|
125
125
|
if (variables.authentication) {
|
|
126
126
|
const auth = {
|
|
127
|
-
|
|
128
|
-
enableSSO: variables.authentication.enableSSO !== undefined ? variables.authentication.enableSSO : true,
|
|
129
|
-
requiredRoles: variables.authentication.requiredRoles || []
|
|
127
|
+
enableSSO: variables.authentication.enableSSO !== undefined ? variables.authentication.enableSSO : true
|
|
130
128
|
};
|
|
129
|
+
|
|
130
|
+
// When enableSSO is false, default type to 'none' and requiredRoles to []
|
|
131
|
+
// When enableSSO is true, require type and requiredRoles
|
|
132
|
+
if (auth.enableSSO === false) {
|
|
133
|
+
auth.type = variables.authentication.type || 'none';
|
|
134
|
+
auth.requiredRoles = variables.authentication.requiredRoles || [];
|
|
135
|
+
} else {
|
|
136
|
+
auth.type = variables.authentication.type || 'azure';
|
|
137
|
+
auth.requiredRoles = variables.authentication.requiredRoles || [];
|
|
138
|
+
}
|
|
139
|
+
|
|
131
140
|
if (variables.authentication.endpoints) {
|
|
132
141
|
auth.endpoints = variables.authentication.endpoints;
|
|
133
142
|
}
|
|
@@ -474,5 +483,6 @@ module.exports = {
|
|
|
474
483
|
buildImageReference,
|
|
475
484
|
buildHealthCheck,
|
|
476
485
|
buildRequirements,
|
|
477
|
-
buildAuthentication
|
|
486
|
+
buildAuthentication,
|
|
487
|
+
buildAuthenticationConfig
|
|
478
488
|
};
|
|
@@ -284,7 +284,7 @@
|
|
|
284
284
|
},
|
|
285
285
|
"healthCheck": {
|
|
286
286
|
"type": "object",
|
|
287
|
-
"description": "Health check configuration",
|
|
287
|
+
"description": "Health check configuration. The health check endpoint must return HTTP 200 and a JSON response with one of the following formats: {\"status\": \"UP\"} (Keycloak format), {\"status\": \"ok\"} (standard format, optionally with {\"database\": \"connected\"}), {\"status\": \"healthy\"} (alternative format), or {\"success\": true} (success-based format). For non-JSON responses, HTTP 200 status code is sufficient.",
|
|
288
288
|
"required": ["path", "interval"],
|
|
289
289
|
"properties": {
|
|
290
290
|
"path": {
|
|
@@ -320,7 +320,51 @@
|
|
|
320
320
|
"maximum": 600
|
|
321
321
|
}
|
|
322
322
|
},
|
|
323
|
-
"additionalProperties": false
|
|
323
|
+
"additionalProperties": false,
|
|
324
|
+
"examples": [
|
|
325
|
+
{
|
|
326
|
+
"path": "/health",
|
|
327
|
+
"interval": 30
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
"path": "/api/health",
|
|
331
|
+
"interval": 60,
|
|
332
|
+
"probePath": "/api/health",
|
|
333
|
+
"probeRequestType": "GET",
|
|
334
|
+
"probeProtocol": "Http",
|
|
335
|
+
"probeIntervalInSeconds": 120
|
|
336
|
+
}
|
|
337
|
+
],
|
|
338
|
+
"responseFormats": {
|
|
339
|
+
"description": "Valid health check response formats",
|
|
340
|
+
"formats": [
|
|
341
|
+
{
|
|
342
|
+
"format": "Keycloak",
|
|
343
|
+
"example": "{\"status\": \"UP\", \"checks\": []}",
|
|
344
|
+
"validation": "status === 'UP'"
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
"format": "Standard",
|
|
348
|
+
"example": "{\"status\": \"ok\", \"database\": \"connected\"}",
|
|
349
|
+
"validation": "status === 'ok' && (database === 'connected' || !database)"
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
"format": "Alternative",
|
|
353
|
+
"example": "{\"status\": \"healthy\", \"service\": \"dataplane\"}",
|
|
354
|
+
"validation": "status === 'healthy'"
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
"format": "Success-based",
|
|
358
|
+
"example": "{\"success\": true, \"message\": \"Service is running\"}",
|
|
359
|
+
"validation": "success === true"
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
"format": "Non-JSON",
|
|
363
|
+
"example": "OK",
|
|
364
|
+
"validation": "HTTP status code === 200"
|
|
365
|
+
}
|
|
366
|
+
]
|
|
367
|
+
}
|
|
324
368
|
},
|
|
325
369
|
"frontDoorRouting": {
|
|
326
370
|
"type": "object",
|
|
@@ -384,8 +428,8 @@
|
|
|
384
428
|
},
|
|
385
429
|
"authentication": {
|
|
386
430
|
"type": "object",
|
|
387
|
-
"description": "Authentication configuration",
|
|
388
|
-
"required": ["
|
|
431
|
+
"description": "Authentication configuration. When enableSSO is false, only enableSSO is required. When enableSSO is true, type and requiredRoles are also required.",
|
|
432
|
+
"required": ["enableSSO"],
|
|
389
433
|
"properties": {
|
|
390
434
|
"type": {
|
|
391
435
|
"type": "string",
|
|
@@ -422,7 +466,21 @@
|
|
|
422
466
|
"additionalProperties": false
|
|
423
467
|
}
|
|
424
468
|
},
|
|
425
|
-
"additionalProperties": false
|
|
469
|
+
"additionalProperties": false,
|
|
470
|
+
"allOf": [
|
|
471
|
+
{
|
|
472
|
+
"if": {
|
|
473
|
+
"properties": {
|
|
474
|
+
"enableSSO": {
|
|
475
|
+
"const": true
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
},
|
|
479
|
+
"then": {
|
|
480
|
+
"required": ["type", "enableSSO", "requiredRoles"]
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
]
|
|
426
484
|
},
|
|
427
485
|
"roles": {
|
|
428
486
|
"type": "array",
|
|
@@ -67,37 +67,61 @@ async function waitForDbInit(appName) {
|
|
|
67
67
|
* @async
|
|
68
68
|
* @function getContainerPort
|
|
69
69
|
* @param {string} appName - Application name
|
|
70
|
+
* @param {boolean} [debug=false] - Enable debug logging
|
|
70
71
|
* @returns {Promise<number>} Container port
|
|
71
72
|
*/
|
|
72
|
-
async function getContainerPort(appName) {
|
|
73
|
+
async function getContainerPort(appName, debug = false) {
|
|
73
74
|
try {
|
|
74
75
|
// Try to get the actual mapped host port from Docker
|
|
75
76
|
// First try docker inspect for the container port mapping
|
|
76
|
-
const
|
|
77
|
+
const inspectCmd = `docker inspect --format='{{range $p, $conf := .NetworkSettings.Ports}}{{if $conf}}{{range $conf}}{{.HostPort}}{{end}}{{end}}{{end}}' aifabrix-${appName}`;
|
|
78
|
+
if (debug) {
|
|
79
|
+
logger.log(chalk.gray(`[DEBUG] Executing: ${inspectCmd}`));
|
|
80
|
+
}
|
|
81
|
+
const { stdout: portMapping } = await execAsync(inspectCmd);
|
|
77
82
|
const ports = portMapping.trim().split('\n').filter(p => p && p !== '');
|
|
78
83
|
if (ports.length > 0) {
|
|
79
84
|
const port = parseInt(ports[0], 10);
|
|
80
85
|
if (!isNaN(port) && port > 0) {
|
|
86
|
+
if (debug) {
|
|
87
|
+
logger.log(chalk.gray(`[DEBUG] Detected port ${port} from docker inspect`));
|
|
88
|
+
}
|
|
81
89
|
return port;
|
|
82
90
|
}
|
|
83
91
|
}
|
|
84
92
|
|
|
85
93
|
// Fallback: try docker ps to get port mapping (format: "0.0.0.0:3010->3000/tcp")
|
|
86
94
|
try {
|
|
87
|
-
const
|
|
95
|
+
const psCmd = `docker ps --filter "name=aifabrix-${appName}" --format "{{.Ports}}"`;
|
|
96
|
+
if (debug) {
|
|
97
|
+
logger.log(chalk.gray(`[DEBUG] Fallback: Executing: ${psCmd}`));
|
|
98
|
+
}
|
|
99
|
+
const { stdout: psOutput } = await execAsync(psCmd);
|
|
88
100
|
const portMatch = psOutput.match(/:(\d+)->/);
|
|
89
101
|
if (portMatch) {
|
|
90
102
|
const port = parseInt(portMatch[1], 10);
|
|
91
103
|
if (!isNaN(port) && port > 0) {
|
|
104
|
+
if (debug) {
|
|
105
|
+
logger.log(chalk.gray(`[DEBUG] Detected port ${port} from docker ps`));
|
|
106
|
+
}
|
|
92
107
|
return port;
|
|
93
108
|
}
|
|
94
109
|
}
|
|
95
110
|
} catch (error) {
|
|
111
|
+
if (debug) {
|
|
112
|
+
logger.log(chalk.gray(`[DEBUG] Fallback port detection failed: ${error.message}`));
|
|
113
|
+
}
|
|
96
114
|
// Fall through
|
|
97
115
|
}
|
|
98
116
|
} catch (error) {
|
|
117
|
+
if (debug) {
|
|
118
|
+
logger.log(chalk.gray(`[DEBUG] Port detection failed: ${error.message}`));
|
|
119
|
+
}
|
|
99
120
|
// Fall through to default
|
|
100
121
|
}
|
|
122
|
+
if (debug) {
|
|
123
|
+
logger.log(chalk.gray('[DEBUG] Using default port 3000'));
|
|
124
|
+
}
|
|
101
125
|
return 3000;
|
|
102
126
|
}
|
|
103
127
|
|
|
@@ -117,6 +141,12 @@ function parseHealthResponse(data, statusCode) {
|
|
|
117
141
|
if (health.status === 'ok') {
|
|
118
142
|
return health.database === 'connected' || !health.database;
|
|
119
143
|
}
|
|
144
|
+
if (health.status === 'healthy') {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
if (health.success === true) {
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
120
150
|
return false;
|
|
121
151
|
} catch (error) {
|
|
122
152
|
return statusCode === 200;
|
|
@@ -128,27 +158,68 @@ function parseHealthResponse(data, statusCode) {
|
|
|
128
158
|
* @async
|
|
129
159
|
* @function checkHealthEndpoint
|
|
130
160
|
* @param {string} healthCheckUrl - Health check URL
|
|
161
|
+
* @param {boolean} [debug=false] - Enable debug logging
|
|
131
162
|
* @returns {Promise<boolean>} True if healthy
|
|
132
163
|
* @throws {Error} If request fails with exception
|
|
133
164
|
*/
|
|
134
|
-
async function checkHealthEndpoint(healthCheckUrl) {
|
|
165
|
+
async function checkHealthEndpoint(healthCheckUrl, debug = false) {
|
|
135
166
|
return new Promise((resolve, reject) => {
|
|
136
167
|
try {
|
|
137
|
-
const
|
|
168
|
+
const urlObj = new URL(healthCheckUrl);
|
|
169
|
+
const options = {
|
|
170
|
+
hostname: urlObj.hostname,
|
|
171
|
+
port: urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80),
|
|
172
|
+
path: urlObj.pathname + urlObj.search,
|
|
173
|
+
method: 'GET',
|
|
174
|
+
timeout: 5000
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
if (debug) {
|
|
178
|
+
logger.log(chalk.gray(`[DEBUG] Health check request: ${healthCheckUrl}`));
|
|
179
|
+
logger.log(chalk.gray(`[DEBUG] Request options: ${JSON.stringify(options, null, 2)}`));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const req = http.request(options, (res) => {
|
|
138
183
|
let data = '';
|
|
184
|
+
if (debug) {
|
|
185
|
+
logger.log(chalk.gray(`[DEBUG] Response status code: ${res.statusCode}`));
|
|
186
|
+
logger.log(chalk.gray(`[DEBUG] Response headers: ${JSON.stringify(res.headers, null, 2)}`));
|
|
187
|
+
}
|
|
139
188
|
res.on('data', (chunk) => {
|
|
140
189
|
data += chunk;
|
|
141
190
|
});
|
|
142
191
|
res.on('end', () => {
|
|
143
|
-
|
|
192
|
+
if (debug) {
|
|
193
|
+
const truncatedData = data.length > 200 ? data.substring(0, 200) + '...' : data;
|
|
194
|
+
logger.log(chalk.gray(`[DEBUG] Response body: ${truncatedData}`));
|
|
195
|
+
}
|
|
196
|
+
const isHealthy = parseHealthResponse(data, res.statusCode);
|
|
197
|
+
if (debug) {
|
|
198
|
+
logger.log(chalk.gray(`[DEBUG] Health check result: ${isHealthy ? 'healthy' : 'unhealthy'}`));
|
|
199
|
+
}
|
|
200
|
+
resolve(isHealthy);
|
|
144
201
|
});
|
|
145
202
|
});
|
|
146
|
-
|
|
203
|
+
|
|
204
|
+
req.on('error', (error) => {
|
|
205
|
+
if (debug) {
|
|
206
|
+
logger.log(chalk.gray(`[DEBUG] Health check request error: ${error.message}`));
|
|
207
|
+
}
|
|
208
|
+
resolve(false);
|
|
209
|
+
});
|
|
147
210
|
req.on('timeout', () => {
|
|
211
|
+
if (debug) {
|
|
212
|
+
logger.log(chalk.gray('[DEBUG] Health check request timeout after 5 seconds'));
|
|
213
|
+
}
|
|
148
214
|
req.destroy();
|
|
149
215
|
resolve(false);
|
|
150
216
|
});
|
|
217
|
+
|
|
218
|
+
req.end();
|
|
151
219
|
} catch (error) {
|
|
220
|
+
if (debug) {
|
|
221
|
+
logger.log(chalk.gray(`[DEBUG] Health check exception: ${error.message}`));
|
|
222
|
+
}
|
|
152
223
|
// Re-throw exceptions (not just network errors)
|
|
153
224
|
reject(error);
|
|
154
225
|
}
|
|
@@ -165,28 +236,47 @@ async function checkHealthEndpoint(healthCheckUrl) {
|
|
|
165
236
|
* @param {number} timeout - Timeout in seconds (default: 90)
|
|
166
237
|
* @param {number} [port] - Application port (auto-detected if not provided)
|
|
167
238
|
* @param {Object} [config] - Application configuration
|
|
239
|
+
* @param {boolean} [debug=false] - Enable debug logging
|
|
168
240
|
* @returns {Promise<void>} Resolves when health check passes
|
|
169
241
|
* @throws {Error} If health check times out
|
|
170
242
|
*/
|
|
171
|
-
async function waitForHealthCheck(appName, timeout = 90, port = null, config = null) {
|
|
243
|
+
async function waitForHealthCheck(appName, timeout = 90, port = null, config = null, debug = false) {
|
|
172
244
|
await waitForDbInit(appName);
|
|
173
245
|
|
|
174
246
|
// Use provided port if given, otherwise detect from Docker
|
|
175
247
|
// Port provided should be the host port (CLI --port or config.port, NOT localPort)
|
|
176
|
-
const healthCheckPort = port !== null && port !== undefined ? port : await getContainerPort(appName);
|
|
248
|
+
const healthCheckPort = port !== null && port !== undefined ? port : await getContainerPort(appName, debug);
|
|
249
|
+
|
|
250
|
+
if (debug) {
|
|
251
|
+
logger.log(chalk.gray(`[DEBUG] Health check port: ${healthCheckPort} (${port !== null && port !== undefined ? 'provided' : 'auto-detected'})`));
|
|
252
|
+
}
|
|
177
253
|
|
|
178
254
|
const healthCheckPath = config?.healthCheck?.path || '/health';
|
|
179
255
|
const healthCheckUrl = `http://localhost:${healthCheckPort}${healthCheckPath}`;
|
|
180
256
|
const maxAttempts = timeout / 2;
|
|
181
257
|
|
|
258
|
+
if (debug) {
|
|
259
|
+
logger.log(chalk.gray(`[DEBUG] Health check URL: ${healthCheckUrl}`));
|
|
260
|
+
logger.log(chalk.gray(`[DEBUG] Timeout: ${timeout} seconds, Max attempts: ${maxAttempts}`));
|
|
261
|
+
}
|
|
262
|
+
|
|
182
263
|
for (let attempts = 0; attempts < maxAttempts; attempts++) {
|
|
183
264
|
try {
|
|
184
|
-
|
|
265
|
+
if (debug) {
|
|
266
|
+
logger.log(chalk.gray(`[DEBUG] Health check attempt ${attempts + 1}/${maxAttempts}`));
|
|
267
|
+
}
|
|
268
|
+
const healthCheckPassed = await checkHealthEndpoint(healthCheckUrl, debug);
|
|
185
269
|
if (healthCheckPassed) {
|
|
186
270
|
logger.log(chalk.green('✓ Application is healthy'));
|
|
271
|
+
if (debug) {
|
|
272
|
+
logger.log(chalk.gray(`[DEBUG] Health check passed after ${attempts + 1} attempt(s)`));
|
|
273
|
+
}
|
|
187
274
|
return;
|
|
188
275
|
}
|
|
189
276
|
} catch (error) {
|
|
277
|
+
if (debug) {
|
|
278
|
+
logger.log(chalk.gray(`[DEBUG] Health check exception on attempt ${attempts + 1}: ${error.message}`));
|
|
279
|
+
}
|
|
190
280
|
// If exception occurs, continue retrying until timeout
|
|
191
281
|
// The error will be handled by timeout error below
|
|
192
282
|
}
|
|
@@ -197,10 +287,14 @@ async function waitForHealthCheck(appName, timeout = 90, port = null, config = n
|
|
|
197
287
|
}
|
|
198
288
|
}
|
|
199
289
|
|
|
290
|
+
if (debug) {
|
|
291
|
+
logger.log(chalk.gray(`[DEBUG] Health check failed after ${maxAttempts} attempts`));
|
|
292
|
+
}
|
|
200
293
|
throw new Error(`Health check timeout after ${timeout} seconds`);
|
|
201
294
|
}
|
|
202
295
|
|
|
203
296
|
module.exports = {
|
|
204
|
-
waitForHealthCheck
|
|
297
|
+
waitForHealthCheck,
|
|
298
|
+
checkHealthEndpoint
|
|
205
299
|
};
|
|
206
300
|
|
|
@@ -55,6 +55,23 @@ function transformFlatStructure(variables, appName) {
|
|
|
55
55
|
type: sanitizeAuthType(result.authentication.type)
|
|
56
56
|
};
|
|
57
57
|
}
|
|
58
|
+
// Handle partial authentication objects (when only enableSSO is provided)
|
|
59
|
+
if (result.authentication && result.authentication.enableSSO !== undefined) {
|
|
60
|
+
const auth = {
|
|
61
|
+
...result.authentication,
|
|
62
|
+
enableSSO: result.authentication.enableSSO
|
|
63
|
+
};
|
|
64
|
+
// When enableSSO is false, default type to 'none' and requiredRoles to []
|
|
65
|
+
// When enableSSO is true, default type to 'azure' if not provided
|
|
66
|
+
if (auth.enableSSO === false) {
|
|
67
|
+
auth.type = sanitizeAuthType(result.authentication.type || 'none');
|
|
68
|
+
auth.requiredRoles = result.authentication.requiredRoles || [];
|
|
69
|
+
} else {
|
|
70
|
+
auth.type = sanitizeAuthType(result.authentication.type || 'azure');
|
|
71
|
+
auth.requiredRoles = result.authentication.requiredRoles || [];
|
|
72
|
+
}
|
|
73
|
+
result.authentication = auth;
|
|
74
|
+
}
|
|
58
75
|
|
|
59
76
|
return result;
|
|
60
77
|
}
|
|
@@ -182,10 +199,24 @@ function transformOptionalFields(variables, transformed) {
|
|
|
182
199
|
}
|
|
183
200
|
|
|
184
201
|
if (variables.authentication) {
|
|
185
|
-
|
|
202
|
+
// Ensure authentication object has enableSSO at minimum
|
|
203
|
+
// Default type and requiredRoles based on enableSSO value
|
|
204
|
+
const auth = {
|
|
186
205
|
...variables.authentication,
|
|
187
|
-
|
|
206
|
+
enableSSO: variables.authentication.enableSSO !== undefined ? variables.authentication.enableSSO : true
|
|
188
207
|
};
|
|
208
|
+
|
|
209
|
+
// When enableSSO is false, default type to 'none' and requiredRoles to []
|
|
210
|
+
// When enableSSO is true, default type to 'azure' if not provided
|
|
211
|
+
if (auth.enableSSO === false) {
|
|
212
|
+
auth.type = sanitizeAuthType(variables.authentication.type || 'none');
|
|
213
|
+
auth.requiredRoles = variables.authentication.requiredRoles || [];
|
|
214
|
+
} else {
|
|
215
|
+
auth.type = sanitizeAuthType(variables.authentication.type || 'azure');
|
|
216
|
+
auth.requiredRoles = variables.authentication.requiredRoles || [];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
transformed.authentication = auth;
|
|
189
220
|
}
|
|
190
221
|
|
|
191
222
|
const repository = validateRepositoryConfig(variables.repository);
|
package/package.json
CHANGED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
roles:
|
|
2
|
-
- name: "AI Fabrix Admin"
|
|
3
|
-
value: "aifabrix-admin"
|
|
4
|
-
description: "Full access to all application features and configurations"
|
|
5
|
-
|
|
6
|
-
- name: "AI Fabrix User"
|
|
7
|
-
value: "aifabrix-user"
|
|
8
|
-
description: "Basic user access to the application"
|
|
9
|
-
|
|
10
|
-
- name: "AI Fabrix Developer"
|
|
11
|
-
value: "aifabrix-developer"
|
|
12
|
-
description: "Developer access for testing and debugging"
|
|
13
|
-
|
|
14
|
-
permissions:
|
|
15
|
-
- name: "myapp:read"
|
|
16
|
-
roles:
|
|
17
|
-
- "aifabrix-user"
|
|
18
|
-
- "aifabrix-admin"
|
|
19
|
-
- "aifabrix-developer"
|
|
20
|
-
description: "Read access to application data"
|
|
21
|
-
|
|
22
|
-
- name: "myapp:write"
|
|
23
|
-
roles:
|
|
24
|
-
- "aifabrix-admin"
|
|
25
|
-
- "aifabrix-developer"
|
|
26
|
-
description: "Create and edit application data"
|
|
27
|
-
|
|
28
|
-
- name: "myapp:delete"
|
|
29
|
-
roles:
|
|
30
|
-
- "aifabrix-admin"
|
|
31
|
-
description: "Delete application data"
|
|
32
|
-
|
|
33
|
-
- name: "myapp:admin"
|
|
34
|
-
roles:
|
|
35
|
-
- "aifabrix-admin"
|
|
36
|
-
description: "Administrative access to application configuration"
|
|
37
|
-
|