@aifabrix/builder 2.0.0 → 2.0.2
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 +6 -2
- package/bin/aifabrix.js +9 -3
- package/jest.config.integration.js +30 -0
- package/lib/app-config.js +157 -0
- package/lib/app-deploy.js +233 -82
- package/lib/app-dockerfile.js +112 -0
- package/lib/app-prompts.js +244 -0
- package/lib/app-push.js +172 -0
- package/lib/app-run.js +334 -133
- package/lib/app.js +208 -274
- package/lib/audit-logger.js +2 -0
- package/lib/build.js +209 -98
- package/lib/cli.js +76 -86
- package/lib/commands/app.js +414 -0
- package/lib/commands/login.js +304 -0
- package/lib/config.js +78 -0
- package/lib/deployer.js +225 -81
- package/lib/env-reader.js +45 -30
- package/lib/generator.js +308 -191
- package/lib/github-generator.js +67 -7
- package/lib/infra.js +156 -61
- package/lib/push.js +105 -10
- package/lib/schema/application-schema.json +30 -2
- package/lib/schema/infrastructure-schema.json +589 -0
- package/lib/secrets.js +229 -24
- package/lib/template-validator.js +205 -0
- package/lib/templates.js +305 -170
- package/lib/utils/api.js +329 -0
- package/lib/utils/cli-utils.js +97 -0
- package/lib/utils/dockerfile-utils.js +131 -0
- package/lib/utils/environment-checker.js +125 -0
- package/lib/utils/error-formatter.js +61 -0
- package/lib/utils/health-check.js +187 -0
- package/lib/utils/logger.js +53 -0
- package/lib/utils/template-helpers.js +223 -0
- package/lib/utils/variable-transformer.js +271 -0
- package/lib/validator.js +27 -112
- package/package.json +13 -10
- package/templates/README.md +75 -3
- package/templates/applications/keycloak/Dockerfile +36 -0
- package/templates/applications/keycloak/env.template +32 -0
- package/templates/applications/keycloak/rbac.yaml +37 -0
- package/templates/applications/keycloak/variables.yaml +56 -0
- package/templates/applications/miso-controller/Dockerfile +125 -0
- package/templates/applications/miso-controller/env.template +129 -0
- package/templates/applications/miso-controller/rbac.yaml +168 -0
- package/templates/applications/miso-controller/variables.yaml +56 -0
- package/templates/github/release.yaml.hbs +5 -26
- package/templates/github/steps/npm.hbs +24 -0
- package/templates/infra/compose.yaml +6 -6
- package/templates/python/docker-compose.hbs +19 -12
- package/templates/python/main.py +80 -0
- package/templates/python/requirements.txt +4 -0
- package/templates/typescript/Dockerfile.hbs +2 -2
- package/templates/typescript/docker-compose.hbs +19 -12
- package/templates/typescript/index.ts +116 -0
- package/templates/typescript/package.json +26 -0
- package/templates/typescript/tsconfig.json +24 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Variable Transformation Utilities
|
|
3
|
+
*
|
|
4
|
+
* Transforms nested variables.yaml structure to flat schema format
|
|
5
|
+
* Converts app.*, image.*, requires.* to schema-compatible structure
|
|
6
|
+
*
|
|
7
|
+
* @fileoverview Variable transformation utilities for AI Fabrix Builder
|
|
8
|
+
* @author AI Fabrix Team
|
|
9
|
+
* @version 2.0.0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Sanitizes authentication type - map keycloak to azure (schema allows: azure, local, none)
|
|
14
|
+
* @function sanitizeAuthType
|
|
15
|
+
* @param {string} authType - Authentication type
|
|
16
|
+
* @returns {string} Sanitized authentication type
|
|
17
|
+
*/
|
|
18
|
+
function sanitizeAuthType(authType) {
|
|
19
|
+
if (authType === 'keycloak') {
|
|
20
|
+
return 'azure';
|
|
21
|
+
}
|
|
22
|
+
if (authType && !['azure', 'local', 'none'].includes(authType)) {
|
|
23
|
+
return 'azure'; // Default to azure if invalid type
|
|
24
|
+
}
|
|
25
|
+
return authType;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Transforms flat structure to schema-compatible format
|
|
30
|
+
* @function transformFlatStructure
|
|
31
|
+
* @param {Object} variables - Raw variables from YAML
|
|
32
|
+
* @param {string} appName - Application name (fallback)
|
|
33
|
+
* @returns {Object} Transformed variables matching schema
|
|
34
|
+
*/
|
|
35
|
+
function transformFlatStructure(variables, appName) {
|
|
36
|
+
const result = {
|
|
37
|
+
key: variables.key || appName,
|
|
38
|
+
displayName: variables.displayName || appName,
|
|
39
|
+
description: variables.description || '',
|
|
40
|
+
type: variables.type || 'webapp',
|
|
41
|
+
image: variables.image,
|
|
42
|
+
registryMode: variables.registryMode || 'external',
|
|
43
|
+
port: variables.port || 3000,
|
|
44
|
+
requiresDatabase: variables.requiresDatabase || false,
|
|
45
|
+
requiresRedis: variables.requiresRedis || false,
|
|
46
|
+
requiresStorage: variables.requiresStorage || false,
|
|
47
|
+
databases: variables.databases || [],
|
|
48
|
+
...variables
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Sanitize authentication if present
|
|
52
|
+
if (result.authentication && result.authentication.type) {
|
|
53
|
+
result.authentication = {
|
|
54
|
+
...result.authentication,
|
|
55
|
+
type: sanitizeAuthType(result.authentication.type)
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Builds image reference string from variables
|
|
64
|
+
* @function buildImageReference
|
|
65
|
+
* @param {Object} variables - Variables configuration
|
|
66
|
+
* @param {string} appName - Application name (fallback)
|
|
67
|
+
* @returns {string} Image reference string
|
|
68
|
+
*/
|
|
69
|
+
function buildImageReference(variables, appName) {
|
|
70
|
+
const imageName = variables.image?.name || variables.app?.key || appName;
|
|
71
|
+
const registry = variables.image?.registry;
|
|
72
|
+
const tag = variables.image?.tag || 'latest';
|
|
73
|
+
return registry ? `${registry}/${imageName}:${tag}` : `${imageName}:${tag}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Validates repository URL format
|
|
78
|
+
* @function validateRepositoryUrl
|
|
79
|
+
* @param {string} url - Repository URL
|
|
80
|
+
* @returns {boolean} True if valid
|
|
81
|
+
*/
|
|
82
|
+
function validateRepositoryUrl(url) {
|
|
83
|
+
if (!url || url.trim() === '') {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
const repoPattern = /^(https:\/\/github\.com\/[^/]+\/[^/]+|https:\/\/gitlab\.com\/[^/]+\/[^/]+|https:\/\/dev\.azure\.com\/[^/]+\/[^/]+\/[^/]+)$/;
|
|
87
|
+
return repoPattern.test(url);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Validates and transforms repository configuration
|
|
92
|
+
* @function validateRepositoryConfig
|
|
93
|
+
* @param {Object} repository - Repository configuration
|
|
94
|
+
* @returns {Object|null} Validated repository config or null
|
|
95
|
+
*/
|
|
96
|
+
function validateRepositoryConfig(repository) {
|
|
97
|
+
if (!repository) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const repo = { enabled: repository.enabled || false };
|
|
102
|
+
if (validateRepositoryUrl(repository.repositoryUrl)) {
|
|
103
|
+
repo.repositoryUrl = repository.repositoryUrl;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (repo.enabled || repo.repositoryUrl) {
|
|
107
|
+
return repo;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Validates and transforms build configuration
|
|
115
|
+
* @function validateBuildConfig
|
|
116
|
+
* @param {Object} build - Build configuration
|
|
117
|
+
* @returns {Object|null} Validated build config or null
|
|
118
|
+
*/
|
|
119
|
+
function validateBuildConfig(build) {
|
|
120
|
+
if (!build) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const buildConfig = {};
|
|
125
|
+
if (build.envOutputPath) {
|
|
126
|
+
buildConfig.envOutputPath = build.envOutputPath;
|
|
127
|
+
}
|
|
128
|
+
if (build.secrets !== null && build.secrets !== undefined && build.secrets !== '') {
|
|
129
|
+
buildConfig.secrets = build.secrets;
|
|
130
|
+
}
|
|
131
|
+
if (build.localPort) {
|
|
132
|
+
buildConfig.localPort = build.localPort;
|
|
133
|
+
}
|
|
134
|
+
if (build.language) {
|
|
135
|
+
buildConfig.language = build.language;
|
|
136
|
+
}
|
|
137
|
+
if (build.context) {
|
|
138
|
+
buildConfig.context = build.context;
|
|
139
|
+
}
|
|
140
|
+
if (build.dockerfile && build.dockerfile.trim() !== '') {
|
|
141
|
+
buildConfig.dockerfile = build.dockerfile;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return Object.keys(buildConfig).length > 0 ? buildConfig : null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Validates and transforms deployment configuration
|
|
149
|
+
* @function validateDeploymentConfig
|
|
150
|
+
* @param {Object} deployment - Deployment configuration
|
|
151
|
+
* @returns {Object|null} Validated deployment config or null
|
|
152
|
+
*/
|
|
153
|
+
function validateDeploymentConfig(deployment) {
|
|
154
|
+
if (!deployment) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const deploymentConfig = {};
|
|
159
|
+
if (deployment.controllerUrl && deployment.controllerUrl.trim() !== '' && /^https:\/\/.*$/.test(deployment.controllerUrl)) {
|
|
160
|
+
deploymentConfig.controllerUrl = deployment.controllerUrl;
|
|
161
|
+
}
|
|
162
|
+
if (deployment.clientId && deployment.clientId.trim() !== '' && /^[a-z0-9-]+$/.test(deployment.clientId)) {
|
|
163
|
+
deploymentConfig.clientId = deployment.clientId;
|
|
164
|
+
}
|
|
165
|
+
if (deployment.clientSecret && deployment.clientSecret.trim() !== '' && /^(kv:\/\/.*|.+)$/.test(deployment.clientSecret)) {
|
|
166
|
+
deploymentConfig.clientSecret = deployment.clientSecret;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return Object.keys(deploymentConfig).length > 0 ? deploymentConfig : null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Transforms optional fields from variables
|
|
174
|
+
* @function transformOptionalFields
|
|
175
|
+
* @param {Object} variables - Raw variables from YAML
|
|
176
|
+
* @param {Object} transformed - Base transformed object
|
|
177
|
+
* @returns {Object} Transformed object with optional fields added
|
|
178
|
+
*/
|
|
179
|
+
function transformOptionalFields(variables, transformed) {
|
|
180
|
+
if (variables.healthCheck) {
|
|
181
|
+
transformed.healthCheck = variables.healthCheck;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (variables.authentication) {
|
|
185
|
+
transformed.authentication = {
|
|
186
|
+
...variables.authentication,
|
|
187
|
+
type: sanitizeAuthType(variables.authentication.type)
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const repository = validateRepositoryConfig(variables.repository);
|
|
192
|
+
if (repository) {
|
|
193
|
+
transformed.repository = repository;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const build = validateBuildConfig(variables.build);
|
|
197
|
+
if (build) {
|
|
198
|
+
transformed.build = build;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const deployment = validateDeploymentConfig(variables.deployment);
|
|
202
|
+
if (deployment) {
|
|
203
|
+
transformed.deployment = deployment;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (variables.startupCommand) {
|
|
207
|
+
transformed.startupCommand = variables.startupCommand;
|
|
208
|
+
}
|
|
209
|
+
if (variables.runtimeVersion) {
|
|
210
|
+
transformed.runtimeVersion = variables.runtimeVersion;
|
|
211
|
+
}
|
|
212
|
+
if (variables.scaling) {
|
|
213
|
+
transformed.scaling = variables.scaling;
|
|
214
|
+
}
|
|
215
|
+
if (variables.frontDoorRouting) {
|
|
216
|
+
transformed.frontDoorRouting = variables.frontDoorRouting;
|
|
217
|
+
}
|
|
218
|
+
if (variables.roles) {
|
|
219
|
+
transformed.roles = variables.roles;
|
|
220
|
+
}
|
|
221
|
+
if (variables.permissions) {
|
|
222
|
+
transformed.permissions = variables.permissions;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return transformed;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Transforms nested variables.yaml structure to flat schema format
|
|
230
|
+
* Converts app.*, image.*, requires.* to schema-compatible structure
|
|
231
|
+
* Handles both flat and nested structures
|
|
232
|
+
*
|
|
233
|
+
* @function transformVariablesForValidation
|
|
234
|
+
* @param {Object} variables - Raw variables from YAML
|
|
235
|
+
* @param {string} appName - Application name (fallback)
|
|
236
|
+
* @returns {Object} Transformed variables matching schema
|
|
237
|
+
*/
|
|
238
|
+
function transformVariablesForValidation(variables, appName) {
|
|
239
|
+
// Check if structure is already flat (has key, displayName, image as string)
|
|
240
|
+
const isFlat = variables.key && variables.image && typeof variables.image === 'string';
|
|
241
|
+
|
|
242
|
+
if (isFlat) {
|
|
243
|
+
return transformFlatStructure(variables, appName);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Nested structure - transform it
|
|
247
|
+
const requires = variables.requires || {};
|
|
248
|
+
const imageRef = buildImageReference(variables, appName);
|
|
249
|
+
|
|
250
|
+
// Transform to flat schema structure
|
|
251
|
+
const transformed = {
|
|
252
|
+
key: variables.app?.key || appName,
|
|
253
|
+
displayName: variables.app?.displayName || appName,
|
|
254
|
+
description: variables.app?.description || '',
|
|
255
|
+
type: variables.app?.type || 'webapp',
|
|
256
|
+
image: imageRef,
|
|
257
|
+
registryMode: variables.image?.registryMode || 'external',
|
|
258
|
+
port: variables.port || 3000,
|
|
259
|
+
requiresDatabase: requires.database || false,
|
|
260
|
+
requiresRedis: requires.redis || false,
|
|
261
|
+
requiresStorage: requires.storage || false,
|
|
262
|
+
databases: requires.databases || (requires.database ? [{ name: variables.app?.key || appName }] : [])
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
return transformOptionalFields(variables, transformed);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
module.exports = {
|
|
269
|
+
transformVariablesForValidation
|
|
270
|
+
};
|
|
271
|
+
|
package/lib/validator.js
CHANGED
|
@@ -14,6 +14,9 @@ const path = require('path');
|
|
|
14
14
|
const yaml = require('js-yaml');
|
|
15
15
|
const Ajv = require('ajv');
|
|
16
16
|
const applicationSchema = require('./schema/application-schema.json');
|
|
17
|
+
const { transformVariablesForValidation } = require('./utils/variable-transformer');
|
|
18
|
+
const { checkEnvironment } = require('./utils/environment-checker');
|
|
19
|
+
const { formatValidationErrors } = require('./utils/error-formatter');
|
|
17
20
|
|
|
18
21
|
/**
|
|
19
22
|
* Validates variables.yaml file against application schema
|
|
@@ -49,9 +52,12 @@ async function validateVariables(appName) {
|
|
|
49
52
|
throw new Error(`Invalid YAML syntax in variables.yaml: ${error.message}`);
|
|
50
53
|
}
|
|
51
54
|
|
|
55
|
+
// Transform nested structure to flat schema format
|
|
56
|
+
const transformed = transformVariablesForValidation(variables, appName);
|
|
57
|
+
|
|
52
58
|
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
53
59
|
const validate = ajv.compile(applicationSchema);
|
|
54
|
-
const valid = validate(
|
|
60
|
+
const valid = validate(transformed);
|
|
55
61
|
|
|
56
62
|
return {
|
|
57
63
|
valid,
|
|
@@ -209,125 +215,33 @@ async function validateEnvTemplate(appName) {
|
|
|
209
215
|
}
|
|
210
216
|
|
|
211
217
|
/**
|
|
212
|
-
*
|
|
213
|
-
*
|
|
218
|
+
* Validates deployment JSON against application schema
|
|
219
|
+
* Ensures generated aifabrix-deploy.json matches the schema structure
|
|
214
220
|
*
|
|
215
|
-
* @
|
|
216
|
-
* @
|
|
217
|
-
* @returns {
|
|
218
|
-
* @throws {Error} If critical issues are found
|
|
221
|
+
* @function validateDeploymentJson
|
|
222
|
+
* @param {Object} deployment - Deployment JSON object to validate
|
|
223
|
+
* @returns {Object} Validation result with errors
|
|
219
224
|
*
|
|
220
225
|
* @example
|
|
221
|
-
* const result =
|
|
222
|
-
* // Returns: {
|
|
226
|
+
* const result = validateDeploymentJson(deployment);
|
|
227
|
+
* // Returns: { valid: true, errors: [] }
|
|
223
228
|
*/
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
await execAsync('docker --version');
|
|
231
|
-
await execAsync('docker-compose --version');
|
|
232
|
-
return 'ok';
|
|
233
|
-
} catch (error) {
|
|
234
|
-
return 'error';
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
async function checkPorts() {
|
|
239
|
-
const requiredPorts = [5432, 6379, 5050, 8081];
|
|
240
|
-
const netstat = require('net');
|
|
241
|
-
let portIssues = 0;
|
|
242
|
-
|
|
243
|
-
for (const port of requiredPorts) {
|
|
244
|
-
try {
|
|
245
|
-
await new Promise((resolve, reject) => {
|
|
246
|
-
const server = netstat.createServer();
|
|
247
|
-
server.listen(port, () => {
|
|
248
|
-
server.close(resolve);
|
|
249
|
-
});
|
|
250
|
-
server.on('error', reject);
|
|
251
|
-
});
|
|
252
|
-
} catch (error) {
|
|
253
|
-
portIssues++;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
return portIssues === 0 ? 'ok' : 'warning';
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function checkSecrets() {
|
|
261
|
-
const os = require('os');
|
|
262
|
-
const secretsPath = path.join(os.homedir(), '.aifabrix', 'secrets.yaml');
|
|
263
|
-
return fs.existsSync(secretsPath) ? 'ok' : 'missing';
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
async function checkEnvironment() {
|
|
267
|
-
const result = {
|
|
268
|
-
docker: 'unknown',
|
|
269
|
-
ports: 'unknown',
|
|
270
|
-
secrets: 'unknown',
|
|
271
|
-
recommendations: []
|
|
272
|
-
};
|
|
273
|
-
|
|
274
|
-
// Check Docker
|
|
275
|
-
result.docker = await checkDocker();
|
|
276
|
-
if (result.docker === 'error') {
|
|
277
|
-
result.recommendations.push('Install Docker and Docker Compose');
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// Check ports
|
|
281
|
-
result.ports = await checkPorts();
|
|
282
|
-
if (result.ports === 'warning') {
|
|
283
|
-
result.recommendations.push('Some required ports (5432, 6379, 5050, 8081) are in use');
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// Check secrets
|
|
287
|
-
result.secrets = checkSecrets();
|
|
288
|
-
if (result.secrets === 'missing') {
|
|
289
|
-
result.recommendations.push('Create secrets file: ~/.aifabrix/secrets.yaml');
|
|
229
|
+
function validateDeploymentJson(deployment) {
|
|
230
|
+
if (!deployment || typeof deployment !== 'object') {
|
|
231
|
+
return {
|
|
232
|
+
valid: false,
|
|
233
|
+
errors: ['Deployment must be an object']
|
|
234
|
+
};
|
|
290
235
|
}
|
|
291
236
|
|
|
292
|
-
|
|
293
|
-
|
|
237
|
+
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
238
|
+
const validate = ajv.compile(applicationSchema);
|
|
239
|
+
const valid = validate(deployment);
|
|
294
240
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
*
|
|
299
|
-
* @function formatValidationErrors
|
|
300
|
-
* @param {Array} errors - Raw validation errors from Ajv
|
|
301
|
-
* @returns {Array} Formatted error messages
|
|
302
|
-
*
|
|
303
|
-
* @example
|
|
304
|
-
* const messages = formatValidationErrors(ajvErrors);
|
|
305
|
-
* // Returns: ['Port must be between 1 and 65535', 'Missing required field: displayName']
|
|
306
|
-
*/
|
|
307
|
-
function formatSingleError(error) {
|
|
308
|
-
const path = error.instancePath ? error.instancePath.slice(1) : 'root';
|
|
309
|
-
const field = path ? `Field "${path}"` : 'Configuration';
|
|
310
|
-
|
|
311
|
-
const errorMessages = {
|
|
312
|
-
required: `${field}: Missing required property "${error.params.missingProperty}"`,
|
|
313
|
-
type: `${field}: Expected ${error.params.type}, got ${typeof error.data}`,
|
|
314
|
-
minimum: `${field}: Value must be at least ${error.params.limit}`,
|
|
315
|
-
maximum: `${field}: Value must be at most ${error.params.limit}`,
|
|
316
|
-
minLength: `${field}: Must be at least ${error.params.limit} characters`,
|
|
317
|
-
maxLength: `${field}: Must be at most ${error.params.limit} characters`,
|
|
318
|
-
pattern: `${field}: Invalid format`,
|
|
319
|
-
enum: `${field}: Must be one of: ${error.params.allowedValues?.join(', ') || 'unknown'}`
|
|
241
|
+
return {
|
|
242
|
+
valid,
|
|
243
|
+
errors: valid ? [] : formatValidationErrors(validate.errors)
|
|
320
244
|
};
|
|
321
|
-
|
|
322
|
-
return errorMessages[error.keyword] || `${field}: ${error.message}`;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
function formatValidationErrors(errors) {
|
|
326
|
-
if (!Array.isArray(errors)) {
|
|
327
|
-
return ['Unknown validation error'];
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
return errors.map(formatSingleError);
|
|
331
245
|
}
|
|
332
246
|
|
|
333
247
|
/**
|
|
@@ -371,6 +285,7 @@ module.exports = {
|
|
|
371
285
|
validateVariables,
|
|
372
286
|
validateRbac,
|
|
373
287
|
validateEnvTemplate,
|
|
288
|
+
validateDeploymentJson,
|
|
374
289
|
checkEnvironment,
|
|
375
290
|
formatValidationErrors,
|
|
376
291
|
validateApplication
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aifabrix/builder",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"description": "AI Fabrix Local Fabric & Deployment SDK",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
"test": "jest --coverage",
|
|
11
11
|
"test:watch": "jest --watch",
|
|
12
12
|
"test:ci": "jest --ci --coverage --watchAll=false",
|
|
13
|
+
"test:integration": "jest --config jest.config.integration.js --runInBand",
|
|
14
|
+
"test:integration:python": "cross-env TEST_LANGUAGE=python jest --config jest.config.integration.js --runInBand",
|
|
15
|
+
"test:integration:typescript": "cross-env TEST_LANGUAGE=typescript jest --config jest.config.integration.js --runInBand",
|
|
13
16
|
"lint": "eslint . --ext .js",
|
|
14
17
|
"lint:fix": "eslint . --ext .js --fix",
|
|
15
18
|
"lint:ci": "eslint . --ext .js --format json --output-file eslint-report.json",
|
|
@@ -17,8 +20,7 @@
|
|
|
17
20
|
"build": "npm run lint && npm run test:ci",
|
|
18
21
|
"validate": "npm run build",
|
|
19
22
|
"prepublishOnly": "npm run validate",
|
|
20
|
-
"precommit": "npm run lint:fix && npm run test"
|
|
21
|
-
"posttest": "npm run lint"
|
|
23
|
+
"precommit": "npm run lint:fix && npm run test"
|
|
22
24
|
},
|
|
23
25
|
"keywords": [
|
|
24
26
|
"aifabrix",
|
|
@@ -34,19 +36,20 @@
|
|
|
34
36
|
"node": ">=18.0.0"
|
|
35
37
|
},
|
|
36
38
|
"dependencies": {
|
|
37
|
-
"commander": "^11.1.0",
|
|
38
|
-
"js-yaml": "^4.1.0",
|
|
39
39
|
"ajv": "^8.12.0",
|
|
40
|
-
"handlebars": "^4.7.8",
|
|
41
40
|
"axios": "^1.6.0",
|
|
42
41
|
"chalk": "^4.1.2",
|
|
43
|
-
"
|
|
44
|
-
"
|
|
42
|
+
"commander": "^11.1.0",
|
|
43
|
+
"handlebars": "^4.7.8",
|
|
44
|
+
"inquirer": "^8.2.5",
|
|
45
|
+
"js-yaml": "^4.1.0",
|
|
46
|
+
"ora": "^5.4.1"
|
|
45
47
|
},
|
|
46
48
|
"devDependencies": {
|
|
47
|
-
"
|
|
49
|
+
"@types/node": "^20.10.0",
|
|
50
|
+
"cross-env": "^10.1.0",
|
|
48
51
|
"eslint": "^8.55.0",
|
|
49
|
-
"
|
|
52
|
+
"jest": "^29.7.0"
|
|
50
53
|
},
|
|
51
54
|
"repository": {
|
|
52
55
|
"type": "git",
|
package/templates/README.md
CHANGED
|
@@ -1,15 +1,56 @@
|
|
|
1
1
|
# AI Fabrix Builder Templates
|
|
2
2
|
|
|
3
|
-
These are Handlebars (.hbs) template files that generate Docker files for AI Fabrix applications. They should NOT be linted as Dockerfiles since they contain template variables that will be replaced during generation.
|
|
3
|
+
These are Handlebars (.hbs) template files that generate Docker files and application configurations for AI Fabrix applications. They should NOT be linted as Dockerfiles since they contain template variables that will be replaced during generation.
|
|
4
4
|
|
|
5
|
-
## Template
|
|
5
|
+
## Template Structure
|
|
6
|
+
|
|
7
|
+
The templates directory is organized as follows:
|
|
8
|
+
|
|
9
|
+
### Application Templates (for `--template` flag)
|
|
10
|
+
|
|
11
|
+
Application templates are folder-based and located under `templates/applications/`. When you use `--template <name>`, the tool looks for `templates/applications/<name>/` and copies all files from that folder to `builder/<app>/`.
|
|
12
|
+
|
|
13
|
+
**Example:**
|
|
14
|
+
- `templates/applications/miso-controller/` - Miso Controller application template
|
|
15
|
+
- `templates/applications/keycloak/` - Keycloak application template
|
|
16
|
+
|
|
17
|
+
**Template Validation:**
|
|
18
|
+
- Template folder must exist in `templates/applications/<name>/`
|
|
19
|
+
- Template folder must contain at least one file
|
|
20
|
+
- Hidden files (starting with `.`) are skipped
|
|
21
|
+
- If a template includes a `Dockerfile`, it will be copied to `builder/<app>/Dockerfile` along with other files
|
|
22
|
+
|
|
23
|
+
### Language Templates
|
|
6
24
|
|
|
7
25
|
- `python/Dockerfile.hbs` - Python application Dockerfile template
|
|
8
26
|
- `python/docker-compose.hbs` - Python application docker-compose template
|
|
9
27
|
- `typescript/Dockerfile.hbs` - TypeScript/Node.js application Dockerfile template
|
|
10
28
|
- `typescript/docker-compose.hbs` - TypeScript/Node.js application docker-compose template
|
|
29
|
+
|
|
30
|
+
### Infrastructure Templates
|
|
31
|
+
|
|
11
32
|
- `infra/compose.yaml` - Infrastructure services docker-compose template
|
|
12
33
|
|
|
34
|
+
### GitHub Workflow Templates
|
|
35
|
+
|
|
36
|
+
- `github/ci.yaml.hbs` - Continuous Integration workflow
|
|
37
|
+
- `github/pr-checks.yaml.hbs` - Pull Request checks workflow
|
|
38
|
+
- `github/release.yaml.hbs` - Release and publish workflow
|
|
39
|
+
- `github/test.yaml.hbs` - Test workflow
|
|
40
|
+
|
|
41
|
+
### GitHub Workflow Step Templates (for `--github-steps` flag)
|
|
42
|
+
|
|
43
|
+
Extra workflow steps are located in `templates/github/steps/`. When you use `--github-steps <steps>`, the tool loads step templates from `templates/github/steps/{step}.hbs` and includes them in the generated workflows.
|
|
44
|
+
|
|
45
|
+
**Example:**
|
|
46
|
+
- `github/steps/npm.hbs` - NPM publishing step
|
|
47
|
+
- `github/steps/test.hbs` - Custom test step
|
|
48
|
+
|
|
49
|
+
**Step Templates:**
|
|
50
|
+
- Step templates are Handlebars templates that generate workflow job definitions
|
|
51
|
+
- They receive the same context as the main workflow templates
|
|
52
|
+
- Step templates are rendered and included in workflow files based on the `githubSteps` array
|
|
53
|
+
|
|
13
54
|
## Template Variables
|
|
14
55
|
|
|
15
56
|
### Application Configuration
|
|
@@ -34,7 +75,38 @@ These are Handlebars (.hbs) template files that generate Docker files for AI Fab
|
|
|
34
75
|
|
|
35
76
|
## Usage
|
|
36
77
|
|
|
37
|
-
|
|
78
|
+
### Application Templates
|
|
79
|
+
|
|
80
|
+
Use `--template <name>` when creating an application:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
aifabrix create myapp --template miso-controller --port 3000
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
This validates that `templates/applications/miso-controller/` exists and copies all files (including Dockerfile if present) from it to `builder/myapp/`.
|
|
87
|
+
|
|
88
|
+
### GitHub Workflow Steps
|
|
89
|
+
|
|
90
|
+
Use `--github-steps <steps>` when creating an application with GitHub workflows:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
aifabrix create myapp --github --github-steps npm
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
This loads step templates from:
|
|
97
|
+
- `templates/github/steps/npm.hbs`
|
|
98
|
+
|
|
99
|
+
And includes them in the generated workflow files (e.g., `release.yaml`).
|
|
100
|
+
|
|
101
|
+
**Currently available step templates:**
|
|
102
|
+
- `npm.hbs` - Adds NPM publishing job to release workflow
|
|
103
|
+
|
|
104
|
+
**Creating custom step templates:**
|
|
105
|
+
Create your own step templates in `templates/github/steps/{your-step}.hbs` and reference them in `--github-steps`.
|
|
106
|
+
|
|
107
|
+
### Language and Infrastructure Templates
|
|
108
|
+
|
|
109
|
+
These templates are processed automatically by the AI Fabrix Builder SDK based on the application schema defined in `variables.yaml`. The generated files will be valid Docker files after template processing.
|
|
38
110
|
|
|
39
111
|
## VS Code Configuration
|
|
40
112
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Keycloak Identity and Access Management with Custom Themes
|
|
2
|
+
# This Dockerfile extends Keycloak 24.0 with custom themes
|
|
3
|
+
|
|
4
|
+
FROM quay.io/keycloak/keycloak:24.0
|
|
5
|
+
|
|
6
|
+
# Set working directory
|
|
7
|
+
WORKDIR /opt/keycloak
|
|
8
|
+
|
|
9
|
+
# Copy custom themes from share directory (only if themes/ folder exists)
|
|
10
|
+
USER root
|
|
11
|
+
# Use a shell script approach: copy themes if it exists, otherwise skip
|
|
12
|
+
# First, copy everything from build context to a staging area
|
|
13
|
+
COPY . /tmp/build-context/
|
|
14
|
+
# Then conditionally copy themes if the directory exists
|
|
15
|
+
RUN if [ -d "/tmp/build-context/themes" ] && [ "$(ls -A /tmp/build-context/themes)" ]; then \
|
|
16
|
+
cp -r /tmp/build-context/themes/* /opt/keycloak/themes/ && \
|
|
17
|
+
chown -R keycloak:keycloak /opt/keycloak/themes/ && \
|
|
18
|
+
echo "Themes copied successfully"; \
|
|
19
|
+
else \
|
|
20
|
+
echo "No themes directory found, skipping theme copy"; \
|
|
21
|
+
fi && \
|
|
22
|
+
rm -rf /tmp/build-context
|
|
23
|
+
|
|
24
|
+
# Build Keycloak with health checks enabled
|
|
25
|
+
# This enables the /health, /health/live, /health/ready, and /health/started endpoints
|
|
26
|
+
RUN /opt/keycloak/bin/kc.sh build --health-enabled=true
|
|
27
|
+
|
|
28
|
+
# Switch back to keycloak user
|
|
29
|
+
USER keycloak
|
|
30
|
+
|
|
31
|
+
# Keycloak runs on port 8080 internally, exposed as 8082
|
|
32
|
+
EXPOSE 8080
|
|
33
|
+
|
|
34
|
+
# Default Keycloak command (can be overridden in docker-compose.yaml)
|
|
35
|
+
# Health checks are enabled via the build step above
|
|
36
|
+
CMD ["start-dev"]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Environment Variables Template
|
|
2
|
+
# Use kv:// references for secrets (resolved from .aifabrix/secrets.yaml)
|
|
3
|
+
# Use ${VAR} for environment-specific values
|
|
4
|
+
|
|
5
|
+
# =============================================================================
|
|
6
|
+
# APPLICATION ENVIRONMENT
|
|
7
|
+
# =============================================================================
|
|
8
|
+
|
|
9
|
+
KEYCLOAK_ADMIN=admin
|
|
10
|
+
KEYCLOAK_ADMIN_PASSWORD=kv://keycloak-admin-passwordKeyVault
|
|
11
|
+
KC_HOSTNAME_STRICT=false
|
|
12
|
+
KC_HTTP_ENABLED=true
|
|
13
|
+
|
|
14
|
+
# =============================================================================
|
|
15
|
+
# HEALTH CHECK CONFIGURATION
|
|
16
|
+
# =============================================================================
|
|
17
|
+
# Enable health check endpoints: /health, /health/live, /health/ready, /health/started
|
|
18
|
+
|
|
19
|
+
KC_HEALTH_ENABLED=true
|
|
20
|
+
|
|
21
|
+
# =============================================================================
|
|
22
|
+
# DATABASE CONFIGURATION
|
|
23
|
+
# =============================================================================
|
|
24
|
+
# Connects to postgres service in Docker network (postgres) or localhost (local)
|
|
25
|
+
|
|
26
|
+
KC_DB=postgres
|
|
27
|
+
KC_DB_URL_HOST=${DB_HOST}
|
|
28
|
+
KC_DB_URL_PORT=5432
|
|
29
|
+
KC_DB_URL_DATABASE=keycloak
|
|
30
|
+
KC_DB_USERNAME=keycloak_user
|
|
31
|
+
KC_DB_PASSWORD=kv://databases-keycloak-0-passwordKeyVault
|
|
32
|
+
|