@aifabrix/builder 2.36.2 → 2.37.5
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/.cursor/rules/project-rules.mdc +19 -0
- package/README.md +68 -104
- package/integration/hubspot/test.js +1 -1
- package/lib/api/wizard.api.js +24 -1
- package/lib/app/deploy.js +43 -7
- package/lib/app/display.js +1 -1
- package/lib/app/list.js +3 -1
- package/lib/app/run-helpers.js +1 -1
- package/lib/build/index.js +3 -4
- package/lib/cli/index.js +45 -0
- package/lib/cli/setup-app.js +230 -0
- package/lib/cli/setup-auth.js +88 -0
- package/lib/cli/setup-dev.js +101 -0
- package/lib/cli/setup-environment.js +53 -0
- package/lib/cli/setup-external-system.js +87 -0
- package/lib/cli/setup-infra.js +219 -0
- package/lib/cli/setup-secrets.js +48 -0
- package/lib/cli/setup-utility.js +202 -0
- package/lib/cli.js +7 -961
- package/lib/commands/up-common.js +31 -1
- package/lib/commands/up-miso.js +6 -2
- package/lib/commands/wizard-core.js +32 -7
- package/lib/core/config.js +10 -0
- package/lib/core/ensure-encryption-key.js +56 -0
- package/lib/deployment/deployer-status.js +101 -0
- package/lib/deployment/deployer.js +62 -110
- package/lib/deployment/environment.js +133 -34
- package/lib/external-system/deploy.js +5 -1
- package/lib/external-system/test-auth.js +14 -7
- package/lib/generator/wizard.js +37 -41
- package/lib/infrastructure/helpers.js +1 -1
- package/lib/schema/environment-deploy-request.schema.json +64 -0
- package/lib/utils/help-builder.js +5 -2
- package/lib/utils/paths.js +22 -4
- package/lib/utils/secrets-generator.js +23 -8
- package/lib/utils/secrets-helpers.js +46 -21
- package/package.json +1 -1
- package/scripts/install-local.js +11 -2
- package/templates/applications/README.md.hbs +3 -3
- package/templates/applications/dataplane/variables.yaml +0 -2
- package/templates/applications/miso-controller/variables.yaml +0 -2
- package/templates/external-system/deploy.js.hbs +69 -0
- package/templates/infra/environment-dev.json +10 -0
|
@@ -9,16 +9,21 @@
|
|
|
9
9
|
* @version 2.0.0
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
12
14
|
const chalk = require('chalk');
|
|
15
|
+
const Ajv = require('ajv');
|
|
13
16
|
const logger = require('../utils/logger');
|
|
14
17
|
const config = require('../core/config');
|
|
15
18
|
const { resolveControllerUrl } = require('../utils/controller-url');
|
|
16
19
|
const { validateControllerUrl, validateEnvironmentKey } = require('../utils/deployment-validation');
|
|
17
20
|
const { getOrRefreshDeviceToken } = require('../utils/token-manager');
|
|
18
|
-
const {
|
|
19
|
-
const { deployEnvironment: deployEnvironmentInfra } = require('../api/deployments.api');
|
|
21
|
+
const { getPipelineDeployment } = require('../api/pipeline.api');
|
|
22
|
+
const { deployEnvironment: deployEnvironmentInfra, getDeployment } = require('../api/deployments.api');
|
|
20
23
|
const { handleDeploymentErrors } = require('../utils/deployment-errors');
|
|
24
|
+
const { formatValidationErrors } = require('../utils/error-formatter');
|
|
21
25
|
const auditLogger = require('../core/audit-logger');
|
|
26
|
+
const environmentDeployRequestSchema = require('../schema/environment-deploy-request.schema.json');
|
|
22
27
|
|
|
23
28
|
/**
|
|
24
29
|
* Validates environment deployment prerequisites
|
|
@@ -78,25 +83,88 @@ async function getEnvironmentAuth(controllerUrl) {
|
|
|
78
83
|
* @throws {Error} If deployment fails
|
|
79
84
|
*/
|
|
80
85
|
/**
|
|
81
|
-
*
|
|
86
|
+
* Loads and validates environment deploy config from a JSON file
|
|
87
|
+
* @param {string} configPath - Absolute or relative path to config JSON
|
|
88
|
+
* @returns {Object} Valid deploy request { environmentConfig, dryRun? }
|
|
89
|
+
* @throws {Error} If file missing, invalid JSON, or validation fails
|
|
90
|
+
*/
|
|
91
|
+
function loadAndValidateEnvironmentDeployConfig(configPath) {
|
|
92
|
+
const resolvedPath = path.isAbsolute(configPath) ? configPath : path.resolve(process.cwd(), configPath);
|
|
93
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`Environment config file not found: ${resolvedPath}\n` +
|
|
96
|
+
'Use --config <file> with a JSON file containing "environmentConfig" (e.g. templates/infra/environment-dev.json).'
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
let raw;
|
|
100
|
+
try {
|
|
101
|
+
raw = fs.readFileSync(resolvedPath, 'utf8');
|
|
102
|
+
} catch (e) {
|
|
103
|
+
throw new Error(`Cannot read config file: ${resolvedPath}. ${e.message}`);
|
|
104
|
+
}
|
|
105
|
+
let parsed;
|
|
106
|
+
try {
|
|
107
|
+
parsed = JSON.parse(raw);
|
|
108
|
+
} catch (e) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
`Invalid JSON in config file: ${resolvedPath}\n${e.message}\n` +
|
|
111
|
+
'Expected format: { "environmentConfig": { "key", "environment", "preset", "serviceName", "location" }, "dryRun": false }'
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
if (parsed === null || typeof parsed !== 'object') {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`Config file must be a JSON object with "environmentConfig". File: ${resolvedPath}\n` +
|
|
117
|
+
'Example: { "environmentConfig": { "key": "dev", "environment": "dev", "preset": "s", "serviceName": "aifabrix", "location": "swedencentral" }, "dryRun": false }'
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
if (parsed.environmentConfig === undefined) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`Config file must contain "environmentConfig" (object). File: ${resolvedPath}\n` +
|
|
123
|
+
'Example: { "environmentConfig": { "key": "dev", "environment": "dev", "preset": "s", "serviceName": "aifabrix", "location": "swedencentral" } }'
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
if (typeof parsed.environmentConfig !== 'object' || parsed.environmentConfig === null) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`"environmentConfig" must be an object. File: ${resolvedPath}`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
132
|
+
const validate = ajv.compile(environmentDeployRequestSchema);
|
|
133
|
+
const valid = validate(parsed);
|
|
134
|
+
if (!valid) {
|
|
135
|
+
const messages = formatValidationErrors(validate.errors);
|
|
136
|
+
throw new Error(
|
|
137
|
+
`Environment config validation failed (${resolvedPath}):\n • ${messages.join('\n • ')}\n` +
|
|
138
|
+
'Fix the config file and run the command again. See templates/infra/environment-dev.json for a valid example.'
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
environmentConfig: parsed.environmentConfig,
|
|
143
|
+
dryRun: Boolean(parsed.dryRun)
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Builds environment deployment request from options (config file required)
|
|
82
149
|
* @function buildEnvironmentDeploymentRequest
|
|
83
150
|
* @param {string} validatedEnvKey - Validated environment key
|
|
84
|
-
* @param {Object} options - Deployment options
|
|
85
|
-
* @returns {Object} Deployment request object
|
|
151
|
+
* @param {Object} options - Deployment options (must include options.config)
|
|
152
|
+
* @returns {Object} Deployment request object for API
|
|
86
153
|
*/
|
|
87
154
|
function buildEnvironmentDeploymentRequest(validatedEnvKey, options) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
if (options.config) {
|
|
96
|
-
request.description += ` (config: ${options.config})`;
|
|
155
|
+
if (!options.config || typeof options.config !== 'string') {
|
|
156
|
+
throw new Error(
|
|
157
|
+
'Environment deploy requires a config file with "environmentConfig". Use --config <file>.\n' +
|
|
158
|
+
'Example: aifabrix environment deploy dev --config templates/infra/environment-dev.json'
|
|
159
|
+
);
|
|
97
160
|
}
|
|
98
|
-
|
|
99
|
-
|
|
161
|
+
const deployRequest = loadAndValidateEnvironmentDeployConfig(options.config);
|
|
162
|
+
if (deployRequest.environmentConfig.key && deployRequest.environmentConfig.key !== validatedEnvKey) {
|
|
163
|
+
logger.log(chalk.yellow(
|
|
164
|
+
`⚠ Config key "${deployRequest.environmentConfig.key}" does not match deploy target "${validatedEnvKey}"; using target "${validatedEnvKey}".`
|
|
165
|
+
));
|
|
166
|
+
}
|
|
167
|
+
return deployRequest;
|
|
100
168
|
}
|
|
101
169
|
|
|
102
170
|
/**
|
|
@@ -155,22 +223,47 @@ async function sendEnvironmentDeployment(controllerUrl, envKey, authConfig, opti
|
|
|
155
223
|
}
|
|
156
224
|
|
|
157
225
|
/**
|
|
158
|
-
*
|
|
159
|
-
*
|
|
226
|
+
* Fetches deployment status by ID (pipeline endpoint first, then environments)
|
|
227
|
+
* Mirrors miso-controller manual test: GET pipeline/deployments/:id then GET environments/deployments/:id
|
|
228
|
+
* @async
|
|
229
|
+
* @param {string} controllerUrl - Controller URL
|
|
230
|
+
* @param {string} envKey - Environment key
|
|
231
|
+
* @param {string} deploymentId - Deployment ID
|
|
232
|
+
* @param {Object} apiAuthConfig - Auth config { type: 'bearer', token }
|
|
233
|
+
* @returns {Promise<Object|null>} Deployment record (status, progress, etc.) or null
|
|
234
|
+
*/
|
|
235
|
+
async function getDeploymentStatusById(controllerUrl, envKey, deploymentId, apiAuthConfig) {
|
|
236
|
+
try {
|
|
237
|
+
const pipelineRes = await getPipelineDeployment(controllerUrl, envKey, deploymentId, apiAuthConfig);
|
|
238
|
+
if (pipelineRes?.data?.data) return pipelineRes.data.data;
|
|
239
|
+
if (pipelineRes?.data && typeof pipelineRes.data === 'object' && pipelineRes.data.status) return pipelineRes.data;
|
|
240
|
+
} catch {
|
|
241
|
+
// Fallback to environments endpoint
|
|
242
|
+
}
|
|
243
|
+
try {
|
|
244
|
+
const envRes = await getDeployment(controllerUrl, envKey, deploymentId, apiAuthConfig);
|
|
245
|
+
if (envRes?.data?.data) return envRes.data.data;
|
|
246
|
+
if (envRes?.data && typeof envRes.data === 'object' && envRes.data.status) return envRes.data;
|
|
247
|
+
} catch {
|
|
248
|
+
// Ignore
|
|
249
|
+
}
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Process deployment record from GET .../deployments/:deploymentId
|
|
255
|
+
* @param {Object|null} deployment - Deployment record (status, progress, message, error)
|
|
160
256
|
* @param {string} validatedEnvKey - Validated environment key
|
|
161
|
-
* @returns {Object|null} Status result if
|
|
257
|
+
* @returns {Object|null} Status result if completed, null if still in progress
|
|
162
258
|
* @throws {Error} If deployment failed
|
|
163
259
|
*/
|
|
164
|
-
function
|
|
165
|
-
if (!
|
|
260
|
+
function processDeploymentStatusResponse(deployment, validatedEnvKey) {
|
|
261
|
+
if (!deployment || typeof deployment !== 'object') {
|
|
166
262
|
return null;
|
|
167
263
|
}
|
|
168
264
|
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
const isReady = status === 'ready' || status === 'completed' || responseData.ready === true;
|
|
172
|
-
|
|
173
|
-
if (isReady) {
|
|
265
|
+
const status = deployment.status;
|
|
266
|
+
if (status === 'completed') {
|
|
174
267
|
return {
|
|
175
268
|
success: true,
|
|
176
269
|
environment: validatedEnvKey,
|
|
@@ -179,9 +272,9 @@ function processEnvironmentStatusResponse(response, validatedEnvKey) {
|
|
|
179
272
|
};
|
|
180
273
|
}
|
|
181
274
|
|
|
182
|
-
// Check for terminal failure states
|
|
183
275
|
if (status === 'failed' || status === 'error') {
|
|
184
|
-
|
|
276
|
+
const msg = deployment.message || deployment.error || 'Unknown error';
|
|
277
|
+
throw new Error(`Environment deployment failed: ${msg}`);
|
|
185
278
|
}
|
|
186
279
|
|
|
187
280
|
return null;
|
|
@@ -213,8 +306,18 @@ async function pollEnvironmentStatus(deploymentId, controllerUrl, envKey, authCo
|
|
|
213
306
|
try {
|
|
214
307
|
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
|
215
308
|
|
|
216
|
-
const
|
|
217
|
-
|
|
309
|
+
const deployment = await getDeploymentStatusById(
|
|
310
|
+
validatedUrl,
|
|
311
|
+
validatedEnvKey,
|
|
312
|
+
deploymentId,
|
|
313
|
+
apiAuthConfig
|
|
314
|
+
);
|
|
315
|
+
const progress = deployment?.progress ?? 0;
|
|
316
|
+
const statusLabel = deployment?.status ?? 'pending';
|
|
317
|
+
if (attempt <= maxAttempts) {
|
|
318
|
+
logger.log(chalk.gray(` Attempt ${attempt}/${maxAttempts}... Status: ${statusLabel} (${progress}%)`));
|
|
319
|
+
}
|
|
320
|
+
const statusResult = processDeploymentStatusResponse(deployment, validatedEnvKey);
|
|
218
321
|
if (statusResult) {
|
|
219
322
|
return statusResult;
|
|
220
323
|
}
|
|
@@ -225,10 +328,6 @@ async function pollEnvironmentStatus(deploymentId, controllerUrl, envKey, authCo
|
|
|
225
328
|
}
|
|
226
329
|
// Otherwise, continue polling
|
|
227
330
|
}
|
|
228
|
-
|
|
229
|
-
if (attempt < maxAttempts) {
|
|
230
|
-
logger.log(chalk.gray(` Attempt ${attempt}/${maxAttempts}...`));
|
|
231
|
-
}
|
|
232
331
|
}
|
|
233
332
|
|
|
234
333
|
// Timeout
|
|
@@ -97,7 +97,11 @@ async function deployExternalSystem(appName, options = {}) {
|
|
|
97
97
|
|
|
98
98
|
return result;
|
|
99
99
|
} catch (error) {
|
|
100
|
-
|
|
100
|
+
let message = `Failed to deploy external system: ${error.message}`;
|
|
101
|
+
if (error.message && error.message.includes('Application not found')) {
|
|
102
|
+
message += `\n\n💡 Register the app in the controller first: aifabrix app register ${appName}`;
|
|
103
|
+
}
|
|
104
|
+
throw new Error(message);
|
|
101
105
|
}
|
|
102
106
|
}
|
|
103
107
|
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* External System Test Authentication Helpers
|
|
3
3
|
*
|
|
4
|
-
* Authentication setup for integration tests
|
|
4
|
+
* Authentication setup for integration tests. Uses the dataplane service URL
|
|
5
|
+
* (discovered from the controller), not the external system app's config—external
|
|
6
|
+
* apps do not store a dataplane URL.
|
|
5
7
|
*
|
|
6
8
|
* @fileoverview Authentication helpers for external system testing
|
|
7
9
|
* @author AI Fabrix Team
|
|
@@ -9,19 +11,19 @@
|
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
13
|
const { getDeploymentAuth } = require('../utils/token-manager');
|
|
12
|
-
const {
|
|
14
|
+
const { resolveDataplaneUrl } = require('../utils/dataplane-resolver');
|
|
13
15
|
const { resolveControllerUrl } = require('../utils/controller-url');
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
18
|
* Setup authentication and get dataplane URL for integration tests
|
|
17
19
|
* @async
|
|
18
|
-
* @param {string} appName - Application name
|
|
19
|
-
* @param {Object} options - Test options
|
|
20
|
-
* @param {Object}
|
|
20
|
+
* @param {string} appName - Application name (used for auth scope; dataplane URL is discovered from controller)
|
|
21
|
+
* @param {Object} options - Test options; options.dataplane overrides discovered URL
|
|
22
|
+
* @param {Object} _config - Configuration object
|
|
21
23
|
* @returns {Promise<Object>} Object with authConfig and dataplaneUrl
|
|
22
24
|
* @throws {Error} If authentication fails
|
|
23
25
|
*/
|
|
24
|
-
async function setupIntegrationTestAuth(appName,
|
|
26
|
+
async function setupIntegrationTestAuth(appName, options, _config) {
|
|
25
27
|
const { resolveEnvironment } = require('../core/config');
|
|
26
28
|
const environment = await resolveEnvironment();
|
|
27
29
|
const controllerUrl = await resolveControllerUrl();
|
|
@@ -31,7 +33,12 @@ async function setupIntegrationTestAuth(appName, _options, _config) {
|
|
|
31
33
|
throw new Error('Authentication required. Run "aifabrix login" or "aifabrix app register" first.');
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
|
|
36
|
+
let dataplaneUrl;
|
|
37
|
+
if (options && options.dataplane && typeof options.dataplane === 'string' && options.dataplane.trim()) {
|
|
38
|
+
dataplaneUrl = options.dataplane.trim();
|
|
39
|
+
} else {
|
|
40
|
+
dataplaneUrl = await resolveDataplaneUrl(controllerUrl, environment, authConfig);
|
|
41
|
+
}
|
|
35
42
|
|
|
36
43
|
return { authConfig, dataplaneUrl };
|
|
37
44
|
}
|
package/lib/generator/wizard.js
CHANGED
|
@@ -12,6 +12,19 @@ const chalk = require('chalk');
|
|
|
12
12
|
const logger = require('../utils/logger');
|
|
13
13
|
const { generateExternalReadmeContent } = require('../utils/external-readme');
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Converts a string to a schema-valid key segment (lowercase letters, numbers, hyphens only).
|
|
17
|
+
* e.g. "recordStorage" -> "record-storage", "documentStorage" -> "document-storage"
|
|
18
|
+
* @param {string} str - Raw entity type or key segment (may be camelCase)
|
|
19
|
+
* @returns {string} Segment matching ^[a-z0-9-]+$
|
|
20
|
+
*/
|
|
21
|
+
function toKeySegment(str) {
|
|
22
|
+
if (!str || typeof str !== 'string') return 'default';
|
|
23
|
+
const withHyphens = str.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
24
|
+
const sanitized = withHyphens.replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
25
|
+
return sanitized || 'default';
|
|
26
|
+
}
|
|
27
|
+
|
|
15
28
|
/**
|
|
16
29
|
* Generate files from dataplane-generated wizard configurations
|
|
17
30
|
* @async
|
|
@@ -53,11 +66,12 @@ async function writeDatasourceJsonFiles(appPath, finalSystemKey, datasourceConfi
|
|
|
53
66
|
const datasourceFileNames = [];
|
|
54
67
|
for (const datasourceConfig of datasourceConfigs) {
|
|
55
68
|
const entityType = datasourceConfig.entityType || datasourceConfig.entityKey || datasourceConfig.key?.split('-').pop() || 'default';
|
|
56
|
-
const
|
|
57
|
-
|
|
69
|
+
const keySegment = toKeySegment(entityType);
|
|
70
|
+
const datasourceKey = datasourceConfig.key || `${finalSystemKey}-${keySegment}`;
|
|
71
|
+
// Extract datasource key (remove system key prefix if present); use normalized segment for filename
|
|
58
72
|
const datasourceKeyOnly = datasourceKey.includes('-') && datasourceKey.startsWith(`${finalSystemKey}-`)
|
|
59
73
|
? datasourceKey.substring(finalSystemKey.length + 1)
|
|
60
|
-
:
|
|
74
|
+
: keySegment;
|
|
61
75
|
const datasourceFileName = `${finalSystemKey}-datasource-${datasourceKeyOnly}.json`;
|
|
62
76
|
const datasourceFilePath = path.join(appPath, datasourceFileName);
|
|
63
77
|
await fs.writeFile(datasourceFilePath, JSON.stringify(datasourceConfig, null, 2), 'utf8');
|
|
@@ -143,13 +157,14 @@ async function generateWizardFiles(appName, systemConfig, datasourceConfigs, sys
|
|
|
143
157
|
displayName: appDisplayName
|
|
144
158
|
};
|
|
145
159
|
|
|
146
|
-
// Update datasource configs to use appName-based keys and systemKey
|
|
160
|
+
// Update datasource configs to use appName-based keys and systemKey (key must match ^[a-z0-9-]+$)
|
|
147
161
|
const updatedDatasourceConfigs = datasourceConfigs.map(ds => {
|
|
148
162
|
const entityType = ds.entityType || ds.entityKey || ds.key?.split('-').pop() || 'default';
|
|
163
|
+
const keySegment = toKeySegment(entityType);
|
|
149
164
|
const entityDisplayName = entityType.charAt(0).toUpperCase() + entityType.slice(1).replace(/-/g, ' ');
|
|
150
165
|
return {
|
|
151
166
|
...ds,
|
|
152
|
-
key: `${finalSystemKey}-${
|
|
167
|
+
key: `${finalSystemKey}-${keySegment}`,
|
|
153
168
|
systemKey: finalSystemKey,
|
|
154
169
|
displayName: `${appDisplayName} ${entityDisplayName}`
|
|
155
170
|
};
|
|
@@ -360,54 +375,34 @@ async function generateEnvTemplate(appPath, systemConfig) {
|
|
|
360
375
|
}
|
|
361
376
|
|
|
362
377
|
/**
|
|
363
|
-
* Generate deployment
|
|
378
|
+
* Generate deployment script (deploy.js) from template
|
|
364
379
|
* @async
|
|
365
380
|
* @function generateDeployScripts
|
|
366
381
|
* @param {string} appPath - Application directory path
|
|
367
382
|
* @param {string} systemKey - System key
|
|
368
383
|
* @param {string} systemFileName - System file name
|
|
369
384
|
* @param {string[]} datasourceFileNames - Array of datasource file names
|
|
370
|
-
* @returns {Promise<Object>} Object with
|
|
385
|
+
* @returns {Promise<Object>} Object with deployJsPath
|
|
371
386
|
* @throws {Error} If generation fails
|
|
372
387
|
*/
|
|
388
|
+
const templatesExternalDir = path.join(__dirname, '..', '..', 'templates', 'external-system');
|
|
389
|
+
|
|
390
|
+
async function writeDeployScriptFromTemplate(templateName, outputPath, context) {
|
|
391
|
+
const templatePath = path.join(templatesExternalDir, templateName);
|
|
392
|
+
const content = Handlebars.compile(await fs.readFile(templatePath, 'utf8'))(context);
|
|
393
|
+
await fs.writeFile(outputPath, content, 'utf8');
|
|
394
|
+
logger.log(chalk.green(`✓ Generated ${path.basename(outputPath)}`));
|
|
395
|
+
}
|
|
396
|
+
|
|
373
397
|
async function generateDeployScripts(appPath, systemKey, systemFileName, datasourceFileNames) {
|
|
374
398
|
try {
|
|
375
399
|
const allJsonFiles = [systemFileName, ...datasourceFileNames];
|
|
400
|
+
const context = { systemKey, allJsonFiles, datasourceFileNames };
|
|
376
401
|
|
|
377
|
-
|
|
378
|
-
const deployShTemplatePath = path.join(__dirname, '..', '..', 'templates', 'external-system', 'deploy.sh.hbs');
|
|
379
|
-
const deployShTemplateContent = await fs.readFile(deployShTemplatePath, 'utf8');
|
|
380
|
-
const deployShTemplate = Handlebars.compile(deployShTemplateContent);
|
|
381
|
-
|
|
382
|
-
// Generate deploy.sh
|
|
383
|
-
const deployShPath = path.join(appPath, 'deploy.sh');
|
|
384
|
-
const deployShContent = deployShTemplate({
|
|
385
|
-
systemKey,
|
|
386
|
-
allJsonFiles,
|
|
387
|
-
datasourceFileNames
|
|
388
|
-
});
|
|
389
|
-
await fs.writeFile(deployShPath, deployShContent, 'utf8');
|
|
390
|
-
await fs.chmod(deployShPath, 0o755); // Make executable
|
|
391
|
-
logger.log(chalk.green('✓ Generated deploy.sh'));
|
|
392
|
-
|
|
393
|
-
// Load and compile deploy.ps1 template
|
|
394
|
-
const deployPs1TemplatePath = path.join(__dirname, '..', '..', 'templates', 'external-system', 'deploy.ps1.hbs');
|
|
395
|
-
const deployPs1TemplateContent = await fs.readFile(deployPs1TemplatePath, 'utf8');
|
|
396
|
-
const deployPs1Template = Handlebars.compile(deployPs1TemplateContent);
|
|
397
|
-
|
|
398
|
-
// Generate deploy.ps1
|
|
399
|
-
const deployPs1Path = path.join(appPath, 'deploy.ps1');
|
|
400
|
-
const deployPs1Content = deployPs1Template({
|
|
401
|
-
systemKey,
|
|
402
|
-
allJsonFiles,
|
|
403
|
-
datasourceFileNames
|
|
404
|
-
});
|
|
405
|
-
await fs.writeFile(deployPs1Path, deployPs1Content, 'utf8');
|
|
406
|
-
logger.log(chalk.green('✓ Generated deploy.ps1'));
|
|
402
|
+
await writeDeployScriptFromTemplate('deploy.js.hbs', path.join(appPath, 'deploy.js'), context);
|
|
407
403
|
|
|
408
404
|
return {
|
|
409
|
-
|
|
410
|
-
deployPs1Path
|
|
405
|
+
deployJsPath: path.join(appPath, 'deploy.js')
|
|
411
406
|
};
|
|
412
407
|
} catch (error) {
|
|
413
408
|
throw new Error(`Failed to generate deployment scripts: ${error.message}`);
|
|
@@ -439,10 +434,11 @@ async function generateReadme(appPath, appName, systemKey, systemConfig, datasou
|
|
|
439
434
|
|
|
440
435
|
const datasources = (Array.isArray(datasourceConfigs) ? datasourceConfigs : []).map((ds, index) => {
|
|
441
436
|
const entityType = ds.entityType || ds.entityKey || ds.key?.split('-').pop() || `datasource${index + 1}`;
|
|
442
|
-
const
|
|
437
|
+
const keySegment = toKeySegment(entityType);
|
|
438
|
+
const datasourceKey = ds.key || `${systemKey}-${keySegment}`;
|
|
443
439
|
const datasourceKeyOnly = datasourceKey.includes('-') && datasourceKey.startsWith(`${systemKey}-`)
|
|
444
440
|
? datasourceKey.substring(systemKey.length + 1)
|
|
445
|
-
:
|
|
441
|
+
: keySegment;
|
|
446
442
|
return {
|
|
447
443
|
entityType,
|
|
448
444
|
displayName: ds.displayName || ds.name || ds.key || `Datasource ${index + 1}`,
|
|
@@ -119,7 +119,7 @@ function extractPasswordFromUrlOrValue(urlOrPassword) {
|
|
|
119
119
|
* Ensures Postgres init script exists for miso-controller app (database miso, user miso_user).
|
|
120
120
|
* Uses password from secrets (databases-miso-controller-0-passwordKeyVault) or default miso_pass123.
|
|
121
121
|
* Init scripts run only when the Postgres data volume is first created. If infra was already
|
|
122
|
-
* started before this script existed, run `aifabrix down -v` then `aifabrix up` to re-init, or
|
|
122
|
+
* started before this script existed, run `aifabrix down-infra -v` then `aifabrix up-infra` to re-init, or
|
|
123
123
|
* create the database and user manually (e.g. via pgAdmin or psql).
|
|
124
124
|
*
|
|
125
125
|
* @async
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://raw.githubusercontent.com/esystemsdev/aifabrix-builder/refs/heads/main/lib/schema/environment-deploy-request.schema.json",
|
|
4
|
+
"title": "Environment Deploy Request",
|
|
5
|
+
"description": "Request body for POST /api/v1/environments/{envKey}/deploy",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["environmentConfig"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"environmentConfig": {
|
|
10
|
+
"type": "object",
|
|
11
|
+
"description": "Environment infrastructure configuration",
|
|
12
|
+
"required": ["key", "environment", "preset", "serviceName", "location"],
|
|
13
|
+
"properties": {
|
|
14
|
+
"key": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"description": "Environment key",
|
|
17
|
+
"pattern": "^[a-z0-9-]+$",
|
|
18
|
+
"minLength": 2,
|
|
19
|
+
"maxLength": 40
|
|
20
|
+
},
|
|
21
|
+
"environment": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"description": "Environment type",
|
|
24
|
+
"enum": ["miso", "dev", "tst", "pro"]
|
|
25
|
+
},
|
|
26
|
+
"preset": {
|
|
27
|
+
"type": "string",
|
|
28
|
+
"description": "Deployment preset size",
|
|
29
|
+
"enum": ["evaluation", "eval", "s", "m", "l", "xl"]
|
|
30
|
+
},
|
|
31
|
+
"serviceName": {
|
|
32
|
+
"type": "string",
|
|
33
|
+
"description": "Service name for resource naming",
|
|
34
|
+
"pattern": "^[a-z0-9-]{2,40}$"
|
|
35
|
+
},
|
|
36
|
+
"location": {
|
|
37
|
+
"type": "string",
|
|
38
|
+
"description": "Azure region (e.g. swedencentral)"
|
|
39
|
+
},
|
|
40
|
+
"resourceGroupName": { "type": "string" },
|
|
41
|
+
"subscriptionId": { "type": "string" },
|
|
42
|
+
"tenantId": { "type": "string" },
|
|
43
|
+
"customDomain": { "type": "object" },
|
|
44
|
+
"frontDoor": { "type": "object" },
|
|
45
|
+
"networking": { "type": "object" },
|
|
46
|
+
"allowedIPs": {
|
|
47
|
+
"type": "array",
|
|
48
|
+
"items": { "type": "string" }
|
|
49
|
+
},
|
|
50
|
+
"infrastructureAccess": {
|
|
51
|
+
"type": "array",
|
|
52
|
+
"items": { "type": "object" }
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"additionalProperties": true
|
|
56
|
+
},
|
|
57
|
+
"dryRun": {
|
|
58
|
+
"type": "boolean",
|
|
59
|
+
"description": "If true, validate only without deploying",
|
|
60
|
+
"default": false
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"additionalProperties": false
|
|
64
|
+
}
|
|
@@ -20,8 +20,11 @@ const CATEGORIES = [
|
|
|
20
20
|
{
|
|
21
21
|
name: 'Infrastructure (Local Development)',
|
|
22
22
|
commands: [
|
|
23
|
-
{ name: 'up' },
|
|
24
|
-
{ name: '
|
|
23
|
+
{ name: 'up-infra' },
|
|
24
|
+
{ name: 'up-platform' },
|
|
25
|
+
{ name: 'up-miso' },
|
|
26
|
+
{ name: 'up-dataplane' },
|
|
27
|
+
{ name: 'down-infra', term: 'down-infra [app]' },
|
|
25
28
|
{ name: 'doctor' },
|
|
26
29
|
{ name: 'status' },
|
|
27
30
|
{ name: 'restart', term: 'restart <service>' }
|
package/lib/utils/paths.js
CHANGED
|
@@ -281,7 +281,23 @@ function getAppPath(appName, appType) {
|
|
|
281
281
|
}
|
|
282
282
|
|
|
283
283
|
/**
|
|
284
|
-
*
|
|
284
|
+
* Base directory for integration/builder: project root when cwd is inside project, else cwd.
|
|
285
|
+
* So deploy works when run from integration/<app> (e.g. node deploy.js), and tests using temp dirs still work.
|
|
286
|
+
* @returns {string} Directory to resolve integration/ and builder/ from
|
|
287
|
+
*/
|
|
288
|
+
function getIntegrationBuilderBaseDir() {
|
|
289
|
+
const root = getProjectRoot();
|
|
290
|
+
const cwd = path.resolve(process.cwd());
|
|
291
|
+
const rootNorm = path.resolve(root);
|
|
292
|
+
if (cwd === rootNorm || cwd.startsWith(rootNorm + path.sep)) {
|
|
293
|
+
return rootNorm;
|
|
294
|
+
}
|
|
295
|
+
return cwd;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Gets the integration folder path for external systems.
|
|
300
|
+
* Uses project root when cwd is inside project so deploy works when run from integration/<app> (e.g. node deploy.js).
|
|
285
301
|
* @param {string} appName - Application name
|
|
286
302
|
* @returns {string} Absolute path to integration directory
|
|
287
303
|
*/
|
|
@@ -289,13 +305,14 @@ function getIntegrationPath(appName) {
|
|
|
289
305
|
if (!appName || typeof appName !== 'string') {
|
|
290
306
|
throw new Error('App name is required and must be a string');
|
|
291
307
|
}
|
|
292
|
-
|
|
308
|
+
const base = getIntegrationBuilderBaseDir();
|
|
309
|
+
return path.join(base, 'integration', appName);
|
|
293
310
|
}
|
|
294
311
|
|
|
295
312
|
/**
|
|
296
313
|
* Gets the builder folder path for regular applications.
|
|
297
314
|
* When AIFABRIX_BUILDER_DIR is set (e.g. by up-miso/up-dataplane from config aifabrix-env-config),
|
|
298
|
-
* uses that as builder root
|
|
315
|
+
* uses that as builder root; otherwise uses project root so deploy works when run from integration/<app>.
|
|
299
316
|
* @param {string} appName - Application name
|
|
300
317
|
* @returns {string} Absolute path to builder directory
|
|
301
318
|
*/
|
|
@@ -309,7 +326,8 @@ function getBuilderPath(appName) {
|
|
|
309
326
|
if (builderRoot) {
|
|
310
327
|
return path.join(builderRoot, appName);
|
|
311
328
|
}
|
|
312
|
-
|
|
329
|
+
const base = getIntegrationBuilderBaseDir();
|
|
330
|
+
return path.join(base, 'builder', appName);
|
|
313
331
|
}
|
|
314
332
|
|
|
315
333
|
/**
|
|
@@ -18,7 +18,17 @@ const logger = require('./logger');
|
|
|
18
18
|
const pathsUtil = require('./paths');
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
*
|
|
21
|
+
* Skips commented or empty lines when scanning env.template
|
|
22
|
+
* @param {string} line - Single line
|
|
23
|
+
* @returns {boolean}
|
|
24
|
+
*/
|
|
25
|
+
function isCommentOrEmptyLine(line) {
|
|
26
|
+
const t = line.trim();
|
|
27
|
+
return t === '' || t.startsWith('#');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Finds missing secret keys from template (skips commented and empty lines)
|
|
22
32
|
* @function findMissingSecretKeys
|
|
23
33
|
* @param {string} envTemplate - Environment template content
|
|
24
34
|
* @param {Object} existingSecrets - Existing secrets object
|
|
@@ -28,13 +38,18 @@ function findMissingSecretKeys(envTemplate, existingSecrets) {
|
|
|
28
38
|
const kvPattern = /kv:\/\/([a-zA-Z0-9-_]+)/g;
|
|
29
39
|
const missingKeys = [];
|
|
30
40
|
const seenKeys = new Set();
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
41
|
+
const lines = envTemplate.split('\n');
|
|
42
|
+
|
|
43
|
+
for (const line of lines) {
|
|
44
|
+
if (isCommentOrEmptyLine(line)) continue;
|
|
45
|
+
let match;
|
|
46
|
+
kvPattern.lastIndex = 0;
|
|
47
|
+
while ((match = kvPattern.exec(line)) !== null) {
|
|
48
|
+
const secretKey = match[1];
|
|
49
|
+
if (!seenKeys.has(secretKey) && !(secretKey in existingSecrets)) {
|
|
50
|
+
missingKeys.push(secretKey);
|
|
51
|
+
seenKeys.add(secretKey);
|
|
52
|
+
}
|
|
38
53
|
}
|
|
39
54
|
}
|
|
40
55
|
|