@aifabrix/builder 2.0.2 → 2.0.3
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/README.md +2 -4
- package/lib/app-run.js +51 -161
- package/lib/build.js +3 -62
- package/lib/schema/env-config.yaml +9 -1
- package/lib/utils/compose-generator.js +185 -0
- package/lib/utils/docker-build.js +173 -0
- package/package.json +2 -1
- package/templates/applications/miso-controller/rbac.yaml +47 -1
- package/templates/applications/miso-controller/variables.yaml +10 -10
package/README.md
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
[](https://www.npmjs.com/package/%40aifabrix%2Fbuilder)
|
|
1
|
+
# AI Fabrix - Builder SDK
|
|
4
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@aifabrix/builder)
|
|
5
4
|
[](https://opensource.org/licenses/MIT)
|
|
6
|
-
|
|
7
5
|
Local development infrastructure + Azure deployment tool.
|
|
8
6
|
|
|
9
7
|
## Install
|
package/lib/app-run.js
CHANGED
|
@@ -15,7 +15,6 @@ const path = require('path');
|
|
|
15
15
|
const net = require('net');
|
|
16
16
|
const chalk = require('chalk');
|
|
17
17
|
const yaml = require('js-yaml');
|
|
18
|
-
const handlebars = require('handlebars');
|
|
19
18
|
const { exec } = require('child_process');
|
|
20
19
|
const { promisify } = require('util');
|
|
21
20
|
const validator = require('./validator');
|
|
@@ -23,6 +22,7 @@ const infra = require('./infra');
|
|
|
23
22
|
const secrets = require('./secrets');
|
|
24
23
|
const logger = require('./utils/logger');
|
|
25
24
|
const { waitForHealthCheck } = require('./utils/health-check');
|
|
25
|
+
const composeGenerator = require('./utils/compose-generator');
|
|
26
26
|
|
|
27
27
|
const execAsync = promisify(exec);
|
|
28
28
|
|
|
@@ -89,22 +89,6 @@ async function checkPortAvailable(port) {
|
|
|
89
89
|
});
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
/**
|
|
93
|
-
* Loads and compiles Docker Compose template
|
|
94
|
-
* @param {string} language - Language type
|
|
95
|
-
* @returns {Function} Compiled Handlebars template
|
|
96
|
-
* @throws {Error} If template not found
|
|
97
|
-
*/
|
|
98
|
-
function loadDockerComposeTemplate(language) {
|
|
99
|
-
const templatePath = path.join(__dirname, '..', 'templates', language, 'docker-compose.hbs');
|
|
100
|
-
if (!fsSync.existsSync(templatePath)) {
|
|
101
|
-
throw new Error(`Docker Compose template not found for language: ${language}`);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const templateContent = fsSync.readFileSync(templatePath, 'utf8');
|
|
105
|
-
return handlebars.compile(templateContent);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
92
|
/**
|
|
109
93
|
* Extracts image name from configuration (same logic as build.js)
|
|
110
94
|
* @param {Object} config - Application configuration
|
|
@@ -112,146 +96,7 @@ function loadDockerComposeTemplate(language) {
|
|
|
112
96
|
* @returns {string} Image name
|
|
113
97
|
*/
|
|
114
98
|
function getImageName(config, appName) {
|
|
115
|
-
|
|
116
|
-
return config.image.split(':')[0];
|
|
117
|
-
} else if (config.image?.name) {
|
|
118
|
-
return config.image.name;
|
|
119
|
-
} else if (config.app?.key) {
|
|
120
|
-
return config.app.key;
|
|
121
|
-
}
|
|
122
|
-
return appName;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Builds app configuration section
|
|
127
|
-
* @param {string} appName - Application name
|
|
128
|
-
* @param {Object} config - Application configuration
|
|
129
|
-
* @returns {Object} App configuration
|
|
130
|
-
*/
|
|
131
|
-
function buildAppConfig(appName, config) {
|
|
132
|
-
return {
|
|
133
|
-
key: appName,
|
|
134
|
-
name: config.displayName || appName
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Builds image configuration section
|
|
140
|
-
* @param {Object} config - Application configuration
|
|
141
|
-
* @param {string} appName - Application name
|
|
142
|
-
* @returns {Object} Image configuration
|
|
143
|
-
*/
|
|
144
|
-
function buildImageConfig(config, appName) {
|
|
145
|
-
const imageName = getImageName(config, appName);
|
|
146
|
-
const imageTag = config.image?.tag || 'latest';
|
|
147
|
-
return {
|
|
148
|
-
name: imageName,
|
|
149
|
-
tag: imageTag
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Builds health check configuration section
|
|
155
|
-
* @param {Object} config - Application configuration
|
|
156
|
-
* @returns {Object} Health check configuration
|
|
157
|
-
*/
|
|
158
|
-
function buildHealthCheckConfig(config) {
|
|
159
|
-
return {
|
|
160
|
-
path: config.healthCheck?.path || '/health',
|
|
161
|
-
interval: config.healthCheck?.interval || 30
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Builds requires configuration section
|
|
167
|
-
* @param {Object} config - Application configuration
|
|
168
|
-
* @returns {Object} Requires configuration
|
|
169
|
-
*/
|
|
170
|
-
function buildRequiresConfig(config) {
|
|
171
|
-
return {
|
|
172
|
-
requiresDatabase: config.requires?.database || config.services?.database || false,
|
|
173
|
-
requiresStorage: config.requires?.storage || config.services?.storage || false,
|
|
174
|
-
requiresRedis: config.requires?.redis || config.services?.redis || false
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Builds service configuration for template data
|
|
180
|
-
* @param {string} appName - Application name
|
|
181
|
-
* @param {Object} config - Application configuration
|
|
182
|
-
* @param {number} port - Application port
|
|
183
|
-
* @returns {Object} Service configuration
|
|
184
|
-
*/
|
|
185
|
-
function buildServiceConfig(appName, config, port) {
|
|
186
|
-
const containerPort = config.build?.containerPort || config.port || 3000;
|
|
187
|
-
|
|
188
|
-
return {
|
|
189
|
-
app: buildAppConfig(appName, config),
|
|
190
|
-
image: buildImageConfig(config, appName),
|
|
191
|
-
port: containerPort,
|
|
192
|
-
build: {
|
|
193
|
-
localPort: port
|
|
194
|
-
},
|
|
195
|
-
healthCheck: buildHealthCheckConfig(config),
|
|
196
|
-
...buildRequiresConfig(config)
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Builds volumes configuration for template data
|
|
202
|
-
* @param {string} appName - Application name
|
|
203
|
-
* @returns {Object} Volumes configuration
|
|
204
|
-
*/
|
|
205
|
-
function buildVolumesConfig(appName) {
|
|
206
|
-
// Use forward slashes for Docker paths (works on both Windows and Unix)
|
|
207
|
-
const volumePath = path.join(process.cwd(), 'data', appName);
|
|
208
|
-
return {
|
|
209
|
-
mountVolume: volumePath.replace(/\\/g, '/')
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Builds networks configuration for template data
|
|
215
|
-
* @param {Object} config - Application configuration
|
|
216
|
-
* @returns {Object} Networks configuration
|
|
217
|
-
*/
|
|
218
|
-
function buildNetworksConfig(config) {
|
|
219
|
-
// Get databases from requires.databases or top-level databases
|
|
220
|
-
const databases = config.requires?.databases || config.databases || [];
|
|
221
|
-
return {
|
|
222
|
-
databases: databases
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Generates Docker Compose configuration from template
|
|
228
|
-
* @param {string} appName - Application name
|
|
229
|
-
* @param {Object} config - Application configuration
|
|
230
|
-
* @param {Object} options - Run options
|
|
231
|
-
* @returns {Promise<string>} Generated compose content
|
|
232
|
-
*/
|
|
233
|
-
async function generateDockerCompose(appName, config, options) {
|
|
234
|
-
const language = config.build?.language || config.language || 'typescript';
|
|
235
|
-
const template = loadDockerComposeTemplate(language);
|
|
236
|
-
|
|
237
|
-
const port = options.port || config.build?.localPort || config.port || 3000;
|
|
238
|
-
|
|
239
|
-
const serviceConfig = buildServiceConfig(appName, config, port);
|
|
240
|
-
const volumesConfig = buildVolumesConfig(appName);
|
|
241
|
-
const networksConfig = buildNetworksConfig(config);
|
|
242
|
-
|
|
243
|
-
// Get absolute path to .env file for docker-compose
|
|
244
|
-
const envFilePath = path.join(process.cwd(), 'builder', appName, '.env');
|
|
245
|
-
const envFileAbsolutePath = envFilePath.replace(/\\/g, '/'); // Use forward slashes for Docker
|
|
246
|
-
|
|
247
|
-
const templateData = {
|
|
248
|
-
...serviceConfig,
|
|
249
|
-
...volumesConfig,
|
|
250
|
-
...networksConfig,
|
|
251
|
-
envFile: envFileAbsolutePath
|
|
252
|
-
};
|
|
253
|
-
|
|
254
|
-
return template(templateData);
|
|
99
|
+
return composeGenerator.getImageName(config, appName);
|
|
255
100
|
}
|
|
256
101
|
|
|
257
102
|
/**
|
|
@@ -267,10 +112,34 @@ async function validateAppConfiguration(appName) {
|
|
|
267
112
|
throw new Error('Application name is required');
|
|
268
113
|
}
|
|
269
114
|
|
|
115
|
+
// Check if we're running from inside the builder directory
|
|
116
|
+
const currentDir = process.cwd();
|
|
117
|
+
const normalizedPath = currentDir.replace(/\\/g, '/');
|
|
118
|
+
const expectedBuilderPath = `builder/${appName}`;
|
|
119
|
+
|
|
120
|
+
// If inside builder/{appName}, suggest moving to project root
|
|
121
|
+
if (normalizedPath.endsWith(expectedBuilderPath)) {
|
|
122
|
+
const projectRoot = path.resolve(currentDir, '../..');
|
|
123
|
+
throw new Error(
|
|
124
|
+
'You\'re running from inside the builder directory.\n' +
|
|
125
|
+
`Current directory: ${currentDir}\n` +
|
|
126
|
+
'Please change to the project root and try again:\n' +
|
|
127
|
+
` cd ${projectRoot}\n` +
|
|
128
|
+
` aifabrix run ${appName}`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
270
132
|
// Load and validate app configuration
|
|
271
133
|
const configPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
|
|
272
134
|
if (!fsSync.existsSync(configPath)) {
|
|
273
|
-
|
|
135
|
+
const expectedDir = path.join(currentDir, 'builder', appName);
|
|
136
|
+
throw new Error(
|
|
137
|
+
`Application configuration not found: ${configPath}\n` +
|
|
138
|
+
`Current directory: ${currentDir}\n` +
|
|
139
|
+
`Expected location: ${expectedDir}\n` +
|
|
140
|
+
'Make sure you\'re running from the project root (where \'builder\' directory exists)\n' +
|
|
141
|
+
`Run 'aifabrix create ${appName}' first if configuration doesn't exist`
|
|
142
|
+
);
|
|
274
143
|
}
|
|
275
144
|
|
|
276
145
|
const configContent = fsSync.readFileSync(configPath, 'utf8');
|
|
@@ -279,7 +148,28 @@ async function validateAppConfiguration(appName) {
|
|
|
279
148
|
// Validate configuration
|
|
280
149
|
const validation = await validator.validateApplication(appName);
|
|
281
150
|
if (!validation.valid) {
|
|
282
|
-
|
|
151
|
+
const allErrors = [];
|
|
152
|
+
|
|
153
|
+
if (validation.variables && validation.variables.errors && validation.variables.errors.length > 0) {
|
|
154
|
+
allErrors.push('variables.yaml:');
|
|
155
|
+
allErrors.push(...validation.variables.errors.map(err => ` ${err}`));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (validation.rbac && validation.rbac.errors && validation.rbac.errors.length > 0) {
|
|
159
|
+
allErrors.push('rbac.yaml:');
|
|
160
|
+
allErrors.push(...validation.rbac.errors.map(err => ` ${err}`));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (validation.env && validation.env.errors && validation.env.errors.length > 0) {
|
|
164
|
+
allErrors.push('env.template:');
|
|
165
|
+
allErrors.push(...validation.env.errors.map(err => ` ${err}`));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (allErrors.length === 0) {
|
|
169
|
+
throw new Error('Configuration validation failed');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
throw new Error(`Configuration validation failed:\n${allErrors.join('\n')}`);
|
|
283
173
|
}
|
|
284
174
|
|
|
285
175
|
return config;
|
|
@@ -354,7 +244,7 @@ async function prepareEnvironment(appName, config, options) {
|
|
|
354
244
|
|
|
355
245
|
// Generate Docker Compose configuration
|
|
356
246
|
logger.log(chalk.blue('Generating Docker Compose configuration...'));
|
|
357
|
-
const composeContent = await generateDockerCompose(appName, config, options);
|
|
247
|
+
const composeContent = await composeGenerator.generateDockerCompose(appName, config, options);
|
|
358
248
|
// Write compose file to temporary location
|
|
359
249
|
const tempComposePath = path.join(process.cwd(), 'builder', appName, 'docker-compose.yaml');
|
|
360
250
|
await fs.writeFile(tempComposePath, composeContent);
|
|
@@ -487,6 +377,6 @@ module.exports = {
|
|
|
487
377
|
checkContainerRunning,
|
|
488
378
|
stopAndRemoveContainer,
|
|
489
379
|
checkPortAvailable,
|
|
490
|
-
generateDockerCompose,
|
|
380
|
+
generateDockerCompose: composeGenerator.generateDockerCompose,
|
|
491
381
|
waitForHealthCheck
|
|
492
382
|
};
|
package/lib/build.js
CHANGED
|
@@ -22,6 +22,7 @@ const secrets = require('./secrets');
|
|
|
22
22
|
const logger = require('./utils/logger');
|
|
23
23
|
const validator = require('./validator');
|
|
24
24
|
const dockerfileUtils = require('./utils/dockerfile-utils');
|
|
25
|
+
const dockerBuild = require('./utils/docker-build');
|
|
25
26
|
|
|
26
27
|
const execAsync = promisify(exec);
|
|
27
28
|
|
|
@@ -71,66 +72,6 @@ function resolveContextPath(builderPath, contextPath) {
|
|
|
71
72
|
return resolvedPath;
|
|
72
73
|
}
|
|
73
74
|
|
|
74
|
-
/**
|
|
75
|
-
* Checks if error indicates Docker is not running or not installed
|
|
76
|
-
* @param {string} errorMessage - Error message to check
|
|
77
|
-
* @returns {boolean} True if Docker is not available
|
|
78
|
-
*/
|
|
79
|
-
function isDockerNotAvailableError(errorMessage) {
|
|
80
|
-
return errorMessage.includes('docker: command not found') ||
|
|
81
|
-
errorMessage.includes('Cannot connect to the Docker daemon') ||
|
|
82
|
-
errorMessage.includes('Is the docker daemon running') ||
|
|
83
|
-
errorMessage.includes('Cannot connect to Docker');
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Handles Docker build errors and provides user-friendly messages
|
|
88
|
-
* @param {Error} error - Build error
|
|
89
|
-
* @throws {Error} Formatted error message
|
|
90
|
-
*/
|
|
91
|
-
function handleBuildError(error) {
|
|
92
|
-
const errorMessage = error.message || error.stderr || String(error);
|
|
93
|
-
|
|
94
|
-
if (isDockerNotAvailableError(errorMessage)) {
|
|
95
|
-
throw new Error('Docker is not running or not installed. Please start Docker Desktop and try again.');
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const detailedError = error.stderr || error.stdout || errorMessage;
|
|
99
|
-
throw new Error(`Docker build failed: ${detailedError}`);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Executes Docker build command with proper error handling
|
|
104
|
-
* @param {string} imageName - Image name to build
|
|
105
|
-
* @param {string} dockerfilePath - Path to Dockerfile
|
|
106
|
-
* @param {string} contextPath - Build context path
|
|
107
|
-
* @param {string} tag - Image tag
|
|
108
|
-
* @returns {Promise<void>} Resolves when build completes
|
|
109
|
-
* @throws {Error} If build fails
|
|
110
|
-
*/
|
|
111
|
-
async function executeDockerBuild(imageName, dockerfilePath, contextPath, tag) {
|
|
112
|
-
const dockerCommand = `docker build -t ${imageName}:${tag} -f "${dockerfilePath}" "${contextPath}"`;
|
|
113
|
-
|
|
114
|
-
try {
|
|
115
|
-
logger.log(chalk.blue('Building image...'));
|
|
116
|
-
logger.log(chalk.gray(`Command: ${dockerCommand}`));
|
|
117
|
-
|
|
118
|
-
const { stdout, stderr } = await execAsync(dockerCommand);
|
|
119
|
-
|
|
120
|
-
if (stderr && !stderr.includes('warning')) {
|
|
121
|
-
logger.log(chalk.yellow(stderr));
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (stdout) {
|
|
125
|
-
logger.log(stdout);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
logger.log(chalk.green(`✓ Image built: ${imageName}:${tag}`));
|
|
129
|
-
} catch (error) {
|
|
130
|
-
handleBuildError(error);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
75
|
/**
|
|
135
76
|
* Detects the runtime language of an application
|
|
136
77
|
* Analyzes project files to determine TypeScript, Python, etc.
|
|
@@ -317,7 +258,7 @@ async function loadAndValidateConfig(appName) {
|
|
|
317
258
|
* @param {Object} options - Build options
|
|
318
259
|
*/
|
|
319
260
|
async function executeBuild(imageName, dockerfilePath, contextPath, tag, options) {
|
|
320
|
-
await executeDockerBuild(imageName, dockerfilePath, contextPath, tag);
|
|
261
|
+
await dockerBuild.executeDockerBuild(imageName, dockerfilePath, contextPath, tag);
|
|
321
262
|
|
|
322
263
|
// Tag image if additional tag provided
|
|
323
264
|
if (options.tag && options.tag !== 'latest') {
|
|
@@ -417,7 +358,7 @@ async function buildApp(appName, options = {}) {
|
|
|
417
358
|
module.exports = {
|
|
418
359
|
loadVariablesYaml,
|
|
419
360
|
resolveContextPath,
|
|
420
|
-
executeDockerBuild,
|
|
361
|
+
executeDockerBuild: dockerBuild.executeDockerBuild,
|
|
421
362
|
detectLanguage,
|
|
422
363
|
generateDockerfile,
|
|
423
364
|
buildApp
|
|
@@ -7,9 +7,17 @@ environments:
|
|
|
7
7
|
REDIS_HOST: redis
|
|
8
8
|
MISO_HOST: miso-controller
|
|
9
9
|
KEYCLOAK_HOST: keycloak
|
|
10
|
-
|
|
10
|
+
MORI_HOST: mori-controller
|
|
11
|
+
OPENWEBUI_HOST: openwebui
|
|
12
|
+
FLOWISE_HOST: flowise
|
|
13
|
+
DATAPLANE_HOST: dataplane
|
|
14
|
+
|
|
11
15
|
local:
|
|
12
16
|
DB_HOST: localhost
|
|
13
17
|
REDIS_HOST: localhost
|
|
14
18
|
MISO_HOST: localhost
|
|
15
19
|
KEYCLOAK_HOST: localhost
|
|
20
|
+
MORI_HOST: localhost
|
|
21
|
+
OPENWEBUI_HOST: localhost
|
|
22
|
+
FLOWISE_HOST: localhost
|
|
23
|
+
DATAPLANE_HOST: localhost
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Docker Compose Generation Utilities
|
|
3
|
+
*
|
|
4
|
+
* This module handles Docker Compose configuration generation for application running.
|
|
5
|
+
* Separated from app-run.js to maintain file size limits.
|
|
6
|
+
*
|
|
7
|
+
* @fileoverview Docker Compose generation utilities
|
|
8
|
+
* @author AI Fabrix Team
|
|
9
|
+
* @version 2.0.0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fsSync = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const handlebars = require('handlebars');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Loads and compiles Docker Compose template
|
|
18
|
+
* @param {string} language - Language type
|
|
19
|
+
* @returns {Function} Compiled Handlebars template
|
|
20
|
+
* @throws {Error} If template not found
|
|
21
|
+
*/
|
|
22
|
+
function loadDockerComposeTemplate(language) {
|
|
23
|
+
const templatePath = path.join(__dirname, '..', '..', 'templates', language, 'docker-compose.hbs');
|
|
24
|
+
if (!fsSync.existsSync(templatePath)) {
|
|
25
|
+
throw new Error(`Docker Compose template not found for language: ${language}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const templateContent = fsSync.readFileSync(templatePath, 'utf8');
|
|
29
|
+
return handlebars.compile(templateContent);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Extracts image name from configuration (same logic as build.js)
|
|
34
|
+
* @param {Object} config - Application configuration
|
|
35
|
+
* @param {string} appName - Application name (fallback)
|
|
36
|
+
* @returns {string} Image name
|
|
37
|
+
*/
|
|
38
|
+
function getImageName(config, appName) {
|
|
39
|
+
if (typeof config.image === 'string') {
|
|
40
|
+
return config.image.split(':')[0];
|
|
41
|
+
} else if (config.image?.name) {
|
|
42
|
+
return config.image.name;
|
|
43
|
+
} else if (config.app?.key) {
|
|
44
|
+
return config.app.key;
|
|
45
|
+
}
|
|
46
|
+
return appName;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Builds app configuration section
|
|
51
|
+
* @param {string} appName - Application name
|
|
52
|
+
* @param {Object} config - Application configuration
|
|
53
|
+
* @returns {Object} App configuration
|
|
54
|
+
*/
|
|
55
|
+
function buildAppConfig(appName, config) {
|
|
56
|
+
return {
|
|
57
|
+
key: appName,
|
|
58
|
+
name: config.displayName || appName
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Builds image configuration section
|
|
64
|
+
* @param {Object} config - Application configuration
|
|
65
|
+
* @param {string} appName - Application name
|
|
66
|
+
* @returns {Object} Image configuration
|
|
67
|
+
*/
|
|
68
|
+
function buildImageConfig(config, appName) {
|
|
69
|
+
const imageName = getImageName(config, appName);
|
|
70
|
+
const imageTag = config.image?.tag || 'latest';
|
|
71
|
+
return {
|
|
72
|
+
name: imageName,
|
|
73
|
+
tag: imageTag
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Builds health check configuration section
|
|
79
|
+
* @param {Object} config - Application configuration
|
|
80
|
+
* @returns {Object} Health check configuration
|
|
81
|
+
*/
|
|
82
|
+
function buildHealthCheckConfig(config) {
|
|
83
|
+
return {
|
|
84
|
+
path: config.healthCheck?.path || '/health',
|
|
85
|
+
interval: config.healthCheck?.interval || 30
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Builds requires configuration section
|
|
91
|
+
* @param {Object} config - Application configuration
|
|
92
|
+
* @returns {Object} Requires configuration
|
|
93
|
+
*/
|
|
94
|
+
function buildRequiresConfig(config) {
|
|
95
|
+
return {
|
|
96
|
+
requiresDatabase: config.requires?.database || config.services?.database || false,
|
|
97
|
+
requiresStorage: config.requires?.storage || config.services?.storage || false,
|
|
98
|
+
requiresRedis: config.requires?.redis || config.services?.redis || false
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Builds service configuration for template data
|
|
104
|
+
* @param {string} appName - Application name
|
|
105
|
+
* @param {Object} config - Application configuration
|
|
106
|
+
* @param {number} port - Application port
|
|
107
|
+
* @returns {Object} Service configuration
|
|
108
|
+
*/
|
|
109
|
+
function buildServiceConfig(appName, config, port) {
|
|
110
|
+
const containerPort = config.build?.containerPort || config.port || 3000;
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
app: buildAppConfig(appName, config),
|
|
114
|
+
image: buildImageConfig(config, appName),
|
|
115
|
+
port: containerPort,
|
|
116
|
+
build: {
|
|
117
|
+
localPort: port
|
|
118
|
+
},
|
|
119
|
+
healthCheck: buildHealthCheckConfig(config),
|
|
120
|
+
...buildRequiresConfig(config)
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Builds volumes configuration for template data
|
|
126
|
+
* @param {string} appName - Application name
|
|
127
|
+
* @returns {Object} Volumes configuration
|
|
128
|
+
*/
|
|
129
|
+
function buildVolumesConfig(appName) {
|
|
130
|
+
// Use forward slashes for Docker paths (works on both Windows and Unix)
|
|
131
|
+
const volumePath = path.join(process.cwd(), 'data', appName);
|
|
132
|
+
return {
|
|
133
|
+
mountVolume: volumePath.replace(/\\/g, '/')
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Builds networks configuration for template data
|
|
139
|
+
* @param {Object} config - Application configuration
|
|
140
|
+
* @returns {Object} Networks configuration
|
|
141
|
+
*/
|
|
142
|
+
function buildNetworksConfig(config) {
|
|
143
|
+
// Get databases from requires.databases or top-level databases
|
|
144
|
+
const databases = config.requires?.databases || config.databases || [];
|
|
145
|
+
return {
|
|
146
|
+
databases: databases
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Generates Docker Compose configuration from template
|
|
152
|
+
* @param {string} appName - Application name
|
|
153
|
+
* @param {Object} config - Application configuration
|
|
154
|
+
* @param {Object} options - Run options
|
|
155
|
+
* @returns {Promise<string>} Generated compose content
|
|
156
|
+
*/
|
|
157
|
+
async function generateDockerCompose(appName, config, options) {
|
|
158
|
+
const language = config.build?.language || config.language || 'typescript';
|
|
159
|
+
const template = loadDockerComposeTemplate(language);
|
|
160
|
+
|
|
161
|
+
const port = options.port || config.build?.localPort || config.port || 3000;
|
|
162
|
+
|
|
163
|
+
const serviceConfig = buildServiceConfig(appName, config, port);
|
|
164
|
+
const volumesConfig = buildVolumesConfig(appName);
|
|
165
|
+
const networksConfig = buildNetworksConfig(config);
|
|
166
|
+
|
|
167
|
+
// Get absolute path to .env file for docker-compose
|
|
168
|
+
const envFilePath = path.join(process.cwd(), 'builder', appName, '.env');
|
|
169
|
+
const envFileAbsolutePath = envFilePath.replace(/\\/g, '/'); // Use forward slashes for Docker
|
|
170
|
+
|
|
171
|
+
const templateData = {
|
|
172
|
+
...serviceConfig,
|
|
173
|
+
...volumesConfig,
|
|
174
|
+
...networksConfig,
|
|
175
|
+
envFile: envFileAbsolutePath
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
return template(templateData);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
module.exports = {
|
|
182
|
+
generateDockerCompose,
|
|
183
|
+
getImageName
|
|
184
|
+
};
|
|
185
|
+
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Docker Build Utilities
|
|
3
|
+
*
|
|
4
|
+
* This module handles Docker image building with progress indicators.
|
|
5
|
+
* Separated from build.js to maintain file size limits.
|
|
6
|
+
*
|
|
7
|
+
* @fileoverview Docker build execution utilities
|
|
8
|
+
* @author AI Fabrix Team
|
|
9
|
+
* @version 2.0.0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { spawn } = require('child_process');
|
|
13
|
+
const ora = require('ora');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Checks if error indicates Docker is not running or not installed
|
|
17
|
+
* @param {string} errorMessage - Error message to check
|
|
18
|
+
* @returns {boolean} True if Docker is not available
|
|
19
|
+
*/
|
|
20
|
+
function isDockerNotAvailableError(errorMessage) {
|
|
21
|
+
return errorMessage.includes('docker: command not found') ||
|
|
22
|
+
errorMessage.includes('Cannot connect to the Docker daemon') ||
|
|
23
|
+
errorMessage.includes('Is the docker daemon running') ||
|
|
24
|
+
errorMessage.includes('Cannot connect to Docker');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Parses Docker build output to extract progress information
|
|
29
|
+
* @param {string} line - Single line of Docker build output
|
|
30
|
+
* @returns {string|null} Progress message or null if no progress info
|
|
31
|
+
*/
|
|
32
|
+
function parseDockerBuildProgress(line) {
|
|
33
|
+
// Match step progress: "Step 1/10 : FROM node:20-alpine"
|
|
34
|
+
const stepMatch = line.match(/^Step\s+(\d+)\/(\d+)\s*:/i);
|
|
35
|
+
if (stepMatch) {
|
|
36
|
+
return `Step ${stepMatch[1]}/${stepMatch[2]}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Match layer pulling: "Pulling from library/node"
|
|
40
|
+
const pullingMatch = line.match(/^Pulling\s+(.+)$/i);
|
|
41
|
+
if (pullingMatch) {
|
|
42
|
+
return `Pulling ${pullingMatch[1]}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Match layer extracting: "Extracting [====> ]"
|
|
46
|
+
const extractingMatch = line.match(/^Extracting/i);
|
|
47
|
+
if (extractingMatch) {
|
|
48
|
+
return 'Extracting layers';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Match build progress: " => [internal] load build context"
|
|
52
|
+
const buildMatch = line.match(/^=>\s*\[(.*?)\]\s*(.+)$/i);
|
|
53
|
+
if (buildMatch) {
|
|
54
|
+
return buildMatch[2].substring(0, 50);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Match progress bars: "[====> ] 10.5MB/50MB"
|
|
58
|
+
const progressMatch = line.match(/\[.*?\]\s+([\d.]+(MB|KB|GB))\/([\d.]+(MB|KB|GB))/i);
|
|
59
|
+
if (progressMatch) {
|
|
60
|
+
return `${progressMatch[1]}/${progressMatch[3]}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Executes Docker build command with progress indicator
|
|
68
|
+
* @param {string} imageName - Image name to build
|
|
69
|
+
* @param {string} dockerfilePath - Path to Dockerfile
|
|
70
|
+
* @param {string} contextPath - Build context path
|
|
71
|
+
* @param {string} tag - Image tag
|
|
72
|
+
* @returns {Promise<void>} Resolves when build completes
|
|
73
|
+
* @throws {Error} If build fails
|
|
74
|
+
*/
|
|
75
|
+
async function executeDockerBuild(imageName, dockerfilePath, contextPath, tag) {
|
|
76
|
+
const spinner = ora({
|
|
77
|
+
text: 'Starting Docker build...',
|
|
78
|
+
spinner: 'dots'
|
|
79
|
+
}).start();
|
|
80
|
+
|
|
81
|
+
return new Promise((resolve, reject) => {
|
|
82
|
+
// Use spawn for streaming output
|
|
83
|
+
const dockerProcess = spawn('docker', [
|
|
84
|
+
'build',
|
|
85
|
+
'-t', `${imageName}:${tag}`,
|
|
86
|
+
'-f', dockerfilePath,
|
|
87
|
+
contextPath
|
|
88
|
+
], {
|
|
89
|
+
shell: process.platform === 'win32'
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
let stdoutBuffer = '';
|
|
93
|
+
let stderrBuffer = '';
|
|
94
|
+
let lastProgressUpdate = Date.now();
|
|
95
|
+
|
|
96
|
+
dockerProcess.stdout.on('data', (data) => {
|
|
97
|
+
const output = data.toString();
|
|
98
|
+
stdoutBuffer += output;
|
|
99
|
+
|
|
100
|
+
// Parse progress from output lines
|
|
101
|
+
const lines = output.split('\n');
|
|
102
|
+
for (const line of lines) {
|
|
103
|
+
const progress = parseDockerBuildProgress(line.trim());
|
|
104
|
+
if (progress) {
|
|
105
|
+
// Update spinner text with progress (throttle updates)
|
|
106
|
+
const now = Date.now();
|
|
107
|
+
if (now - lastProgressUpdate > 200) {
|
|
108
|
+
spinner.text = `Building image... ${progress}`;
|
|
109
|
+
lastProgressUpdate = now;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
dockerProcess.stderr.on('data', (data) => {
|
|
116
|
+
const output = data.toString();
|
|
117
|
+
stderrBuffer += output;
|
|
118
|
+
|
|
119
|
+
// Check for warnings vs errors
|
|
120
|
+
if (!output.toLowerCase().includes('warning')) {
|
|
121
|
+
// Parse progress from stderr too (Docker outputs progress to stderr)
|
|
122
|
+
const lines = output.split('\n');
|
|
123
|
+
for (const line of lines) {
|
|
124
|
+
const progress = parseDockerBuildProgress(line.trim());
|
|
125
|
+
if (progress) {
|
|
126
|
+
const now = Date.now();
|
|
127
|
+
if (now - lastProgressUpdate > 200) {
|
|
128
|
+
spinner.text = `Building image... ${progress}`;
|
|
129
|
+
lastProgressUpdate = now;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
dockerProcess.on('close', (code) => {
|
|
137
|
+
if (code === 0) {
|
|
138
|
+
spinner.succeed(`Image built: ${imageName}:${tag}`);
|
|
139
|
+
resolve();
|
|
140
|
+
} else {
|
|
141
|
+
spinner.fail('Build failed');
|
|
142
|
+
|
|
143
|
+
const errorMessage = stderrBuffer || stdoutBuffer || 'Docker build failed';
|
|
144
|
+
|
|
145
|
+
if (isDockerNotAvailableError(errorMessage)) {
|
|
146
|
+
reject(new Error('Docker is not running or not installed. Please start Docker Desktop and try again.'));
|
|
147
|
+
} else {
|
|
148
|
+
// Show last few lines of error output
|
|
149
|
+
const errorLines = errorMessage.split('\n').filter(line => line.trim());
|
|
150
|
+
const lastError = errorLines.slice(-5).join('\n');
|
|
151
|
+
reject(new Error(`Docker build failed: ${lastError}`));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
dockerProcess.on('error', (error) => {
|
|
157
|
+
spinner.fail('Build failed');
|
|
158
|
+
const errorMessage = error.message || String(error);
|
|
159
|
+
|
|
160
|
+
if (isDockerNotAvailableError(errorMessage)) {
|
|
161
|
+
reject(new Error('Docker is not running or not installed. Please start Docker Desktop and try again.'));
|
|
162
|
+
} else {
|
|
163
|
+
reject(new Error(`Docker build failed: ${errorMessage}`));
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
module.exports = {
|
|
170
|
+
executeDockerBuild,
|
|
171
|
+
isDockerNotAvailableError
|
|
172
|
+
};
|
|
173
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aifabrix/builder",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.3",
|
|
4
4
|
"description": "AI Fabrix Local Fabric & Deployment SDK",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"lint:ci": "eslint . --ext .js --format json --output-file eslint-report.json",
|
|
19
19
|
"dev": "node bin/aifabrix.js",
|
|
20
20
|
"build": "npm run lint && npm run test:ci",
|
|
21
|
+
"pack": "npm run build && npm pack",
|
|
21
22
|
"validate": "npm run build",
|
|
22
23
|
"prepublishOnly": "npm run validate",
|
|
23
24
|
"precommit": "npm run lint:fix && npm run test"
|
|
@@ -52,6 +52,53 @@ permissions:
|
|
|
52
52
|
roles: ["aifabrix-platform-admin", "aifabrix-security-admin"]
|
|
53
53
|
description: "Deactivate service users"
|
|
54
54
|
|
|
55
|
+
# User Management
|
|
56
|
+
- name: "users:create"
|
|
57
|
+
roles: ["aifabrix-platform-admin", "aifabrix-security-admin"]
|
|
58
|
+
description: "Create new users"
|
|
59
|
+
|
|
60
|
+
- name: "users:read"
|
|
61
|
+
roles: ["aifabrix-platform-admin", "aifabrix-security-admin", "aifabrix-observer"]
|
|
62
|
+
description: "View user information and profiles"
|
|
63
|
+
|
|
64
|
+
- name: "users:update"
|
|
65
|
+
roles: ["aifabrix-platform-admin", "aifabrix-security-admin"]
|
|
66
|
+
description: "Update user information and manage group memberships"
|
|
67
|
+
|
|
68
|
+
- name: "users:delete"
|
|
69
|
+
roles: ["aifabrix-platform-admin", "aifabrix-security-admin"]
|
|
70
|
+
description: "Delete users"
|
|
71
|
+
|
|
72
|
+
# Group Management
|
|
73
|
+
- name: "groups:create"
|
|
74
|
+
roles: ["aifabrix-platform-admin", "aifabrix-security-admin"]
|
|
75
|
+
description: "Create new groups"
|
|
76
|
+
|
|
77
|
+
- name: "groups:read"
|
|
78
|
+
roles: ["aifabrix-platform-admin", "aifabrix-security-admin", "aifabrix-observer"]
|
|
79
|
+
description: "View group information and members"
|
|
80
|
+
|
|
81
|
+
- name: "groups:update"
|
|
82
|
+
roles: ["aifabrix-platform-admin", "aifabrix-security-admin"]
|
|
83
|
+
description: "Update group information"
|
|
84
|
+
|
|
85
|
+
- name: "groups:delete"
|
|
86
|
+
roles: ["aifabrix-platform-admin", "aifabrix-security-admin"]
|
|
87
|
+
description: "Delete groups"
|
|
88
|
+
|
|
89
|
+
# Administrative Permissions
|
|
90
|
+
- name: "admin:read"
|
|
91
|
+
roles: ["aifabrix-platform-admin"]
|
|
92
|
+
description: "Administrative read access to all resources"
|
|
93
|
+
|
|
94
|
+
- name: "admin:write"
|
|
95
|
+
roles: ["aifabrix-platform-admin"]
|
|
96
|
+
description: "Administrative write access to all resources"
|
|
97
|
+
|
|
98
|
+
- name: "admin:delete"
|
|
99
|
+
roles: ["aifabrix-platform-admin"]
|
|
100
|
+
description: "Administrative delete access to all resources"
|
|
101
|
+
|
|
55
102
|
# Template Applications (environment = null)
|
|
56
103
|
- name: "applications:create"
|
|
57
104
|
roles: ["aifabrix-platform-admin", "aifabrix-infrastructure-admin", "aifabrix-deployment-admin"]
|
|
@@ -165,4 +212,3 @@ permissions:
|
|
|
165
212
|
- name: "dashboard:read"
|
|
166
213
|
roles: ["aifabrix-platform-admin", "aifabrix-deployment-admin", "aifabrix-developer", "aifabrix-observer"]
|
|
167
214
|
description: "View dashboard summaries and aggregates"
|
|
168
|
-
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Application Metadata
|
|
2
2
|
app:
|
|
3
|
-
key: miso
|
|
3
|
+
key: miso-controller
|
|
4
4
|
displayName: "Miso Controller"
|
|
5
5
|
description: "AI Fabrix Miso Controller - Backend API and orchestration service"
|
|
6
6
|
type: webapp
|
|
@@ -36,21 +36,21 @@ healthCheck:
|
|
|
36
36
|
authentication:
|
|
37
37
|
type: keycloak
|
|
38
38
|
enableSSO: true
|
|
39
|
-
requiredRoles:
|
|
39
|
+
requiredRoles:
|
|
40
|
+
- aifabrix-user
|
|
40
41
|
endpoints:
|
|
41
|
-
local:
|
|
42
|
+
local: http://localhost:3000/auth/callback
|
|
42
43
|
|
|
43
44
|
# Build Configuration
|
|
44
45
|
build:
|
|
45
|
-
context: ..
|
|
46
|
-
dockerfile: builder/Dockerfile
|
|
47
|
-
envOutputPath:
|
|
48
|
-
localPort: 3010
|
|
49
|
-
language: typescript
|
|
50
|
-
secrets:
|
|
46
|
+
context: .. # Docker build context (relative to builder/)
|
|
47
|
+
dockerfile: builder/miso-controller/Dockerfile # Dockerfile name (empty = use template)
|
|
48
|
+
envOutputPath: # Copy .env to repo root for local dev (relative to builder/) (if null, no .env file is copied) (if empty, .env file is copied to repo root)
|
|
49
|
+
localPort: 3010 # Port for local development (different from Docker port)
|
|
50
|
+
language: typescript # Runtime language for template selection (typescript or python)
|
|
51
|
+
secrets: # Path to secrets file
|
|
51
52
|
|
|
52
53
|
# Docker Compose
|
|
53
54
|
compose:
|
|
54
55
|
file: docker-compose.yaml
|
|
55
56
|
service: miso-controller
|
|
56
|
-
|