@aifabrix/builder 2.0.2 → 2.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -4
- package/lib/app-run.js +55 -165
- package/lib/build.js +3 -62
- package/lib/schema/env-config.yaml +9 -1
- package/lib/secrets.js +52 -194
- package/lib/utils/compose-generator.js +185 -0
- package/lib/utils/docker-build.js +173 -0
- package/lib/utils/health-check.js +26 -7
- package/lib/utils/secrets-generator.js +209 -0
- package/lib/validator.js +7 -3
- package/package.json +2 -1
- package/templates/applications/miso-controller/rbac.yaml +47 -1
- package/templates/applications/miso-controller/variables.yaml +10 -10
- package/templates/infra/compose.yaml +0 -2
- package/templates/python/docker-compose.hbs +22 -13
- package/templates/typescript/docker-compose.hbs +22 -13
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);
|
|
@@ -391,11 +281,11 @@ async function startContainer(appName, composePath, port, config = null) {
|
|
|
391
281
|
await execAsync(`docker-compose -f "${composePath}" up -d`, { env });
|
|
392
282
|
logger.log(chalk.green(`✓ Container aifabrix-${appName} started`));
|
|
393
283
|
|
|
394
|
-
// Wait for health check
|
|
284
|
+
// Wait for health check - detect actual mapped port from Docker
|
|
285
|
+
// Don't pass port so waitForHealthCheck will auto-detect it from container
|
|
395
286
|
const healthCheckPath = config?.healthCheck?.path || '/health';
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
await waitForHealthCheck(appName, 90, port, config);
|
|
287
|
+
logger.log(chalk.blue(`Waiting for application to be healthy at http://localhost:${port}${healthCheckPath}...`));
|
|
288
|
+
await waitForHealthCheck(appName, 90, null, config);
|
|
399
289
|
}
|
|
400
290
|
|
|
401
291
|
/**
|
|
@@ -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
|