@aifabrix/builder 2.40.0 → 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 +7 -5
- package/integration/hubspot/test.js +1 -1
- package/jest.config.manual.js +29 -0
- 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-auth.js +1 -0
- 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 +19 -4
- 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/auth-status.js +36 -3
- 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 +13 -1
- 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 +49 -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/validate.js +1 -1
- package/lib/validation/validator.js +65 -0
- package/package.json +4 -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/external-system/external-system.json.hbs +1 -16
- package/templates/python/docker-compose.hbs +49 -23
- package/templates/typescript/docker-compose.hbs +48 -22
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Builder Server (dev) API type definitions – issue-cert, settings, users, SSH keys, secrets
|
|
3
|
+
* @author AI Fabrix Team
|
|
4
|
+
* @version 2.0.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Issue certificate request (POST /api/dev/issue-cert). Public; no client cert.
|
|
9
|
+
* @typedef {Object} IssueCertDto
|
|
10
|
+
* @property {string} developerId - Developer ID (must match user for whom PIN was created)
|
|
11
|
+
* @property {string} pin - One-time PIN from POST /api/dev/users/:id/pin
|
|
12
|
+
* @property {string} csr - PEM-encoded Certificate Signing Request
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Issue certificate response (POST /api/dev/issue-cert)
|
|
17
|
+
* @typedef {Object} IssueCertResponseDto
|
|
18
|
+
* @property {string} certificate - PEM-encoded X.509 certificate
|
|
19
|
+
* @property {number} validDays - Validity in days
|
|
20
|
+
* @property {string} validNotAfter - ISO 8601 validity end (UTC)
|
|
21
|
+
* @property {string} [caCertificate] - Optional PEM-encoded CA certificate (for remote Docker TLS; saved as ca.pem)
|
|
22
|
+
* @property {string} [ca] - Optional alias for caCertificate
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Developer settings (GET /api/dev/settings). Cert-authenticated.
|
|
27
|
+
* @typedef {Object} SettingsResponseDto
|
|
28
|
+
* @property {string} user-mutagen-folder - Server path to workspace root (no app segment)
|
|
29
|
+
* @property {string} secrets-encryption - Encryption key (hex)
|
|
30
|
+
* @property {string} aifabrix-secrets - Path or URL for secrets
|
|
31
|
+
* @property {string} aifabrix-env-config - Env config path
|
|
32
|
+
* @property {string} remote-server - Builder-server base URL
|
|
33
|
+
* @property {string} docker-endpoint - Docker API endpoint
|
|
34
|
+
* @property {string} sync-ssh-user - SSH user for Mutagen
|
|
35
|
+
* @property {string} sync-ssh-host - SSH host for Mutagen
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* User list item (GET /api/dev/users)
|
|
40
|
+
* @typedef {Object} UserResponseDto
|
|
41
|
+
* @property {string} id - Developer ID
|
|
42
|
+
* @property {string} name - Display name
|
|
43
|
+
* @property {string} email - Email
|
|
44
|
+
* @property {string} createdAt - ISO 8601
|
|
45
|
+
* @property {boolean} certificateIssued - Whether cert was issued
|
|
46
|
+
* @property {string} [certificateValidNotAfter] - Cert validity end (optional)
|
|
47
|
+
* @property {string[]} groups - Access groups (admin, secret-manager, developer)
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Create user request (POST /api/dev/users)
|
|
52
|
+
* @typedef {Object} CreateUserDto
|
|
53
|
+
* @property {string} developerId - Unique developer ID (numeric string)
|
|
54
|
+
* @property {string} name - Display name
|
|
55
|
+
* @property {string} email - Email
|
|
56
|
+
* @property {string[]} [groups] - Default [developer]
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Update user request (PATCH /api/dev/users/:id). At least one field.
|
|
61
|
+
* @typedef {Object} UpdateUserDto
|
|
62
|
+
* @property {string} [name] - Display name
|
|
63
|
+
* @property {string} [email] - Email
|
|
64
|
+
* @property {string[]} [groups] - Access groups
|
|
65
|
+
*/
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Create PIN response (POST /api/dev/users/:id/pin)
|
|
69
|
+
* @typedef {Object} CreatePinResponseDto
|
|
70
|
+
* @property {string} pin - One-time PIN
|
|
71
|
+
* @property {string} expiresAt - ISO 8601
|
|
72
|
+
*/
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Add SSH key request (POST /api/dev/users/:id/ssh-keys)
|
|
76
|
+
* @typedef {Object} AddSshKeyDto
|
|
77
|
+
* @property {string} publicKey - SSH public key line
|
|
78
|
+
* @property {string} [label] - Optional label
|
|
79
|
+
*/
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* SSH key item (list/add response)
|
|
83
|
+
* @typedef {Object} SshKeyItemDto
|
|
84
|
+
* @property {string} fingerprint - Key fingerprint
|
|
85
|
+
* @property {string} [label] - Optional label
|
|
86
|
+
* @property {string} [createdAt] - ISO 8601
|
|
87
|
+
*/
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Deleted response (DELETE endpoints)
|
|
91
|
+
* @typedef {Object} DeletedResponseDto
|
|
92
|
+
* @property {string} deleted - ID or key of deleted resource
|
|
93
|
+
*/
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Secret item (GET /api/dev/secrets)
|
|
97
|
+
* @typedef {Object} SecretItemDto
|
|
98
|
+
* @property {string} name - Secret key
|
|
99
|
+
* @property {string} value - Decrypted value
|
|
100
|
+
*/
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Add secret request (POST /api/dev/secrets)
|
|
104
|
+
* @typedef {Object} AddSecretDto
|
|
105
|
+
* @property {string} key - Secret key
|
|
106
|
+
* @property {string} value - Secret value
|
|
107
|
+
*/
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Add secret response
|
|
111
|
+
* @typedef {Object} AddSecretResponseDto
|
|
112
|
+
* @property {string} key - Key that was added/updated
|
|
113
|
+
*/
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Delete secret response
|
|
117
|
+
* @typedef {Object} DeleteSecretResponseDto
|
|
118
|
+
* @property {string} deleted - Key that was removed
|
|
119
|
+
*/
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Health response (GET /health)
|
|
123
|
+
* @typedef {Object} HealthResponseDto
|
|
124
|
+
* @property {string} status - Overall status, e.g. "ok"
|
|
125
|
+
* @property {Object} checks - Per-component health checks
|
|
126
|
+
* @property {string} checks.dataDir - Data directory check ("ok" or error)
|
|
127
|
+
* @property {string} checks.encryptionKey - Encryption key check ("ok" or error)
|
|
128
|
+
* @property {string} checks.ca - CA certificate check ("ok" or error)
|
|
129
|
+
* @property {string} checks.users - Users store check ("ok" or error)
|
|
130
|
+
* @property {string} checks.tokens - Tokens store check ("ok" or error)
|
|
131
|
+
*/
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Error response (all error responses)
|
|
135
|
+
* @typedef {Object} ErrorResponseDto
|
|
136
|
+
* @property {number} statusCode - HTTP status
|
|
137
|
+
* @property {string} error - Short error type
|
|
138
|
+
* @property {string} message - Human-readable message
|
|
139
|
+
* @property {string} [code] - Optional machine-readable code
|
|
140
|
+
*/
|
package/lib/app/config.js
CHANGED
|
@@ -31,6 +31,26 @@ async function fileExists(filePath) {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Renames legacy variables.yaml to application.yaml if only variables.yaml exists.
|
|
36
|
+
* Ensures create always results in application.yaml.
|
|
37
|
+
* @async
|
|
38
|
+
* @param {string} appPath - Path to application directory
|
|
39
|
+
*/
|
|
40
|
+
async function normalizeLegacyVariablesYaml(appPath) {
|
|
41
|
+
const applicationYaml = path.join(appPath, 'application.yaml');
|
|
42
|
+
const applicationYml = path.join(appPath, 'application.yml');
|
|
43
|
+
const applicationJson = path.join(appPath, 'application.json');
|
|
44
|
+
const variablesYaml = path.join(appPath, 'variables.yaml');
|
|
45
|
+
const hasAppYaml = await fileExists(applicationYaml);
|
|
46
|
+
const hasAppYml = await fileExists(applicationYml);
|
|
47
|
+
const hasAppJson = await fileExists(applicationJson);
|
|
48
|
+
const hasVariables = await fileExists(variablesYaml);
|
|
49
|
+
if (hasVariables && !hasAppYaml && !hasAppYml && !hasAppJson) {
|
|
50
|
+
await fs.rename(variablesYaml, applicationYaml);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
34
54
|
/**
|
|
35
55
|
* Generates application.yaml file if no application config exists
|
|
36
56
|
* @async
|
|
@@ -39,6 +59,7 @@ async function fileExists(filePath) {
|
|
|
39
59
|
* @param {Object} config - Application configuration
|
|
40
60
|
*/
|
|
41
61
|
async function generateVariablesYamlFile(appPath, appName, config) {
|
|
62
|
+
await normalizeLegacyVariablesYaml(appPath);
|
|
42
63
|
const { resolveApplicationConfigPath } = require('../utils/app-config-resolver');
|
|
43
64
|
try {
|
|
44
65
|
resolveApplicationConfigPath(appPath);
|
package/lib/app/down.js
CHANGED
package/lib/app/index.js
CHANGED
|
@@ -31,6 +31,8 @@ const {
|
|
|
31
31
|
processTemplateFiles,
|
|
32
32
|
setupAppFiles
|
|
33
33
|
} = require('./helpers');
|
|
34
|
+
const path = require('path');
|
|
35
|
+
const secretsEnsure = require('../core/secrets-ensure');
|
|
34
36
|
|
|
35
37
|
/**
|
|
36
38
|
* Creates new application with scaffolded configuration files
|
|
@@ -130,6 +132,13 @@ async function generateApplicationFiles(finalAppPath, appName, config, options)
|
|
|
130
132
|
|
|
131
133
|
await generateConfigFiles(finalAppPath, appName, config, existingEnv);
|
|
132
134
|
|
|
135
|
+
const envTemplatePath = path.join(finalAppPath, 'env.template');
|
|
136
|
+
try {
|
|
137
|
+
await secretsEnsure.ensureSecretsFromEnvTemplate(envTemplatePath, {});
|
|
138
|
+
} catch (err) {
|
|
139
|
+
if (err.code !== 'ENOENT') throw err;
|
|
140
|
+
}
|
|
141
|
+
|
|
133
142
|
// Generate external system files if type is external
|
|
134
143
|
if (config.type === 'external') {
|
|
135
144
|
const externalGenerator = require('../external-system/generator');
|
package/lib/app/push.js
CHANGED
|
@@ -40,21 +40,38 @@ function validateAppName(appName) {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* @param {
|
|
46
|
-
* @returns {
|
|
43
|
+
* Returns effective config (unwrap variables wrapper if present so image/app are at top level).
|
|
44
|
+
* application.yaml may have top-level image/app or a variables: { image, app } wrapper.
|
|
45
|
+
* @param {Object} config - Raw config from loadConfigFile
|
|
46
|
+
* @returns {Object} Config with image and app at top level
|
|
47
|
+
*/
|
|
48
|
+
function getEffectiveConfig(config) {
|
|
49
|
+
if (!config || typeof config !== 'object') return config || {};
|
|
50
|
+
if (config.variables && typeof config.variables === 'object' && (config.variables.image !== undefined || config.variables.app !== undefined)) {
|
|
51
|
+
return config.variables;
|
|
52
|
+
}
|
|
53
|
+
return config;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Extracts image name from configuration using the same logic as build command.
|
|
58
|
+
* Uses image.name (e.g. aifabrix/dataplane) so ACR repository matches application.yaml.
|
|
59
|
+
* @param {Object} config - Configuration object from application.yaml (or effective config)
|
|
60
|
+
* @param {string} appName - Application name (fallback when image not set)
|
|
61
|
+
* @returns {string} Image name (e.g. aifabrix/dataplane, not just dataplane)
|
|
47
62
|
*/
|
|
48
63
|
function extractImageName(config, appName) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return
|
|
64
|
+
const c = getEffectiveConfig(config);
|
|
65
|
+
if (typeof c.image === 'string') {
|
|
66
|
+
return c.image.split(':')[0];
|
|
67
|
+
}
|
|
68
|
+
if (c.image?.name) {
|
|
69
|
+
return c.image.name;
|
|
70
|
+
}
|
|
71
|
+
if (c.app?.key) {
|
|
72
|
+
return c.app.key;
|
|
55
73
|
}
|
|
56
74
|
return appName;
|
|
57
|
-
|
|
58
75
|
}
|
|
59
76
|
|
|
60
77
|
/**
|
|
@@ -72,7 +89,8 @@ async function loadPushConfig(appName, options) {
|
|
|
72
89
|
try {
|
|
73
90
|
const configPath = resolveApplicationConfigPath(appPath);
|
|
74
91
|
const config = loadConfigFile(configPath);
|
|
75
|
-
const
|
|
92
|
+
const effective = getEffectiveConfig(config);
|
|
93
|
+
const registry = options.registry || effective.image?.registry;
|
|
76
94
|
if (!registry) {
|
|
77
95
|
throw new Error('Registry URL is required. Provide via --registry flag or configure in application config under image.registry');
|
|
78
96
|
}
|
|
@@ -110,6 +128,12 @@ async function validatePushConfig(registry, imageName, appName) {
|
|
|
110
128
|
if (!await pushUtils.checkAzureCLIInstalled()) {
|
|
111
129
|
throw new Error('Azure CLI is not installed. Install from: https://docs.microsoft.com/cli/azure/install-azure-cli');
|
|
112
130
|
}
|
|
131
|
+
|
|
132
|
+
if (!await pushUtils.checkAzureLogin()) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
'Not logged in to Azure. Run "az login" first, then run push again.'
|
|
135
|
+
);
|
|
136
|
+
}
|
|
113
137
|
}
|
|
114
138
|
|
|
115
139
|
/**
|
package/lib/app/readme.js
CHANGED
|
@@ -120,9 +120,7 @@ function buildExternalDatasourcePlaceholders(systemKey, datasourceCount) {
|
|
|
120
120
|
function buildReadmeContext(appName, config) {
|
|
121
121
|
const displayName = config.displayName || formatAppDisplayName(appName);
|
|
122
122
|
const port = config.port ?? 3000;
|
|
123
|
-
const localPort =
|
|
124
|
-
? config.build.localPort
|
|
125
|
-
: port;
|
|
123
|
+
const localPort = port;
|
|
126
124
|
const imageName = config.image?.name || `aifabrix/${appName}`;
|
|
127
125
|
// Extract registry from nested structure (config.image.registry) or flattened (config.registry)
|
|
128
126
|
const registry = config.image?.registry || config.registry || 'myacr.azurecr.io';
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers for application run: clean applications dir, build merged .env, compose safeguard.
|
|
3
|
+
* Keeps run-helpers.js under line limit.
|
|
4
|
+
*
|
|
5
|
+
* @fileoverview Run env and compose helpers
|
|
6
|
+
* @author AI Fabrix Team
|
|
7
|
+
* @version 2.0.0
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const fs = require('fs').promises;
|
|
14
|
+
const fsSync = require('fs');
|
|
15
|
+
const pathsUtil = require('../utils/paths');
|
|
16
|
+
const adminSecrets = require('../core/admin-secrets');
|
|
17
|
+
const secretsEnvWrite = require('../core/secrets-env-write');
|
|
18
|
+
const { getContainerPort } = require('../utils/port-resolver');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Clean applications directory: remove generated docker-compose.yaml and .env.* files.
|
|
22
|
+
* @param {string|number} developerId - Developer ID
|
|
23
|
+
*/
|
|
24
|
+
function cleanApplicationsDir(developerId) {
|
|
25
|
+
const baseDir = pathsUtil.getApplicationsBaseDir(developerId);
|
|
26
|
+
if (!fsSync.existsSync(baseDir)) return;
|
|
27
|
+
const toRemove = [path.join(baseDir, 'docker-compose.yaml')];
|
|
28
|
+
try {
|
|
29
|
+
const entries = fsSync.readdirSync(baseDir);
|
|
30
|
+
for (const name of entries) {
|
|
31
|
+
if (name.startsWith('.env.')) toRemove.push(path.join(baseDir, name));
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
// Ignore readdir errors
|
|
35
|
+
}
|
|
36
|
+
for (const filePath of toRemove) {
|
|
37
|
+
try {
|
|
38
|
+
if (fsSync.existsSync(filePath)) fsSync.unlinkSync(filePath);
|
|
39
|
+
} catch {
|
|
40
|
+
// Ignore unlink errors
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Derive PostgreSQL user from database name (same as compose-handlebars-helpers pgUserName).
|
|
47
|
+
* @param {string} dbName - Database name (e.g. keycloak)
|
|
48
|
+
* @returns {string} User name (e.g. keycloak_user)
|
|
49
|
+
*/
|
|
50
|
+
function pgUserName(dbName) {
|
|
51
|
+
if (!dbName) return '';
|
|
52
|
+
return `${String(dbName).replace(/-/g, '_')}_user`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Inject DB_N_NAME and DB_N_USER from application.yaml databases into env so .env has everything.
|
|
57
|
+
* @param {Object} env - Merged env object (mutated)
|
|
58
|
+
* @param {Object} appConfig - Application config (requires.databases or databases array)
|
|
59
|
+
*/
|
|
60
|
+
function injectDatabaseNamesAndUsers(env, appConfig) {
|
|
61
|
+
const databases = appConfig?.requires?.databases || appConfig?.databases;
|
|
62
|
+
if (!Array.isArray(databases) || databases.length === 0) return;
|
|
63
|
+
for (let i = 0; i < databases.length; i++) {
|
|
64
|
+
const db = databases[i];
|
|
65
|
+
const name = db?.name || (appConfig?.app?.key || 'app');
|
|
66
|
+
env[`DB_${i}_NAME`] = name;
|
|
67
|
+
env[`DB_${i}_USER`] = pgUserName(name);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get the env var name used for PORT in env.template (e.g. PORT=${MISO_PORT} -> MISO_PORT).
|
|
73
|
+
* @param {string} appName - Application name
|
|
74
|
+
* @returns {string|null} Variable name or null if not found
|
|
75
|
+
*/
|
|
76
|
+
function getPortVarFromEnvTemplate(appName) {
|
|
77
|
+
const builderPath = pathsUtil.getBuilderPath(appName);
|
|
78
|
+
const templatePath = path.join(builderPath, 'env.template');
|
|
79
|
+
if (!fsSync.existsSync(templatePath)) return null;
|
|
80
|
+
try {
|
|
81
|
+
const content = fsSync.readFileSync(templatePath, 'utf8');
|
|
82
|
+
const m = content.match(/^PORT\s*=\s*\$\{([A-Za-z0-9_]+)\}/m);
|
|
83
|
+
return m ? m[1] : null;
|
|
84
|
+
} catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Override PORT and the template's port variable (e.g. MISO_PORT) with container port from application.yaml.
|
|
91
|
+
* Run .env only: when running in Docker, the app listens on the container port (port or build.containerPort), not localPort.
|
|
92
|
+
* For envOutputPath .env (local, not reload) we use localPort instead - see adjustLocalEnvPortsInContent in secrets-helpers.
|
|
93
|
+
* @param {Object} env - Merged env object (mutated)
|
|
94
|
+
* @param {Object} appConfig - Application configuration (port, build.containerPort)
|
|
95
|
+
* @param {string} appName - Application name (to resolve env.template port var)
|
|
96
|
+
*/
|
|
97
|
+
function injectContainerPortForRun(env, appConfig, appName) {
|
|
98
|
+
const containerPort = getContainerPort(appConfig, 3000);
|
|
99
|
+
env.PORT = String(containerPort);
|
|
100
|
+
const portVar = getPortVarFromEnvTemplate(appName);
|
|
101
|
+
if (portVar) {
|
|
102
|
+
env[portVar] = String(containerPort);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Keys that must never be passed to the app container (admin/start-only). */
|
|
107
|
+
const ADMIN_ONLY_KEYS = [
|
|
108
|
+
'POSTGRES_PASSWORD',
|
|
109
|
+
'PGADMIN_DEFAULT_EMAIL',
|
|
110
|
+
'PGADMIN_DEFAULT_PASSWORD',
|
|
111
|
+
'REDIS_HOST',
|
|
112
|
+
'REDIS_COMMANDER_USER',
|
|
113
|
+
'REDIS_COMMANDER_PASSWORD'
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Build app-only env (merged minus admin secrets). App container must not receive admin passwords.
|
|
118
|
+
* @param {Object} merged - Full merged env
|
|
119
|
+
* @returns {Object} Env object safe for app container
|
|
120
|
+
*/
|
|
121
|
+
function buildAppOnlyEnv(merged) {
|
|
122
|
+
const appOnly = {};
|
|
123
|
+
for (const [k, v] of Object.entries(merged)) {
|
|
124
|
+
if (ADMIN_ONLY_KEYS.includes(k)) continue;
|
|
125
|
+
appOnly[k] = v;
|
|
126
|
+
}
|
|
127
|
+
return appOnly;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Build env for db-init only: POSTGRES_PASSWORD + DB_N_PASSWORD, DB_N_NAME, DB_N_USER. Used only for start, not in app container.
|
|
132
|
+
* @param {Object} merged - Full merged env
|
|
133
|
+
* @returns {Object} Env object for db-init service only
|
|
134
|
+
*/
|
|
135
|
+
function buildDbInitOnlyEnv(merged) {
|
|
136
|
+
const dbInit = {};
|
|
137
|
+
if (merged.POSTGRES_PASSWORD !== undefined) {
|
|
138
|
+
dbInit.POSTGRES_PASSWORD = merged.POSTGRES_PASSWORD;
|
|
139
|
+
}
|
|
140
|
+
for (const [k, v] of Object.entries(merged)) {
|
|
141
|
+
if (k.startsWith('DB_') && (k.endsWith('_PASSWORD') || k.endsWith('_NAME') || k.endsWith('_USER'))) {
|
|
142
|
+
dbInit[k] = v;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return dbInit;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Build two run env files: .env.run (app-only, no admin secrets) and .env.run.admin (start-only, for db-init).
|
|
150
|
+
* Admin password is never set in the app container; .env.run.admin is used only for start and then deleted.
|
|
151
|
+
* @async
|
|
152
|
+
* @param {string} appName - Application name
|
|
153
|
+
* @param {Object} appConfig - Application configuration
|
|
154
|
+
* @param {string} devDir - Applications directory path
|
|
155
|
+
* @returns {Promise<{ runEnvPath: string, runEnvAdminPath: string }>} Paths to .env.run and .env.run.admin
|
|
156
|
+
*/
|
|
157
|
+
async function buildMergedRunEnvAndWrite(appName, appConfig, devDir) {
|
|
158
|
+
const infra = require('../infrastructure');
|
|
159
|
+
const ensureAdminSecretsFn = typeof infra.ensureAdminSecrets === 'function'
|
|
160
|
+
? infra.ensureAdminSecrets
|
|
161
|
+
: require('../infrastructure/helpers').ensureAdminSecrets;
|
|
162
|
+
await ensureAdminSecretsFn();
|
|
163
|
+
const adminObj = await adminSecrets.readAndDecryptAdminSecrets();
|
|
164
|
+
const appObj = await secretsEnvWrite.resolveAndGetEnvMap(appName, {
|
|
165
|
+
environment: 'docker',
|
|
166
|
+
secretsPath: null,
|
|
167
|
+
force: false
|
|
168
|
+
});
|
|
169
|
+
const merged = { ...adminObj, ...appObj };
|
|
170
|
+
injectDatabaseNamesAndUsers(merged, appConfig);
|
|
171
|
+
injectContainerPortForRun(merged, appConfig, appName);
|
|
172
|
+
|
|
173
|
+
const runEnvPath = path.join(devDir, '.env.run');
|
|
174
|
+
const runEnvAdminPath = path.join(devDir, '.env.run.admin');
|
|
175
|
+
|
|
176
|
+
const appOnly = buildAppOnlyEnv(merged);
|
|
177
|
+
const dbInitOnly = buildDbInitOnlyEnv(merged);
|
|
178
|
+
|
|
179
|
+
await fs.writeFile(runEnvPath, adminSecrets.envObjectToContent(appOnly), { mode: 0o600 });
|
|
180
|
+
await fs.writeFile(runEnvAdminPath, adminSecrets.envObjectToContent(dbInitOnly), { mode: 0o600 });
|
|
181
|
+
|
|
182
|
+
return { runEnvPath, runEnvAdminPath };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Assert generated compose does not contain password literals in environment (ISO 27K).
|
|
187
|
+
* @param {string} composeContent - Generated docker-compose content
|
|
188
|
+
* @throws {Error} If password keys appear in environment-like assignment
|
|
189
|
+
*/
|
|
190
|
+
function assertNoPasswordLiteralsInCompose(composeContent) {
|
|
191
|
+
const badPattern = /\n\s+(-?\s*)(POSTGRES_PASSWORD|DB_\d+_PASSWORD)\s*[:=]/;
|
|
192
|
+
if (badPattern.test(composeContent)) {
|
|
193
|
+
throw new Error('Generated compose must not contain password literals (POSTGRES_PASSWORD, DB_*_PASSWORD). Use env_file only.');
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
module.exports = {
|
|
198
|
+
cleanApplicationsDir,
|
|
199
|
+
buildMergedRunEnvAndWrite,
|
|
200
|
+
assertNoPasswordLiteralsInCompose
|
|
201
|
+
};
|