@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 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
- return { registry };
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} appName - Application name
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(appName, 'latest')) {
87
- throw new Error(`Docker image ${appName}:latest not found locally.\nRun 'aifabrix build ${appName}' first`);
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} appName - Application name
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(appName, registry, tags) {
116
- await Promise.all(tags.map(async(tag) => {
117
- await pushUtils.tagImage(`${appName}:latest`, `${registry}/${appName}:${tag}`);
118
- await pushUtils.pushImage(`${registry}/${appName}:${tag}`);
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} appName - Application name
164
+ * @param {string} imageName - Image name (from config)
126
165
  * @param {Array<string>} tags - Image tags
127
166
  */
128
- function displayPushResults(registry, appName, tags) {
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}/${appName}:*`));
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(appName, registry, tags);
197
+ await pushImageTags(imageName, registry, tags);
159
198
 
160
199
  // Display results
161
- displayPushResults(registry, appName, tags);
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
- hasDatabase: config.database || false,
76
- hasRedis: config.redis || false,
77
- hasStorage: config.storage || false,
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
- const icon = info.status === 'running' ? '✅' : '❌';
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: stdout.trim(),
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
- throw new Error(`Failed to push image: ${error.message}`);
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
- req.on('error', (error) => {
208
+ // Set timeout for the request using setTimeout
209
+ timeoutId = setTimeout(() => {
205
210
  if (debug) {
206
- logger.log(chalk.gray(`[DEBUG] Health check request error: ${error.message}`));
211
+ logger.log(chalk.gray('[DEBUG] Health check request timeout after 5 seconds'));
207
212
  }
213
+ req.destroy();
208
214
  resolve(false);
209
- });
210
- req.on('timeout', () => {
215
+ }, 5000);
216
+
217
+ req.on('error', (error) => {
218
+ clearTimeout(timeoutId);
211
219
  if (debug) {
212
- logger.log(chalk.gray('[DEBUG] Health check request timeout after 5 seconds'));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aifabrix/builder",
3
- "version": "2.1.2",
3
+ "version": "2.1.4",
4
4
  "description": "AI Fabrix Local Fabric & Deployment SDK",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -37,7 +37,7 @@ docker stop {{appName}}
37
37
  ## Push to Azure Container Registry
38
38
 
39
39
  ```bash
40
- aifabrix push {{appName}} --action acr --tag latest
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`
@@ -29,4 +29,5 @@ KC_DB_URL_PORT=5432
29
29
  KC_DB_URL_DATABASE=keycloak
30
30
  KC_DB_USERNAME=keycloak_user
31
31
  KC_DB_PASSWORD=kv://databases-keycloak-0-passwordKeyVault
32
+ DB_0_PASSWORD=kv://databases-keycloak-0-passwordKeyVault
32
33
 
@@ -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-urlKeyVault
34
- REDIS_HOST=localhost
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-admin-passwordKeyVault
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 ://web-server-url
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 ://miso-client-idKeyVault
106
- MISO_CLIENTSECRET=kv ://miso-client-secretKeyVault
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-base-urlKeyVault
113
- MORI_API_KEY=kv ://mori-api-keyKeyVault
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 'CREATE DATABASE {{name}};' || true) &&
65
- (psql -d postgres -c \"CREATE USER {{name}}_user WITH PASSWORD '{{name}}_pass123';\" || true) &&
66
- psql -d postgres -c 'GRANT ALL PRIVILEGES ON DATABASE {{name}} TO {{name}}_user;' || true &&
67
- psql -d {{name}} -c 'ALTER SCHEMA public OWNER TO {{name}}_user;' || true &&
68
- psql -d {{name}} -c 'GRANT ALL ON SCHEMA public TO {{name}}_user;' || true &&
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 'CREATE DATABASE {{app.key}};' || true) &&
73
- (psql -d postgres -c \"CREATE USER {{app.key}}_user WITH PASSWORD '{{app.key}}_pass123';\" || true) &&
74
- psql -d postgres -c 'GRANT ALL PRIVILEGES ON DATABASE {{app.key}} TO {{app.key}}_user;' || true &&
75
- psql -d {{app.key}} -c 'ALTER SCHEMA public OWNER TO {{app.key}}_user;' || true &&
76
- psql -d {{app.key}} -c 'GRANT ALL ON SCHEMA public TO {{app.key}}_user;' || true &&
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 'CREATE DATABASE {{name}};' || true) &&
65
- (psql -d postgres -c \"CREATE USER {{name}}_user WITH PASSWORD '{{name}}_pass123';\" || true) &&
66
- psql -d postgres -c 'GRANT ALL PRIVILEGES ON DATABASE {{name}} TO {{name}}_user;' || true &&
67
- psql -d {{name}} -c 'ALTER SCHEMA public OWNER TO {{name}}_user;' || true &&
68
- psql -d {{name}} -c 'GRANT ALL ON SCHEMA public TO {{name}}_user;' || true &&
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 'CREATE DATABASE {{app.key}};' || true) &&
73
- (psql -d postgres -c \"CREATE USER {{app.key}}_user WITH PASSWORD '{{app.key}}_pass123';\" || true) &&
74
- psql -d postgres -c 'GRANT ALL PRIVILEGES ON DATABASE {{app.key}} TO {{app.key}}_user;' || true &&
75
- psql -d {{app.key}} -c 'ALTER SCHEMA public OWNER TO {{app.key}}_user;' || true &&
76
- psql -d {{app.key}} -c 'GRANT ALL ON SCHEMA public TO {{app.key}}_user;' || true &&
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
  "