@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/LICENSE +21 -0
- package/README.md +75 -0
- package/bin/aifabrix.js +51 -0
- package/lib/app-deploy.js +209 -0
- package/lib/app-run.js +291 -0
- package/lib/app.js +472 -0
- package/lib/audit-logger.js +162 -0
- package/lib/build.js +313 -0
- package/lib/cli.js +307 -0
- package/lib/deployer.js +256 -0
- package/lib/env-reader.js +250 -0
- package/lib/generator.js +361 -0
- package/lib/github-generator.js +220 -0
- package/lib/infra.js +300 -0
- package/lib/key-generator.js +93 -0
- package/lib/push.js +141 -0
- package/lib/schema/application-schema.json +649 -0
- package/lib/schema/env-config.yaml +15 -0
- package/lib/secrets.js +282 -0
- package/lib/templates.js +301 -0
- package/lib/validator.js +377 -0
- package/package.json +59 -0
- package/templates/README.md +51 -0
- package/templates/github/ci.yaml.hbs +15 -0
- package/templates/github/pr-checks.yaml.hbs +35 -0
- package/templates/github/release.yaml.hbs +79 -0
- package/templates/github/test.hbs +11 -0
- package/templates/github/test.yaml.hbs +11 -0
- package/templates/infra/compose.yaml +93 -0
- package/templates/python/Dockerfile.hbs +49 -0
- package/templates/python/docker-compose.hbs +69 -0
- package/templates/typescript/Dockerfile.hbs +46 -0
- package/templates/typescript/docker-compose.hbs +69 -0
package/lib/app.js
ADDED
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Fabrix Builder Application Management
|
|
3
|
+
*
|
|
4
|
+
* This module handles application building, running, and deployment.
|
|
5
|
+
* Includes runtime detection, Dockerfile generation, and container management.
|
|
6
|
+
*
|
|
7
|
+
* @fileoverview Application build and run management for AI Fabrix Builder
|
|
8
|
+
* @author AI Fabrix Team
|
|
9
|
+
* @version 2.0.0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs').promises;
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const inquirer = require('inquirer');
|
|
15
|
+
const chalk = require('chalk');
|
|
16
|
+
const yaml = require('js-yaml');
|
|
17
|
+
const { generateVariablesYaml, generateEnvTemplate, generateRbacYaml } = require('./templates');
|
|
18
|
+
const { readExistingEnv, generateEnvTemplate: generateEnvTemplateFromReader } = require('./env-reader');
|
|
19
|
+
const build = require('./build');
|
|
20
|
+
const appRun = require('./app-run');
|
|
21
|
+
const pushUtils = require('./push');
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Validate application name format
|
|
25
|
+
* @param {string} appName - Application name to validate
|
|
26
|
+
* @throws {Error} If app name is invalid
|
|
27
|
+
*/
|
|
28
|
+
function validateAppName(appName) {
|
|
29
|
+
if (!appName || typeof appName !== 'string') {
|
|
30
|
+
throw new Error('Application name is required');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// App name should be lowercase, alphanumeric with dashes, 3-40 characters
|
|
34
|
+
const nameRegex = /^[a-z0-9-]{3,40}$/;
|
|
35
|
+
if (!nameRegex.test(appName)) {
|
|
36
|
+
throw new Error('Application name must be 3-40 characters, lowercase letters, numbers, and dashes only');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Cannot start or end with dash
|
|
40
|
+
if (appName.startsWith('-') || appName.endsWith('-')) {
|
|
41
|
+
throw new Error('Application name cannot start or end with a dash');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Cannot have consecutive dashes
|
|
45
|
+
if (appName.includes('--')) {
|
|
46
|
+
throw new Error('Application name cannot have consecutive dashes');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Prompt for missing configuration options
|
|
52
|
+
* @param {string} appName - Application name
|
|
53
|
+
* @param {Object} options - Provided options
|
|
54
|
+
* @returns {Promise<Object>} Complete configuration
|
|
55
|
+
*/
|
|
56
|
+
async function promptForOptions(appName, options) {
|
|
57
|
+
const questions = [];
|
|
58
|
+
|
|
59
|
+
// Port validation
|
|
60
|
+
if (!options.port) {
|
|
61
|
+
questions.push({
|
|
62
|
+
type: 'input',
|
|
63
|
+
name: 'port',
|
|
64
|
+
message: 'What port should the application run on?',
|
|
65
|
+
default: '3000',
|
|
66
|
+
validate: (input) => {
|
|
67
|
+
const port = parseInt(input, 10);
|
|
68
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
69
|
+
return 'Port must be a number between 1 and 65535';
|
|
70
|
+
}
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Language selection
|
|
77
|
+
if (!options.language) {
|
|
78
|
+
questions.push({
|
|
79
|
+
type: 'list',
|
|
80
|
+
name: 'language',
|
|
81
|
+
message: 'What language is your application written in?',
|
|
82
|
+
choices: [
|
|
83
|
+
{ name: 'TypeScript/Node.js', value: 'typescript' },
|
|
84
|
+
{ name: 'Python', value: 'python' }
|
|
85
|
+
],
|
|
86
|
+
default: 'typescript'
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Service options
|
|
91
|
+
if (!Object.prototype.hasOwnProperty.call(options, 'database')) {
|
|
92
|
+
questions.push({
|
|
93
|
+
type: 'confirm',
|
|
94
|
+
name: 'database',
|
|
95
|
+
message: 'Does your application need a database?',
|
|
96
|
+
default: false
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!Object.prototype.hasOwnProperty.call(options, 'redis')) {
|
|
101
|
+
questions.push({
|
|
102
|
+
type: 'confirm',
|
|
103
|
+
name: 'redis',
|
|
104
|
+
message: 'Does your application need Redis?',
|
|
105
|
+
default: false
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!Object.prototype.hasOwnProperty.call(options, 'storage')) {
|
|
110
|
+
questions.push({
|
|
111
|
+
type: 'confirm',
|
|
112
|
+
name: 'storage',
|
|
113
|
+
message: 'Does your application need file storage?',
|
|
114
|
+
default: false
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!Object.prototype.hasOwnProperty.call(options, 'authentication')) {
|
|
119
|
+
questions.push({
|
|
120
|
+
type: 'confirm',
|
|
121
|
+
name: 'authentication',
|
|
122
|
+
message: 'Does your application need authentication/RBAC?',
|
|
123
|
+
default: false
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Prompt for missing options
|
|
128
|
+
const answers = questions.length > 0 ? await inquirer.prompt(questions) : {};
|
|
129
|
+
|
|
130
|
+
// Merge provided options with answers
|
|
131
|
+
return {
|
|
132
|
+
appName,
|
|
133
|
+
port: parseInt(options.port || answers.port || 3000, 10),
|
|
134
|
+
language: options.language || answers.language || 'typescript',
|
|
135
|
+
database: options.database || answers.database || false,
|
|
136
|
+
redis: options.redis || answers.redis || false,
|
|
137
|
+
storage: options.storage || answers.storage || false,
|
|
138
|
+
authentication: options.authentication || answers.authentication || false
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Generate all configuration files for the application
|
|
144
|
+
* @param {string} appPath - Path to application directory
|
|
145
|
+
* @param {string} appName - Application name
|
|
146
|
+
* @param {Object} config - Application configuration
|
|
147
|
+
* @param {Object} existingEnv - Existing environment variables
|
|
148
|
+
*/
|
|
149
|
+
async function generateConfigFiles(appPath, appName, config, existingEnv) {
|
|
150
|
+
try {
|
|
151
|
+
// Generate variables.yaml
|
|
152
|
+
const variablesYaml = generateVariablesYaml(appName, config);
|
|
153
|
+
await fs.writeFile(path.join(appPath, 'variables.yaml'), variablesYaml);
|
|
154
|
+
|
|
155
|
+
// Generate env.template
|
|
156
|
+
let envTemplate;
|
|
157
|
+
if (existingEnv) {
|
|
158
|
+
const envResult = await generateEnvTemplateFromReader(config, existingEnv);
|
|
159
|
+
envTemplate = envResult.template;
|
|
160
|
+
|
|
161
|
+
if (envResult.warnings.length > 0) {
|
|
162
|
+
console.log(chalk.yellow('\n⚠️ Environment conversion warnings:'));
|
|
163
|
+
envResult.warnings.forEach(warning => console.log(chalk.yellow(` - ${warning}`)));
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
envTemplate = generateEnvTemplate(config);
|
|
167
|
+
}
|
|
168
|
+
await fs.writeFile(path.join(appPath, 'env.template'), envTemplate);
|
|
169
|
+
|
|
170
|
+
// Generate rbac.yaml if authentication is enabled
|
|
171
|
+
if (config.authentication) {
|
|
172
|
+
const rbacYaml = generateRbacYaml(appName, config);
|
|
173
|
+
if (rbacYaml) {
|
|
174
|
+
await fs.writeFile(path.join(appPath, 'rbac.yaml'), rbacYaml);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Generate aifabrix-deploy.json template
|
|
179
|
+
const deployJson = {
|
|
180
|
+
apiVersion: 'v1',
|
|
181
|
+
kind: 'ApplicationDeployment',
|
|
182
|
+
metadata: {
|
|
183
|
+
name: appName,
|
|
184
|
+
namespace: 'default'
|
|
185
|
+
},
|
|
186
|
+
spec: {
|
|
187
|
+
application: {
|
|
188
|
+
name: appName,
|
|
189
|
+
version: '1.0.0',
|
|
190
|
+
language: config.language,
|
|
191
|
+
port: config.port
|
|
192
|
+
},
|
|
193
|
+
services: {
|
|
194
|
+
database: config.database,
|
|
195
|
+
redis: config.redis,
|
|
196
|
+
storage: config.storage,
|
|
197
|
+
authentication: config.authentication
|
|
198
|
+
},
|
|
199
|
+
deployment: {
|
|
200
|
+
replicas: 1,
|
|
201
|
+
strategy: 'RollingUpdate'
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
await fs.writeFile(
|
|
207
|
+
path.join(appPath, 'aifabrix-deploy.json'),
|
|
208
|
+
JSON.stringify(deployJson, null, 2)
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
} catch (error) {
|
|
212
|
+
throw new Error(`Failed to generate configuration files: ${error.message}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Creates new application with scaffolded configuration files
|
|
218
|
+
* Prompts for configuration options and generates builder/ folder structure
|
|
219
|
+
*
|
|
220
|
+
* @async
|
|
221
|
+
* @function createApp
|
|
222
|
+
* @param {string} appName - Name of the application to create
|
|
223
|
+
* @param {Object} options - Creation options
|
|
224
|
+
* @param {number} [options.port] - Application port
|
|
225
|
+
* @param {boolean} [options.database] - Requires database
|
|
226
|
+
* @param {boolean} [options.redis] - Requires Redis
|
|
227
|
+
* @param {boolean} [options.storage] - Requires file storage
|
|
228
|
+
* @param {boolean} [options.authentication] - Requires authentication/RBAC
|
|
229
|
+
* @param {string} [options.language] - Runtime language (typescript/python)
|
|
230
|
+
* @param {string} [options.template] - Template to use (platform for Keycloak/Miso)
|
|
231
|
+
* @returns {Promise<void>} Resolves when app is created
|
|
232
|
+
* @throws {Error} If creation fails
|
|
233
|
+
*
|
|
234
|
+
* @example
|
|
235
|
+
* await createApp('myapp', { port: 3000, database: true, language: 'typescript' });
|
|
236
|
+
* // Creates builder/ with variables.yaml, env.template, rbac.yaml
|
|
237
|
+
*/
|
|
238
|
+
async function createApp(appName, options = {}) {
|
|
239
|
+
try {
|
|
240
|
+
// Validate app name format
|
|
241
|
+
validateAppName(appName);
|
|
242
|
+
|
|
243
|
+
// Check if directory already exists
|
|
244
|
+
const builderPath = path.join(process.cwd(), 'builder');
|
|
245
|
+
const appPath = path.join(builderPath, appName);
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
await fs.access(appPath);
|
|
249
|
+
throw new Error(`Application '${appName}' already exists in builder/${appName}/`);
|
|
250
|
+
} catch (error) {
|
|
251
|
+
if (error.code !== 'ENOENT') {
|
|
252
|
+
throw error;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Prompt for missing options
|
|
257
|
+
const config = await promptForOptions(appName, options);
|
|
258
|
+
|
|
259
|
+
// Create directory structure
|
|
260
|
+
await fs.mkdir(appPath, { recursive: true });
|
|
261
|
+
|
|
262
|
+
// Check for existing .env file
|
|
263
|
+
const existingEnv = await readExistingEnv(process.cwd());
|
|
264
|
+
let envConversionMessage = '';
|
|
265
|
+
|
|
266
|
+
if (existingEnv) {
|
|
267
|
+
envConversionMessage = '\n✓ Found existing .env file - sensitive values will be converted to kv:// references';
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Generate configuration files
|
|
271
|
+
await generateConfigFiles(appPath, appName, config, existingEnv);
|
|
272
|
+
|
|
273
|
+
// Generate GitHub workflows if requested
|
|
274
|
+
if (options.github) {
|
|
275
|
+
const githubGen = require('./github-generator');
|
|
276
|
+
const workflowFiles = await githubGen.generateGithubWorkflows(
|
|
277
|
+
process.cwd(),
|
|
278
|
+
config,
|
|
279
|
+
{
|
|
280
|
+
mainBranch: options.mainBranch || 'main',
|
|
281
|
+
uploadCoverage: true,
|
|
282
|
+
publishToNpm: options.template === 'platform'
|
|
283
|
+
}
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
console.log(chalk.green('✓ Generated GitHub Actions workflows:'));
|
|
287
|
+
workflowFiles.forEach(file => console.log(chalk.gray(` - ${file}`)));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Display success message
|
|
291
|
+
console.log(chalk.green('\n✓ Application created successfully!'));
|
|
292
|
+
console.log(chalk.blue(`\nApplication: ${appName}`));
|
|
293
|
+
console.log(chalk.blue(`Location: builder/${appName}/`));
|
|
294
|
+
console.log(chalk.blue(`Language: ${config.language}`));
|
|
295
|
+
console.log(chalk.blue(`Port: ${config.port}`));
|
|
296
|
+
|
|
297
|
+
if (config.database) console.log(chalk.yellow(' - Database enabled'));
|
|
298
|
+
if (config.redis) console.log(chalk.yellow(' - Redis enabled'));
|
|
299
|
+
if (config.storage) console.log(chalk.yellow(' - Storage enabled'));
|
|
300
|
+
if (config.authentication) console.log(chalk.yellow(' - Authentication enabled'));
|
|
301
|
+
|
|
302
|
+
console.log(chalk.gray(envConversionMessage));
|
|
303
|
+
|
|
304
|
+
console.log(chalk.green('\nNext steps:'));
|
|
305
|
+
console.log(chalk.white('1. Copy env.template to .env and fill in your values'));
|
|
306
|
+
console.log(chalk.white('2. Run: aifabrix build ' + appName));
|
|
307
|
+
console.log(chalk.white('3. Run: aifabrix run ' + appName));
|
|
308
|
+
|
|
309
|
+
} catch (error) {
|
|
310
|
+
throw new Error(`Failed to create application: ${error.message}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Builds a container image for the specified application
|
|
316
|
+
* Auto-detects runtime and generates Dockerfile if needed
|
|
317
|
+
*
|
|
318
|
+
* @async
|
|
319
|
+
* @function buildApp
|
|
320
|
+
* @param {string} appName - Name of the application to build
|
|
321
|
+
* @param {Object} options - Build options
|
|
322
|
+
* @param {string} [options.language] - Override language detection
|
|
323
|
+
* @param {boolean} [options.forceTemplate] - Force rebuild from template
|
|
324
|
+
* @param {string} [options.tag] - Image tag (default: latest)
|
|
325
|
+
* @returns {Promise<string>} Image tag that was built
|
|
326
|
+
* @throws {Error} If build fails or app configuration is invalid
|
|
327
|
+
*
|
|
328
|
+
* @example
|
|
329
|
+
* const imageTag = await buildApp('myapp', { language: 'typescript' });
|
|
330
|
+
* // Returns: 'myapp:latest'
|
|
331
|
+
*/
|
|
332
|
+
async function buildApp(appName, options = {}) {
|
|
333
|
+
return build.buildApp(appName, options);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Detects the runtime language of an application
|
|
338
|
+
* Analyzes project files to determine TypeScript, Python, etc.
|
|
339
|
+
*
|
|
340
|
+
* @function detectLanguage
|
|
341
|
+
* @param {string} appPath - Path to application directory
|
|
342
|
+
* @returns {string} Detected language ('typescript', 'python', etc.)
|
|
343
|
+
* @throws {Error} If language cannot be detected
|
|
344
|
+
*
|
|
345
|
+
* @example
|
|
346
|
+
* const language = detectLanguage('./myapp');
|
|
347
|
+
* // Returns: 'typescript'
|
|
348
|
+
*/
|
|
349
|
+
function detectLanguage(appPath) {
|
|
350
|
+
return build.detectLanguage(appPath);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Generates a Dockerfile from template based on detected language
|
|
355
|
+
* Uses Handlebars templates to create optimized Dockerfiles
|
|
356
|
+
*
|
|
357
|
+
* @async
|
|
358
|
+
* @function generateDockerfile
|
|
359
|
+
* @param {string} appPath - Path to application directory
|
|
360
|
+
* @param {string} language - Target language ('typescript', 'python')
|
|
361
|
+
* @param {Object} config - Application configuration from variables.yaml
|
|
362
|
+
* @returns {Promise<string>} Path to generated Dockerfile
|
|
363
|
+
* @throws {Error} If template generation fails
|
|
364
|
+
*
|
|
365
|
+
* @example
|
|
366
|
+
* const dockerfilePath = await generateDockerfile('./myapp', 'typescript', config);
|
|
367
|
+
* // Returns: './myapp/.aifabrix/Dockerfile.typescript'
|
|
368
|
+
*/
|
|
369
|
+
async function generateDockerfile(appPath, language, config) {
|
|
370
|
+
return build.generateDockerfile(appPath, language, config);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Runs the application locally using Docker
|
|
375
|
+
* Starts container with proper port mapping and environment
|
|
376
|
+
*
|
|
377
|
+
* @async
|
|
378
|
+
* @function runApp
|
|
379
|
+
* @param {string} appName - Name of the application to run
|
|
380
|
+
* @param {Object} options - Run options
|
|
381
|
+
* @param {number} [options.port] - Override local port
|
|
382
|
+
* @returns {Promise<void>} Resolves when app is running
|
|
383
|
+
* @throws {Error} If run fails or app is not built
|
|
384
|
+
*
|
|
385
|
+
* @example
|
|
386
|
+
* await runApp('myapp', { port: 3001 });
|
|
387
|
+
* // Application is now running on localhost:3001
|
|
388
|
+
*/
|
|
389
|
+
async function runApp(appName, options = {}) {
|
|
390
|
+
return appRun.runApp(appName, options);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Pushes application image to Azure Container Registry
|
|
395
|
+
* @async
|
|
396
|
+
* @function pushApp
|
|
397
|
+
* @param {string} appName - Name of the application
|
|
398
|
+
* @param {Object} options - Push options (registry, tag)
|
|
399
|
+
* @returns {Promise<void>} Resolves when push is complete
|
|
400
|
+
*/
|
|
401
|
+
async function pushApp(appName, options = {}) {
|
|
402
|
+
try {
|
|
403
|
+
// Validate app name
|
|
404
|
+
validateAppName(appName);
|
|
405
|
+
|
|
406
|
+
const configPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
|
|
407
|
+
let config;
|
|
408
|
+
try {
|
|
409
|
+
config = yaml.load(await fs.readFile(configPath, 'utf8'));
|
|
410
|
+
} catch (error) {
|
|
411
|
+
throw new Error(`Failed to load configuration: ${configPath}\nRun 'aifabrix create ${appName}' first`);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const registry = options.registry || config.image?.registry;
|
|
415
|
+
if (!registry) {
|
|
416
|
+
throw new Error('Registry URL is required. Provide via --registry flag or configure in variables.yaml under image.registry');
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (!pushUtils.validateRegistryURL(registry)) {
|
|
420
|
+
throw new Error(`Invalid registry URL format: ${registry}. Expected format: *.azurecr.io`);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const tags = options.tag ? options.tag.split(',').map(t => t.trim()) : ['latest'];
|
|
424
|
+
|
|
425
|
+
if (!await pushUtils.checkLocalImageExists(appName, 'latest')) {
|
|
426
|
+
throw new Error(`Docker image ${appName}:latest not found locally.\nRun 'aifabrix build ${appName}' first`);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (!await pushUtils.checkAzureCLIInstalled()) {
|
|
430
|
+
throw new Error('Azure CLI is not installed. Install from: https://docs.microsoft.com/cli/azure/install-azure-cli');
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (await pushUtils.checkACRAuthentication(registry)) {
|
|
434
|
+
console.log(chalk.green(`✓ Already authenticated with ${registry}`));
|
|
435
|
+
} else {
|
|
436
|
+
await pushUtils.authenticateACR(registry);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
await Promise.all(tags.map(async(tag) => {
|
|
440
|
+
await pushUtils.tagImage(`${appName}:latest`, `${registry}/${appName}:${tag}`);
|
|
441
|
+
await pushUtils.pushImage(`${registry}/${appName}:${tag}`);
|
|
442
|
+
}));
|
|
443
|
+
|
|
444
|
+
console.log(chalk.green(`\n✓ Successfully pushed ${tags.length} tag(s) to ${registry}`));
|
|
445
|
+
console.log(chalk.gray(`Image: ${registry}/${appName}:*`));
|
|
446
|
+
console.log(chalk.gray(`Tags: ${tags.join(', ')}`));
|
|
447
|
+
|
|
448
|
+
} catch (error) {
|
|
449
|
+
throw new Error(`Failed to push application: ${error.message}`);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
async function deployApp(appName, options = {}) {
|
|
454
|
+
const appDeploy = require('./app-deploy');
|
|
455
|
+
return appDeploy.deployApp(appName, options);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
module.exports = {
|
|
459
|
+
createApp,
|
|
460
|
+
buildApp,
|
|
461
|
+
runApp,
|
|
462
|
+
detectLanguage,
|
|
463
|
+
generateDockerfile,
|
|
464
|
+
pushApp,
|
|
465
|
+
deployApp,
|
|
466
|
+
checkImageExists: appRun.checkImageExists,
|
|
467
|
+
checkContainerRunning: appRun.checkContainerRunning,
|
|
468
|
+
stopAndRemoveContainer: appRun.stopAndRemoveContainer,
|
|
469
|
+
checkPortAvailable: appRun.checkPortAvailable,
|
|
470
|
+
generateDockerCompose: appRun.generateDockerCompose,
|
|
471
|
+
waitForHealthCheck: appRun.waitForHealthCheck
|
|
472
|
+
};
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Fabrix Builder Audit Logger
|
|
3
|
+
*
|
|
4
|
+
* ISO 27001 compliant structured logging for audit trails.
|
|
5
|
+
* Provides secure, audit-ready logging with sensitive data masking.
|
|
6
|
+
*
|
|
7
|
+
* @fileoverview Audit and compliance logging for AI Fabrix Builder
|
|
8
|
+
* @author AI Fabrix Team
|
|
9
|
+
* @version 2.0.0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Masks sensitive data in strings
|
|
14
|
+
* Prevents secrets, keys, and passwords from appearing in logs
|
|
15
|
+
*
|
|
16
|
+
* @param {string} value - Value to mask
|
|
17
|
+
* @returns {string} Masked value
|
|
18
|
+
*/
|
|
19
|
+
function maskSensitiveData(value) {
|
|
20
|
+
if (!value || typeof value !== 'string') {
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Mask patterns: passwords, secrets, keys, tokens
|
|
25
|
+
const sensitivePatterns = [
|
|
26
|
+
{ pattern: /password[=:]\s*([^\s]+)/gi, replacement: 'password=***' },
|
|
27
|
+
{ pattern: /secret[=:]\s*([^\s]+)/gi, replacement: 'secret=***' },
|
|
28
|
+
{ pattern: /key[=:]\s*([^\s]+)/gi, replacement: 'key=***' },
|
|
29
|
+
{ pattern: /token[=:]\s*([^\s]+)/gi, replacement: 'token=***' },
|
|
30
|
+
{ pattern: /api[_-]?key[=:]\s*([^\s]+)/gi, replacement: 'api_key=***' }
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
let masked = value;
|
|
34
|
+
for (const { pattern, replacement } of sensitivePatterns) {
|
|
35
|
+
masked = masked.replace(pattern, replacement);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// If value looks like a hash/key (long hex string), mask it
|
|
39
|
+
if (/^[a-f0-9]{32,}$/i.test(masked.trim())) {
|
|
40
|
+
return '***';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return masked;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Creates an audit log entry with ISO 27001 compliance
|
|
48
|
+
*
|
|
49
|
+
* @param {string} level - Log level (INFO, WARN, ERROR, AUDIT)
|
|
50
|
+
* @param {string} message - Log message
|
|
51
|
+
* @param {Object} metadata - Additional metadata
|
|
52
|
+
* @returns {Object} Structured log entry
|
|
53
|
+
*/
|
|
54
|
+
function createAuditEntry(level, message, metadata = {}) {
|
|
55
|
+
const entry = {
|
|
56
|
+
timestamp: new Date().toISOString(),
|
|
57
|
+
level: level.toUpperCase(),
|
|
58
|
+
message: maskSensitiveData(message),
|
|
59
|
+
metadata: {}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Mask sensitive metadata
|
|
63
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
64
|
+
entry.metadata[key] = maskSensitiveData(
|
|
65
|
+
typeof value === 'string' ? value : JSON.stringify(value)
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return entry;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Logs audit entry to console (structured JSON format)
|
|
74
|
+
*
|
|
75
|
+
* @param {string} level - Log level
|
|
76
|
+
* @param {string} message - Log message
|
|
77
|
+
* @param {Object} metadata - Additional metadata
|
|
78
|
+
*/
|
|
79
|
+
function auditLog(level, message, metadata) {
|
|
80
|
+
const entry = createAuditEntry(level, message, metadata);
|
|
81
|
+
console.log(JSON.stringify(entry));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Logs deployment attempt with full audit trail
|
|
86
|
+
*
|
|
87
|
+
* @param {string} appName - Application name
|
|
88
|
+
* @param {string} controllerUrl - Controller URL
|
|
89
|
+
* @param {Object} options - Deployment options
|
|
90
|
+
*/
|
|
91
|
+
function logDeploymentAttempt(appName, controllerUrl, options = {}) {
|
|
92
|
+
auditLog('AUDIT', 'Deployment initiated', {
|
|
93
|
+
action: 'deploy',
|
|
94
|
+
appName,
|
|
95
|
+
controllerUrl,
|
|
96
|
+
environment: options.environment || 'unknown',
|
|
97
|
+
timestamp: Date.now()
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Logs deployment success
|
|
103
|
+
*
|
|
104
|
+
* @param {string} appName - Application name
|
|
105
|
+
* @param {string} deploymentId - Deployment ID
|
|
106
|
+
* @param {string} controllerUrl - Controller URL
|
|
107
|
+
*/
|
|
108
|
+
function logDeploymentSuccess(appName, deploymentId, controllerUrl) {
|
|
109
|
+
auditLog('AUDIT', 'Deployment succeeded', {
|
|
110
|
+
action: 'deploy',
|
|
111
|
+
appName,
|
|
112
|
+
deploymentId,
|
|
113
|
+
controllerUrl,
|
|
114
|
+
status: 'success',
|
|
115
|
+
timestamp: Date.now()
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Logs deployment failure with error details
|
|
121
|
+
*
|
|
122
|
+
* @param {string} appName - Application name
|
|
123
|
+
* @param {string} controllerUrl - Controller URL
|
|
124
|
+
* @param {Error} error - Error that occurred
|
|
125
|
+
*/
|
|
126
|
+
function logDeploymentFailure(appName, controllerUrl, error) {
|
|
127
|
+
auditLog('ERROR', 'Deployment failed', {
|
|
128
|
+
action: 'deploy',
|
|
129
|
+
appName,
|
|
130
|
+
controllerUrl,
|
|
131
|
+
status: 'failure',
|
|
132
|
+
errorMessage: error.message,
|
|
133
|
+
errorCode: error.code || 'UNKNOWN',
|
|
134
|
+
timestamp: Date.now()
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Logs security-related events
|
|
140
|
+
*
|
|
141
|
+
* @param {string} event - Security event name
|
|
142
|
+
* @param {Object} details - Event details
|
|
143
|
+
*/
|
|
144
|
+
function logSecurityEvent(event, details = {}) {
|
|
145
|
+
auditLog('AUDIT', `Security event: ${event}`, {
|
|
146
|
+
eventType: 'security',
|
|
147
|
+
event,
|
|
148
|
+
...details,
|
|
149
|
+
timestamp: Date.now()
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
module.exports = {
|
|
154
|
+
auditLog,
|
|
155
|
+
logDeploymentAttempt,
|
|
156
|
+
logDeploymentSuccess,
|
|
157
|
+
logDeploymentFailure,
|
|
158
|
+
logSecurityEvent,
|
|
159
|
+
maskSensitiveData,
|
|
160
|
+
createAuditEntry
|
|
161
|
+
};
|
|
162
|
+
|