@aifabrix/builder 2.40.2 → 2.41.0
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 -4
- package/integration/hubspot/test.js +1 -1
- package/lib/api/credential.api.js +40 -0
- package/lib/api/dev.api.js +423 -0
- package/lib/api/types/credential.types.js +23 -0
- package/lib/api/types/dev.types.js +140 -0
- package/lib/app/config.js +21 -0
- package/lib/app/down.js +2 -1
- package/lib/app/index.js +9 -0
- package/lib/app/push.js +36 -12
- package/lib/app/readme.js +1 -3
- package/lib/app/run-env-compose.js +201 -0
- package/lib/app/run-helpers.js +121 -118
- package/lib/app/run.js +148 -28
- package/lib/app/show.js +5 -2
- package/lib/build/index.js +11 -3
- package/lib/cli/setup-app.js +140 -14
- package/lib/cli/setup-dev.js +180 -17
- package/lib/cli/setup-environment.js +4 -2
- package/lib/cli/setup-external-system.js +71 -21
- package/lib/cli/setup-infra.js +29 -2
- package/lib/cli/setup-secrets.js +52 -5
- package/lib/cli/setup-utility.js +12 -3
- package/lib/commands/app-install.js +172 -0
- package/lib/commands/app-shell.js +75 -0
- package/lib/commands/app-test.js +282 -0
- package/lib/commands/app.js +1 -1
- package/lib/commands/dev-cli-handlers.js +141 -0
- package/lib/commands/dev-down.js +114 -0
- package/lib/commands/dev-init.js +309 -0
- package/lib/commands/secrets-list.js +118 -0
- package/lib/commands/secrets-remove.js +97 -0
- package/lib/commands/secrets-set.js +30 -17
- package/lib/commands/secrets-validate.js +50 -0
- package/lib/commands/up-dataplane.js +2 -2
- package/lib/commands/up-miso.js +0 -25
- package/lib/commands/upload.js +26 -1
- package/lib/core/admin-secrets.js +96 -0
- package/lib/core/secrets-ensure.js +378 -0
- package/lib/core/secrets-env-write.js +157 -0
- package/lib/core/secrets.js +147 -81
- package/lib/datasource/field-reference-validator.js +91 -0
- package/lib/datasource/validate.js +21 -3
- package/lib/deployment/environment-config.js +137 -0
- package/lib/deployment/environment.js +21 -98
- package/lib/deployment/push.js +32 -2
- package/lib/external-system/download.js +7 -0
- package/lib/external-system/test-auth.js +7 -3
- package/lib/external-system/test.js +5 -1
- package/lib/generator/index.js +174 -25
- package/lib/generator/wizard.js +8 -0
- package/lib/infrastructure/helpers.js +103 -20
- package/lib/infrastructure/index.js +88 -10
- package/lib/infrastructure/services.js +70 -15
- package/lib/schema/application-schema.json +24 -3
- package/lib/schema/external-system.schema.json +435 -413
- package/lib/utils/api.js +3 -3
- package/lib/utils/app-register-auth.js +25 -3
- package/lib/utils/cli-utils.js +20 -0
- package/lib/utils/compose-generator.js +76 -75
- package/lib/utils/compose-handlebars-helpers.js +43 -0
- package/lib/utils/compose-vector-helper.js +18 -0
- package/lib/utils/config-paths.js +127 -2
- package/lib/utils/credential-secrets-env.js +267 -0
- package/lib/utils/dev-cert-helper.js +122 -0
- package/lib/utils/device-code-helpers.js +224 -0
- package/lib/utils/device-code.js +37 -336
- package/lib/utils/docker-build.js +40 -8
- package/lib/utils/env-copy.js +83 -13
- package/lib/utils/env-map.js +35 -5
- package/lib/utils/env-template.js +6 -5
- package/lib/utils/error-formatters/http-status-errors.js +20 -1
- package/lib/utils/help-builder.js +15 -2
- package/lib/utils/infra-status.js +30 -1
- package/lib/utils/local-secrets.js +7 -52
- package/lib/utils/mutagen-install.js +195 -0
- package/lib/utils/mutagen.js +146 -0
- package/lib/utils/paths.js +43 -33
- package/lib/utils/port-resolver.js +28 -16
- package/lib/utils/remote-dev-auth.js +38 -0
- package/lib/utils/remote-docker-env.js +43 -0
- package/lib/utils/remote-secrets-loader.js +60 -0
- package/lib/utils/secrets-generator.js +94 -6
- package/lib/utils/secrets-helpers.js +33 -25
- package/lib/utils/secrets-path.js +2 -2
- package/lib/utils/secrets-utils.js +52 -1
- package/lib/utils/secrets-validation.js +84 -0
- package/lib/utils/ssh-key-helper.js +116 -0
- package/lib/utils/token-manager-messages.js +90 -0
- package/lib/utils/token-manager.js +5 -4
- package/lib/utils/variable-transformer.js +3 -3
- package/lib/validation/validator.js +65 -0
- package/package.json +2 -2
- package/scripts/install-local.js +34 -15
- package/templates/README.md +0 -1
- package/templates/applications/README.md.hbs +4 -4
- package/templates/applications/dataplane/application.yaml +5 -4
- package/templates/applications/dataplane/env.template +12 -7
- package/templates/applications/keycloak/env.template +2 -0
- package/templates/applications/miso-controller/application.yaml +1 -0
- package/templates/applications/miso-controller/env.template +11 -9
- package/templates/python/docker-compose.hbs +49 -23
- package/templates/typescript/docker-compose.hbs +48 -22
|
@@ -9,10 +9,7 @@
|
|
|
9
9
|
* @version 2.0.0
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
const fs = require('fs');
|
|
13
|
-
const path = require('path');
|
|
14
12
|
const chalk = require('chalk');
|
|
15
|
-
const Ajv = require('ajv');
|
|
16
13
|
const logger = require('../utils/logger');
|
|
17
14
|
const config = require('../core/config');
|
|
18
15
|
const { resolveControllerUrl } = require('../utils/controller-url');
|
|
@@ -21,9 +18,12 @@ const { getOrRefreshDeviceToken } = require('../utils/token-manager');
|
|
|
21
18
|
const { getPipelineDeployment } = require('../api/pipeline.api');
|
|
22
19
|
const { deployEnvironment: deployEnvironmentInfra, getDeployment } = require('../api/deployments.api');
|
|
23
20
|
const { handleDeploymentErrors } = require('../utils/deployment-errors');
|
|
24
|
-
const { formatValidationErrors } = require('../utils/error-formatter');
|
|
25
21
|
const auditLogger = require('../core/audit-logger');
|
|
26
|
-
const
|
|
22
|
+
const {
|
|
23
|
+
normalizePreset,
|
|
24
|
+
buildEnvironmentConfigFromPreset,
|
|
25
|
+
loadAndValidateEnvironmentDeployConfig
|
|
26
|
+
} = require('./environment-config');
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
29
|
* Validates environment deployment prerequisites
|
|
@@ -82,102 +82,26 @@ async function getEnvironmentAuth(controllerUrl) {
|
|
|
82
82
|
* @returns {Promise<Object>} Deployment result
|
|
83
83
|
* @throws {Error} If deployment fails
|
|
84
84
|
*/
|
|
85
|
-
/** Reads and parses config file; throws if missing, unreadable, or invalid structure. */
|
|
86
|
-
function parseEnvironmentConfigFile(resolvedPath) {
|
|
87
|
-
let raw;
|
|
88
|
-
try {
|
|
89
|
-
raw = fs.readFileSync(resolvedPath, 'utf8');
|
|
90
|
-
} catch (e) {
|
|
91
|
-
throw new Error(`Cannot read config file: ${resolvedPath}. ${e.message}`);
|
|
92
|
-
}
|
|
93
|
-
let parsed;
|
|
94
|
-
try {
|
|
95
|
-
parsed = JSON.parse(raw);
|
|
96
|
-
} catch (e) {
|
|
97
|
-
throw new Error(
|
|
98
|
-
`Invalid JSON in config file: ${resolvedPath}\n${e.message}\n` +
|
|
99
|
-
'Expected format: { "environmentConfig": { "key", "environment", "preset", "serviceName", "location" }, "dryRun": false }'
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
if (parsed === null || typeof parsed !== 'object') {
|
|
103
|
-
throw new Error(
|
|
104
|
-
`Config file must be a JSON object with "environmentConfig". File: ${resolvedPath}\n` +
|
|
105
|
-
'Example: { "environmentConfig": { "key": "dev", "environment": "dev", "preset": "s", "serviceName": "aifabrix", "location": "swedencentral" }, "dryRun": false }'
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
if (parsed.environmentConfig === undefined) {
|
|
109
|
-
throw new Error(
|
|
110
|
-
`Config file must contain "environmentConfig" (object). File: ${resolvedPath}\n` +
|
|
111
|
-
'Example: { "environmentConfig": { "key": "dev", "environment": "dev", "preset": "s", "serviceName": "aifabrix", "location": "swedencentral" } }'
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
if (typeof parsed.environmentConfig !== 'object' || parsed.environmentConfig === null) {
|
|
115
|
-
throw new Error(`"environmentConfig" must be an object. File: ${resolvedPath}`);
|
|
116
|
-
}
|
|
117
|
-
return parsed;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
85
|
/**
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
* @param {string} resolvedPath - Path for error messages
|
|
124
|
-
* @returns {Object} { environmentConfig, dryRun? }
|
|
125
|
-
*/
|
|
126
|
-
function validateEnvironmentDeployParsed(parsed, resolvedPath) {
|
|
127
|
-
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
128
|
-
const validate = ajv.compile(environmentDeployRequestSchema);
|
|
129
|
-
if (!validate(parsed)) {
|
|
130
|
-
const messages = formatValidationErrors(validate.errors);
|
|
131
|
-
throw new Error(
|
|
132
|
-
`Environment config validation failed (${resolvedPath}):\n • ${messages.join('\n • ')}\n` +
|
|
133
|
-
'Fix the config file and run the command again. See templates/infra/environment-dev.json for a valid example.'
|
|
134
|
-
);
|
|
135
|
-
}
|
|
136
|
-
return {
|
|
137
|
-
environmentConfig: parsed.environmentConfig,
|
|
138
|
-
dryRun: Boolean(parsed.dryRun)
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Loads and validates environment deploy config from a JSON file
|
|
144
|
-
* @param {string} configPath - Absolute or relative path to config JSON
|
|
145
|
-
* @returns {Object} Valid deploy request { environmentConfig, dryRun? }
|
|
146
|
-
* @throws {Error} If file missing, invalid JSON, or validation fails
|
|
147
|
-
*/
|
|
148
|
-
function loadAndValidateEnvironmentDeployConfig(configPath) {
|
|
149
|
-
const resolvedPath = path.isAbsolute(configPath) ? configPath : path.resolve(process.cwd(), configPath);
|
|
150
|
-
if (!fs.existsSync(resolvedPath)) {
|
|
151
|
-
throw new Error(
|
|
152
|
-
`Environment config file not found: ${resolvedPath}\n` +
|
|
153
|
-
'Use --config <file> with a JSON file containing "environmentConfig" (e.g. templates/infra/environment-dev.json).'
|
|
154
|
-
);
|
|
155
|
-
}
|
|
156
|
-
const parsed = parseEnvironmentConfigFile(resolvedPath);
|
|
157
|
-
return validateEnvironmentDeployParsed(parsed, resolvedPath);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Builds environment deployment request from options (config file required)
|
|
86
|
+
* Builds environment deployment request from options (config file or --preset)
|
|
87
|
+
* When --config is provided, uses that file; otherwise uses --preset (default s).
|
|
162
88
|
* @function buildEnvironmentDeploymentRequest
|
|
163
89
|
* @param {string} validatedEnvKey - Validated environment key
|
|
164
|
-
* @param {Object} options - Deployment options (
|
|
90
|
+
* @param {Object} options - Deployment options (options.config optional; options.preset used when no config)
|
|
165
91
|
* @returns {Object} Deployment request object for API
|
|
166
92
|
*/
|
|
167
93
|
function buildEnvironmentDeploymentRequest(validatedEnvKey, options) {
|
|
168
|
-
if (
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
logger.log(chalk.yellow(
|
|
177
|
-
`⚠ Config key "${deployRequest.environmentConfig.key}" does not match deploy target "${validatedEnvKey}"; using target "${validatedEnvKey}".`
|
|
178
|
-
));
|
|
94
|
+
if (options.config && typeof options.config === 'string') {
|
|
95
|
+
const deployRequest = loadAndValidateEnvironmentDeployConfig(options.config);
|
|
96
|
+
if (deployRequest.environmentConfig.key && deployRequest.environmentConfig.key !== validatedEnvKey) {
|
|
97
|
+
logger.log(chalk.yellow(
|
|
98
|
+
`⚠ Config key "${deployRequest.environmentConfig.key}" does not match deploy target "${validatedEnvKey}"; using target "${validatedEnvKey}".`
|
|
99
|
+
));
|
|
100
|
+
}
|
|
101
|
+
return deployRequest;
|
|
179
102
|
}
|
|
180
|
-
|
|
103
|
+
const preset = normalizePreset(options.preset);
|
|
104
|
+
return buildEnvironmentConfigFromPreset(validatedEnvKey, preset);
|
|
181
105
|
}
|
|
182
106
|
|
|
183
107
|
/**
|
|
@@ -371,7 +295,8 @@ function displayDeploymentResults(result) {
|
|
|
371
295
|
* @param {string} envKey - Environment key (miso, dev, tst, pro)
|
|
372
296
|
* @param {Object} options - Deployment options
|
|
373
297
|
* @param {string} options.controller - Controller URL (required)
|
|
374
|
-
* @param {string} [options.config] - Environment configuration file (optional)
|
|
298
|
+
* @param {string} [options.config] - Environment configuration file (optional; if omitted, --preset is used)
|
|
299
|
+
* @param {string} [options.preset] - Environment size preset: s, m, l, xl (default: s), used when config is not provided
|
|
375
300
|
* @param {boolean} [options.skipValidation] - Skip validation checks
|
|
376
301
|
* @param {boolean} [options.poll] - Poll for deployment status (default: true)
|
|
377
302
|
* @param {boolean} [options.noPoll] - Do not poll for status
|
|
@@ -492,6 +417,4 @@ async function deployEnvironment(envKey, options = {}) {
|
|
|
492
417
|
throw error;
|
|
493
418
|
}
|
|
494
419
|
}
|
|
495
|
-
module.exports = {
|
|
496
|
-
deployEnvironment
|
|
497
|
-
};
|
|
420
|
+
module.exports = { deployEnvironment };
|
package/lib/deployment/push.js
CHANGED
|
@@ -16,6 +16,29 @@ const logger = require('../utils/logger');
|
|
|
16
16
|
|
|
17
17
|
const execAsync = promisify(exec);
|
|
18
18
|
|
|
19
|
+
/** Timeout for az account show (fail fast when not logged in) */
|
|
20
|
+
const AZ_ACCOUNT_SHOW_TIMEOUT_MS = 10000;
|
|
21
|
+
/** Timeout for az acr show (avoid hang when Azure CLI prompts for login) */
|
|
22
|
+
const AZ_ACR_SHOW_TIMEOUT_MS = 15000;
|
|
23
|
+
/** Timeout for az acr login */
|
|
24
|
+
const AZ_ACR_LOGIN_TIMEOUT_MS = 120000;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Check if user is logged in to Azure CLI
|
|
28
|
+
* Fails fast so push does not hang when user is not logged in.
|
|
29
|
+
* @param {Object} [execOptions] - Options for exec (e.g. shell on Windows)
|
|
30
|
+
* @returns {Promise<boolean>} True if logged in
|
|
31
|
+
*/
|
|
32
|
+
async function checkAzureLogin(execOptions = {}) {
|
|
33
|
+
const options = process.platform === 'win32' ? { shell: true, ...execOptions } : execOptions;
|
|
34
|
+
try {
|
|
35
|
+
await execAsync('az account show', { ...options, timeout: AZ_ACCOUNT_SHOW_TIMEOUT_MS });
|
|
36
|
+
return true;
|
|
37
|
+
} catch (_error) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
19
42
|
/**
|
|
20
43
|
* Check if Azure CLI is installed
|
|
21
44
|
* @returns {Promise<boolean>} True if Azure CLI is available
|
|
@@ -128,7 +151,7 @@ async function checkACRAuthentication(registry) {
|
|
|
128
151
|
const registryName = extractRegistryName(registry);
|
|
129
152
|
// On Windows, use shell option to ensure proper command resolution
|
|
130
153
|
const options = process.platform === 'win32' ? { shell: true } : {};
|
|
131
|
-
await execAsync(`az acr show --name ${registryName}`, options);
|
|
154
|
+
await execAsync(`az acr show --name ${registryName}`, { ...options, timeout: AZ_ACR_SHOW_TIMEOUT_MS });
|
|
132
155
|
return true;
|
|
133
156
|
} catch (error) {
|
|
134
157
|
return false;
|
|
@@ -146,9 +169,15 @@ async function authenticateACR(registry) {
|
|
|
146
169
|
logger.log(chalk.blue(`Authenticating with ${registry}...`));
|
|
147
170
|
// On Windows, use shell option to ensure proper command resolution
|
|
148
171
|
const options = process.platform === 'win32' ? { shell: true } : {};
|
|
149
|
-
await execAsync(`az acr login --name ${registryName}`, options);
|
|
172
|
+
await execAsync(`az acr login --name ${registryName}`, { ...options, timeout: AZ_ACR_LOGIN_TIMEOUT_MS });
|
|
150
173
|
logger.log(chalk.green(`✓ Authenticated with ${registry}`));
|
|
151
174
|
} catch (error) {
|
|
175
|
+
const msg = error.message || String(error);
|
|
176
|
+
if (msg.includes('ETIMEDOUT') || msg.includes('timeout')) {
|
|
177
|
+
throw new Error(
|
|
178
|
+
'Authentication timed out. Make sure you\'re logged in to Azure first: az login'
|
|
179
|
+
);
|
|
180
|
+
}
|
|
152
181
|
throw new Error(`Failed to authenticate with Azure Container Registry: ${error.message}`);
|
|
153
182
|
}
|
|
154
183
|
}
|
|
@@ -267,6 +296,7 @@ async function pushImage(imageWithTag, registry = null) {
|
|
|
267
296
|
|
|
268
297
|
module.exports = {
|
|
269
298
|
checkAzureCLIInstalled,
|
|
299
|
+
checkAzureLogin,
|
|
270
300
|
extractRegistryName,
|
|
271
301
|
validateRegistryURL,
|
|
272
302
|
checkACRAuthentication,
|
|
@@ -390,6 +390,13 @@ async function processDownloadedSystem(systemKey, application, dataSources, temp
|
|
|
390
390
|
// Move files from temp to final location
|
|
391
391
|
await moveFilesToFinalLocation(tempDir, finalPath, systemKey, filePaths);
|
|
392
392
|
|
|
393
|
+
try {
|
|
394
|
+
const secretsEnsure = require('../core/secrets-ensure');
|
|
395
|
+
await secretsEnsure.ensureSecretsFromEnvTemplate(path.join(finalPath, 'env.template'), { emptyValuesForCredentials: true });
|
|
396
|
+
} catch (err) {
|
|
397
|
+
if (err.code !== 'ENOENT') logger.warn(`Could not ensure integration placeholder secrets: ${err.message}`);
|
|
398
|
+
}
|
|
399
|
+
|
|
393
400
|
// Clean up temporary folder
|
|
394
401
|
await fs.rm(tempDir, { recursive: true, force: true });
|
|
395
402
|
|
|
@@ -15,17 +15,21 @@ const { resolveDataplaneUrl } = require('../utils/dataplane-resolver');
|
|
|
15
15
|
const { resolveControllerUrl } = require('../utils/controller-url');
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
* Setup authentication and get dataplane URL for integration tests
|
|
18
|
+
* Setup authentication and get dataplane URL for integration tests.
|
|
19
|
+
* Dataplane URL is always discovered from the controller for the given environment.
|
|
20
|
+
*
|
|
19
21
|
* @async
|
|
20
22
|
* @param {string} appName - Application name (used for auth scope; dataplane URL is discovered from controller)
|
|
21
|
-
* @param {Object} options - Test options; options.dataplane overrides
|
|
23
|
+
* @param {Object} options - Test options; options.environment overrides config env; options.dataplane overrides URL (programmatic only)
|
|
22
24
|
* @param {Object} _config - Configuration object
|
|
23
25
|
* @returns {Promise<Object>} Object with authConfig and dataplaneUrl
|
|
24
26
|
* @throws {Error} If authentication fails
|
|
25
27
|
*/
|
|
26
28
|
async function setupIntegrationTestAuth(appName, options, _config) {
|
|
27
29
|
const { resolveEnvironment } = require('../core/config');
|
|
28
|
-
const environment =
|
|
30
|
+
const environment = (options && options.environment && typeof options.environment === 'string' && options.environment.trim())
|
|
31
|
+
? options.environment.trim()
|
|
32
|
+
: await resolveEnvironment();
|
|
29
33
|
const controllerUrl = await resolveControllerUrl();
|
|
30
34
|
const authConfig = await getDeploymentAuth(controllerUrl, environment, appName);
|
|
31
35
|
|
|
@@ -50,7 +50,11 @@ async function loadVariablesYamlFile(variablesPath) {
|
|
|
50
50
|
try {
|
|
51
51
|
const variables = loadConfigFile(variablesPath);
|
|
52
52
|
if (!variables.externalIntegration) {
|
|
53
|
-
throw new Error(
|
|
53
|
+
throw new Error(
|
|
54
|
+
'externalIntegration block not found in application config. ' +
|
|
55
|
+
'test-integration is for external systems only (integration/<app> with externalIntegration in application.yaml). ' +
|
|
56
|
+
'For builder apps use: aifabrix test <app> or aifabrix test-e2e <app>'
|
|
57
|
+
);
|
|
54
58
|
}
|
|
55
59
|
return variables;
|
|
56
60
|
} catch (error) {
|
package/lib/generator/index.js
CHANGED
|
@@ -20,6 +20,8 @@ const { loadVariables, loadEnvTemplate, loadRbac, parseEnvironmentVariables } =
|
|
|
20
20
|
const { generateExternalSystemApplicationSchema, splitExternalApplicationSchema } = require('./external');
|
|
21
21
|
const { generateControllerManifest } = require('./external-controller-manifest');
|
|
22
22
|
const { resolveVersionForApp } = require('../utils/image-version');
|
|
23
|
+
const { getContainerPort } = require('../utils/port-resolver');
|
|
24
|
+
const { buildEnvVarMap } = require('../utils/env-map');
|
|
23
25
|
|
|
24
26
|
/**
|
|
25
27
|
* Generates deployment JSON from application configuration files
|
|
@@ -59,6 +61,96 @@ function loadDeploymentConfigFiles(appPath, appType, appName) {
|
|
|
59
61
|
return { variables, envTemplate, rbac, jsonPath };
|
|
60
62
|
}
|
|
61
63
|
|
|
64
|
+
/** Placeholder replaced with application port from application.yaml */
|
|
65
|
+
const PORT_PLACEHOLDER = '${PORT}';
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Returns the numeric port to use when substituting ${PORT} in the manifest.
|
|
69
|
+
* When application.yaml has port: "${PORT}", uses defaultPort (e.g. 3000).
|
|
70
|
+
*
|
|
71
|
+
* @param {Object} variables - Parsed application config
|
|
72
|
+
* @param {number} [defaultPort=3000] - Default when port is "${PORT}" or invalid
|
|
73
|
+
* @returns {number} Port number for substitution
|
|
74
|
+
*/
|
|
75
|
+
function getEffectivePortForSubstitution(variables, defaultPort = 3000) {
|
|
76
|
+
const raw = getContainerPort(variables, defaultPort);
|
|
77
|
+
if (raw === PORT_PLACEHOLDER || (typeof raw === 'string' && raw.trim() === PORT_PLACEHOLDER)) {
|
|
78
|
+
return defaultPort;
|
|
79
|
+
}
|
|
80
|
+
if (typeof raw === 'number' && raw > 0) {
|
|
81
|
+
return raw;
|
|
82
|
+
}
|
|
83
|
+
const num = Number(raw);
|
|
84
|
+
return Number.isFinite(num) && num > 0 ? num : defaultPort;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Recursively replaces ${PORT} with the given port number in all string values of obj (in-place).
|
|
89
|
+
*
|
|
90
|
+
* @param {Object} obj - Deployment manifest or any nested object
|
|
91
|
+
* @param {number} portNumber - Port to substitute (e.g. from application.yaml)
|
|
92
|
+
*/
|
|
93
|
+
function substitutePortInDeployment(obj, portNumber) {
|
|
94
|
+
if (obj === null || obj === undefined) return;
|
|
95
|
+
if (Array.isArray(obj)) {
|
|
96
|
+
for (let i = 0; i < obj.length; i++) {
|
|
97
|
+
if (typeof obj[i] === 'string' && obj[i].includes(PORT_PLACEHOLDER)) {
|
|
98
|
+
obj[i] = obj[i].split(PORT_PLACEHOLDER).join(String(portNumber));
|
|
99
|
+
} else if (typeof obj[i] === 'object' && obj[i] !== null) {
|
|
100
|
+
substitutePortInDeployment(obj[i], portNumber);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (typeof obj === 'object') {
|
|
106
|
+
for (const key of Object.keys(obj)) {
|
|
107
|
+
const value = obj[key];
|
|
108
|
+
if (typeof value === 'string' && value.includes(PORT_PLACEHOLDER)) {
|
|
109
|
+
obj[key] = value.split(PORT_PLACEHOLDER).join(String(portNumber));
|
|
110
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
111
|
+
substitutePortInDeployment(value, portNumber);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Regex to find ${VAR} placeholders for env substitution */
|
|
118
|
+
const ENV_VAR_PLACEHOLDER_REGEX = /\$\{([^}]+)\}/g;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Recursively replaces ${VAR} with envVarMap[VAR] in all string values of obj (in-place).
|
|
122
|
+
* Only substitutes when VAR is a key in envVarMap (from env-config.yaml / config).
|
|
123
|
+
*
|
|
124
|
+
* @param {Object} obj - Deployment manifest or any nested object
|
|
125
|
+
* @param {Object} envVarMap - Flat map of variable names to values (e.g. from buildEnvVarMap('docker'))
|
|
126
|
+
*/
|
|
127
|
+
function substituteEnvVarsInDeployment(obj, envVarMap) {
|
|
128
|
+
if (!envVarMap || typeof envVarMap !== 'object') return;
|
|
129
|
+
if (obj === null || obj === undefined) return;
|
|
130
|
+
if (Array.isArray(obj)) {
|
|
131
|
+
for (let i = 0; i < obj.length; i++) {
|
|
132
|
+
if (typeof obj[i] === 'string') {
|
|
133
|
+
obj[i] = obj[i].replace(ENV_VAR_PLACEHOLDER_REGEX, (match, varName) =>
|
|
134
|
+
Object.prototype.hasOwnProperty.call(envVarMap, varName) ? String(envVarMap[varName]) : match);
|
|
135
|
+
} else if (typeof obj[i] === 'object' && obj[i] !== null) {
|
|
136
|
+
substituteEnvVarsInDeployment(obj[i], envVarMap);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (typeof obj === 'object') {
|
|
142
|
+
for (const key of Object.keys(obj)) {
|
|
143
|
+
const value = obj[key];
|
|
144
|
+
if (typeof value === 'string') {
|
|
145
|
+
obj[key] = value.replace(ENV_VAR_PLACEHOLDER_REGEX, (match, varName) =>
|
|
146
|
+
Object.prototype.hasOwnProperty.call(envVarMap, varName) ? String(envVarMap[varName]) : match);
|
|
147
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
148
|
+
substituteEnvVarsInDeployment(value, envVarMap);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
62
154
|
/**
|
|
63
155
|
* Builds and validates deployment manifest
|
|
64
156
|
* @function buildAndValidateDeployment
|
|
@@ -66,10 +158,12 @@ function loadDeploymentConfigFiles(appPath, appType, appName) {
|
|
|
66
158
|
* @param {Object} variables - Variables configuration
|
|
67
159
|
* @param {Object} envTemplate - Environment template
|
|
68
160
|
* @param {Object} rbac - RBAC configuration
|
|
161
|
+
* @param {Object} [options] - Optional options
|
|
162
|
+
* @param {Object} [options.envVarMap] - Env vars from env-config (e.g. REDIS_HOST, DB_HOST) to resolve ${VAR} in manifest
|
|
69
163
|
* @returns {Object} Deployment manifest
|
|
70
164
|
* @throws {Error} If validation fails
|
|
71
165
|
*/
|
|
72
|
-
function buildAndValidateDeployment(appName, variables, envTemplate, rbac) {
|
|
166
|
+
function buildAndValidateDeployment(appName, variables, envTemplate, rbac, options = null) {
|
|
73
167
|
// Parse environment variables from template and merge portalInput from application config
|
|
74
168
|
const configuration = parseEnvironmentVariables(envTemplate, variables);
|
|
75
169
|
|
|
@@ -83,6 +177,23 @@ function buildAndValidateDeployment(appName, variables, envTemplate, rbac) {
|
|
|
83
177
|
throw new Error(`Generated deployment JSON does not match schema:\n${errorMessages}`);
|
|
84
178
|
}
|
|
85
179
|
|
|
180
|
+
// Replace ${PORT} with port from application.yaml so manifest deploys correctly
|
|
181
|
+
const effectivePort = getEffectivePortForSubstitution(variables, 3000);
|
|
182
|
+
substitutePortInDeployment(deployment, effectivePort);
|
|
183
|
+
if (deployment.port !== undefined) {
|
|
184
|
+
deployment.port = typeof deployment.port === 'string' && /^\d+$/.test(deployment.port)
|
|
185
|
+
? parseInt(deployment.port, 10) : (typeof deployment.port === 'number' ? deployment.port : effectivePort);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Resolve ${REDIS_HOST}, ${DB_HOST}, etc. from env-config.yaml so manifest has no unresolved vars
|
|
189
|
+
const envVarMap = options && options.envVarMap;
|
|
190
|
+
if (envVarMap) {
|
|
191
|
+
substituteEnvVarsInDeployment(deployment, envVarMap);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Ensure no other ${...} placeholders remain in manifest
|
|
195
|
+
_validator.validateNoUnresolvedVariablesInDeployment(deployment);
|
|
196
|
+
|
|
86
197
|
return deployment;
|
|
87
198
|
}
|
|
88
199
|
|
|
@@ -117,46 +228,81 @@ async function buildDeploymentManifestInMemory(appName, options = {}) {
|
|
|
117
228
|
const configuration = parseEnvironmentVariables(envTemplate, variables);
|
|
118
229
|
const deployment = builders.buildManifestStructure(appName, variablesWithVersion, configuration, rbac);
|
|
119
230
|
|
|
231
|
+
const effectivePort = getEffectivePortForSubstitution(variablesWithVersion, 3000);
|
|
232
|
+
substitutePortInDeployment(deployment, effectivePort);
|
|
233
|
+
if (deployment.port !== undefined) {
|
|
234
|
+
deployment.port = typeof deployment.port === 'string' && /^\d+$/.test(deployment.port)
|
|
235
|
+
? parseInt(deployment.port, 10) : (typeof deployment.port === 'number' ? deployment.port : effectivePort);
|
|
236
|
+
}
|
|
237
|
+
const envVarMap = await buildEnvVarMap('docker', null, null, { appPort: effectivePort });
|
|
238
|
+
substituteEnvVarsInDeployment(deployment, envVarMap);
|
|
239
|
+
_validator.validateNoUnresolvedVariablesInDeployment(deployment);
|
|
120
240
|
return { deployment, appPath };
|
|
121
241
|
}
|
|
122
242
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
return deployJsonPath;
|
|
243
|
+
/**
|
|
244
|
+
* Writes external system deploy JSON (manifest + ${PORT} substitution + validation).
|
|
245
|
+
* @async
|
|
246
|
+
* @param {string} appName - Application name
|
|
247
|
+
* @param {string} appPath - Application path
|
|
248
|
+
* @param {Object} options - Generation options
|
|
249
|
+
* @returns {Promise<string>} Path to written deploy JSON
|
|
250
|
+
*/
|
|
251
|
+
async function writeExternalDeployJson(appName, appPath, options) {
|
|
252
|
+
const manifest = await generateControllerManifest(appName, options);
|
|
253
|
+
let effectivePort = 3000;
|
|
254
|
+
try {
|
|
255
|
+
const variablesPath = resolveApplicationConfigPath(appPath);
|
|
256
|
+
const { parsed: variables } = loadVariables(variablesPath);
|
|
257
|
+
effectivePort = getEffectivePortForSubstitution(variables, 3000);
|
|
258
|
+
substitutePortInDeployment(manifest, effectivePort);
|
|
259
|
+
} catch {
|
|
260
|
+
substitutePortInDeployment(manifest, 3000);
|
|
142
261
|
}
|
|
262
|
+
const envVarMap = await buildEnvVarMap('docker', null, null, { appPort: effectivePort });
|
|
263
|
+
substituteEnvVarsInDeployment(manifest, envVarMap);
|
|
264
|
+
_validator.validateNoUnresolvedVariablesInDeployment(manifest);
|
|
265
|
+
const systemKey = manifest.key || appName;
|
|
266
|
+
const deployJsonPath = path.join(appPath, `${systemKey}-deploy.json`);
|
|
267
|
+
await fs.promises.writeFile(deployJsonPath, JSON.stringify(manifest, null, 2), { mode: 0o644, encoding: 'utf8' });
|
|
268
|
+
return deployJsonPath;
|
|
269
|
+
}
|
|
143
270
|
|
|
144
|
-
|
|
271
|
+
/**
|
|
272
|
+
* Writes regular app deploy JSON (build + validate + write).
|
|
273
|
+
* @async
|
|
274
|
+
* @param {string} appName - Application name
|
|
275
|
+
* @param {string} appPath - Application path
|
|
276
|
+
* @param {string} appType - Application type
|
|
277
|
+
* @returns {Promise<string>} Path to written deploy JSON
|
|
278
|
+
*/
|
|
279
|
+
async function writeRegularDeployJson(appName, appPath, appType) {
|
|
145
280
|
const { variables, envTemplate, rbac, jsonPath } = loadDeploymentConfigFiles(appPath, appType, appName);
|
|
146
281
|
const resolved = await resolveVersionForApp(appName, variables, { updateBuilder: false });
|
|
147
282
|
const variablesWithVersion = {
|
|
148
283
|
...variables,
|
|
149
284
|
app: { ...variables.app, version: resolved.version }
|
|
150
285
|
};
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
286
|
+
const effectivePort = getEffectivePortForSubstitution(variablesWithVersion, 3000);
|
|
287
|
+
const envVarMap = await buildEnvVarMap('docker', null, null, { appPort: effectivePort });
|
|
288
|
+
const deployment = buildAndValidateDeployment(appName, variablesWithVersion, envTemplate, rbac, { envVarMap });
|
|
154
289
|
const jsonContent = JSON.stringify(deployment, null, 2);
|
|
155
290
|
fs.writeFileSync(jsonPath, jsonContent, { mode: 0o644 });
|
|
156
|
-
|
|
157
291
|
return jsonPath;
|
|
158
292
|
}
|
|
159
293
|
|
|
294
|
+
async function generateDeployJson(appName, options = {}) {
|
|
295
|
+
if (!appName || typeof appName !== 'string') {
|
|
296
|
+
throw new Error('App name is required and must be a string');
|
|
297
|
+
}
|
|
298
|
+
const { isExternal, appPath, appType } = await detectAppType(appName);
|
|
299
|
+
logOfflinePathWhenType(appPath);
|
|
300
|
+
if (isExternal) {
|
|
301
|
+
return writeExternalDeployJson(appName, appPath, options);
|
|
302
|
+
}
|
|
303
|
+
return await writeRegularDeployJson(appName, appPath, appType);
|
|
304
|
+
}
|
|
305
|
+
|
|
160
306
|
async function generateDeployJsonWithValidation(appName, options = {}) {
|
|
161
307
|
const jsonPath = await generateDeployJson(appName, options);
|
|
162
308
|
const jsonContent = fs.readFileSync(jsonPath, 'utf8');
|
|
@@ -187,6 +333,9 @@ module.exports = {
|
|
|
187
333
|
generateDeployJson,
|
|
188
334
|
generateDeployJsonWithValidation,
|
|
189
335
|
buildDeploymentManifestInMemory,
|
|
336
|
+
getEffectivePortForSubstitution,
|
|
337
|
+
substitutePortInDeployment,
|
|
338
|
+
substituteEnvVarsInDeployment,
|
|
190
339
|
generateExternalSystemApplicationSchema,
|
|
191
340
|
splitExternalApplicationSchema,
|
|
192
341
|
parseEnvironmentVariables,
|
package/lib/generator/wizard.js
CHANGED
|
@@ -117,6 +117,14 @@ async function generateConfigFilesForWizard(params) {
|
|
|
117
117
|
// Generate env.template with authentication variables
|
|
118
118
|
await generateEnvTemplate(appPath, systemConfig);
|
|
119
119
|
|
|
120
|
+
const envTemplatePath = path.join(appPath, 'env.template');
|
|
121
|
+
try {
|
|
122
|
+
const secretsEnsure = require('../core/secrets-ensure');
|
|
123
|
+
await secretsEnsure.ensureSecretsFromEnvTemplate(envTemplatePath, { emptyValuesForCredentials: true });
|
|
124
|
+
} catch (err) {
|
|
125
|
+
if (err.code !== 'ENOENT') logger.warn(`Could not ensure integration placeholder secrets: ${err.message}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
120
128
|
// Generate README.md (use AI-generated content if available)
|
|
121
129
|
await generateReadme(appPath, appName, finalSystemKey, systemConfig, datasourceConfigs, aiGeneratedReadme);
|
|
122
130
|
|