@aifabrix/builder 2.1.2 → 2.1.4
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/utils/compose-generator.js +111 -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/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 +20 -12
- package/templates/typescript/docker-compose.hbs +20 -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
|
|
|
@@ -10,9 +10,29 @@
|
|
|
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 `"${String(identifier).replace(/"/g, '""')}"`;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Helper to generate quoted PostgreSQL user name from database name
|
|
28
|
+
handlebars.registerHelper('pgUser', (dbName) => {
|
|
29
|
+
if (!dbName) {
|
|
30
|
+
return '';
|
|
31
|
+
}
|
|
32
|
+
const userName = `${String(dbName)}_user`;
|
|
33
|
+
return `"${userName.replace(/"/g, '""')}"`;
|
|
34
|
+
});
|
|
35
|
+
|
|
16
36
|
/**
|
|
17
37
|
* Loads and compiles Docker Compose template
|
|
18
38
|
* @param {string} language - Language type
|
|
@@ -92,8 +112,9 @@ function buildHealthCheckConfig(config) {
|
|
|
92
112
|
* @returns {Object} Requires configuration
|
|
93
113
|
*/
|
|
94
114
|
function buildRequiresConfig(config) {
|
|
115
|
+
const hasDatabases = config.requires?.databases || config.databases;
|
|
95
116
|
return {
|
|
96
|
-
requiresDatabase: config.requires?.database || config.services?.database || false,
|
|
117
|
+
requiresDatabase: config.requires?.database || config.services?.database || !!hasDatabases || false,
|
|
97
118
|
requiresStorage: config.requires?.storage || config.services?.storage || false,
|
|
98
119
|
requiresRedis: config.requires?.redis || config.services?.redis || false
|
|
99
120
|
};
|
|
@@ -154,6 +175,89 @@ function buildNetworksConfig(config) {
|
|
|
154
175
|
};
|
|
155
176
|
}
|
|
156
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Reads database passwords from .env file
|
|
180
|
+
* Requires DB_0_PASSWORD, DB_1_PASSWORD, etc. to be set in .env file
|
|
181
|
+
* @async
|
|
182
|
+
* @param {string} envPath - Path to .env file
|
|
183
|
+
* @param {Array<Object>} databases - Array of database configurations
|
|
184
|
+
* @param {string} appKey - Application key (fallback for single database)
|
|
185
|
+
* @returns {Promise<Object>} Object with passwords array and lookup map
|
|
186
|
+
* @throws {Error} If required password variables are missing
|
|
187
|
+
*/
|
|
188
|
+
async function readDatabasePasswords(envPath, databases, appKey) {
|
|
189
|
+
const passwords = {};
|
|
190
|
+
const passwordsArray = [];
|
|
191
|
+
|
|
192
|
+
// Read .env file
|
|
193
|
+
const envVars = {};
|
|
194
|
+
if (!fsSync.existsSync(envPath)) {
|
|
195
|
+
throw new Error(`.env file not found: ${envPath}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
const envContent = await fs.readFile(envPath, 'utf8');
|
|
200
|
+
const lines = envContent.split('\n');
|
|
201
|
+
|
|
202
|
+
for (const line of lines) {
|
|
203
|
+
const trimmed = line.trim();
|
|
204
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const equalIndex = trimmed.indexOf('=');
|
|
209
|
+
if (equalIndex > 0) {
|
|
210
|
+
const key = trimmed.substring(0, equalIndex).trim();
|
|
211
|
+
const value = trimmed.substring(equalIndex + 1).trim();
|
|
212
|
+
envVars[key] = value;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
} catch (error) {
|
|
216
|
+
throw new Error(`Failed to read .env file: ${error.message}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Process each database
|
|
220
|
+
if (databases && databases.length > 0) {
|
|
221
|
+
for (let i = 0; i < databases.length; i++) {
|
|
222
|
+
const db = databases[i];
|
|
223
|
+
const dbName = db.name || appKey;
|
|
224
|
+
const passwordKey = `DB_${i}_PASSWORD`;
|
|
225
|
+
|
|
226
|
+
if (!(passwordKey in envVars)) {
|
|
227
|
+
throw new Error(`Missing required password variable ${passwordKey} in .env file`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const password = envVars[passwordKey].trim();
|
|
231
|
+
if (!password || password.length === 0) {
|
|
232
|
+
throw new Error(`Password variable ${passwordKey} is empty in .env file`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
passwords[dbName] = password;
|
|
236
|
+
passwordsArray.push(password);
|
|
237
|
+
}
|
|
238
|
+
} else {
|
|
239
|
+
// Single database case - use DB_0_PASSWORD or DB_PASSWORD
|
|
240
|
+
const passwordKey = ('DB_0_PASSWORD' in envVars) ? 'DB_0_PASSWORD' : 'DB_PASSWORD';
|
|
241
|
+
|
|
242
|
+
if (!(passwordKey in envVars)) {
|
|
243
|
+
throw new Error(`Missing required password variable ${passwordKey} in .env file. Add DB_0_PASSWORD or DB_PASSWORD to your .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[appKey] = password;
|
|
252
|
+
passwordsArray.push(password);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
map: passwords,
|
|
257
|
+
array: passwordsArray
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
157
261
|
/**
|
|
158
262
|
* Generates Docker Compose configuration from template
|
|
159
263
|
* @param {string} appName - Application name
|
|
@@ -177,11 +281,16 @@ async function generateDockerCompose(appName, config, options) {
|
|
|
177
281
|
const envFilePath = path.join(process.cwd(), 'builder', appName, '.env');
|
|
178
282
|
const envFileAbsolutePath = envFilePath.replace(/\\/g, '/'); // Use forward slashes for Docker
|
|
179
283
|
|
|
284
|
+
// Read database passwords from .env file
|
|
285
|
+
const databases = networksConfig.databases || [];
|
|
286
|
+
const databasePasswords = await readDatabasePasswords(envFilePath, databases, appName);
|
|
287
|
+
|
|
180
288
|
const templateData = {
|
|
181
289
|
...serviceConfig,
|
|
182
290
|
...volumesConfig,
|
|
183
291
|
...networksConfig,
|
|
184
|
-
envFile: envFileAbsolutePath
|
|
292
|
+
envFile: envFileAbsolutePath,
|
|
293
|
+
databasePasswords: databasePasswords
|
|
185
294
|
};
|
|
186
295
|
|
|
187
296
|
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`
|
|
@@ -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,31 @@ 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 \"CREATE USER {{name}}_user WITH PASSWORD '{{
|
|
66
|
-
psql -d postgres -c
|
|
67
|
-
psql -d {{name}} -c
|
|
68
|
-
psql -d {{name}} -c
|
|
72
|
+
(psql -d postgres -c \"CREATE DATABASE \\\"{{name}}\\\";\" || true) &&
|
|
73
|
+
(psql -d postgres -c \"CREATE USER \\\"{{name}}_user\\\" WITH PASSWORD '${DB_{{@index}}_PASSWORD}';\" || true) &&
|
|
74
|
+
psql -d postgres -c \"GRANT ALL PRIVILEGES ON DATABASE \\\"{{name}}\\\" TO \\\"{{name}}_user\\\";\" || true &&
|
|
75
|
+
psql -d {{name}} -c \"ALTER SCHEMA public OWNER TO \\\"{{name}}_user\\\";\" || true &&
|
|
76
|
+
psql -d {{name}} -c \"GRANT ALL ON SCHEMA public TO \\\"{{name}}_user\\\";\" || true &&
|
|
69
77
|
{{/each}}
|
|
70
78
|
{{else}}
|
|
71
79
|
echo 'Creating {{app.key}} database and user...' &&
|
|
72
|
-
(psql -d postgres -c
|
|
73
|
-
(psql -d postgres -c \"CREATE USER {{app.key}}_user WITH PASSWORD '{{
|
|
74
|
-
psql -d postgres -c
|
|
75
|
-
psql -d {{app.key}} -c
|
|
76
|
-
psql -d {{app.key}} -c
|
|
80
|
+
(psql -d postgres -c \"CREATE DATABASE \\\"{{app.key}}\\\";\" || true) &&
|
|
81
|
+
(psql -d postgres -c \"CREATE USER \\\"{{app.key}}_user\\\" WITH PASSWORD '${DB_0_PASSWORD:-${DB_PASSWORD}}';\" || true) &&
|
|
82
|
+
psql -d postgres -c \"GRANT ALL PRIVILEGES ON DATABASE \\\"{{app.key}}\\\" TO \\\"{{app.key}}_user\\\";\" || true &&
|
|
83
|
+
psql -d {{app.key}} -c \"ALTER SCHEMA public OWNER TO \\\"{{app.key}}_user\\\";\" || true &&
|
|
84
|
+
psql -d {{app.key}} -c \"GRANT ALL ON SCHEMA public TO \\\"{{app.key}}_user\\\";\" || true &&
|
|
77
85
|
{{/if}}
|
|
78
86
|
echo 'Database initialization complete!'
|
|
79
87
|
"
|
|
@@ -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,31 @@ 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 \"CREATE USER {{name}}_user WITH PASSWORD '{{
|
|
66
|
-
psql -d postgres -c
|
|
67
|
-
psql -d {{name}} -c
|
|
68
|
-
psql -d {{name}} -c
|
|
72
|
+
(psql -d postgres -c \"CREATE DATABASE \\\"{{name}}\\\";\" || true) &&
|
|
73
|
+
(psql -d postgres -c \"CREATE USER \\\"{{name}}_user\\\" WITH PASSWORD '${DB_{{@index}}_PASSWORD}';\" || true) &&
|
|
74
|
+
psql -d postgres -c \"GRANT ALL PRIVILEGES ON DATABASE \\\"{{name}}\\\" TO \\\"{{name}}_user\\\";\" || true &&
|
|
75
|
+
psql -d {{name}} -c \"ALTER SCHEMA public OWNER TO \\\"{{name}}_user\\\";\" || true &&
|
|
76
|
+
psql -d {{name}} -c \"GRANT ALL ON SCHEMA public TO \\\"{{name}}_user\\\";\" || true &&
|
|
69
77
|
{{/each}}
|
|
70
78
|
{{else}}
|
|
71
79
|
echo 'Creating {{app.key}} database and user...' &&
|
|
72
|
-
(psql -d postgres -c
|
|
73
|
-
(psql -d postgres -c \"CREATE USER {{app.key}}_user WITH PASSWORD '{{
|
|
74
|
-
psql -d postgres -c
|
|
75
|
-
psql -d {{app.key}} -c
|
|
76
|
-
psql -d {{app.key}} -c
|
|
80
|
+
(psql -d postgres -c \"CREATE DATABASE \\\"{{app.key}}\\\";\" || true) &&
|
|
81
|
+
(psql -d postgres -c \"CREATE USER \\\"{{app.key}}_user\\\" WITH PASSWORD '${DB_0_PASSWORD:-${DB_PASSWORD}}';\" || true) &&
|
|
82
|
+
psql -d postgres -c \"GRANT ALL PRIVILEGES ON DATABASE \\\"{{app.key}}\\\" TO \\\"{{app.key}}_user\\\";\" || true &&
|
|
83
|
+
psql -d {{app.key}} -c \"ALTER SCHEMA public OWNER TO \\\"{{app.key}}_user\\\";\" || true &&
|
|
84
|
+
psql -d {{app.key}} -c \"GRANT ALL ON SCHEMA public TO \\\"{{app.key}}_user\\\";\" || true &&
|
|
77
85
|
{{/if}}
|
|
78
86
|
echo 'Database initialization complete!'
|
|
79
87
|
"
|