@aifabrix/builder 2.1.2 → 2.1.5
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 +1 -1
- package/lib/app-push.js +58 -19
- package/lib/app-readme.js +6 -3
- package/lib/cli.js +3 -1
- package/lib/infra.js +3 -1
- package/lib/push.js +18 -2
- package/lib/templates.js +4 -1
- package/lib/utils/compose-generator.js +127 -2
- package/lib/utils/health-check.js +15 -8
- package/package.json +1 -1
- package/templates/applications/README.md.hbs +1 -1
- package/templates/applications/keycloak/Dockerfile +2 -2
- package/templates/applications/keycloak/env.template +1 -0
- package/templates/applications/miso-controller/env.template +34 -9
- package/templates/applications/miso-controller/rbac.yaml +16 -0
- package/templates/applications/miso-controller/variables.yaml +1 -0
- package/templates/python/docker-compose.hbs +22 -12
- package/templates/typescript/docker-compose.hbs +22 -12
package/lib/app-deploy.js
CHANGED
|
@@ -83,7 +83,7 @@ async function executePush(appName, registry, tags) {
|
|
|
83
83
|
|
|
84
84
|
await Promise.all(tags.map(async(tag) => {
|
|
85
85
|
await pushUtils.tagImage(`${appName}:latest`, `${registry}/${appName}:${tag}`);
|
|
86
|
-
await pushUtils.pushImage(`${registry}/${appName}:${tag}
|
|
86
|
+
await pushUtils.pushImage(`${registry}/${appName}:${tag}`, registry);
|
|
87
87
|
}));
|
|
88
88
|
}
|
|
89
89
|
|
package/lib/app-push.js
CHANGED
|
@@ -42,12 +42,30 @@ function validateAppName(appName) {
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Extracts image name from configuration using the same logic as build command
|
|
47
|
+
* @param {Object} config - Configuration object from variables.yaml
|
|
48
|
+
* @param {string} appName - Application name (fallback)
|
|
49
|
+
* @returns {string} Image name
|
|
50
|
+
*/
|
|
51
|
+
function extractImageName(config, appName) {
|
|
52
|
+
if (typeof config.image === 'string') {
|
|
53
|
+
return config.image.split(':')[0];
|
|
54
|
+
} else if (config.image?.name) {
|
|
55
|
+
return config.image.name;
|
|
56
|
+
} else if (config.app?.key) {
|
|
57
|
+
return config.app.key;
|
|
58
|
+
}
|
|
59
|
+
return appName;
|
|
60
|
+
|
|
61
|
+
}
|
|
62
|
+
|
|
45
63
|
/**
|
|
46
64
|
* Loads push configuration from variables.yaml
|
|
47
65
|
* @async
|
|
48
66
|
* @param {string} appName - Application name
|
|
49
67
|
* @param {Object} options - Push options
|
|
50
|
-
* @returns {Promise<Object>} Configuration with registry
|
|
68
|
+
* @returns {Promise<Object>} Configuration with registry and imageName
|
|
51
69
|
* @throws {Error} If configuration cannot be loaded
|
|
52
70
|
*/
|
|
53
71
|
async function loadPushConfig(appName, options) {
|
|
@@ -58,7 +76,8 @@ async function loadPushConfig(appName, options) {
|
|
|
58
76
|
if (!registry) {
|
|
59
77
|
throw new Error('Registry URL is required. Provide via --registry flag or configure in variables.yaml under image.registry');
|
|
60
78
|
}
|
|
61
|
-
|
|
79
|
+
const imageName = extractImageName(config, appName);
|
|
80
|
+
return { registry, imageName };
|
|
62
81
|
} catch (error) {
|
|
63
82
|
if (error.message.includes('Registry URL')) {
|
|
64
83
|
throw error;
|
|
@@ -70,10 +89,11 @@ async function loadPushConfig(appName, options) {
|
|
|
70
89
|
/**
|
|
71
90
|
* Validates push configuration
|
|
72
91
|
* @param {string} registry - Registry URL
|
|
73
|
-
* @param {string}
|
|
92
|
+
* @param {string} imageName - Image name (from config)
|
|
93
|
+
* @param {string} appName - Application name (for error messages)
|
|
74
94
|
* @throws {Error} If validation fails
|
|
75
95
|
*/
|
|
76
|
-
async function validatePushConfig(registry, appName) {
|
|
96
|
+
async function validatePushConfig(registry, imageName, appName) {
|
|
77
97
|
// Validate ACR URL format specifically (must be *.azurecr.io)
|
|
78
98
|
if (!/^[^.]+\.azurecr\.io$/.test(registry)) {
|
|
79
99
|
throw new Error(`Invalid ACR URL format: ${registry}. Expected format: *.azurecr.io`);
|
|
@@ -83,8 +103,8 @@ async function validatePushConfig(registry, appName) {
|
|
|
83
103
|
throw new Error(`Invalid registry URL format: ${registry}. Expected format: *.azurecr.io`);
|
|
84
104
|
}
|
|
85
105
|
|
|
86
|
-
if (!await pushUtils.checkLocalImageExists(
|
|
87
|
-
throw new Error(`Docker image ${
|
|
106
|
+
if (!await pushUtils.checkLocalImageExists(imageName, 'latest')) {
|
|
107
|
+
throw new Error(`Docker image ${imageName}:latest not found locally.\nRun 'aifabrix build ${appName}' first`);
|
|
88
108
|
}
|
|
89
109
|
|
|
90
110
|
if (!await pushUtils.checkAzureCLIInstalled()) {
|
|
@@ -108,26 +128,45 @@ async function authenticateWithRegistry(registry) {
|
|
|
108
128
|
/**
|
|
109
129
|
* Pushes image tags to registry
|
|
110
130
|
* @async
|
|
111
|
-
* @param {string}
|
|
131
|
+
* @param {string} imageName - Image name (from config)
|
|
112
132
|
* @param {string} registry - Registry URL
|
|
113
133
|
* @param {Array<string>} tags - Image tags
|
|
114
134
|
*/
|
|
115
|
-
async function pushImageTags(
|
|
116
|
-
|
|
117
|
-
await
|
|
118
|
-
|
|
119
|
-
|
|
135
|
+
async function pushImageTags(imageName, registry, tags) {
|
|
136
|
+
try {
|
|
137
|
+
await Promise.all(tags.map(async(tag) => {
|
|
138
|
+
await pushUtils.tagImage(`${imageName}:latest`, `${registry}/${imageName}:${tag}`);
|
|
139
|
+
await pushUtils.pushImage(`${registry}/${imageName}:${tag}`, registry);
|
|
140
|
+
}));
|
|
141
|
+
} catch (error) {
|
|
142
|
+
// If authentication error, try to re-authenticate and retry once
|
|
143
|
+
const errorMessage = error.message || String(error);
|
|
144
|
+
const isAuthError = errorMessage.includes('Authentication required') ||
|
|
145
|
+
errorMessage.includes('authentication required') ||
|
|
146
|
+
(errorMessage.includes('authentication') && errorMessage.includes('401'));
|
|
147
|
+
|
|
148
|
+
if (isAuthError) {
|
|
149
|
+
logger.log(chalk.yellow('⚠ Authentication expired, re-authenticating...'));
|
|
150
|
+
await authenticateWithRegistry(registry);
|
|
151
|
+
// Retry push after re-authentication
|
|
152
|
+
await Promise.all(tags.map(async(tag) => {
|
|
153
|
+
await pushUtils.pushImage(`${registry}/${imageName}:${tag}`, registry);
|
|
154
|
+
}));
|
|
155
|
+
} else {
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
120
159
|
}
|
|
121
160
|
|
|
122
161
|
/**
|
|
123
162
|
* Displays push results
|
|
124
163
|
* @param {string} registry - Registry URL
|
|
125
|
-
* @param {string}
|
|
164
|
+
* @param {string} imageName - Image name (from config)
|
|
126
165
|
* @param {Array<string>} tags - Image tags
|
|
127
166
|
*/
|
|
128
|
-
function displayPushResults(registry,
|
|
167
|
+
function displayPushResults(registry, imageName, tags) {
|
|
129
168
|
logger.log(chalk.green(`\n✓ Successfully pushed ${tags.length} tag(s) to ${registry}`));
|
|
130
|
-
logger.log(chalk.gray(`Image: ${registry}/${
|
|
169
|
+
logger.log(chalk.gray(`Image: ${registry}/${imageName}:*`));
|
|
131
170
|
logger.log(chalk.gray(`Tags: ${tags.join(', ')}`));
|
|
132
171
|
}
|
|
133
172
|
|
|
@@ -145,20 +184,20 @@ async function pushApp(appName, options = {}) {
|
|
|
145
184
|
validateAppName(appName);
|
|
146
185
|
|
|
147
186
|
// Load configuration
|
|
148
|
-
const { registry } = await loadPushConfig(appName, options);
|
|
187
|
+
const { registry, imageName } = await loadPushConfig(appName, options);
|
|
149
188
|
|
|
150
189
|
// Validate push configuration
|
|
151
|
-
await validatePushConfig(registry, appName);
|
|
190
|
+
await validatePushConfig(registry, imageName, appName);
|
|
152
191
|
|
|
153
192
|
// Authenticate with registry
|
|
154
193
|
await authenticateWithRegistry(registry);
|
|
155
194
|
|
|
156
195
|
// Push image tags
|
|
157
196
|
const tags = options.tag ? options.tag.split(',').map(t => t.trim()) : ['latest'];
|
|
158
|
-
await pushImageTags(
|
|
197
|
+
await pushImageTags(imageName, registry, tags);
|
|
159
198
|
|
|
160
199
|
// Display results
|
|
161
|
-
displayPushResults(registry,
|
|
200
|
+
displayPushResults(registry, imageName, tags);
|
|
162
201
|
|
|
163
202
|
} catch (error) {
|
|
164
203
|
throw new Error(`Failed to push application: ${error.message}`);
|
package/lib/app-readme.js
CHANGED
|
@@ -66,15 +66,18 @@ function generateReadmeMd(appName, config) {
|
|
|
66
66
|
const displayName = formatAppDisplayName(appName);
|
|
67
67
|
const imageName = `aifabrix/${appName}`;
|
|
68
68
|
const port = config.port || 3000;
|
|
69
|
+
// Extract registry from nested structure (config.image.registry) or flattened (config.registry)
|
|
70
|
+
const registry = config.image?.registry || config.registry || 'myacr.azurecr.io';
|
|
69
71
|
|
|
70
72
|
const context = {
|
|
71
73
|
appName,
|
|
72
74
|
displayName,
|
|
73
75
|
imageName,
|
|
74
76
|
port,
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
registry,
|
|
78
|
+
hasDatabase: config.database || config.requires?.database || false,
|
|
79
|
+
hasRedis: config.redis || config.requires?.redis || false,
|
|
80
|
+
hasStorage: config.storage || config.requires?.storage || false,
|
|
78
81
|
hasAuthentication: config.authentication || false
|
|
79
82
|
};
|
|
80
83
|
|
package/lib/cli.js
CHANGED
|
@@ -198,7 +198,9 @@ function setupCommands(program) {
|
|
|
198
198
|
logger.log('\n📊 Infrastructure Status\n');
|
|
199
199
|
|
|
200
200
|
Object.entries(status).forEach(([service, info]) => {
|
|
201
|
-
|
|
201
|
+
// Normalize status value for comparison (handle edge cases)
|
|
202
|
+
const normalizedStatus = String(info.status).trim().toLowerCase();
|
|
203
|
+
const icon = normalizedStatus === 'running' ? '✅' : '❌';
|
|
202
204
|
logger.log(`${icon} ${service}:`);
|
|
203
205
|
logger.log(` Status: ${info.status}`);
|
|
204
206
|
logger.log(` Port: ${info.port}`);
|
package/lib/infra.js
CHANGED
|
@@ -290,8 +290,10 @@ async function getInfraStatus() {
|
|
|
290
290
|
const containerName = await findContainer(serviceName);
|
|
291
291
|
if (containerName) {
|
|
292
292
|
const { stdout } = await execAsync(`docker inspect --format='{{.State.Status}}' ${containerName}`);
|
|
293
|
+
// Normalize status value (trim whitespace and remove quotes)
|
|
294
|
+
const normalizedStatus = stdout.trim().replace(/['"]/g, '');
|
|
293
295
|
status[serviceName] = {
|
|
294
|
-
status:
|
|
296
|
+
status: normalizedStatus,
|
|
295
297
|
port: config.port,
|
|
296
298
|
url: config.url
|
|
297
299
|
};
|
package/lib/push.js
CHANGED
|
@@ -210,15 +210,31 @@ async function tagImage(sourceImage, targetImage) {
|
|
|
210
210
|
/**
|
|
211
211
|
* Push Docker image to registry
|
|
212
212
|
* @param {string} imageWithTag - Image with full tag
|
|
213
|
+
* @param {string} registry - Registry URL (for error messages)
|
|
213
214
|
* @throws {Error} If push fails
|
|
214
215
|
*/
|
|
215
|
-
async function pushImage(imageWithTag) {
|
|
216
|
+
async function pushImage(imageWithTag, registry = null) {
|
|
216
217
|
try {
|
|
217
218
|
logger.log(chalk.blue(`Pushing ${imageWithTag}...`));
|
|
218
219
|
await execAsync(`docker push ${imageWithTag}`);
|
|
219
220
|
logger.log(chalk.green(`✓ Pushed: ${imageWithTag}`));
|
|
220
221
|
} catch (error) {
|
|
221
|
-
|
|
222
|
+
const errorMessage = error.message || error.stderr || String(error);
|
|
223
|
+
const isAuthError = errorMessage.includes('authentication required') ||
|
|
224
|
+
errorMessage.includes('unauthorized') ||
|
|
225
|
+
errorMessage.includes('authentication') ||
|
|
226
|
+
errorMessage.includes('401');
|
|
227
|
+
|
|
228
|
+
if (isAuthError && registry) {
|
|
229
|
+
const registryName = extractRegistryName(registry);
|
|
230
|
+
throw new Error(
|
|
231
|
+
`Authentication required for ${registry}.\n` +
|
|
232
|
+
`Run: az acr login --name ${registryName}\n` +
|
|
233
|
+
'Make sure you\'re logged into Azure CLI first: az login'
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
throw new Error(`Failed to push image: ${errorMessage}`);
|
|
222
238
|
}
|
|
223
239
|
}
|
|
224
240
|
|
package/lib/templates.js
CHANGED
|
@@ -128,7 +128,10 @@ function buildDatabaseEnv(config) {
|
|
|
128
128
|
'DB_PORT': '5432',
|
|
129
129
|
'DB_NAME': dbName,
|
|
130
130
|
'DB_USER': `${dbName}_user`,
|
|
131
|
-
'DB_PASSWORD': `kv://databases-${appName}-0-passwordKeyVault
|
|
131
|
+
'DB_PASSWORD': `kv://databases-${appName}-0-passwordKeyVault`,
|
|
132
|
+
// Also include DB_0_PASSWORD for compatibility with compose generator
|
|
133
|
+
// (compose generator expects DB_0_PASSWORD when databases array is present)
|
|
134
|
+
'DB_0_PASSWORD': `kv://databases-${appName}-0-passwordKeyVault`
|
|
132
135
|
};
|
|
133
136
|
}
|
|
134
137
|
|
|
@@ -10,9 +10,45 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
const fsSync = require('fs');
|
|
13
|
+
const fs = require('fs').promises;
|
|
13
14
|
const path = require('path');
|
|
14
15
|
const handlebars = require('handlebars');
|
|
15
16
|
|
|
17
|
+
// Register Handlebars helper for quoting PostgreSQL identifiers
|
|
18
|
+
// PostgreSQL requires identifiers with hyphens or special characters to be quoted
|
|
19
|
+
handlebars.registerHelper('pgQuote', (identifier) => {
|
|
20
|
+
if (!identifier) {
|
|
21
|
+
return '';
|
|
22
|
+
}
|
|
23
|
+
// Always quote identifiers to handle hyphens and special characters
|
|
24
|
+
// Return SafeString to prevent HTML escaping
|
|
25
|
+
return new handlebars.SafeString(`"${String(identifier).replace(/"/g, '""')}"`);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Helper to generate quoted PostgreSQL user name from database name
|
|
29
|
+
// User names must use underscores (not hyphens) for PostgreSQL compatibility
|
|
30
|
+
handlebars.registerHelper('pgUser', (dbName) => {
|
|
31
|
+
if (!dbName) {
|
|
32
|
+
return '';
|
|
33
|
+
}
|
|
34
|
+
// Replace hyphens with underscores in user name (database names can have hyphens, but user names should not)
|
|
35
|
+
const userName = `${String(dbName).replace(/-/g, '_')}_user`;
|
|
36
|
+
// Return SafeString to prevent HTML escaping
|
|
37
|
+
return new handlebars.SafeString(`"${userName.replace(/"/g, '""')}"`);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Helper to generate old user name format (for migration - drops old users with hyphens)
|
|
41
|
+
// This is used to drop legacy users that were created with hyphens before the fix
|
|
42
|
+
handlebars.registerHelper('pgUserOld', (dbName) => {
|
|
43
|
+
if (!dbName) {
|
|
44
|
+
return '';
|
|
45
|
+
}
|
|
46
|
+
// Old format: database name + _user (preserving hyphens)
|
|
47
|
+
const userName = `${String(dbName)}_user`;
|
|
48
|
+
// Return SafeString to prevent HTML escaping
|
|
49
|
+
return new handlebars.SafeString(`"${userName.replace(/"/g, '""')}"`);
|
|
50
|
+
});
|
|
51
|
+
|
|
16
52
|
/**
|
|
17
53
|
* Loads and compiles Docker Compose template
|
|
18
54
|
* @param {string} language - Language type
|
|
@@ -92,8 +128,9 @@ function buildHealthCheckConfig(config) {
|
|
|
92
128
|
* @returns {Object} Requires configuration
|
|
93
129
|
*/
|
|
94
130
|
function buildRequiresConfig(config) {
|
|
131
|
+
const hasDatabases = config.requires?.databases || config.databases;
|
|
95
132
|
return {
|
|
96
|
-
requiresDatabase: config.requires?.database || config.services?.database || false,
|
|
133
|
+
requiresDatabase: config.requires?.database || config.services?.database || !!hasDatabases || false,
|
|
97
134
|
requiresStorage: config.requires?.storage || config.services?.storage || false,
|
|
98
135
|
requiresRedis: config.requires?.redis || config.services?.redis || false
|
|
99
136
|
};
|
|
@@ -154,6 +191,89 @@ function buildNetworksConfig(config) {
|
|
|
154
191
|
};
|
|
155
192
|
}
|
|
156
193
|
|
|
194
|
+
/**
|
|
195
|
+
* Reads database passwords from .env file
|
|
196
|
+
* Requires DB_0_PASSWORD, DB_1_PASSWORD, etc. to be set in .env file
|
|
197
|
+
* @async
|
|
198
|
+
* @param {string} envPath - Path to .env file
|
|
199
|
+
* @param {Array<Object>} databases - Array of database configurations
|
|
200
|
+
* @param {string} appKey - Application key (fallback for single database)
|
|
201
|
+
* @returns {Promise<Object>} Object with passwords array and lookup map
|
|
202
|
+
* @throws {Error} If required password variables are missing
|
|
203
|
+
*/
|
|
204
|
+
async function readDatabasePasswords(envPath, databases, appKey) {
|
|
205
|
+
const passwords = {};
|
|
206
|
+
const passwordsArray = [];
|
|
207
|
+
|
|
208
|
+
// Read .env file
|
|
209
|
+
const envVars = {};
|
|
210
|
+
if (!fsSync.existsSync(envPath)) {
|
|
211
|
+
throw new Error(`.env file not found: ${envPath}`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
const envContent = await fs.readFile(envPath, 'utf8');
|
|
216
|
+
const lines = envContent.split('\n');
|
|
217
|
+
|
|
218
|
+
for (const line of lines) {
|
|
219
|
+
const trimmed = line.trim();
|
|
220
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const equalIndex = trimmed.indexOf('=');
|
|
225
|
+
if (equalIndex > 0) {
|
|
226
|
+
const key = trimmed.substring(0, equalIndex).trim();
|
|
227
|
+
const value = trimmed.substring(equalIndex + 1).trim();
|
|
228
|
+
envVars[key] = value;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
} catch (error) {
|
|
232
|
+
throw new Error(`Failed to read .env file: ${error.message}`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Process each database
|
|
236
|
+
if (databases && databases.length > 0) {
|
|
237
|
+
for (let i = 0; i < databases.length; i++) {
|
|
238
|
+
const db = databases[i];
|
|
239
|
+
const dbName = db.name || appKey;
|
|
240
|
+
const passwordKey = `DB_${i}_PASSWORD`;
|
|
241
|
+
|
|
242
|
+
if (!(passwordKey in envVars)) {
|
|
243
|
+
throw new Error(`Missing required password variable ${passwordKey} in .env file`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const password = envVars[passwordKey].trim();
|
|
247
|
+
if (!password || password.length === 0) {
|
|
248
|
+
throw new Error(`Password variable ${passwordKey} is empty in .env file`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
passwords[dbName] = password;
|
|
252
|
+
passwordsArray.push(password);
|
|
253
|
+
}
|
|
254
|
+
} else {
|
|
255
|
+
// Single database case - use DB_0_PASSWORD or DB_PASSWORD
|
|
256
|
+
const passwordKey = ('DB_0_PASSWORD' in envVars) ? 'DB_0_PASSWORD' : 'DB_PASSWORD';
|
|
257
|
+
|
|
258
|
+
if (!(passwordKey in envVars)) {
|
|
259
|
+
throw new Error(`Missing required password variable ${passwordKey} in .env file. Add DB_0_PASSWORD or DB_PASSWORD to your .env file.`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const password = envVars[passwordKey].trim();
|
|
263
|
+
if (!password || password.length === 0) {
|
|
264
|
+
throw new Error(`Password variable ${passwordKey} is empty in .env file`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
passwords[appKey] = password;
|
|
268
|
+
passwordsArray.push(password);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
map: passwords,
|
|
273
|
+
array: passwordsArray
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
157
277
|
/**
|
|
158
278
|
* Generates Docker Compose configuration from template
|
|
159
279
|
* @param {string} appName - Application name
|
|
@@ -177,11 +297,16 @@ async function generateDockerCompose(appName, config, options) {
|
|
|
177
297
|
const envFilePath = path.join(process.cwd(), 'builder', appName, '.env');
|
|
178
298
|
const envFileAbsolutePath = envFilePath.replace(/\\/g, '/'); // Use forward slashes for Docker
|
|
179
299
|
|
|
300
|
+
// Read database passwords from .env file
|
|
301
|
+
const databases = networksConfig.databases || [];
|
|
302
|
+
const databasePasswords = await readDatabasePasswords(envFilePath, databases, appName);
|
|
303
|
+
|
|
180
304
|
const templateData = {
|
|
181
305
|
...serviceConfig,
|
|
182
306
|
...volumesConfig,
|
|
183
307
|
...networksConfig,
|
|
184
|
-
envFile: envFileAbsolutePath
|
|
308
|
+
envFile: envFileAbsolutePath,
|
|
309
|
+
databasePasswords: databasePasswords
|
|
185
310
|
};
|
|
186
311
|
|
|
187
312
|
return template(templateData);
|
|
@@ -170,8 +170,7 @@ async function checkHealthEndpoint(healthCheckUrl, debug = false) {
|
|
|
170
170
|
hostname: urlObj.hostname,
|
|
171
171
|
port: urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80),
|
|
172
172
|
path: urlObj.pathname + urlObj.search,
|
|
173
|
-
method: 'GET'
|
|
174
|
-
timeout: 5000
|
|
173
|
+
method: 'GET'
|
|
175
174
|
};
|
|
176
175
|
|
|
177
176
|
if (debug) {
|
|
@@ -179,7 +178,12 @@ async function checkHealthEndpoint(healthCheckUrl, debug = false) {
|
|
|
179
178
|
logger.log(chalk.gray(`[DEBUG] Request options: ${JSON.stringify(options, null, 2)}`));
|
|
180
179
|
}
|
|
181
180
|
|
|
181
|
+
// Declare timeoutId before creating req so it can be used in callbacks
|
|
182
|
+
// eslint-disable-next-line prefer-const
|
|
183
|
+
let timeoutId;
|
|
184
|
+
|
|
182
185
|
const req = http.request(options, (res) => {
|
|
186
|
+
clearTimeout(timeoutId);
|
|
183
187
|
let data = '';
|
|
184
188
|
if (debug) {
|
|
185
189
|
logger.log(chalk.gray(`[DEBUG] Response status code: ${res.statusCode}`));
|
|
@@ -201,17 +205,20 @@ async function checkHealthEndpoint(healthCheckUrl, debug = false) {
|
|
|
201
205
|
});
|
|
202
206
|
});
|
|
203
207
|
|
|
204
|
-
|
|
208
|
+
// Set timeout for the request using setTimeout
|
|
209
|
+
timeoutId = setTimeout(() => {
|
|
205
210
|
if (debug) {
|
|
206
|
-
logger.log(chalk.gray(
|
|
211
|
+
logger.log(chalk.gray('[DEBUG] Health check request timeout after 5 seconds'));
|
|
207
212
|
}
|
|
213
|
+
req.destroy();
|
|
208
214
|
resolve(false);
|
|
209
|
-
});
|
|
210
|
-
|
|
215
|
+
}, 5000);
|
|
216
|
+
|
|
217
|
+
req.on('error', (error) => {
|
|
218
|
+
clearTimeout(timeoutId);
|
|
211
219
|
if (debug) {
|
|
212
|
-
logger.log(chalk.gray(
|
|
220
|
+
logger.log(chalk.gray(`[DEBUG] Health check request error: ${error.message}`));
|
|
213
221
|
}
|
|
214
|
-
req.destroy();
|
|
215
222
|
resolve(false);
|
|
216
223
|
});
|
|
217
224
|
|
package/package.json
CHANGED
|
@@ -37,7 +37,7 @@ docker stop {{appName}}
|
|
|
37
37
|
## Push to Azure Container Registry
|
|
38
38
|
|
|
39
39
|
```bash
|
|
40
|
-
aifabrix push {{appName}} --
|
|
40
|
+
aifabrix push {{appName}} --registry {{registry}} --tag latest
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
**Note:** ACR push requires `az login` or ACR credentials in `variables.yaml`
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Keycloak Identity and Access Management with Custom Themes
|
|
2
|
-
# This Dockerfile extends Keycloak
|
|
2
|
+
# This Dockerfile extends Keycloak 26.4 with custom themes
|
|
3
3
|
|
|
4
|
-
FROM quay.io/keycloak/keycloak:
|
|
4
|
+
FROM quay.io/keycloak/keycloak:26.4
|
|
5
5
|
|
|
6
6
|
# Set working directory
|
|
7
7
|
WORKDIR /opt/keycloak
|
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
# Use kv:// references for secrets (resolved from secrets.local.yaml)
|
|
3
3
|
# Use ${VAR} for environment-specific values
|
|
4
4
|
|
|
5
|
+
|
|
6
|
+
# =============================================================================
|
|
7
|
+
# FIRST-TIME ONBOARDING CONFIGURATION
|
|
8
|
+
# =============================================================================
|
|
9
|
+
|
|
10
|
+
# Skip automatic first-time onboarding (default: false)
|
|
11
|
+
# Set to true to disable automatic onboarding on first startup
|
|
12
|
+
SKIP_FIRST_TIME_SETUP=false
|
|
13
|
+
|
|
14
|
+
# Optional custom controller key for onboarding (default: miso-controller)
|
|
15
|
+
ONBOARDING_CONTROLLER_KEY=miso-controller
|
|
16
|
+
|
|
17
|
+
# Optional infrastructure name override for onboarding (default: from INFRASTRUCTURE_NAME or aifabrix)
|
|
18
|
+
ONBOARDING_INFRASTRUCTURE_NAME=
|
|
19
|
+
|
|
20
|
+
# Required for admin user creation during onboarding
|
|
21
|
+
# Password for the initial administrator user (username: admin)
|
|
22
|
+
ONBOARDING_ADMIN_PASSWORD=kv://miso-controller-admin-passwordKeyVault
|
|
23
|
+
|
|
24
|
+
# Optional admin email for onboarding (default: admin@aifabrix.ai)
|
|
25
|
+
ONBOARDING_ADMIN_EMAIL=kv://miso-controller-admin-emailKeyVault
|
|
26
|
+
|
|
5
27
|
# =============================================================================
|
|
6
28
|
# APPLICATION ENVIRONMENT
|
|
7
29
|
# =============================================================================
|
|
@@ -22,7 +44,11 @@ ENABLE_API_DOCS=true
|
|
|
22
44
|
# Connects to external postgres from aifabrix-setup
|
|
23
45
|
|
|
24
46
|
DATABASE_URL=kv://databases-miso-controller-0-urlKeyVault
|
|
47
|
+
DB_0_PASSWORD=kv://databases-miso-controller-0-passwordKeyVault
|
|
48
|
+
|
|
25
49
|
DATABASELOG_URL=kv://databases-miso-controller-1-urlKeyVault
|
|
50
|
+
DB_1_PASSWORD=kv://databases-miso-controller-1-passwordKeyVault
|
|
51
|
+
|
|
26
52
|
MISO_ADMIN_PASSWORD=kv://miso-controller-admin-passwordKeyVault
|
|
27
53
|
|
|
28
54
|
# =============================================================================
|
|
@@ -30,8 +56,8 @@ MISO_ADMIN_PASSWORD=kv://miso-controller-admin-passwordKeyVault
|
|
|
30
56
|
# =============================================================================
|
|
31
57
|
# Connects to external redis from aifabrix-setup
|
|
32
58
|
|
|
33
|
-
REDIS_URL=kv://redis-
|
|
34
|
-
REDIS_HOST
|
|
59
|
+
REDIS_URL=kv://redis-url
|
|
60
|
+
REDIS_HOST=${REDIS_HOST}
|
|
35
61
|
REDIS_PORT=6379
|
|
36
62
|
REDIS_PASSWORD=kv://redis-passwordKeyVault
|
|
37
63
|
REDIS_DB=0
|
|
@@ -53,7 +79,7 @@ KEYCLOAK_ADMIN_PASSWORD=kv://keycloak-admin-passwordKeyVault
|
|
|
53
79
|
KEYCLOAK_PUBLIC_KEY=
|
|
54
80
|
KEYCLOAK_VERIFY_AUDIENCE=false
|
|
55
81
|
KEYCLOAK_TOKEN_TIMEOUT=5000
|
|
56
|
-
KEYCLOAK_DEFAULT_PASSWORD=kv://keycloak-
|
|
82
|
+
KEYCLOAK_DEFAULT_PASSWORD=kv://keycloak-default-passwordKeyVault
|
|
57
83
|
|
|
58
84
|
# Keycloak Events Configuration
|
|
59
85
|
KEYCLOAK_EVENTS_ENABLED=true
|
|
@@ -67,7 +93,6 @@ KEYCLOAK_EVENTS_SECRET=kv://keycloak-events-secretKeyVault
|
|
|
67
93
|
AZURE_SUBSCRIPTION_ID=kv://azure-subscription-idKeyVault
|
|
68
94
|
AZURE_TENANT_ID=kv://azure-tenant-idKeyVault
|
|
69
95
|
AZURE_SERVICE_NAME=kv://azure-service-nameKeyVault
|
|
70
|
-
MOCK=true
|
|
71
96
|
AZURE_CLIENT_ID=kv://azure-client-idKeyVault
|
|
72
97
|
AZURE_CLIENT_SECRET=kv://azure-client-secretKeyVault
|
|
73
98
|
|
|
@@ -96,21 +121,21 @@ MISO_CONTROLLER_URL=kv://miso-controller-url
|
|
|
96
121
|
|
|
97
122
|
# Web Server URL (for OpenAPI documentation server URLs)
|
|
98
123
|
# Used to generate correct server URLs in OpenAPI spec
|
|
99
|
-
WEB_SERVER_URL=kv
|
|
124
|
+
WEB_SERVER_URL=kv://miso-web-server-url
|
|
100
125
|
|
|
101
126
|
# MISO Environment Configuration (miso, dev, tst, pro)
|
|
102
127
|
MISO_ENVIRONMENT=miso
|
|
103
128
|
|
|
104
129
|
# MISO Application Client Credentials (per application)
|
|
105
|
-
MISO_CLIENTID=kv
|
|
106
|
-
MISO_CLIENTSECRET=kv
|
|
130
|
+
MISO_CLIENTID=kv://miso-client-idKeyVault
|
|
131
|
+
MISO_CLIENTSECRET=kv://miso-client-secretKeyVault
|
|
107
132
|
|
|
108
133
|
# =============================================================================
|
|
109
134
|
# MORI SERVICE CONFIGURATION
|
|
110
135
|
# =============================================================================
|
|
111
136
|
|
|
112
|
-
MORI_BASE_URL=kv://mori-
|
|
113
|
-
MORI_API_KEY=kv
|
|
137
|
+
MORI_BASE_URL=kv://mori-controller-url
|
|
138
|
+
MORI_API_KEY=kv://mori-controller-api-keyKeyVault
|
|
114
139
|
|
|
115
140
|
# =============================================================================
|
|
116
141
|
# LOGGING CONFIGURATION
|
|
@@ -190,6 +190,22 @@ permissions:
|
|
|
190
190
|
roles: ["aifabrix-platform-admin", "aifabrix-developer"]
|
|
191
191
|
description: "Write audit and error logs"
|
|
192
192
|
|
|
193
|
+
- name: "logs:export"
|
|
194
|
+
roles: ["aifabrix-platform-admin", "aifabrix-security-admin", "aifabrix-compliance-admin"]
|
|
195
|
+
description: "Export logs for archival and compliance"
|
|
196
|
+
|
|
197
|
+
- name: "audit:read"
|
|
198
|
+
roles: ["aifabrix-platform-admin", "aifabrix-security-admin", "aifabrix-compliance-admin"]
|
|
199
|
+
description: "View audit trail logs"
|
|
200
|
+
|
|
201
|
+
- name: "jobs:read"
|
|
202
|
+
roles: ["aifabrix-platform-admin", "aifabrix-infrastructure-admin", "aifabrix-deployment-admin", "aifabrix-observer"]
|
|
203
|
+
description: "View job and performance logs"
|
|
204
|
+
|
|
205
|
+
- name: "admin:export"
|
|
206
|
+
roles: ["aifabrix-platform-admin"]
|
|
207
|
+
description: "Administrative export access to all data"
|
|
208
|
+
|
|
193
209
|
# Admin Operations
|
|
194
210
|
- name: "admin.sync"
|
|
195
211
|
roles: ["aifabrix-platform-admin", "aifabrix-infrastructure-admin"]
|
|
@@ -49,6 +49,7 @@ build:
|
|
|
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
51
|
secrets: # Path to secrets file
|
|
52
|
+
envFilePath: .env # Generated in builder/
|
|
52
53
|
|
|
53
54
|
# Docker Compose
|
|
54
55
|
compose:
|
|
@@ -36,11 +36,19 @@ services:
|
|
|
36
36
|
container_name: aifabrix-{{app.key}}-db-init
|
|
37
37
|
env_file:
|
|
38
38
|
- ${ADMIN_SECRETS_PATH}
|
|
39
|
+
- {{envFile}}
|
|
39
40
|
environment:
|
|
40
41
|
POSTGRES_DB: postgres
|
|
41
42
|
PGHOST: postgres
|
|
42
43
|
PGPORT: "5432"
|
|
43
44
|
PGUSER: pgadmin
|
|
45
|
+
{{#if databases}}
|
|
46
|
+
{{#each databases}}
|
|
47
|
+
DB_{{@index}}_PASSWORD: {{lookup ../databasePasswords.array @index}}
|
|
48
|
+
{{/each}}
|
|
49
|
+
{{else}}
|
|
50
|
+
DB_PASSWORD: {{lookup databasePasswords.array 0}}
|
|
51
|
+
{{/if}}
|
|
44
52
|
networks:
|
|
45
53
|
- infra_aifabrix-network
|
|
46
54
|
command: >
|
|
@@ -49,31 +57,33 @@ services:
|
|
|
49
57
|
export PGPASSWORD=\"${POSTGRES_PASSWORD}\" &&
|
|
50
58
|
echo 'Waiting for PostgreSQL to be ready...' &&
|
|
51
59
|
counter=0 &&
|
|
52
|
-
while [ $counter -lt 30 ]; do
|
|
60
|
+
while [ ${counter:-0} -lt 30 ]; do
|
|
53
61
|
if pg_isready -h postgres -p 5432 -U pgadmin >/dev/null 2>&1; then
|
|
54
62
|
echo 'PostgreSQL is ready!'
|
|
55
63
|
break
|
|
56
64
|
fi
|
|
57
65
|
echo 'Waiting for PostgreSQL...'
|
|
58
66
|
sleep 1
|
|
59
|
-
counter=$((counter + 1))
|
|
67
|
+
counter=$((${counter:-0} + 1))
|
|
60
68
|
done &&
|
|
61
69
|
{{#if databases}}
|
|
62
70
|
{{#each databases}}
|
|
63
71
|
echo 'Creating {{name}} database and user...' &&
|
|
64
|
-
(psql -d postgres -c
|
|
65
|
-
(psql -d postgres -c \"
|
|
66
|
-
psql -d postgres -c
|
|
67
|
-
psql -d
|
|
68
|
-
psql -d {{name}} -c
|
|
72
|
+
(psql -d postgres -c \"CREATE DATABASE \\\"{{name}}\\\";\" || true) &&
|
|
73
|
+
(psql -d postgres -c \"DROP USER IF EXISTS {{pgUserOld name}};\" || true) &&
|
|
74
|
+
(psql -d postgres -c \"CREATE USER {{pgUser name}} WITH PASSWORD '${DB_{{@index}}_PASSWORD}';\" || true) &&
|
|
75
|
+
psql -d postgres -c \"GRANT ALL PRIVILEGES ON DATABASE \\\"{{name}}\\\" TO {{pgUser name}};\" || true &&
|
|
76
|
+
psql -d {{name}} -c \"ALTER SCHEMA public OWNER TO {{pgUser name}};\" || true &&
|
|
77
|
+
psql -d {{name}} -c \"GRANT ALL ON SCHEMA public TO {{pgUser name}};\" || true &&
|
|
69
78
|
{{/each}}
|
|
70
79
|
{{else}}
|
|
71
80
|
echo 'Creating {{app.key}} database and user...' &&
|
|
72
|
-
(psql -d postgres -c
|
|
73
|
-
(psql -d postgres -c \"
|
|
74
|
-
psql -d postgres -c
|
|
75
|
-
psql -d {{app.key}}
|
|
76
|
-
psql -d {{app.key}} -c
|
|
81
|
+
(psql -d postgres -c \"CREATE DATABASE \\\"{{app.key}}\\\";\" || true) &&
|
|
82
|
+
(psql -d postgres -c \"DROP USER IF EXISTS {{pgUserOld app.key}};\" || true) &&
|
|
83
|
+
(psql -d postgres -c \"CREATE USER {{pgUser app.key}} WITH PASSWORD '${DB_0_PASSWORD:-${DB_PASSWORD}}';\" || true) &&
|
|
84
|
+
psql -d postgres -c \"GRANT ALL PRIVILEGES ON DATABASE \\\"{{app.key}}\\\" TO {{pgUser app.key}};\" || true &&
|
|
85
|
+
psql -d {{app.key}} -c \"ALTER SCHEMA public OWNER TO {{pgUser app.key}};\" || true &&
|
|
86
|
+
psql -d {{app.key}} -c \"GRANT ALL ON SCHEMA public TO {{pgUser app.key}};\" || true &&
|
|
77
87
|
{{/if}}
|
|
78
88
|
echo 'Database initialization complete!'
|
|
79
89
|
"
|
|
@@ -36,11 +36,19 @@ services:
|
|
|
36
36
|
container_name: aifabrix-{{app.key}}-db-init
|
|
37
37
|
env_file:
|
|
38
38
|
- ${ADMIN_SECRETS_PATH}
|
|
39
|
+
- {{envFile}}
|
|
39
40
|
environment:
|
|
40
41
|
POSTGRES_DB: postgres
|
|
41
42
|
PGHOST: postgres
|
|
42
43
|
PGPORT: "5432"
|
|
43
44
|
PGUSER: pgadmin
|
|
45
|
+
{{#if databases}}
|
|
46
|
+
{{#each databases}}
|
|
47
|
+
DB_{{@index}}_PASSWORD: {{lookup ../databasePasswords.array @index}}
|
|
48
|
+
{{/each}}
|
|
49
|
+
{{else}}
|
|
50
|
+
DB_PASSWORD: {{lookup databasePasswords.array 0}}
|
|
51
|
+
{{/if}}
|
|
44
52
|
networks:
|
|
45
53
|
- infra_aifabrix-network
|
|
46
54
|
command: >
|
|
@@ -49,31 +57,33 @@ services:
|
|
|
49
57
|
export PGPASSWORD=\"${POSTGRES_PASSWORD}\" &&
|
|
50
58
|
echo 'Waiting for PostgreSQL to be ready...' &&
|
|
51
59
|
counter=0 &&
|
|
52
|
-
while [ $counter -lt 30 ]; do
|
|
60
|
+
while [ ${counter:-0} -lt 30 ]; do
|
|
53
61
|
if pg_isready -h postgres -p 5432 -U pgadmin >/dev/null 2>&1; then
|
|
54
62
|
echo 'PostgreSQL is ready!'
|
|
55
63
|
break
|
|
56
64
|
fi
|
|
57
65
|
echo 'Waiting for PostgreSQL...'
|
|
58
66
|
sleep 1
|
|
59
|
-
counter=$((counter + 1))
|
|
67
|
+
counter=$((${counter:-0} + 1))
|
|
60
68
|
done &&
|
|
61
69
|
{{#if databases}}
|
|
62
70
|
{{#each databases}}
|
|
63
71
|
echo 'Creating {{name}} database and user...' &&
|
|
64
|
-
(psql -d postgres -c
|
|
65
|
-
(psql -d postgres -c \"
|
|
66
|
-
psql -d postgres -c
|
|
67
|
-
psql -d
|
|
68
|
-
psql -d {{name}} -c
|
|
72
|
+
(psql -d postgres -c \"CREATE DATABASE \\\"{{name}}\\\";\" || true) &&
|
|
73
|
+
(psql -d postgres -c \"DROP USER IF EXISTS {{pgUserOld name}};\" || true) &&
|
|
74
|
+
(psql -d postgres -c \"CREATE USER {{pgUser name}} WITH PASSWORD '${DB_{{@index}}_PASSWORD}';\" || true) &&
|
|
75
|
+
psql -d postgres -c \"GRANT ALL PRIVILEGES ON DATABASE \\\"{{name}}\\\" TO {{pgUser name}};\" || true &&
|
|
76
|
+
psql -d {{name}} -c \"ALTER SCHEMA public OWNER TO {{pgUser name}};\" || true &&
|
|
77
|
+
psql -d {{name}} -c \"GRANT ALL ON SCHEMA public TO {{pgUser name}};\" || true &&
|
|
69
78
|
{{/each}}
|
|
70
79
|
{{else}}
|
|
71
80
|
echo 'Creating {{app.key}} database and user...' &&
|
|
72
|
-
(psql -d postgres -c
|
|
73
|
-
(psql -d postgres -c \"
|
|
74
|
-
psql -d postgres -c
|
|
75
|
-
psql -d {{app.key}}
|
|
76
|
-
psql -d {{app.key}} -c
|
|
81
|
+
(psql -d postgres -c \"CREATE DATABASE \\\"{{app.key}}\\\";\" || true) &&
|
|
82
|
+
(psql -d postgres -c \"DROP USER IF EXISTS {{pgUserOld app.key}};\" || true) &&
|
|
83
|
+
(psql -d postgres -c \"CREATE USER {{pgUser app.key}} WITH PASSWORD '${DB_0_PASSWORD:-${DB_PASSWORD}}';\" || true) &&
|
|
84
|
+
psql -d postgres -c \"GRANT ALL PRIVILEGES ON DATABASE \\\"{{app.key}}\\\" TO {{pgUser app.key}};\" || true &&
|
|
85
|
+
psql -d {{app.key}} -c \"ALTER SCHEMA public OWNER TO {{pgUser app.key}};\" || true &&
|
|
86
|
+
psql -d {{app.key}} -c \"GRANT ALL ON SCHEMA public TO {{pgUser app.key}};\" || true &&
|
|
77
87
|
{{/if}}
|
|
78
88
|
echo 'Database initialization complete!'
|
|
79
89
|
"
|