@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.
@@ -0,0 +1,361 @@
1
+ /**
2
+ * AI Fabrix Builder Deployment JSON Generator
3
+ *
4
+ * This module generates deployment JSON manifests for Miso Controller.
5
+ * Combines variables.yaml, env.template, and rbac.yaml into deployment configuration.
6
+ *
7
+ * @fileoverview Deployment JSON generation for AI Fabrix Builder
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const yaml = require('js-yaml');
15
+ const _secrets = require('./secrets');
16
+ const _keyGenerator = require('./key-generator');
17
+
18
+ /**
19
+ * Generates deployment JSON from application configuration files
20
+ * Creates aifabrix-deploy.json for Miso Controller deployment
21
+ *
22
+ * @async
23
+ * @function generateDeployJson
24
+ * @param {string} appName - Name of the application
25
+ * @returns {Promise<string>} Path to generated deployment JSON file
26
+ * @throws {Error} If generation fails or configuration is invalid
27
+ *
28
+ * @example
29
+ * const jsonPath = await generateDeployJson('myapp');
30
+ * // Returns: './builder/myapp/aifabrix-deploy.json'
31
+ */
32
+ async function generateDeployJson(appName) {
33
+ if (!appName || typeof appName !== 'string') {
34
+ throw new Error('App name is required and must be a string');
35
+ }
36
+
37
+ const builderPath = path.join(process.cwd(), 'builder', appName);
38
+ const variablesPath = path.join(builderPath, 'variables.yaml');
39
+ const templatePath = path.join(builderPath, 'env.template');
40
+ const rbacPath = path.join(builderPath, 'rbac.yaml');
41
+ const jsonPath = path.join(builderPath, 'aifabrix-deploy.json');
42
+
43
+ // Load variables.yaml
44
+ if (!fs.existsSync(variablesPath)) {
45
+ throw new Error(`variables.yaml not found: ${variablesPath}`);
46
+ }
47
+
48
+ const variablesContent = fs.readFileSync(variablesPath, 'utf8');
49
+ let variables;
50
+ try {
51
+ variables = yaml.load(variablesContent);
52
+ } catch (error) {
53
+ throw new Error(`Invalid YAML syntax in variables.yaml: ${error.message}`);
54
+ }
55
+
56
+ // Load env.template
57
+ if (!fs.existsSync(templatePath)) {
58
+ throw new Error(`env.template not found: ${templatePath}`);
59
+ }
60
+
61
+ const envTemplate = fs.readFileSync(templatePath, 'utf8');
62
+
63
+ // Load rbac.yaml (optional)
64
+ let rbac = null;
65
+ if (fs.existsSync(rbacPath)) {
66
+ const rbacContent = fs.readFileSync(rbacPath, 'utf8');
67
+ try {
68
+ rbac = yaml.load(rbacContent);
69
+ } catch (error) {
70
+ throw new Error(`Invalid YAML syntax in rbac.yaml: ${error.message}`);
71
+ }
72
+ }
73
+
74
+ // Generate deployment key
75
+ const deploymentKey = _keyGenerator.generateDeploymentKeyFromContent(variablesContent);
76
+
77
+ // Parse environment variables from template
78
+ const configuration = parseEnvironmentVariables(envTemplate);
79
+
80
+ // Build deployment manifest
81
+ const deployment = {
82
+ key: variables.app?.key || appName,
83
+ displayName: variables.app?.displayName || appName,
84
+ description: variables.app?.description || '',
85
+ type: variables.app?.type || 'webapp',
86
+ image: buildImageReference(variables),
87
+ port: variables.port || 3000,
88
+ deploymentKey,
89
+ configuration,
90
+ healthCheck: buildHealthCheck(variables),
91
+ requires: buildRequirements(variables),
92
+ authentication: buildAuthentication(rbac)
93
+ };
94
+
95
+ // Add roles and permissions if RBAC is configured
96
+ if (rbac) {
97
+ deployment.roles = rbac.roles || [];
98
+ deployment.permissions = rbac.permissions || [];
99
+ }
100
+
101
+ // Write deployment JSON
102
+ const jsonContent = JSON.stringify(deployment, null, 2);
103
+ fs.writeFileSync(jsonPath, jsonContent, { mode: 0o644 });
104
+
105
+ return jsonPath;
106
+ }
107
+
108
+ /**
109
+ * Parses environment variables from env.template
110
+ * Converts kv:// references to KeyVault configuration
111
+ *
112
+ * @function parseEnvironmentVariables
113
+ * @param {string} envTemplate - Environment template content
114
+ * @returns {Array} Array of configuration objects
115
+ */
116
+ function parseEnvironmentVariables(envTemplate) {
117
+ const configuration = [];
118
+ const lines = envTemplate.split('\n');
119
+
120
+ for (const line of lines) {
121
+ const trimmed = line.trim();
122
+
123
+ // Skip empty lines and comments
124
+ if (!trimmed || trimmed.startsWith('#')) {
125
+ continue;
126
+ }
127
+
128
+ // Parse KEY=VALUE format
129
+ const equalIndex = trimmed.indexOf('=');
130
+ if (equalIndex === -1) {
131
+ continue;
132
+ }
133
+
134
+ const key = trimmed.substring(0, equalIndex).trim();
135
+ const value = trimmed.substring(equalIndex + 1).trim();
136
+
137
+ if (!key || !value) {
138
+ continue;
139
+ }
140
+
141
+ // Determine location and required status
142
+ let location = 'variable';
143
+ let required = false;
144
+
145
+ if (value.startsWith('kv://')) {
146
+ location = 'keyvault';
147
+ required = true;
148
+ }
149
+
150
+ // Check if it's a sensitive variable
151
+ const sensitiveKeys = ['password', 'secret', 'key', 'token', 'auth'];
152
+ if (sensitiveKeys.some(sensitive => key.toLowerCase().includes(sensitive))) {
153
+ required = true;
154
+ }
155
+
156
+ configuration.push({
157
+ name: key,
158
+ value: value.replace('kv://', ''), // Remove kv:// prefix for KeyVault
159
+ location,
160
+ required
161
+ });
162
+ }
163
+
164
+ return configuration;
165
+ }
166
+
167
+ /**
168
+ * Builds image reference from variables configuration
169
+ * Handles registry, name, and tag formatting
170
+ *
171
+ * @function buildImageReference
172
+ * @param {Object} variables - Variables configuration
173
+ * @returns {string} Complete image reference
174
+ */
175
+ function buildImageReference(variables) {
176
+ const imageName = variables.image?.name || variables.app?.key || 'app';
177
+ const registry = variables.image?.registry;
178
+ const tag = variables.image?.tag || 'latest';
179
+
180
+ if (registry) {
181
+ return `${registry}/${imageName}:${tag}`;
182
+ }
183
+
184
+ return `${imageName}:${tag}`;
185
+ }
186
+
187
+ /**
188
+ * Builds health check configuration from variables
189
+ * Provides default health check settings
190
+ *
191
+ * @function buildHealthCheck
192
+ * @param {Object} variables - Variables configuration
193
+ * @returns {Object} Health check configuration
194
+ */
195
+ function buildHealthCheck(variables) {
196
+ return {
197
+ path: variables.healthCheck?.path || '/health',
198
+ interval: variables.healthCheck?.interval || 30,
199
+ timeout: variables.healthCheck?.timeout || 10,
200
+ retries: variables.healthCheck?.retries || 3
201
+ };
202
+ }
203
+
204
+ /**
205
+ * Builds requirements configuration from variables
206
+ * Maps database, Redis, and storage requirements
207
+ *
208
+ * @function buildRequirements
209
+ * @param {Object} variables - Variables configuration
210
+ * @returns {Object} Requirements configuration
211
+ */
212
+ function buildRequirements(variables) {
213
+ const requires = variables.requires || {};
214
+
215
+ return {
216
+ database: requires.database || false,
217
+ databases: requires.databases || (requires.database ? [{ name: variables.app?.key || 'app' }] : []),
218
+ redis: requires.redis || false,
219
+ storage: requires.storage || false,
220
+ storageSize: requires.storageSize || '1Gi'
221
+ };
222
+ }
223
+
224
+ /**
225
+ * Builds authentication configuration from RBAC
226
+ * Configures authentication settings for the application
227
+ *
228
+ * @function buildAuthentication
229
+ * @param {Object} rbac - RBAC configuration (optional)
230
+ * @returns {Object} Authentication configuration
231
+ */
232
+ function buildAuthentication(rbac) {
233
+ if (!rbac) {
234
+ return {
235
+ enabled: false,
236
+ type: 'none'
237
+ };
238
+ }
239
+
240
+ return {
241
+ enabled: true,
242
+ type: 'keycloak', // Default to Keycloak
243
+ sso: true,
244
+ requiredRoles: rbac.roles?.map(role => role.value) || [],
245
+ permissions: rbac.permissions?.map(perm => perm.name) || []
246
+ };
247
+ }
248
+
249
+ /**
250
+ * Validates deployment JSON before writing
251
+ * Ensures all required fields are present and valid
252
+ *
253
+ * @function validateDeploymentJson
254
+ * @param {Object} deployment - Deployment configuration object
255
+ * @returns {Object} Validation result
256
+ */
257
+ function validateDeploymentJson(deployment) {
258
+ const errors = [];
259
+ const warnings = [];
260
+
261
+ // Required fields validation
262
+ if (!deployment.key) {
263
+ errors.push('Missing required field: key');
264
+ }
265
+
266
+ if (!deployment.displayName) {
267
+ errors.push('Missing required field: displayName');
268
+ }
269
+
270
+ if (!deployment.image) {
271
+ errors.push('Missing required field: image');
272
+ }
273
+
274
+ if (!deployment.port || deployment.port < 1 || deployment.port > 65535) {
275
+ errors.push('Invalid port: must be between 1 and 65535');
276
+ }
277
+
278
+ if (!deployment.deploymentKey) {
279
+ errors.push('Missing required field: deploymentKey');
280
+ }
281
+
282
+ // Validate deployment key format
283
+ if (deployment.deploymentKey && !_keyGenerator.validateDeploymentKey(deployment.deploymentKey)) {
284
+ errors.push('Invalid deployment key format');
285
+ }
286
+
287
+ // Configuration validation
288
+ if (!deployment.configuration || !Array.isArray(deployment.configuration)) {
289
+ errors.push('Missing or invalid configuration array');
290
+ }
291
+
292
+ // Health check validation
293
+ if (deployment.healthCheck) {
294
+ if (deployment.healthCheck.path && !deployment.healthCheck.path.startsWith('/')) {
295
+ warnings.push('Health check path should start with /');
296
+ }
297
+
298
+ if (deployment.healthCheck.interval && (deployment.healthCheck.interval < 5 || deployment.healthCheck.interval > 300)) {
299
+ warnings.push('Health check interval should be between 5 and 300 seconds');
300
+ }
301
+ }
302
+
303
+ // Authentication validation
304
+ if (deployment.authentication?.enabled) {
305
+ if (!deployment.roles || deployment.roles.length === 0) {
306
+ warnings.push('Authentication enabled but no roles defined');
307
+ }
308
+
309
+ if (!deployment.permissions || deployment.permissions.length === 0) {
310
+ warnings.push('Authentication enabled but no permissions defined');
311
+ }
312
+ }
313
+
314
+ return {
315
+ valid: errors.length === 0,
316
+ errors,
317
+ warnings
318
+ };
319
+ }
320
+
321
+ /**
322
+ * Generates deployment JSON with validation
323
+ * Validates configuration before writing the file
324
+ *
325
+ * @async
326
+ * @function generateDeployJsonWithValidation
327
+ * @param {string} appName - Name of the application
328
+ * @returns {Promise<Object>} Generation result with validation info
329
+ * @throws {Error} If generation fails
330
+ *
331
+ * @example
332
+ * const result = await generateDeployJsonWithValidation('myapp');
333
+ * // Returns: { success: true, path: './builder/myapp/aifabrix-deploy.json', validation: {...} }
334
+ */
335
+ async function generateDeployJsonWithValidation(appName) {
336
+ const jsonPath = await generateDeployJson(appName);
337
+
338
+ // Read back the generated file for validation
339
+ const jsonContent = fs.readFileSync(jsonPath, 'utf8');
340
+ const deployment = JSON.parse(jsonContent);
341
+
342
+ const validation = validateDeploymentJson(deployment);
343
+
344
+ return {
345
+ success: validation.valid,
346
+ path: jsonPath,
347
+ validation,
348
+ deployment
349
+ };
350
+ }
351
+
352
+ module.exports = {
353
+ generateDeployJson,
354
+ generateDeployJsonWithValidation,
355
+ parseEnvironmentVariables,
356
+ buildImageReference,
357
+ buildHealthCheck,
358
+ buildRequirements,
359
+ buildAuthentication,
360
+ validateDeploymentJson
361
+ };
@@ -0,0 +1,220 @@
1
+ /**
2
+ * GitHub Actions Workflow Generator Module
3
+ *
4
+ * Generates GitHub Actions workflow files from Handlebars templates
5
+ * following ISO 27001 security standards
6
+ */
7
+
8
+ const fs = require('fs').promises;
9
+ const path = require('path');
10
+ const Handlebars = require('handlebars');
11
+
12
+ /**
13
+ * Generate GitHub Actions workflow files from templates
14
+ * @param {string} appPath - Path to application directory
15
+ * @param {Object} config - Configuration from variables.yaml
16
+ * @param {Object} options - Generation options
17
+ * @returns {Promise<string[]>} Array of generated file paths
18
+ */
19
+ async function generateGithubWorkflows(appPath, config, options = {}) {
20
+ try {
21
+ // Create .github/workflows directory
22
+ const workflowsDir = path.join(appPath, '.github', 'workflows');
23
+ await fs.mkdir(workflowsDir, { recursive: true });
24
+
25
+ // Get template context
26
+ const templateContext = getTemplateContext(config, options);
27
+
28
+ // Load and compile templates
29
+ const templatesDir = path.join(__dirname, '..', 'templates', 'github');
30
+ const templateFiles = await fs.readdir(templatesDir);
31
+
32
+ const generatedFiles = [];
33
+
34
+ for (const templateFile of templateFiles) {
35
+ if (templateFile.endsWith('.hbs')) {
36
+ const templatePath = path.join(templatesDir, templateFile);
37
+ const templateContent = await fs.readFile(templatePath, 'utf8');
38
+
39
+ // Compile template
40
+ const template = Handlebars.compile(templateContent);
41
+
42
+ // Generate content
43
+ const generatedContent = template(templateContext);
44
+
45
+ // Write to workflows directory
46
+ const outputFileName = templateFile.replace('.hbs', '');
47
+ const outputPath = path.join(workflowsDir, outputFileName);
48
+ await fs.writeFile(outputPath, generatedContent);
49
+
50
+ generatedFiles.push(outputPath);
51
+ }
52
+ }
53
+
54
+ return generatedFiles;
55
+
56
+ } catch (error) {
57
+ throw new Error(`Failed to generate GitHub workflows: ${error.message}`);
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Get template context from config
63
+ * @param {Object} config - Application configuration
64
+ * @param {Object} options - Additional options
65
+ * @returns {Object} Template context
66
+ */
67
+ function getTemplateContext(config, options = {}) {
68
+ return {
69
+ appName: config.appName || 'myapp',
70
+ mainBranch: options.mainBranch || 'main',
71
+ language: config.language || 'typescript',
72
+ fileExtension: config.language === 'python' ? 'py' : 'js',
73
+ sourceDir: config.language === 'python' ? 'src' : 'lib',
74
+ buildCommand: options.buildCommand || 'npm run build',
75
+ uploadCoverage: options.uploadCoverage !== false,
76
+ publishToNpm: options.publishToNpm || false,
77
+ port: config.port || 3000,
78
+ database: config.database || false,
79
+ redis: config.redis || false,
80
+ storage: config.storage || false,
81
+ authentication: config.authentication || false
82
+ };
83
+ }
84
+
85
+ /**
86
+ * Generate a specific workflow file
87
+ * @param {string} appPath - Path to application directory
88
+ * @param {string} templateName - Name of template file (without .hbs)
89
+ * @param {Object} config - Application configuration
90
+ * @param {Object} options - Generation options
91
+ * @returns {Promise<string>} Path to generated file
92
+ */
93
+ async function generateWorkflowFile(appPath, templateName, config, options = {}) {
94
+ try {
95
+ // Create .github/workflows directory
96
+ const workflowsDir = path.join(appPath, '.github', 'workflows');
97
+ await fs.mkdir(workflowsDir, { recursive: true });
98
+
99
+ // Load template
100
+ const templatePath = path.join(__dirname, '..', 'templates', 'github', `${templateName}.hbs`);
101
+ const templateContent = await fs.readFile(templatePath, 'utf8');
102
+
103
+ // Compile template
104
+ const template = Handlebars.compile(templateContent);
105
+
106
+ // Get template context
107
+ const templateContext = getTemplateContext(config, options);
108
+
109
+ // Generate content
110
+ const generatedContent = template(templateContext);
111
+
112
+ // Write to workflows directory
113
+ const outputPath = path.join(workflowsDir, templateName);
114
+ await fs.writeFile(outputPath, generatedContent);
115
+
116
+ return outputPath;
117
+
118
+ } catch (error) {
119
+ throw new Error(`Failed to generate workflow file ${templateName}: ${error.message}`);
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Validate GitHub workflow configuration
125
+ * @param {Object} config - Application configuration
126
+ * @param {Object} options - Generation options
127
+ * @returns {Object} Validation result
128
+ */
129
+ function validateWorkflowConfig(config, options = {}) {
130
+ const errors = [];
131
+ const warnings = [];
132
+
133
+ // Validate required fields
134
+ if (!config.appName) {
135
+ errors.push('Application name is required');
136
+ }
137
+
138
+ if (!config.language) {
139
+ errors.push('Language is required');
140
+ }
141
+
142
+ if (!['typescript', 'python'].includes(config.language)) {
143
+ errors.push('Language must be either typescript or python');
144
+ }
145
+
146
+ // Validate port
147
+ if (config.port && (config.port < 1 || config.port > 65535)) {
148
+ errors.push('Port must be between 1 and 65535');
149
+ }
150
+
151
+ // Validate branch name
152
+ if (options.mainBranch && !/^[a-zA-Z0-9_-]+$/.test(options.mainBranch)) {
153
+ errors.push('Main branch name contains invalid characters');
154
+ }
155
+
156
+ // Warnings
157
+ if (config.language === 'python' && !config.database) {
158
+ warnings.push('Python applications typically require a database');
159
+ }
160
+
161
+ if (config.authentication && !config.database) {
162
+ warnings.push('Authentication typically requires a database for user storage');
163
+ }
164
+
165
+ return {
166
+ valid: errors.length === 0,
167
+ errors,
168
+ warnings
169
+ };
170
+ }
171
+
172
+ /**
173
+ * Generate workflow files with validation
174
+ * @param {string} appPath - Path to application directory
175
+ * @param {Object} config - Application configuration
176
+ * @param {Object} options - Generation options
177
+ * @returns {Promise<Object>} Generation result
178
+ */
179
+ async function generateWorkflowsWithValidation(appPath, config, options = {}) {
180
+ try {
181
+ // Validate configuration
182
+ const validation = validateWorkflowConfig(config, options);
183
+
184
+ if (!validation.valid) {
185
+ return {
186
+ success: false,
187
+ validation,
188
+ files: []
189
+ };
190
+ }
191
+
192
+ // Generate workflows
193
+ const files = await generateGithubWorkflows(appPath, config, options);
194
+
195
+ return {
196
+ success: true,
197
+ validation,
198
+ files
199
+ };
200
+
201
+ } catch (error) {
202
+ return {
203
+ success: false,
204
+ validation: {
205
+ valid: false,
206
+ errors: [error.message],
207
+ warnings: []
208
+ },
209
+ files: []
210
+ };
211
+ }
212
+ }
213
+
214
+ module.exports = {
215
+ generateGithubWorkflows,
216
+ getTemplateContext,
217
+ generateWorkflowFile,
218
+ validateWorkflowConfig,
219
+ generateWorkflowsWithValidation
220
+ };