@aifabrix/builder 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/infra.js ADDED
@@ -0,0 +1,300 @@
1
+ /**
2
+ * AI Fabrix Builder Infrastructure Management
3
+ *
4
+ * This module manages local infrastructure services using Docker Compose.
5
+ * Handles starting/stopping Postgres, Redis, Keycloak, and Controller services.
6
+ *
7
+ * @fileoverview Local infrastructure management for AI Fabrix Builder
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ const { exec } = require('child_process');
13
+ const { promisify } = require('util');
14
+ const path = require('path');
15
+ const fs = require('fs');
16
+ const os = require('os');
17
+ const secrets = require('./secrets');
18
+
19
+ const execAsync = promisify(exec);
20
+
21
+ /**
22
+ * Starts local infrastructure services
23
+ * Launches Postgres, Redis, Keycloak, and Controller in Docker containers
24
+ *
25
+ * @async
26
+ * @function startInfra
27
+ * @returns {Promise<void>} Resolves when infrastructure is started
28
+ * @throws {Error} If Docker is not running or compose fails
29
+ *
30
+ * @example
31
+ * await startInfra();
32
+ * // Infrastructure services are now running
33
+ */
34
+ async function checkDockerAvailability() {
35
+ try {
36
+ await execAsync('docker --version');
37
+ await execAsync('docker-compose --version');
38
+ } catch (error) {
39
+ throw new Error('Docker or Docker Compose is not available. Please install and start Docker.');
40
+ }
41
+ }
42
+
43
+ async function ensureAdminSecrets() {
44
+ const adminSecretsPath = path.join(os.homedir(), '.aifabrix', 'admin-secrets.env');
45
+ if (!fs.existsSync(adminSecretsPath)) {
46
+ console.log('Generating admin-secrets.env...');
47
+ await secrets.generateAdminSecretsEnv();
48
+ }
49
+ return adminSecretsPath;
50
+ }
51
+
52
+ async function startInfra() {
53
+ await checkDockerAvailability();
54
+ const adminSecretsPath = await ensureAdminSecrets();
55
+
56
+ // Load compose template
57
+ const templatePath = path.join(__dirname, '..', 'templates', 'infra', 'compose.yaml');
58
+ if (!fs.existsSync(templatePath)) {
59
+ throw new Error(`Compose template not found: ${templatePath}`);
60
+ }
61
+
62
+ const template = fs.readFileSync(templatePath, 'utf8');
63
+ const tempComposePath = path.join(os.tmpdir(), 'aifabrix-compose.yaml');
64
+ fs.writeFileSync(tempComposePath, template);
65
+
66
+ try {
67
+ console.log('Starting infrastructure services...');
68
+ await execAsync(`docker-compose -f "${tempComposePath}" --env-file "${adminSecretsPath}" up -d`);
69
+ console.log('Infrastructure services started successfully');
70
+
71
+ await waitForServices();
72
+ console.log('All services are healthy and ready');
73
+ } finally {
74
+ if (fs.existsSync(tempComposePath)) {
75
+ fs.unlinkSync(tempComposePath);
76
+ }
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Stops and removes local infrastructure services
82
+ * Cleanly shuts down all infrastructure containers
83
+ *
84
+ * @async
85
+ * @function stopInfra
86
+ * @returns {Promise<void>} Resolves when infrastructure is stopped
87
+ * @throws {Error} If Docker compose fails
88
+ *
89
+ * @example
90
+ * await stopInfra();
91
+ * // All infrastructure containers are stopped and removed
92
+ */
93
+ async function stopInfra() {
94
+ const templatePath = path.join(__dirname, '..', 'templates', 'infra', 'compose.yaml');
95
+ const adminSecretsPath = path.join(os.homedir(), '.aifabrix', 'admin-secrets.env');
96
+
97
+ if (!fs.existsSync(templatePath) || !fs.existsSync(adminSecretsPath)) {
98
+ console.log('Infrastructure not running or not properly configured');
99
+ return;
100
+ }
101
+
102
+ const tempComposePath = path.join(os.tmpdir(), 'aifabrix-compose.yaml');
103
+ fs.writeFileSync(tempComposePath, fs.readFileSync(templatePath, 'utf8'));
104
+
105
+ try {
106
+ console.log('Stopping infrastructure services...');
107
+ await execAsync(`docker-compose -f "${tempComposePath}" --env-file "${adminSecretsPath}" down`);
108
+ console.log('Infrastructure services stopped');
109
+ } finally {
110
+ if (fs.existsSync(tempComposePath)) {
111
+ fs.unlinkSync(tempComposePath);
112
+ }
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Stops and removes local infrastructure services with volumes
118
+ * Cleanly shuts down all infrastructure containers and removes all data
119
+ *
120
+ * @async
121
+ * @function stopInfraWithVolumes
122
+ * @returns {Promise<void>} Resolves when infrastructure is stopped and volumes removed
123
+ * @throws {Error} If Docker compose fails
124
+ *
125
+ * @example
126
+ * await stopInfraWithVolumes();
127
+ * // All infrastructure containers and data are removed
128
+ */
129
+ async function stopInfraWithVolumes() {
130
+ const templatePath = path.join(__dirname, '..', 'templates', 'infra', 'compose.yaml');
131
+ const adminSecretsPath = path.join(os.homedir(), '.aifabrix', 'admin-secrets.env');
132
+
133
+ if (!fs.existsSync(templatePath) || !fs.existsSync(adminSecretsPath)) {
134
+ console.log('Infrastructure not running or not properly configured');
135
+ return;
136
+ }
137
+
138
+ const tempComposePath = path.join(os.tmpdir(), 'aifabrix-compose.yaml');
139
+ fs.writeFileSync(tempComposePath, fs.readFileSync(templatePath, 'utf8'));
140
+
141
+ try {
142
+ console.log('Stopping infrastructure services and removing all data...');
143
+ await execAsync(`docker-compose -f "${tempComposePath}" --env-file "${adminSecretsPath}" down -v`);
144
+ console.log('Infrastructure services stopped and all data removed');
145
+ } finally {
146
+ if (fs.existsSync(tempComposePath)) {
147
+ fs.unlinkSync(tempComposePath);
148
+ }
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Checks if infrastructure services are running
154
+ * Validates that all required services are healthy and accessible
155
+ *
156
+ * @async
157
+ * @function checkInfraHealth
158
+ * @returns {Promise<Object>} Health status of each service
159
+ * @throws {Error} If health check fails
160
+ *
161
+ * @example
162
+ * const health = await checkInfraHealth();
163
+ * // Returns: { postgres: 'healthy', redis: 'healthy', keycloak: 'healthy', controller: 'healthy' }
164
+ */
165
+ async function checkInfraHealth() {
166
+ const services = ['postgres', 'redis', 'pgadmin', 'redis-commander'];
167
+ const health = {};
168
+
169
+ for (const service of services) {
170
+ try {
171
+ const { stdout } = await execAsync(`docker inspect --format='{{.State.Health.Status}}' aifabrix-${service}`);
172
+ health[service] = stdout.trim();
173
+ } catch (error) {
174
+ health[service] = 'unknown';
175
+ }
176
+ }
177
+
178
+ return health;
179
+ }
180
+
181
+ /**
182
+ * Gets the status of infrastructure services
183
+ * Returns detailed information about running containers
184
+ *
185
+ * @async
186
+ * @function getInfraStatus
187
+ * @returns {Promise<Object>} Status information for each service
188
+ *
189
+ * @example
190
+ * const status = await getInfraStatus();
191
+ * // Returns: { postgres: { status: 'running', port: 5432, url: 'localhost:5432' }, ... }
192
+ */
193
+ async function getInfraStatus() {
194
+ const services = {
195
+ postgres: { port: 5432, url: 'localhost:5432' },
196
+ redis: { port: 6379, url: 'localhost:6379' },
197
+ pgadmin: { port: 5050, url: 'http://localhost:5050' },
198
+ 'redis-commander': { port: 8081, url: 'http://localhost:8081' }
199
+ };
200
+
201
+ const status = {};
202
+
203
+ for (const [serviceName, config] of Object.entries(services)) {
204
+ try {
205
+ const { stdout } = await execAsync(`docker inspect --format='{{.State.Status}}' aifabrix-${serviceName}`);
206
+ status[serviceName] = {
207
+ status: stdout.trim(),
208
+ port: config.port,
209
+ url: config.url
210
+ };
211
+ } catch (error) {
212
+ status[serviceName] = {
213
+ status: 'not running',
214
+ port: config.port,
215
+ url: config.url
216
+ };
217
+ }
218
+ }
219
+
220
+ return status;
221
+ }
222
+
223
+ /**
224
+ * Restarts a specific infrastructure service
225
+ * Useful for applying configuration changes
226
+ *
227
+ * @async
228
+ * @function restartService
229
+ * @param {string} serviceName - Name of service to restart
230
+ * @returns {Promise<void>} Resolves when service is restarted
231
+ * @throws {Error} If service doesn't exist or restart fails
232
+ *
233
+ * @example
234
+ * await restartService('keycloak');
235
+ * // Keycloak service is restarted
236
+ */
237
+ async function restartService(serviceName) {
238
+ if (!serviceName || typeof serviceName !== 'string') {
239
+ throw new Error('Service name is required and must be a string');
240
+ }
241
+
242
+ const validServices = ['postgres', 'redis', 'pgadmin', 'redis-commander'];
243
+ if (!validServices.includes(serviceName)) {
244
+ throw new Error(`Invalid service name. Must be one of: ${validServices.join(', ')}`);
245
+ }
246
+
247
+ const templatePath = path.join(__dirname, '..', 'templates', 'infra', 'compose.yaml');
248
+ const adminSecretsPath = path.join(os.homedir(), '.aifabrix', 'admin-secrets.env');
249
+
250
+ if (!fs.existsSync(templatePath) || !fs.existsSync(adminSecretsPath)) {
251
+ throw new Error('Infrastructure not properly configured');
252
+ }
253
+
254
+ const tempComposePath = path.join(os.tmpdir(), 'aifabrix-compose.yaml');
255
+ fs.writeFileSync(tempComposePath, fs.readFileSync(templatePath, 'utf8'));
256
+
257
+ try {
258
+ console.log(`Restarting ${serviceName} service...`);
259
+ await execAsync(`docker-compose -f "${tempComposePath}" --env-file "${adminSecretsPath}" restart ${serviceName}`);
260
+ console.log(`${serviceName} service restarted successfully`);
261
+ } finally {
262
+ if (fs.existsSync(tempComposePath)) {
263
+ fs.unlinkSync(tempComposePath);
264
+ }
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Waits for services to be healthy
270
+ * @private
271
+ */
272
+ async function waitForServices() {
273
+ const maxAttempts = 30;
274
+ const delay = 2000; // 2 seconds
275
+
276
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
277
+ const health = await checkInfraHealth();
278
+ const allHealthy = Object.values(health).every(status => status === 'healthy');
279
+
280
+ if (allHealthy) {
281
+ return;
282
+ }
283
+
284
+ if (attempt < maxAttempts) {
285
+ console.log(`Waiting for services to be healthy... (${attempt}/${maxAttempts})`);
286
+ await new Promise(resolve => setTimeout(resolve, delay));
287
+ }
288
+ }
289
+
290
+ throw new Error('Services failed to become healthy within timeout period');
291
+ }
292
+
293
+ module.exports = {
294
+ startInfra,
295
+ stopInfra,
296
+ stopInfraWithVolumes,
297
+ checkInfraHealth,
298
+ getInfraStatus,
299
+ restartService
300
+ };
@@ -0,0 +1,93 @@
1
+ /**
2
+ * AI Fabrix Builder Deployment Key Generator
3
+ *
4
+ * This module generates SHA256-based deployment keys for controller authentication.
5
+ * Keys are computed from variables.yaml content to ensure deployment integrity.
6
+ *
7
+ * @fileoverview Deployment key generation for AI Fabrix Builder
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ const crypto = require('crypto');
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+
16
+ /**
17
+ * Generates deployment key from variables.yaml content
18
+ * Creates SHA256 hash for controller authentication and deployment integrity
19
+ *
20
+ * @async
21
+ * @function generateDeploymentKey
22
+ * @param {string} appName - Name of the application
23
+ * @returns {Promise<string>} SHA256 hash of variables.yaml content
24
+ * @throws {Error} If variables.yaml cannot be read
25
+ *
26
+ * @example
27
+ * const key = await generateDeploymentKey('myapp');
28
+ * // Returns: 'a1b2c3d4e5f6...' (64-character SHA256 hash)
29
+ */
30
+ async function generateDeploymentKey(appName) {
31
+ if (!appName || typeof appName !== 'string') {
32
+ throw new Error('App name is required and must be a string');
33
+ }
34
+
35
+ const variablesPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
36
+
37
+ if (!fs.existsSync(variablesPath)) {
38
+ throw new Error(`variables.yaml not found: ${variablesPath}`);
39
+ }
40
+
41
+ const content = fs.readFileSync(variablesPath, 'utf8');
42
+ return generateDeploymentKeyFromContent(content);
43
+ }
44
+
45
+ /**
46
+ * Generates deployment key from raw variables.yaml content
47
+ * Useful for testing or when content is already loaded
48
+ *
49
+ * @function generateDeploymentKeyFromContent
50
+ * @param {string} content - Raw variables.yaml content
51
+ * @returns {string} SHA256 hash of content
52
+ *
53
+ * @example
54
+ * const key = generateDeploymentKeyFromContent(yamlContent);
55
+ * // Returns: 'a1b2c3d4e5f6...' (64-character SHA256 hash)
56
+ */
57
+ function generateDeploymentKeyFromContent(content) {
58
+ if (typeof content !== 'string') {
59
+ throw new Error('Content is required and must be a string');
60
+ }
61
+
62
+ const hash = crypto.createHash('sha256');
63
+ hash.update(content, 'utf8');
64
+ return hash.digest('hex');
65
+ }
66
+
67
+ /**
68
+ * Validates deployment key format
69
+ * Ensures key is a valid SHA256 hash
70
+ *
71
+ * @function validateDeploymentKey
72
+ * @param {string} key - Deployment key to validate
73
+ * @returns {boolean} True if key is valid SHA256 hash
74
+ *
75
+ * @example
76
+ * const isValid = validateDeploymentKey('a1b2c3d4e5f6...');
77
+ * // Returns: true
78
+ */
79
+ function validateDeploymentKey(key) {
80
+ if (!key || typeof key !== 'string') {
81
+ return false;
82
+ }
83
+
84
+ // SHA256 produces 64-character hex string
85
+ const sha256Pattern = /^[a-f0-9]{64}$/i;
86
+ return sha256Pattern.test(key);
87
+ }
88
+
89
+ module.exports = {
90
+ generateDeploymentKey,
91
+ generateDeploymentKeyFromContent,
92
+ validateDeploymentKey
93
+ };
package/lib/push.js ADDED
@@ -0,0 +1,141 @@
1
+ /**
2
+ * AI Fabrix Builder Push Utilities
3
+ *
4
+ * This module handles pushing Docker images to Azure Container Registry.
5
+ * Includes authentication, tagging, and push operations.
6
+ *
7
+ * @fileoverview Push utilities for AI Fabrix Builder
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ const { exec } = require('child_process');
13
+ const { promisify } = require('util');
14
+ const chalk = require('chalk');
15
+
16
+ const execAsync = promisify(exec);
17
+
18
+ /**
19
+ * Check if Azure CLI is installed
20
+ * @returns {Promise<boolean>} True if Azure CLI is available
21
+ */
22
+ async function checkAzureCLIInstalled() {
23
+ try {
24
+ await execAsync('az --version');
25
+ return true;
26
+ } catch (error) {
27
+ return false;
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Extract registry name from ACR URL
33
+ * @param {string} registryUrl - Registry URL (e.g., myacr.azurecr.io)
34
+ * @returns {string} Registry name (e.g., myacr)
35
+ * @throws {Error} If URL format is invalid
36
+ */
37
+ function extractRegistryName(registryUrl) {
38
+ const match = registryUrl.match(/^([^/]+)\.azurecr\.io$/);
39
+ if (!match) {
40
+ throw new Error(`Invalid ACR URL format: ${registryUrl}. Expected format: *.azurecr.io`);
41
+ }
42
+ return match[1];
43
+ }
44
+
45
+ /**
46
+ * Validate registry URL format
47
+ * @param {string} registryUrl - Registry URL to validate
48
+ * @returns {boolean} True if valid
49
+ */
50
+ function validateRegistryURL(registryUrl) {
51
+ return /^[^.]+\.azurecr\.io$/.test(registryUrl);
52
+ }
53
+
54
+ /**
55
+ * Check if already authenticated with ACR
56
+ * @param {string} registry - Registry URL
57
+ * @returns {Promise<boolean>} True if authenticated
58
+ */
59
+ async function checkACRAuthentication(registry) {
60
+ try {
61
+ const registryName = extractRegistryName(registry);
62
+ await execAsync(`az acr show --name ${registryName}`);
63
+ return true;
64
+ } catch (error) {
65
+ return false;
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Authenticate with Azure Container Registry
71
+ * @param {string} registry - Registry URL
72
+ * @throws {Error} If authentication fails
73
+ */
74
+ async function authenticateACR(registry) {
75
+ try {
76
+ const registryName = extractRegistryName(registry);
77
+ console.log(chalk.blue(`Authenticating with ${registry}...`));
78
+ await execAsync(`az acr login --name ${registryName}`);
79
+ console.log(chalk.green(`✓ Authenticated with ${registry}`));
80
+ } catch (error) {
81
+ throw new Error(`Failed to authenticate with Azure Container Registry: ${error.message}`);
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Check if Docker image exists locally
87
+ * @param {string} imageName - Image name
88
+ * @param {string} tag - Image tag (default: latest)
89
+ * @returns {Promise<boolean>} True if image exists
90
+ */
91
+ async function checkLocalImageExists(imageName, tag = 'latest') {
92
+ try {
93
+ const { stdout } = await execAsync(`docker images --format "{{.Repository}}:{{.Tag}}" | grep "^${imageName}:${tag}$"`);
94
+ return stdout.trim() === `${imageName}:${tag}`;
95
+ } catch (error) {
96
+ return false;
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Tag Docker image for ACR
102
+ * @param {string} sourceImage - Source image tag
103
+ * @param {string} targetImage - Target image tag
104
+ * @throws {Error} If tagging fails
105
+ */
106
+ async function tagImage(sourceImage, targetImage) {
107
+ try {
108
+ console.log(chalk.blue(`Tagging ${sourceImage} as ${targetImage}...`));
109
+ await execAsync(`docker tag ${sourceImage} ${targetImage}`);
110
+ console.log(chalk.green(`✓ Tagged: ${targetImage}`));
111
+ } catch (error) {
112
+ throw new Error(`Failed to tag image: ${error.message}`);
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Push Docker image to registry
118
+ * @param {string} imageWithTag - Image with full tag
119
+ * @throws {Error} If push fails
120
+ */
121
+ async function pushImage(imageWithTag) {
122
+ try {
123
+ console.log(chalk.blue(`Pushing ${imageWithTag}...`));
124
+ await execAsync(`docker push ${imageWithTag}`);
125
+ console.log(chalk.green(`✓ Pushed: ${imageWithTag}`));
126
+ } catch (error) {
127
+ throw new Error(`Failed to push image: ${error.message}`);
128
+ }
129
+ }
130
+
131
+ module.exports = {
132
+ checkAzureCLIInstalled,
133
+ extractRegistryName,
134
+ validateRegistryURL,
135
+ checkACRAuthentication,
136
+ authenticateACR,
137
+ checkLocalImageExists,
138
+ tagImage,
139
+ pushImage
140
+ };
141
+