@aifabrix/builder 2.61.2 → 2.63.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/lib/api/applications.api.js +22 -2
- package/lib/api/bootstrap-snapshot.api.js +115 -0
- package/lib/api/onboarding-status.api.js +117 -0
- package/lib/app/deploy.js +14 -5
- package/lib/app/register.js +24 -2
- package/lib/app/run-env-compose.js +44 -8
- package/lib/app/run-env-recovery.js +1 -0
- package/lib/app/run-helpers.js +1 -1
- package/lib/app/run.js +6 -4
- package/lib/build/standard-docker-build.js +4 -0
- package/lib/channels/channel-artifact-generator.js +13 -3
- package/lib/cli/infra-guided-footers.js +10 -8
- package/lib/cli/infra-guided.js +60 -12
- package/lib/cli/setup-infra-up-platform-action.js +14 -1
- package/lib/cli/setup-platform.js +20 -3
- package/lib/commands/auth-config.js +2 -0
- package/lib/commands/login.js +4 -0
- package/lib/commands/repair-datasource-apply.js +3 -1
- package/lib/commands/repair-datasource-legacy.js +94 -0
- package/lib/commands/setup-image-refresh.js +26 -0
- package/lib/commands/setup-modes.js +9 -2
- package/lib/commands/setup-onboarding-readiness.js +83 -0
- package/lib/commands/setup-prompts-platform-mode.js +34 -0
- package/lib/commands/setup.js +35 -10
- package/lib/commands/up-builder-api.js +24 -8
- package/lib/commands/up-common.js +16 -1
- package/lib/commands/up-dataplane-bootstrap.js +149 -16
- package/lib/commands/up-dataplane-credentials.js +21 -0
- package/lib/commands/up-dataplane.js +26 -34
- package/lib/commands/up-integration-server.js +112 -0
- package/lib/commands/wizard-config-normalizer.js +5 -1
- package/lib/core/admin-secrets-env-overlay.js +8 -3
- package/lib/core/environment-access-policy.js +121 -0
- package/lib/core/environment-mode-policy.js +177 -0
- package/lib/core/local-env-overrides.js +149 -0
- package/lib/core/secrets-ensure.js +4 -2
- package/lib/core/secrets-env-content.js +12 -9
- package/lib/core/secrets-env-write.js +1 -1
- package/lib/datasource/capability/capability-manifest.js +1 -0
- package/lib/deployment/installation/index.js +14 -1
- package/lib/deployment/installation/infra-catalog.js +3 -0
- package/lib/deployment/installation/local-environment-stage.js +285 -0
- package/lib/deployment/installation/local-installation.js +76 -0
- package/lib/deployment/installation/stage-input.js +5 -1
- package/lib/generator/builders.js +3 -1
- package/lib/generator/index.js +32 -9
- package/lib/generator/split-variables.js +2 -2
- package/lib/parameters/database-adoption.js +125 -0
- package/lib/parameters/physical-database-naming.js +136 -0
- package/lib/parameters/physical-secret-name.js +194 -0
- package/lib/parameters/rsa-secret-values.js +165 -0
- package/lib/programmatic/run-channel-install-artifacts.js +28 -2
- package/lib/programmatic/run-channel-package.js +3 -1
- package/lib/programmatic/workspace-context.js +37 -2
- package/lib/schema/infra.parameter.yaml +61 -27
- package/lib/schema/infrastructure-schema.json +213 -120
- package/lib/utils/compose-generate-docker-compose.js +2 -2
- package/lib/utils/compose-generator.js +4 -7
- package/lib/utils/compose-miso-env.js +2 -2
- package/lib/utils/compose-traefik-ingress-base.js +3 -4
- package/lib/utils/dataplane-dimension-abac-setting.js +120 -0
- package/lib/utils/declarative-url-ports.js +3 -0
- package/lib/utils/env-environment-file-paths.js +105 -0
- package/lib/utils/environment-scoped-resources.js +48 -14
- package/lib/utils/log-redaction.js +4 -1
- package/lib/utils/paths-system-builder-keys.js +7 -1
- package/lib/utils/paths.js +7 -2
- package/lib/utils/postgres-platform-bootstrap.js +35 -1
- package/lib/utils/redis-env-scope.js +21 -1
- package/lib/utils/registry-auth-sources.js +154 -0
- package/lib/utils/registry-credentials.js +131 -14
- package/lib/utils/secrets-generator.js +30 -21
- package/lib/utils/secrets-helpers.js +5 -7
- package/lib/utils/secrets-kv-scope.js +127 -28
- package/lib/utils/secrets-materialize-local.js +9 -0
- package/lib/utils/secrets-missing-error.js +35 -1
- package/lib/utils/token-manager.js +14 -8
- package/lib/utils/url-public-path-prefix.js +6 -10
- package/package.json +4 -2
- package/templates/README.md +6 -0
- package/templates/applications/builder-api/application.yaml +1 -1
- package/templates/applications/dataplane/application.yaml +2 -2
- package/templates/applications/dataplane/env.template +1 -1
- package/templates/applications/integration-server/application.yaml +91 -0
- package/templates/applications/integration-server/env.template +215 -0
- package/templates/applications/mirrored-templates.json +52 -0
- package/templates/applications/miso-controller/application.yaml +43 -1
- package/templates/applications/miso-controller/env.template +45 -39
- package/templates/marketplace/createUiDefinition.json +52 -4
- package/templates/marketplace/main.json +85 -7
|
@@ -22,7 +22,8 @@ const { isVectorDatabaseName } = require('./compose-vector-helper');
|
|
|
22
22
|
const { resolveMisoEnvironment } = require('./compose-miso-env');
|
|
23
23
|
const {
|
|
24
24
|
buildScopedLocalContainerName,
|
|
25
|
-
composeTraefikServiceKey
|
|
25
|
+
composeTraefikServiceKey,
|
|
26
|
+
isScopeActiveForOptions
|
|
26
27
|
} = require('./environment-scoped-resources');
|
|
27
28
|
const {
|
|
28
29
|
derivePathFromPattern,
|
|
@@ -308,9 +309,7 @@ function buildServiceConfig(appName, config, port, devId, runExtras = {}) {
|
|
|
308
309
|
const hostPort = port;
|
|
309
310
|
const useTraefikScope =
|
|
310
311
|
scopeOpts &&
|
|
311
|
-
scopeOpts
|
|
312
|
-
scopeOpts.runEnvKey &&
|
|
313
|
-
(scopeOpts.runEnvKey === 'dev' || scopeOpts.runEnvKey === 'tst');
|
|
312
|
+
isScopeActiveForOptions(scopeOpts);
|
|
314
313
|
const composeAppKey = useTraefikScope
|
|
315
314
|
? composeTraefikServiceKey(appName, scopeOpts.runEnvKey)
|
|
316
315
|
: null;
|
|
@@ -380,9 +379,7 @@ function buildNetworkAndContainerNames(appName, devId, idNum, scopeOpts = null)
|
|
|
380
379
|
let containerName;
|
|
381
380
|
if (
|
|
382
381
|
scopeOpts &&
|
|
383
|
-
scopeOpts
|
|
384
|
-
scopeOpts.runEnvKey &&
|
|
385
|
-
(scopeOpts.runEnvKey === 'dev' || scopeOpts.runEnvKey === 'tst')
|
|
382
|
+
isScopeActiveForOptions(scopeOpts)
|
|
386
383
|
) {
|
|
387
384
|
containerName = buildScopedLocalContainerName(appName, devId, idNum, scopeOpts.runEnvKey);
|
|
388
385
|
} else {
|
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* @param {Object} options - Run options
|
|
11
|
-
* @returns {'dev'|'tst'|'pro'}
|
|
11
|
+
* @returns {'miso'|'dev'|'tst'|'pro'}
|
|
12
12
|
*/
|
|
13
13
|
function resolveMisoEnvironment(options) {
|
|
14
14
|
const env = (options.env && typeof options.env === 'string') ? options.env.toLowerCase() : 'dev';
|
|
15
|
-
return (env === 'tst' || env === 'pro') ? env : 'dev';
|
|
15
|
+
return (env === 'miso' || env === 'tst' || env === 'pro') ? env : 'dev';
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
module.exports = { resolveMisoEnvironment };
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
const {
|
|
10
|
-
buildEnvScopedTraefikPath
|
|
10
|
+
buildEnvScopedTraefikPath,
|
|
11
|
+
isScopeActiveForOptions
|
|
11
12
|
} = require('./environment-scoped-resources');
|
|
12
13
|
const { parseDeveloperIdNum } = require('./declarative-url-ports');
|
|
13
14
|
const {
|
|
@@ -122,9 +123,7 @@ function buildTraefikIngressBase(config, devId, scopeOpts, remoteServer, develop
|
|
|
122
123
|
let pathOut = basePath;
|
|
123
124
|
if (
|
|
124
125
|
scopeOpts &&
|
|
125
|
-
scopeOpts
|
|
126
|
-
scopeOpts.runEnvKey &&
|
|
127
|
-
(scopeOpts.runEnvKey === 'dev' || scopeOpts.runEnvKey === 'tst')
|
|
126
|
+
isScopeActiveForOptions(scopeOpts)
|
|
128
127
|
) {
|
|
129
128
|
pathOut = buildEnvScopedTraefikPath(basePath, scopeOpts.runEnvKey);
|
|
130
129
|
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve and materialize the Dataplane Dimension ABAC installer setting.
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Strict non-secret Dimension ABAC setting helpers
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 1.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const SETTING_NAME = 'DIMENSION_ABAC_ENFORCEMENT_ENABLED';
|
|
12
|
+
const CONFIG_KEY = 'dimensionAbacEnforcementEnabled';
|
|
13
|
+
const DATAPLANE_APP = 'dataplane';
|
|
14
|
+
|
|
15
|
+
function settingError(message) {
|
|
16
|
+
const error = new Error(message);
|
|
17
|
+
error.code = 'DIMENSION_ABAC_SETTING_INVALID';
|
|
18
|
+
return error;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parseDimensionAbacOption(raw) {
|
|
22
|
+
if (raw === undefined || raw === null) return undefined;
|
|
23
|
+
if (raw === true || raw === false) return raw;
|
|
24
|
+
const value = String(raw).trim().toLowerCase();
|
|
25
|
+
if (value === 'on') return true;
|
|
26
|
+
if (value === 'off') return false;
|
|
27
|
+
throw settingError('Invalid --dimension-abac value. Use on or off.');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readStoredValue(userConfig) {
|
|
31
|
+
const applications = userConfig && userConfig.applications;
|
|
32
|
+
if (applications === undefined || applications === null) return undefined;
|
|
33
|
+
if (typeof applications !== 'object' || Array.isArray(applications)) {
|
|
34
|
+
throw settingError('Invalid applications configuration; expected an object.');
|
|
35
|
+
}
|
|
36
|
+
const entry = applications[DATAPLANE_APP];
|
|
37
|
+
if (entry === undefined || entry === null) return undefined;
|
|
38
|
+
if (typeof entry !== 'object' || Array.isArray(entry)) {
|
|
39
|
+
throw settingError('Invalid applications.dataplane configuration; expected an object.');
|
|
40
|
+
}
|
|
41
|
+
if (!Object.prototype.hasOwnProperty.call(entry, CONFIG_KEY)) return undefined;
|
|
42
|
+
if (typeof entry[CONFIG_KEY] !== 'boolean') {
|
|
43
|
+
throw settingError(`Invalid applications.dataplane.${CONFIG_KEY}; expected a boolean.`);
|
|
44
|
+
}
|
|
45
|
+
return entry[CONFIG_KEY];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function resolveDimensionAbacSetting(userConfig, explicitValue) {
|
|
49
|
+
const stored = readStoredValue(userConfig);
|
|
50
|
+
if (explicitValue !== undefined && typeof explicitValue !== 'boolean') {
|
|
51
|
+
throw settingError('Explicit Dimension ABAC selection must be a boolean.');
|
|
52
|
+
}
|
|
53
|
+
if (explicitValue !== undefined) return { value: explicitValue, source: 'explicit' };
|
|
54
|
+
if (stored !== undefined) return { value: stored, source: 'stored' };
|
|
55
|
+
return { value: false, source: 'default' };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function persistExplicitDimensionAbacSetting(configApi, value) {
|
|
59
|
+
if (value === undefined) return undefined;
|
|
60
|
+
if (!configApi || typeof configApi.getConfig !== 'function' || typeof configApi.saveConfig !== 'function') {
|
|
61
|
+
throw settingError('Configuration API is unavailable.');
|
|
62
|
+
}
|
|
63
|
+
if (typeof value !== 'boolean') {
|
|
64
|
+
throw settingError('Persisted Dimension ABAC selection must be a boolean.');
|
|
65
|
+
}
|
|
66
|
+
const userConfig = await configApi.getConfig();
|
|
67
|
+
readStoredValue(userConfig);
|
|
68
|
+
const applications = { ...(userConfig.applications || {}) };
|
|
69
|
+
const dataplane = { ...(applications[DATAPLANE_APP] || {}), [CONFIG_KEY]: value };
|
|
70
|
+
const nextConfig = { ...userConfig, applications: { ...applications, [DATAPLANE_APP]: dataplane } };
|
|
71
|
+
await configApi.saveConfig(nextConfig);
|
|
72
|
+
return nextConfig;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function toDimensionAbacEnvValue(value) {
|
|
76
|
+
if (typeof value !== 'boolean') {
|
|
77
|
+
throw settingError('Dimension ABAC value must be a boolean.');
|
|
78
|
+
}
|
|
79
|
+
return value ? 'true' : 'false';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function applyDimensionAbacToEnvMap(appName, envMap, value) {
|
|
83
|
+
if (appName !== DATAPLANE_APP) return envMap;
|
|
84
|
+
if (!envMap || typeof envMap !== 'object' || Array.isArray(envMap)) {
|
|
85
|
+
throw settingError('Dataplane environment map must be an object.');
|
|
86
|
+
}
|
|
87
|
+
envMap[SETTING_NAME] = toDimensionAbacEnvValue(value);
|
|
88
|
+
return envMap;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function applyDimensionAbacToConfiguration(appName, configuration, value) {
|
|
92
|
+
if (appName !== DATAPLANE_APP) return configuration;
|
|
93
|
+
if (!Array.isArray(configuration)) {
|
|
94
|
+
throw settingError('Dataplane deployment configuration must be an array.');
|
|
95
|
+
}
|
|
96
|
+
const matches = configuration.filter(item => item && item.name === SETTING_NAME);
|
|
97
|
+
if (matches.length > 1) {
|
|
98
|
+
throw settingError(`Dataplane deployment configuration contains duplicate ${SETTING_NAME} entries.`);
|
|
99
|
+
}
|
|
100
|
+
const entry = {
|
|
101
|
+
name: SETTING_NAME,
|
|
102
|
+
value: toDimensionAbacEnvValue(value),
|
|
103
|
+
location: 'variable',
|
|
104
|
+
required: false
|
|
105
|
+
};
|
|
106
|
+
if (matches.length === 0) configuration.push(entry);
|
|
107
|
+
else Object.assign(matches[0], entry);
|
|
108
|
+
return configuration;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = {
|
|
112
|
+
SETTING_NAME,
|
|
113
|
+
CONFIG_KEY,
|
|
114
|
+
parseDimensionAbacOption,
|
|
115
|
+
resolveDimensionAbacSetting,
|
|
116
|
+
persistExplicitDimensionAbacSetting,
|
|
117
|
+
toDimensionAbacEnvValue,
|
|
118
|
+
applyDimensionAbacToEnvMap,
|
|
119
|
+
applyDimensionAbacToConfiguration
|
|
120
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-environment env file names (plan 210.0).
|
|
3
|
+
*
|
|
4
|
+
* One host runs several environments, so the env file an environment is given has to be its
|
|
5
|
+
* own. The suffix order is **environment before profile**:
|
|
6
|
+
*
|
|
7
|
+
* ```text
|
|
8
|
+
* .env .env.local dev
|
|
9
|
+
* .env.tst .env.tst.local tst
|
|
10
|
+
* .env.pro .env.pro.local pro
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* Two properties make that ordering the right one. `dev` keeps the names it has, so nothing
|
|
14
|
+
* existing is renamed and no tooling that looks for `.env` stops finding it. And `.local`
|
|
15
|
+
* stays the last segment, so the rule "a `.local` file is host-only and is never mounted
|
|
16
|
+
* into a container" is still readable from the name alone — `.env.local.tst` would have
|
|
17
|
+
* quietly broken that, since the profile would no longer be where every existing check looks
|
|
18
|
+
* for it.
|
|
19
|
+
*
|
|
20
|
+
* Pure: no filesystem access. The environment comes from the caller.
|
|
21
|
+
*
|
|
22
|
+
* @fileoverview Environment-suffixed env and env.local file paths
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
'use strict';
|
|
26
|
+
|
|
27
|
+
const path = require('path');
|
|
28
|
+
const { isPromotionOnlyEnvironment } = require('../core/environment-mode-policy');
|
|
29
|
+
|
|
30
|
+
/** The host-only profile suffix, which must stay last. */
|
|
31
|
+
const LOCAL_PROFILE_SUFFIX = '.local';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The environment segment for an env file name.
|
|
35
|
+
*
|
|
36
|
+
* Empty for `dev` and for an absent environment, which is what keeps `.env` meaning what it
|
|
37
|
+
* has always meant.
|
|
38
|
+
*
|
|
39
|
+
* @param {string} [envKey] - Environment key
|
|
40
|
+
* @returns {string} `''`, `.tst` or `.pro`
|
|
41
|
+
*/
|
|
42
|
+
function environmentEnvFileSegment(envKey) {
|
|
43
|
+
return isPromotionOnlyEnvironment(envKey) ? `.${String(envKey).trim().toLowerCase()}` : '';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The env file an environment is given.
|
|
48
|
+
*
|
|
49
|
+
* @param {string} envOutputPath - Resolved path ending in `.env`
|
|
50
|
+
* @param {string} [envKey] - Environment key
|
|
51
|
+
* @returns {string} Path ending in `.env`, `.env.tst` or `.env.pro`
|
|
52
|
+
* @throws {Error} When no base path is given
|
|
53
|
+
*/
|
|
54
|
+
function resolveEnvOutputPathForEnvironment(envOutputPath, envKey) {
|
|
55
|
+
const base = typeof envOutputPath === 'string' ? envOutputPath.trim() : '';
|
|
56
|
+
if (!base) {
|
|
57
|
+
throw new Error('envOutputPath is required to resolve an environment env file');
|
|
58
|
+
}
|
|
59
|
+
const segment = environmentEnvFileSegment(envKey);
|
|
60
|
+
if (!segment || base.endsWith(segment)) {
|
|
61
|
+
return base;
|
|
62
|
+
}
|
|
63
|
+
return `${base}${segment}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The host-only sibling of an environment's env file.
|
|
68
|
+
*
|
|
69
|
+
* Built from the environment path so the profile suffix is always last, whichever
|
|
70
|
+
* environment it belongs to.
|
|
71
|
+
*
|
|
72
|
+
* @param {string} envOutputPath - Resolved path ending in `.env`
|
|
73
|
+
* @param {string} [envKey] - Environment key
|
|
74
|
+
* @returns {string} Path ending in `.local`
|
|
75
|
+
* @throws {Error} When no base path is given
|
|
76
|
+
*/
|
|
77
|
+
function resolveEnvLocalOutputPathForEnvironment(envOutputPath, envKey) {
|
|
78
|
+
const base = typeof envOutputPath === 'string' ? envOutputPath.trim() : '';
|
|
79
|
+
if (!base) {
|
|
80
|
+
throw new Error('envOutputPath is required to resolve an environment .local env file');
|
|
81
|
+
}
|
|
82
|
+
const withoutProfile = base.endsWith(LOCAL_PROFILE_SUFFIX)
|
|
83
|
+
? base.slice(0, -LOCAL_PROFILE_SUFFIX.length)
|
|
84
|
+
: base;
|
|
85
|
+
const forEnvironment = resolveEnvOutputPathForEnvironment(withoutProfile, envKey);
|
|
86
|
+
return `${forEnvironment}${LOCAL_PROFILE_SUFFIX}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Whether a file name is the host-only profile, for any environment.
|
|
91
|
+
*
|
|
92
|
+
* @param {string} filePath - Path or file name
|
|
93
|
+
* @returns {boolean}
|
|
94
|
+
*/
|
|
95
|
+
function isHostOnlyEnvFile(filePath) {
|
|
96
|
+
return path.basename(String(filePath ?? '')).endsWith(LOCAL_PROFILE_SUFFIX);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = {
|
|
100
|
+
LOCAL_PROFILE_SUFFIX,
|
|
101
|
+
environmentEnvFileSegment,
|
|
102
|
+
resolveEnvOutputPathForEnvironment,
|
|
103
|
+
resolveEnvLocalOutputPathForEnvironment,
|
|
104
|
+
isHostOnlyEnvFile
|
|
105
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Environment-scoped resource naming (shared infra: dev/tst on same Postgres/Redis/Docker).
|
|
3
|
-
* Effective only when user gate AND app flag AND run env is dev|tst.
|
|
3
|
+
* Effective only when user gate AND app flag AND run env is dev|tst|pro.
|
|
4
4
|
*
|
|
5
5
|
* @fileoverview environmentScopedResources + useEnvironmentScopedResources helpers
|
|
6
6
|
* @author AI Fabrix Team
|
|
@@ -10,14 +10,44 @@
|
|
|
10
10
|
'use strict';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* Run/env keys that participate in resource prefixing
|
|
13
|
+
* Run/env keys that participate in resource prefixing.
|
|
14
|
+
*
|
|
15
|
+
* `pro` is included (plan 210.0). It was excluded while `tst` was the only promotion target,
|
|
16
|
+
* which left `/pro/data` ungenerated and every pro resource sharing dev's name, Redis index
|
|
17
|
+
* and port. On a shared developer host that is a collision, not an absence.
|
|
18
|
+
*
|
|
19
|
+
* Callers must use this predicate rather than comparing keys inline: the same `dev || tst`
|
|
20
|
+
* test was written out in five other modules, and every one of them had to be found to add
|
|
21
|
+
* a single environment.
|
|
22
|
+
*/
|
|
23
|
+
const SCOPED_RUN_ENVIRONMENTS = Object.freeze(['dev', 'tst', 'pro']);
|
|
24
|
+
|
|
25
|
+
/** Redis logical DB per environment on one shared instance. Distinct by construction. */
|
|
26
|
+
const REDIS_DB_INDEX_BY_ENV = Object.freeze({ dev: 0, tst: 1, pro: 2 });
|
|
27
|
+
|
|
28
|
+
/**
|
|
14
29
|
* @param {string} envKey - Normalized lowercase env key
|
|
15
30
|
* @returns {boolean}
|
|
16
31
|
*/
|
|
17
32
|
function isScopedRunEnvironmentKey(envKey) {
|
|
18
33
|
if (!envKey || typeof envKey !== 'string') return false;
|
|
19
|
-
|
|
20
|
-
|
|
34
|
+
return SCOPED_RUN_ENVIRONMENTS.includes(envKey.toLowerCase());
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Whether a scope context carries both the gate and a scopable environment.
|
|
39
|
+
*
|
|
40
|
+
* The five call sites that need this each combined `effectiveEnvironmentScopedResources`
|
|
41
|
+
* with their own copy of the environment test; this is that combination, once.
|
|
42
|
+
*
|
|
43
|
+
* @param {{ effectiveEnvironmentScopedResources?: boolean, runEnvKey?: string }|null} scopeOpts
|
|
44
|
+
* @returns {boolean}
|
|
45
|
+
*/
|
|
46
|
+
function isScopeActiveForOptions(scopeOpts) {
|
|
47
|
+
if (!scopeOpts || !scopeOpts.effectiveEnvironmentScopedResources) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
return isScopedRunEnvironmentKey(scopeOpts.runEnvKey);
|
|
21
51
|
}
|
|
22
52
|
|
|
23
53
|
/**
|
|
@@ -42,17 +72,18 @@ function computeEffectiveEnvironmentScopedResources(
|
|
|
42
72
|
|
|
43
73
|
/**
|
|
44
74
|
* Redis logical DB index when env-scoped resources are effective.
|
|
45
|
-
* dev → 0, tst → 1 (single shared Redis instance).
|
|
75
|
+
* dev → 0, tst → 1, pro → 2 (single shared Redis instance).
|
|
76
|
+
*
|
|
77
|
+
* Returning null for a scoped environment would put it on DB 0 alongside dev, so this must
|
|
78
|
+
* cover every key in {@link SCOPED_RUN_ENVIRONMENTS}.
|
|
46
79
|
*
|
|
47
|
-
* @param {string} runEnvKey - dev | tst
|
|
80
|
+
* @param {string} runEnvKey - dev | tst | pro
|
|
48
81
|
* @returns {number|null} Index or null if not applicable
|
|
49
82
|
*/
|
|
50
83
|
function redisDbIndexForScopedRunEnv(runEnvKey) {
|
|
51
84
|
if (!runEnvKey || typeof runEnvKey !== 'string') return null;
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
if (k === 'tst') return 1;
|
|
55
|
-
return null;
|
|
85
|
+
const index = REDIS_DB_INDEX_BY_ENV[runEnvKey.toLowerCase()];
|
|
86
|
+
return index === undefined ? null : index;
|
|
56
87
|
}
|
|
57
88
|
|
|
58
89
|
/**
|
|
@@ -61,7 +92,7 @@ function redisDbIndexForScopedRunEnv(runEnvKey) {
|
|
|
61
92
|
* @param {string} appName - Application key
|
|
62
93
|
* @param {string|number} devId - Developer id
|
|
63
94
|
* @param {number} idNum - Parsed numeric developer id
|
|
64
|
-
* @param {string} envKey - dev | tst
|
|
95
|
+
* @param {string} envKey - dev | tst | pro
|
|
65
96
|
* @returns {string}
|
|
66
97
|
*/
|
|
67
98
|
function buildScopedLocalContainerName(appName, devId, idNum, envKey) {
|
|
@@ -93,7 +124,7 @@ function buildDefaultLocalContainerName(appName, devId, idNum) {
|
|
|
93
124
|
* @param {string} appName - Application key
|
|
94
125
|
* @param {string|number} developerId - Developer id
|
|
95
126
|
* @param {boolean} effectiveScoped - From {@link computeEffectiveEnvironmentScopedResources}
|
|
96
|
-
* @param {string} [runEnvKey] - dev | tst (required when effectiveScoped)
|
|
127
|
+
* @param {string} [runEnvKey] - dev | tst | pro (required when effectiveScoped)
|
|
97
128
|
* @returns {string}
|
|
98
129
|
*/
|
|
99
130
|
function resolveRunContainerName(appName, developerId, effectiveScoped, runEnvKey) {
|
|
@@ -108,7 +139,7 @@ function resolveRunContainerName(appName, developerId, effectiveScoped, runEnvKe
|
|
|
108
139
|
* Traefik PathPrefix: prefix /{envKey} before pattern base (e.g. /api → /dev/api).
|
|
109
140
|
*
|
|
110
141
|
* @param {string} basePath - From derivePathFromPattern
|
|
111
|
-
* @param {string} envKey - dev | tst
|
|
142
|
+
* @param {string} envKey - dev | tst | pro
|
|
112
143
|
* @returns {string}
|
|
113
144
|
*/
|
|
114
145
|
function buildEnvScopedTraefikPath(basePath, envKey) {
|
|
@@ -125,7 +156,7 @@ function buildEnvScopedTraefikPath(basePath, envKey) {
|
|
|
125
156
|
* Compose service + Traefik router key when env-scoped (unique per env on shared host).
|
|
126
157
|
*
|
|
127
158
|
* @param {string} appName - Application key
|
|
128
|
-
* @param {string} envKey - dev | tst
|
|
159
|
+
* @param {string} envKey - dev | tst | pro
|
|
129
160
|
* @returns {string}
|
|
130
161
|
*/
|
|
131
162
|
function composeTraefikServiceKey(appName, envKey) {
|
|
@@ -133,7 +164,10 @@ function composeTraefikServiceKey(appName, envKey) {
|
|
|
133
164
|
}
|
|
134
165
|
|
|
135
166
|
module.exports = {
|
|
167
|
+
SCOPED_RUN_ENVIRONMENTS,
|
|
168
|
+
REDIS_DB_INDEX_BY_ENV,
|
|
136
169
|
isScopedRunEnvironmentKey,
|
|
170
|
+
isScopeActiveForOptions,
|
|
137
171
|
computeEffectiveEnvironmentScopedResources,
|
|
138
172
|
redisDbIndexForScopedRunEnv,
|
|
139
173
|
buildScopedLocalContainerName,
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
/** Env key patterns that indicate a secret (mask value) — same rules as aifabrix logs <app>. */
|
|
14
14
|
const SECRET_KEY_PATTERN = /password|secret|token|credential|api[_-]?key/i;
|
|
15
15
|
|
|
16
|
+
/** Bounded encryption/master-key names; avoids treating every `key` variable as secret. */
|
|
17
|
+
const ENCRYPTION_KEY_PATTERN = /(?:^|[_-])(?:encryption|master)[_-]?key(?:$|[_-])/i;
|
|
18
|
+
|
|
16
19
|
/** Prefixes to strip before checking key (avoids masking KEYCLOAK_SERVER_URL etc.) */
|
|
17
20
|
const KEY_PREFIXES_TO_STRIP = /^KEYCLOAK_|^KEY_VAULT_/;
|
|
18
21
|
|
|
@@ -50,7 +53,7 @@ function maskEnvLine(line) {
|
|
|
50
53
|
const value = line.slice(eq + 1);
|
|
51
54
|
|
|
52
55
|
const keyForCheck = key.replace(KEY_PREFIXES_TO_STRIP, '');
|
|
53
|
-
const isSecretKey = SECRET_KEY_PATTERN.test(keyForCheck);
|
|
56
|
+
const isSecretKey = SECRET_KEY_PATTERN.test(keyForCheck) || ENCRYPTION_KEY_PATTERN.test(keyForCheck);
|
|
54
57
|
|
|
55
58
|
const maskedValue = maskUrlEmbeddedCredentials(value);
|
|
56
59
|
const hasUrlCredentials = maskedValue !== value;
|
|
@@ -8,7 +8,13 @@
|
|
|
8
8
|
|
|
9
9
|
'use strict';
|
|
10
10
|
|
|
11
|
-
const SYSTEM_BUILDER_APP_KEYS = Object.freeze([
|
|
11
|
+
const SYSTEM_BUILDER_APP_KEYS = Object.freeze([
|
|
12
|
+
'keycloak',
|
|
13
|
+
'miso-controller',
|
|
14
|
+
'dataplane',
|
|
15
|
+
'builder-api',
|
|
16
|
+
'integration-server'
|
|
17
|
+
]);
|
|
12
18
|
|
|
13
19
|
/**
|
|
14
20
|
* @param {string} appName
|
package/lib/utils/paths.js
CHANGED
|
@@ -473,7 +473,13 @@ function getBuilderRoot() {
|
|
|
473
473
|
* {@link getSystemBuilderRoot} (= {@link getAppsMaterializationParent}`/builder`), never the global CLI package tree.
|
|
474
474
|
* @readonly
|
|
475
475
|
*/
|
|
476
|
-
const SYSTEM_BUILDER_APP_KEYS = Object.freeze([
|
|
476
|
+
const SYSTEM_BUILDER_APP_KEYS = Object.freeze([
|
|
477
|
+
'keycloak',
|
|
478
|
+
'miso-controller',
|
|
479
|
+
'dataplane',
|
|
480
|
+
'builder-api',
|
|
481
|
+
'integration-server'
|
|
482
|
+
]);
|
|
477
483
|
|
|
478
484
|
/**
|
|
479
485
|
* @param {string} appName
|
|
@@ -1156,4 +1162,3 @@ module.exports = {
|
|
|
1156
1162
|
resolveAppWorkspace: require('./app-path-resolve').resolveAppWorkspace,
|
|
1157
1163
|
resolveExternalSystemPath: require('./app-path-resolve').resolveExternalSystemPath
|
|
1158
1164
|
};
|
|
1159
|
-
|
|
@@ -164,6 +164,35 @@ async function ensurePlatformDatabaseSlot(container, slot, secrets, adminPasswor
|
|
|
164
164
|
return slot.dbName;
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Databases this server already has.
|
|
169
|
+
*
|
|
170
|
+
* Read before anything is created, so an environment can adopt a database an older
|
|
171
|
+
* installation left behind instead of renaming it — a rename here means dropping and
|
|
172
|
+
* recreating, which means losing the data.
|
|
173
|
+
*
|
|
174
|
+
* @async
|
|
175
|
+
* @param {string} container - Postgres container name
|
|
176
|
+
* @param {string} adminPassword - Superuser password
|
|
177
|
+
* @returns {Promise<string[]>} Database names, empty when the server cannot be listed
|
|
178
|
+
*/
|
|
179
|
+
async function listExistingDatabaseNames(container, adminPassword) {
|
|
180
|
+
try {
|
|
181
|
+
const result = await dockerExec.execWithDockerEnv(
|
|
182
|
+
`docker exec -e PGPASSWORD -i ${container} psql -U ${SUPERUSER_ROLE} -d postgres -tAc "SELECT datname FROM pg_database WHERE datistemplate = false"`,
|
|
183
|
+
{ env: { PGPASSWORD: adminPassword } }
|
|
184
|
+
);
|
|
185
|
+
return String((result && result.stdout) || '')
|
|
186
|
+
.split('\n')
|
|
187
|
+
.map((line) => line.trim())
|
|
188
|
+
.filter(Boolean);
|
|
189
|
+
} catch {
|
|
190
|
+
// An unreadable server adopts nothing and creates under the contract name. Failing here
|
|
191
|
+
// would block a fresh install, where there is nothing to list in the first place.
|
|
192
|
+
return [];
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
167
196
|
/**
|
|
168
197
|
* @async
|
|
169
198
|
* @returns {Promise<{ container: string, adminPassword: string, secrets: Object }>}
|
|
@@ -202,5 +231,10 @@ module.exports = {
|
|
|
202
231
|
PLATFORM_DATABASE_SLOTS,
|
|
203
232
|
bootstrapPlatformPostgresDatabases,
|
|
204
233
|
pgRoleName,
|
|
205
|
-
extractPasswordFromUrlOrValue
|
|
234
|
+
extractPasswordFromUrlOrValue,
|
|
235
|
+
// Reused by the local environment stage, which needs the same primitives under
|
|
236
|
+
// environment-scoped names rather than the unprefixed defaults this module creates.
|
|
237
|
+
resolveBootstrapContext,
|
|
238
|
+
listExistingDatabaseNames,
|
|
239
|
+
ensureRoleAndDatabase
|
|
206
240
|
};
|
|
@@ -59,4 +59,24 @@ function applyRedisDbIndexToEnvContent(content, dbIndex) {
|
|
|
59
59
|
return out.join('\n');
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Point a scoped environment at its own Redis logical database.
|
|
64
|
+
*
|
|
65
|
+
* Here rather than at the call site because this module already owns the rewrite, and the
|
|
66
|
+
* caller is a 500-line file whose budget this would spend. Separating it is honest anyway:
|
|
67
|
+
* it is the only step that rewrites a resolved value instead of resolving one.
|
|
68
|
+
*
|
|
69
|
+
* @param {string} content - Resolved env content
|
|
70
|
+
* @param {string} runEnvKey - Run environment key
|
|
71
|
+
* @param {boolean} effective - Whether env-scoped resources apply
|
|
72
|
+
* @returns {string} Content, unchanged when scoping does not apply
|
|
73
|
+
*/
|
|
74
|
+
function applyScopedRedisDbIndex(content, runEnvKey, effective) {
|
|
75
|
+
if (!effective) {
|
|
76
|
+
return content;
|
|
77
|
+
}
|
|
78
|
+
const { redisDbIndexForScopedRunEnv } = require('./environment-scoped-resources');
|
|
79
|
+
return applyRedisDbIndexToEnvContent(content, redisDbIndexForScopedRunEnv(runEnvKey));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = { applyRedisDbIndexToEnvContent, setRedisUrlDbIndex, applyScopedRedisDbIndex };
|