@aifabrix/builder 2.0.0 → 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 +5 -3
- 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 +235 -144
- package/lib/app.js +208 -274
- package/lib/audit-logger.js +2 -0
- package/lib/build.js +177 -125
- 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/env-config.yaml +9 -1
- 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/compose-generator.js +185 -0
- package/lib/utils/docker-build.js +173 -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 +14 -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 +214 -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
package/lib/generator.js
CHANGED
|
@@ -14,6 +14,278 @@ const path = require('path');
|
|
|
14
14
|
const yaml = require('js-yaml');
|
|
15
15
|
const _secrets = require('./secrets');
|
|
16
16
|
const _keyGenerator = require('./key-generator');
|
|
17
|
+
const _validator = require('./validator');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Loads variables.yaml file
|
|
21
|
+
* @param {string} variablesPath - Path to variables.yaml
|
|
22
|
+
* @returns {Object} Parsed variables
|
|
23
|
+
* @throws {Error} If file not found or invalid YAML
|
|
24
|
+
*/
|
|
25
|
+
function loadVariables(variablesPath) {
|
|
26
|
+
if (!fs.existsSync(variablesPath)) {
|
|
27
|
+
throw new Error(`variables.yaml not found: ${variablesPath}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const variablesContent = fs.readFileSync(variablesPath, 'utf8');
|
|
31
|
+
try {
|
|
32
|
+
return { content: variablesContent, parsed: yaml.load(variablesContent) };
|
|
33
|
+
} catch (error) {
|
|
34
|
+
throw new Error(`Invalid YAML syntax in variables.yaml: ${error.message}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Loads env.template file
|
|
40
|
+
* @param {string} templatePath - Path to env.template
|
|
41
|
+
* @returns {string} Template content
|
|
42
|
+
* @throws {Error} If file not found
|
|
43
|
+
*/
|
|
44
|
+
function loadEnvTemplate(templatePath) {
|
|
45
|
+
if (!fs.existsSync(templatePath)) {
|
|
46
|
+
throw new Error(`env.template not found: ${templatePath}`);
|
|
47
|
+
}
|
|
48
|
+
return fs.readFileSync(templatePath, 'utf8');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Loads rbac.yaml file if it exists
|
|
53
|
+
* @param {string} rbacPath - Path to rbac.yaml
|
|
54
|
+
* @returns {Object|null} Parsed RBAC configuration or null
|
|
55
|
+
* @throws {Error} If file exists but has invalid YAML
|
|
56
|
+
*/
|
|
57
|
+
function loadRbac(rbacPath) {
|
|
58
|
+
if (!fs.existsSync(rbacPath)) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const rbacContent = fs.readFileSync(rbacPath, 'utf8');
|
|
63
|
+
try {
|
|
64
|
+
return yaml.load(rbacContent);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
throw new Error(`Invalid YAML syntax in rbac.yaml: ${error.message}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Filters configuration based on registry mode
|
|
72
|
+
* When registryMode is "external", only DOCKER_REGISTRY_SERVER_* variables are allowed
|
|
73
|
+
* @function filterConfigurationByRegistryMode
|
|
74
|
+
* @param {Array} configuration - Environment configuration
|
|
75
|
+
* @param {string} registryMode - Registry mode ('external' or 'internal')
|
|
76
|
+
* @returns {Array} Filtered configuration
|
|
77
|
+
*/
|
|
78
|
+
function filterConfigurationByRegistryMode(configuration, registryMode) {
|
|
79
|
+
if (registryMode !== 'external') {
|
|
80
|
+
return configuration;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const allowedDockerRegistryVars = [
|
|
84
|
+
'DOCKER_REGISTRY_SERVER_URL',
|
|
85
|
+
'DOCKER_REGISTRY_SERVER_USERNAME',
|
|
86
|
+
'DOCKER_REGISTRY_SERVER_PASSWORD'
|
|
87
|
+
];
|
|
88
|
+
return configuration.filter(config => allowedDockerRegistryVars.includes(config.name));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Builds base deployment structure
|
|
93
|
+
* @function buildBaseDeployment
|
|
94
|
+
* @param {string} appName - Application name
|
|
95
|
+
* @param {Object} variables - Variables configuration
|
|
96
|
+
* @param {Array} filteredConfiguration - Filtered environment configuration
|
|
97
|
+
* @returns {Object} Base deployment structure
|
|
98
|
+
*/
|
|
99
|
+
function buildBaseDeployment(appName, variables, filteredConfiguration) {
|
|
100
|
+
const requires = variables.requires || {};
|
|
101
|
+
return {
|
|
102
|
+
key: variables.app?.key || appName,
|
|
103
|
+
displayName: variables.app?.displayName || appName,
|
|
104
|
+
description: variables.app?.description || '',
|
|
105
|
+
type: variables.app?.type || 'webapp',
|
|
106
|
+
image: buildImageReference(variables),
|
|
107
|
+
registryMode: variables.image?.registryMode || 'external',
|
|
108
|
+
port: variables.port || 3000,
|
|
109
|
+
requiresDatabase: requires.database || false,
|
|
110
|
+
requiresRedis: requires.redis || false,
|
|
111
|
+
requiresStorage: requires.storage || false,
|
|
112
|
+
databases: requires.databases || (requires.database ? [{ name: variables.app?.key || 'app' }] : []),
|
|
113
|
+
configuration: filteredConfiguration
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Builds authentication configuration from variables or RBAC
|
|
119
|
+
* @function buildAuthenticationConfig
|
|
120
|
+
* @param {Object} variables - Variables configuration
|
|
121
|
+
* @param {Object|null} rbac - RBAC configuration
|
|
122
|
+
* @returns {Object} Authentication configuration
|
|
123
|
+
*/
|
|
124
|
+
function buildAuthenticationConfig(variables, rbac) {
|
|
125
|
+
if (variables.authentication) {
|
|
126
|
+
const auth = {
|
|
127
|
+
type: variables.authentication.type || 'azure',
|
|
128
|
+
enableSSO: variables.authentication.enableSSO !== undefined ? variables.authentication.enableSSO : true,
|
|
129
|
+
requiredRoles: variables.authentication.requiredRoles || []
|
|
130
|
+
};
|
|
131
|
+
if (variables.authentication.endpoints) {
|
|
132
|
+
auth.endpoints = variables.authentication.endpoints;
|
|
133
|
+
}
|
|
134
|
+
return auth;
|
|
135
|
+
}
|
|
136
|
+
return buildAuthentication(rbac);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Validates and transforms repository configuration
|
|
141
|
+
* @function validateRepositoryConfig
|
|
142
|
+
* @param {Object} repository - Repository configuration
|
|
143
|
+
* @returns {Object|null} Validated repository config or null
|
|
144
|
+
*/
|
|
145
|
+
function validateRepositoryConfig(repository) {
|
|
146
|
+
if (!repository || (!repository.enabled && !repository.repositoryUrl)) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (repository.repositoryUrl && repository.repositoryUrl.trim()) {
|
|
151
|
+
return {
|
|
152
|
+
enabled: repository.enabled || false,
|
|
153
|
+
repositoryUrl: repository.repositoryUrl
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (repository.enabled) {
|
|
158
|
+
return { enabled: true };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Validates and transforms build fields
|
|
166
|
+
* @function validateBuildFields
|
|
167
|
+
* @param {Object} build - Build configuration
|
|
168
|
+
* @returns {Object|null} Validated build config or null
|
|
169
|
+
*/
|
|
170
|
+
function validateBuildFields(build) {
|
|
171
|
+
if (!build) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const buildConfig = {};
|
|
176
|
+
if (build.envOutputPath) {
|
|
177
|
+
buildConfig.envOutputPath = build.envOutputPath;
|
|
178
|
+
}
|
|
179
|
+
if (build.secrets && typeof build.secrets === 'string') {
|
|
180
|
+
buildConfig.secrets = build.secrets;
|
|
181
|
+
}
|
|
182
|
+
if (build.dockerfile && build.dockerfile.trim()) {
|
|
183
|
+
buildConfig.dockerfile = build.dockerfile;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return Object.keys(buildConfig).length > 0 ? buildConfig : null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Validates and transforms deployment fields
|
|
191
|
+
* @function validateDeploymentFields
|
|
192
|
+
* @param {Object} deployment - Deployment configuration
|
|
193
|
+
* @returns {Object|null} Validated deployment config or null
|
|
194
|
+
*/
|
|
195
|
+
function validateDeploymentFields(deployment) {
|
|
196
|
+
if (!deployment) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const deploymentConfig = {};
|
|
201
|
+
if (deployment.controllerUrl && deployment.controllerUrl.trim() && deployment.controllerUrl.startsWith('https://')) {
|
|
202
|
+
deploymentConfig.controllerUrl = deployment.controllerUrl;
|
|
203
|
+
}
|
|
204
|
+
if (deployment.clientId && deployment.clientId.trim()) {
|
|
205
|
+
deploymentConfig.clientId = deployment.clientId;
|
|
206
|
+
}
|
|
207
|
+
if (deployment.clientSecret && deployment.clientSecret.trim()) {
|
|
208
|
+
deploymentConfig.clientSecret = deployment.clientSecret;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return Object.keys(deploymentConfig).length > 0 ? deploymentConfig : null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Adds optional fields to deployment manifest
|
|
216
|
+
* @function buildOptionalFields
|
|
217
|
+
* @param {Object} deployment - Deployment manifest
|
|
218
|
+
* @param {Object} variables - Variables configuration
|
|
219
|
+
* @param {Object|null} rbac - RBAC configuration
|
|
220
|
+
* @returns {Object} Deployment manifest with optional fields
|
|
221
|
+
*/
|
|
222
|
+
function buildOptionalFields(deployment, variables, rbac) {
|
|
223
|
+
if (variables.healthCheck) {
|
|
224
|
+
deployment.healthCheck = buildHealthCheck(variables);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
deployment.authentication = buildAuthenticationConfig(variables, rbac);
|
|
228
|
+
|
|
229
|
+
// Add roles and permissions (from variables.yaml or rbac.yaml)
|
|
230
|
+
// Priority: variables.yaml > rbac.yaml
|
|
231
|
+
if (variables.roles) {
|
|
232
|
+
deployment.roles = variables.roles;
|
|
233
|
+
} else if (rbac && rbac.roles) {
|
|
234
|
+
deployment.roles = rbac.roles;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (variables.permissions) {
|
|
238
|
+
deployment.permissions = variables.permissions;
|
|
239
|
+
} else if (rbac && rbac.permissions) {
|
|
240
|
+
deployment.permissions = rbac.permissions;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const repository = validateRepositoryConfig(variables.repository);
|
|
244
|
+
if (repository) {
|
|
245
|
+
deployment.repository = repository;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const build = validateBuildFields(variables.build);
|
|
249
|
+
if (build) {
|
|
250
|
+
deployment.build = build;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const deploymentConfig = validateDeploymentFields(variables.deployment);
|
|
254
|
+
if (deploymentConfig) {
|
|
255
|
+
deployment.deployment = deploymentConfig;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (variables.startupCommand) {
|
|
259
|
+
deployment.startupCommand = variables.startupCommand;
|
|
260
|
+
}
|
|
261
|
+
if (variables.runtimeVersion) {
|
|
262
|
+
deployment.runtimeVersion = variables.runtimeVersion;
|
|
263
|
+
}
|
|
264
|
+
if (variables.scaling) {
|
|
265
|
+
deployment.scaling = variables.scaling;
|
|
266
|
+
}
|
|
267
|
+
if (variables.frontDoorRouting) {
|
|
268
|
+
deployment.frontDoorRouting = variables.frontDoorRouting;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return deployment;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Builds deployment manifest structure
|
|
276
|
+
* @param {string} appName - Application name
|
|
277
|
+
* @param {Object} variables - Variables configuration
|
|
278
|
+
* @param {string} deploymentKey - Deployment key
|
|
279
|
+
* @param {Array} configuration - Environment configuration
|
|
280
|
+
* @param {Object|null} rbac - RBAC configuration
|
|
281
|
+
* @returns {Object} Deployment manifest
|
|
282
|
+
*/
|
|
283
|
+
function buildManifestStructure(appName, variables, deploymentKey, configuration, rbac) {
|
|
284
|
+
const registryMode = variables.image?.registryMode || 'external';
|
|
285
|
+
const filteredConfiguration = filterConfigurationByRegistryMode(configuration, registryMode);
|
|
286
|
+
const deployment = buildBaseDeployment(appName, variables, filteredConfiguration);
|
|
287
|
+
return buildOptionalFields(deployment, variables, rbac);
|
|
288
|
+
}
|
|
17
289
|
|
|
18
290
|
/**
|
|
19
291
|
* Generates deployment JSON from application configuration files
|
|
@@ -40,36 +312,10 @@ async function generateDeployJson(appName) {
|
|
|
40
312
|
const rbacPath = path.join(builderPath, 'rbac.yaml');
|
|
41
313
|
const jsonPath = path.join(builderPath, 'aifabrix-deploy.json');
|
|
42
314
|
|
|
43
|
-
// Load
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const variablesContent = fs.readFileSync(variablesPath, 'utf8');
|
|
49
|
-
let variables;
|
|
50
|
-
try {
|
|
51
|
-
variables = yaml.load(variablesContent);
|
|
52
|
-
} catch (error) {
|
|
53
|
-
throw new Error(`Invalid YAML syntax in variables.yaml: ${error.message}`);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Load env.template
|
|
57
|
-
if (!fs.existsSync(templatePath)) {
|
|
58
|
-
throw new Error(`env.template not found: ${templatePath}`);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const envTemplate = fs.readFileSync(templatePath, 'utf8');
|
|
62
|
-
|
|
63
|
-
// Load rbac.yaml (optional)
|
|
64
|
-
let rbac = null;
|
|
65
|
-
if (fs.existsSync(rbacPath)) {
|
|
66
|
-
const rbacContent = fs.readFileSync(rbacPath, 'utf8');
|
|
67
|
-
try {
|
|
68
|
-
rbac = yaml.load(rbacContent);
|
|
69
|
-
} catch (error) {
|
|
70
|
-
throw new Error(`Invalid YAML syntax in rbac.yaml: ${error.message}`);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
315
|
+
// Load configuration files
|
|
316
|
+
const { content: variablesContent, parsed: variables } = loadVariables(variablesPath);
|
|
317
|
+
const envTemplate = loadEnvTemplate(templatePath);
|
|
318
|
+
const rbac = loadRbac(rbacPath);
|
|
73
319
|
|
|
74
320
|
// Generate deployment key
|
|
75
321
|
const deploymentKey = _keyGenerator.generateDeploymentKeyFromContent(variablesContent);
|
|
@@ -78,24 +324,13 @@ async function generateDeployJson(appName) {
|
|
|
78
324
|
const configuration = parseEnvironmentVariables(envTemplate);
|
|
79
325
|
|
|
80
326
|
// Build deployment manifest
|
|
81
|
-
const deployment =
|
|
82
|
-
key: variables.app?.key || appName,
|
|
83
|
-
displayName: variables.app?.displayName || appName,
|
|
84
|
-
description: variables.app?.description || '',
|
|
85
|
-
type: variables.app?.type || 'webapp',
|
|
86
|
-
image: buildImageReference(variables),
|
|
87
|
-
port: variables.port || 3000,
|
|
88
|
-
deploymentKey,
|
|
89
|
-
configuration,
|
|
90
|
-
healthCheck: buildHealthCheck(variables),
|
|
91
|
-
requires: buildRequirements(variables),
|
|
92
|
-
authentication: buildAuthentication(rbac)
|
|
93
|
-
};
|
|
327
|
+
const deployment = buildManifestStructure(appName, variables, deploymentKey, configuration, rbac);
|
|
94
328
|
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
329
|
+
// Validate deployment JSON against schema
|
|
330
|
+
const validation = _validator.validateDeploymentJson(deployment);
|
|
331
|
+
if (!validation.valid) {
|
|
332
|
+
const errorMessages = validation.errors.join('\n');
|
|
333
|
+
throw new Error(`Generated deployment JSON does not match schema:\n${errorMessages}`);
|
|
99
334
|
}
|
|
100
335
|
|
|
101
336
|
// Write deployment JSON
|
|
@@ -105,14 +340,6 @@ async function generateDeployJson(appName) {
|
|
|
105
340
|
return jsonPath;
|
|
106
341
|
}
|
|
107
342
|
|
|
108
|
-
/**
|
|
109
|
-
* Parses environment variables from env.template
|
|
110
|
-
* Converts kv:// references to KeyVault configuration
|
|
111
|
-
*
|
|
112
|
-
* @function parseEnvironmentVariables
|
|
113
|
-
* @param {string} envTemplate - Environment template content
|
|
114
|
-
* @returns {Array} Array of configuration objects
|
|
115
|
-
*/
|
|
116
343
|
function parseEnvironmentVariables(envTemplate) {
|
|
117
344
|
const configuration = [];
|
|
118
345
|
const lines = envTemplate.split('\n');
|
|
@@ -164,14 +391,6 @@ function parseEnvironmentVariables(envTemplate) {
|
|
|
164
391
|
return configuration;
|
|
165
392
|
}
|
|
166
393
|
|
|
167
|
-
/**
|
|
168
|
-
* Builds image reference from variables configuration
|
|
169
|
-
* Handles registry, name, and tag formatting
|
|
170
|
-
*
|
|
171
|
-
* @function buildImageReference
|
|
172
|
-
* @param {Object} variables - Variables configuration
|
|
173
|
-
* @returns {string} Complete image reference
|
|
174
|
-
*/
|
|
175
394
|
function buildImageReference(variables) {
|
|
176
395
|
const imageName = variables.image?.name || variables.app?.key || 'app';
|
|
177
396
|
const registry = variables.image?.registry;
|
|
@@ -184,31 +403,29 @@ function buildImageReference(variables) {
|
|
|
184
403
|
return `${imageName}:${tag}`;
|
|
185
404
|
}
|
|
186
405
|
|
|
187
|
-
/**
|
|
188
|
-
* Builds health check configuration from variables
|
|
189
|
-
* Provides default health check settings
|
|
190
|
-
*
|
|
191
|
-
* @function buildHealthCheck
|
|
192
|
-
* @param {Object} variables - Variables configuration
|
|
193
|
-
* @returns {Object} Health check configuration
|
|
194
|
-
*/
|
|
195
406
|
function buildHealthCheck(variables) {
|
|
196
|
-
|
|
407
|
+
const healthCheck = {
|
|
197
408
|
path: variables.healthCheck?.path || '/health',
|
|
198
|
-
interval: variables.healthCheck?.interval || 30
|
|
199
|
-
timeout: variables.healthCheck?.timeout || 10,
|
|
200
|
-
retries: variables.healthCheck?.retries || 3
|
|
409
|
+
interval: variables.healthCheck?.interval || 30
|
|
201
410
|
};
|
|
411
|
+
|
|
412
|
+
// Add optional probe fields if present
|
|
413
|
+
if (variables.healthCheck?.probePath) {
|
|
414
|
+
healthCheck.probePath = variables.healthCheck.probePath;
|
|
415
|
+
}
|
|
416
|
+
if (variables.healthCheck?.probeRequestType) {
|
|
417
|
+
healthCheck.probeRequestType = variables.healthCheck.probeRequestType;
|
|
418
|
+
}
|
|
419
|
+
if (variables.healthCheck?.probeProtocol) {
|
|
420
|
+
healthCheck.probeProtocol = variables.healthCheck.probeProtocol;
|
|
421
|
+
}
|
|
422
|
+
if (variables.healthCheck?.probeIntervalInSeconds) {
|
|
423
|
+
healthCheck.probeIntervalInSeconds = variables.healthCheck.probeIntervalInSeconds;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return healthCheck;
|
|
202
427
|
}
|
|
203
428
|
|
|
204
|
-
/**
|
|
205
|
-
* Builds requirements configuration from variables
|
|
206
|
-
* Maps database, Redis, and storage requirements
|
|
207
|
-
*
|
|
208
|
-
* @function buildRequirements
|
|
209
|
-
* @param {Object} variables - Variables configuration
|
|
210
|
-
* @returns {Object} Requirements configuration
|
|
211
|
-
*/
|
|
212
429
|
function buildRequirements(variables) {
|
|
213
430
|
const requires = variables.requires || {};
|
|
214
431
|
|
|
@@ -221,126 +438,27 @@ function buildRequirements(variables) {
|
|
|
221
438
|
};
|
|
222
439
|
}
|
|
223
440
|
|
|
224
|
-
/**
|
|
225
|
-
* Builds authentication configuration from RBAC
|
|
226
|
-
* Configures authentication settings for the application
|
|
227
|
-
*
|
|
228
|
-
* @function buildAuthentication
|
|
229
|
-
* @param {Object} rbac - RBAC configuration (optional)
|
|
230
|
-
* @returns {Object} Authentication configuration
|
|
231
|
-
*/
|
|
232
441
|
function buildAuthentication(rbac) {
|
|
233
442
|
if (!rbac) {
|
|
234
443
|
return {
|
|
235
|
-
|
|
236
|
-
|
|
444
|
+
type: 'none',
|
|
445
|
+
enableSSO: false,
|
|
446
|
+
requiredRoles: []
|
|
237
447
|
};
|
|
238
448
|
}
|
|
239
449
|
|
|
240
450
|
return {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
requiredRoles: rbac.roles?.map(role => role.value) || [],
|
|
245
|
-
permissions: rbac.permissions?.map(perm => perm.name) || []
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Validates deployment JSON before writing
|
|
251
|
-
* Ensures all required fields are present and valid
|
|
252
|
-
*
|
|
253
|
-
* @function validateDeploymentJson
|
|
254
|
-
* @param {Object} deployment - Deployment configuration object
|
|
255
|
-
* @returns {Object} Validation result
|
|
256
|
-
*/
|
|
257
|
-
function validateDeploymentJson(deployment) {
|
|
258
|
-
const errors = [];
|
|
259
|
-
const warnings = [];
|
|
260
|
-
|
|
261
|
-
// Required fields validation
|
|
262
|
-
if (!deployment.key) {
|
|
263
|
-
errors.push('Missing required field: key');
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
if (!deployment.displayName) {
|
|
267
|
-
errors.push('Missing required field: displayName');
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
if (!deployment.image) {
|
|
271
|
-
errors.push('Missing required field: image');
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
if (!deployment.port || deployment.port < 1 || deployment.port > 65535) {
|
|
275
|
-
errors.push('Invalid port: must be between 1 and 65535');
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
if (!deployment.deploymentKey) {
|
|
279
|
-
errors.push('Missing required field: deploymentKey');
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// Validate deployment key format
|
|
283
|
-
if (deployment.deploymentKey && !_keyGenerator.validateDeploymentKey(deployment.deploymentKey)) {
|
|
284
|
-
errors.push('Invalid deployment key format');
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// Configuration validation
|
|
288
|
-
if (!deployment.configuration || !Array.isArray(deployment.configuration)) {
|
|
289
|
-
errors.push('Missing or invalid configuration array');
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Health check validation
|
|
293
|
-
if (deployment.healthCheck) {
|
|
294
|
-
if (deployment.healthCheck.path && !deployment.healthCheck.path.startsWith('/')) {
|
|
295
|
-
warnings.push('Health check path should start with /');
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
if (deployment.healthCheck.interval && (deployment.healthCheck.interval < 5 || deployment.healthCheck.interval > 300)) {
|
|
299
|
-
warnings.push('Health check interval should be between 5 and 300 seconds');
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// Authentication validation
|
|
304
|
-
if (deployment.authentication?.enabled) {
|
|
305
|
-
if (!deployment.roles || deployment.roles.length === 0) {
|
|
306
|
-
warnings.push('Authentication enabled but no roles defined');
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
if (!deployment.permissions || deployment.permissions.length === 0) {
|
|
310
|
-
warnings.push('Authentication enabled but no permissions defined');
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
return {
|
|
315
|
-
valid: errors.length === 0,
|
|
316
|
-
errors,
|
|
317
|
-
warnings
|
|
451
|
+
type: 'azure', // Default to azure (enum: azure, local, none)
|
|
452
|
+
enableSSO: true,
|
|
453
|
+
requiredRoles: rbac.roles?.map(role => role.value) || []
|
|
318
454
|
};
|
|
319
455
|
}
|
|
320
456
|
|
|
321
|
-
/**
|
|
322
|
-
* Generates deployment JSON with validation
|
|
323
|
-
* Validates configuration before writing the file
|
|
324
|
-
*
|
|
325
|
-
* @async
|
|
326
|
-
* @function generateDeployJsonWithValidation
|
|
327
|
-
* @param {string} appName - Name of the application
|
|
328
|
-
* @returns {Promise<Object>} Generation result with validation info
|
|
329
|
-
* @throws {Error} If generation fails
|
|
330
|
-
*
|
|
331
|
-
* @example
|
|
332
|
-
* const result = await generateDeployJsonWithValidation('myapp');
|
|
333
|
-
* // Returns: { success: true, path: './builder/myapp/aifabrix-deploy.json', validation: {...} }
|
|
334
|
-
*/
|
|
335
457
|
async function generateDeployJsonWithValidation(appName) {
|
|
336
458
|
const jsonPath = await generateDeployJson(appName);
|
|
337
|
-
|
|
338
|
-
// Read back the generated file for validation
|
|
339
459
|
const jsonContent = fs.readFileSync(jsonPath, 'utf8');
|
|
340
460
|
const deployment = JSON.parse(jsonContent);
|
|
341
|
-
|
|
342
|
-
const validation = validateDeploymentJson(deployment);
|
|
343
|
-
|
|
461
|
+
const validation = _validator.validateDeploymentJson(deployment);
|
|
344
462
|
return {
|
|
345
463
|
success: validation.valid,
|
|
346
464
|
path: jsonPath,
|
|
@@ -356,6 +474,5 @@ module.exports = {
|
|
|
356
474
|
buildImageReference,
|
|
357
475
|
buildHealthCheck,
|
|
358
476
|
buildRequirements,
|
|
359
|
-
buildAuthentication
|
|
360
|
-
validateDeploymentJson
|
|
477
|
+
buildAuthentication
|
|
361
478
|
};
|
package/lib/github-generator.js
CHANGED
|
@@ -6,9 +6,42 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
const fs = require('fs').promises;
|
|
9
|
+
const fsSync = require('fs');
|
|
9
10
|
const path = require('path');
|
|
10
11
|
const Handlebars = require('handlebars');
|
|
11
12
|
|
|
13
|
+
// Register Handlebars helper for checking array contains
|
|
14
|
+
Handlebars.registerHelper('contains', (array, value) => {
|
|
15
|
+
return array && array.includes(value);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Load extra workflow step templates from files
|
|
20
|
+
* @param {string[]} stepNames - Array of step names to load
|
|
21
|
+
* @returns {Promise<Object>} Object mapping step names to their compiled content
|
|
22
|
+
*/
|
|
23
|
+
async function loadStepTemplates(stepNames = []) {
|
|
24
|
+
const stepsDir = path.join(__dirname, '..', 'templates', 'github', 'steps');
|
|
25
|
+
const stepTemplates = {};
|
|
26
|
+
|
|
27
|
+
for (const stepName of stepNames) {
|
|
28
|
+
const stepPath = path.join(stepsDir, `${stepName}.hbs`);
|
|
29
|
+
|
|
30
|
+
if (fsSync.existsSync(stepPath)) {
|
|
31
|
+
try {
|
|
32
|
+
const stepContent = await fs.readFile(stepPath, 'utf8');
|
|
33
|
+
stepTemplates[stepName] = Handlebars.compile(stepContent);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
throw new Error(`Failed to load step template '${stepName}': ${error.message}`);
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
throw new Error(`Step template '${stepName}' not found at ${stepPath}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return stepTemplates;
|
|
43
|
+
}
|
|
44
|
+
|
|
12
45
|
/**
|
|
13
46
|
* Generate GitHub Actions workflow files from templates
|
|
14
47
|
* @param {string} appPath - Path to application directory
|
|
@@ -22,8 +55,13 @@ async function generateGithubWorkflows(appPath, config, options = {}) {
|
|
|
22
55
|
const workflowsDir = path.join(appPath, '.github', 'workflows');
|
|
23
56
|
await fs.mkdir(workflowsDir, { recursive: true });
|
|
24
57
|
|
|
58
|
+
// Load step templates if githubSteps are provided
|
|
59
|
+
const stepTemplates = options.githubSteps && options.githubSteps.length > 0
|
|
60
|
+
? await loadStepTemplates(options.githubSteps)
|
|
61
|
+
: {};
|
|
62
|
+
|
|
25
63
|
// Get template context
|
|
26
|
-
const templateContext = getTemplateContext(config, options);
|
|
64
|
+
const templateContext = await getTemplateContext(config, options, stepTemplates);
|
|
27
65
|
|
|
28
66
|
// Load and compile templates
|
|
29
67
|
const templatesDir = path.join(__dirname, '..', 'templates', 'github');
|
|
@@ -62,24 +100,40 @@ async function generateGithubWorkflows(appPath, config, options = {}) {
|
|
|
62
100
|
* Get template context from config
|
|
63
101
|
* @param {Object} config - Application configuration
|
|
64
102
|
* @param {Object} options - Additional options
|
|
65
|
-
* @
|
|
103
|
+
* @param {Object} stepTemplates - Compiled step templates
|
|
104
|
+
* @returns {Promise<Object>} Template context
|
|
66
105
|
*/
|
|
67
|
-
function getTemplateContext(config, options = {}) {
|
|
68
|
-
|
|
106
|
+
async function getTemplateContext(config, options = {}, stepTemplates = {}) {
|
|
107
|
+
const githubSteps = options.githubSteps || [];
|
|
108
|
+
|
|
109
|
+
// Render step templates with the base context
|
|
110
|
+
const renderedSteps = {};
|
|
111
|
+
const baseContext = {
|
|
69
112
|
appName: config.appName || 'myapp',
|
|
70
113
|
mainBranch: options.mainBranch || 'main',
|
|
71
114
|
language: config.language || 'typescript',
|
|
72
115
|
fileExtension: config.language === 'python' ? 'py' : 'js',
|
|
73
116
|
sourceDir: config.language === 'python' ? 'src' : 'lib',
|
|
74
117
|
buildCommand: options.buildCommand || 'npm run build',
|
|
75
|
-
uploadCoverage: options.uploadCoverage !== false,
|
|
76
|
-
publishToNpm: options.publishToNpm || false,
|
|
77
118
|
port: config.port || 3000,
|
|
78
119
|
database: config.database || false,
|
|
79
120
|
redis: config.redis || false,
|
|
80
121
|
storage: config.storage || false,
|
|
81
122
|
authentication: config.authentication || false
|
|
82
123
|
};
|
|
124
|
+
|
|
125
|
+
for (const [stepName, template] of Object.entries(stepTemplates)) {
|
|
126
|
+
renderedSteps[stepName] = template(baseContext);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
...baseContext,
|
|
131
|
+
uploadCoverage: options.uploadCoverage !== false,
|
|
132
|
+
githubSteps: githubSteps,
|
|
133
|
+
stepContent: renderedSteps,
|
|
134
|
+
hasSteps: githubSteps.length > 0,
|
|
135
|
+
hasNpmStep: githubSteps.includes('npm')
|
|
136
|
+
};
|
|
83
137
|
}
|
|
84
138
|
|
|
85
139
|
/**
|
|
@@ -103,8 +157,13 @@ async function generateWorkflowFile(appPath, templateName, config, options = {})
|
|
|
103
157
|
// Compile template
|
|
104
158
|
const template = Handlebars.compile(templateContent);
|
|
105
159
|
|
|
160
|
+
// Load step templates if githubSteps are provided
|
|
161
|
+
const stepTemplates = options.githubSteps && options.githubSteps.length > 0
|
|
162
|
+
? await loadStepTemplates(options.githubSteps)
|
|
163
|
+
: {};
|
|
164
|
+
|
|
106
165
|
// Get template context
|
|
107
|
-
const templateContext = getTemplateContext(config, options);
|
|
166
|
+
const templateContext = await getTemplateContext(config, options, stepTemplates);
|
|
108
167
|
|
|
109
168
|
// Generate content
|
|
110
169
|
const generatedContent = template(templateContext);
|
|
@@ -214,6 +273,7 @@ async function generateWorkflowsWithValidation(appPath, config, options = {}) {
|
|
|
214
273
|
module.exports = {
|
|
215
274
|
generateGithubWorkflows,
|
|
216
275
|
getTemplateContext,
|
|
276
|
+
loadStepTemplates,
|
|
217
277
|
generateWorkflowFile,
|
|
218
278
|
validateWorkflowConfig,
|
|
219
279
|
generateWorkflowsWithValidation
|