@aifabrix/builder 2.61.2 → 2.62.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/app/deploy.js +14 -5
- package/lib/app/run-env-compose.js +5 -6
- 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 +30 -10
- package/lib/cli/setup-infra-up-platform-action.js +14 -1
- package/lib/cli/setup-platform.js +2 -0
- 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 +7 -1
- package/lib/commands/setup-prompts-platform-mode.js +34 -0
- package/lib/commands/setup.js +3 -0
- package/lib/commands/up-builder-api.js +24 -8
- package/lib/commands/up-common.js +16 -1
- package/lib/commands/up-dataplane-bootstrap.js +70 -14
- package/lib/commands/up-dataplane-credentials.js +21 -0
- package/lib/commands/up-dataplane.js +37 -18
- 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/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/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/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 +3 -2
- 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,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,
|
|
@@ -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 };
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a registry credential comes from when none is stored.
|
|
3
|
+
*
|
|
4
|
+
* Builder is registry-agnostic: Docker Hub, GHCR and ACR are all a host, a username and a
|
|
5
|
+
* password, and an individual identity, a registry token and an admin key all fit that
|
|
6
|
+
* shape. The only special case is that an `*.azurecr.io` host can also be authenticated by
|
|
7
|
+
* the Azure CLI, which is worth trying before asking a person to paste a secret.
|
|
8
|
+
*
|
|
9
|
+
* Order: the stored pair (handled by the caller), then `az acr login` for ACR, then a
|
|
10
|
+
* prompt. A non-interactive run never prompts — it fails naming the host and how to
|
|
11
|
+
* configure it, because a hung prompt in CI is worse than a clear failure.
|
|
12
|
+
*
|
|
13
|
+
* @fileoverview Credential source strategy for registries without a stored pair
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
/** Azure Container Registry login server suffix. */
|
|
19
|
+
const ACR_HOST_SUFFIX = '.azurecr.io';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {string} host - Registry host
|
|
23
|
+
* @returns {boolean} True for an Azure Container Registry login server
|
|
24
|
+
*/
|
|
25
|
+
function isAzureContainerRegistryHost(host) {
|
|
26
|
+
return String(host || '').trim().toLowerCase().endsWith(ACR_HOST_SUFFIX);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Message for a run that cannot ask for credentials.
|
|
31
|
+
*
|
|
32
|
+
* @param {string} host - Registry host
|
|
33
|
+
* @returns {string}
|
|
34
|
+
*/
|
|
35
|
+
function nonInteractiveCredentialMessage(host) {
|
|
36
|
+
const acrHint = isAzureContainerRegistryHost(host)
|
|
37
|
+
? ` For Azure Container Registry you can instead run "az login" so "az acr login --name ${String(host).split('.')[0]}" succeeds.`
|
|
38
|
+
: '';
|
|
39
|
+
return (
|
|
40
|
+
`No credentials for ${host} and nothing to ask: this run is not interactive. ` +
|
|
41
|
+
`Store a pair with "aifabrix dev set-registry ${host} --username <username> --password-stdin".${acrHint}`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Build the fallback `ensureRegistryLogin` uses when no pair is stored.
|
|
47
|
+
*
|
|
48
|
+
* Dependencies are injected so the chain can be tested without a terminal, an Azure CLI or
|
|
49
|
+
* a Docker daemon.
|
|
50
|
+
*
|
|
51
|
+
* @param {Object} [deps] - Injectable dependencies
|
|
52
|
+
* @param {boolean} [deps.interactive] - Whether a person can be asked; defaults to TTY
|
|
53
|
+
* @param {Function} [deps.acrLogin] - async (host) => void, defaults to `az acr login`
|
|
54
|
+
* @param {Function} [deps.promptCredentials] - async (host) => { username, password }
|
|
55
|
+
* @param {Function} [deps.saveCredentials] - async (host, username, password) => void
|
|
56
|
+
* @param {Function} [deps.dockerLogin] - async (host, username, password) => void
|
|
57
|
+
* @returns {(host: string) => Promise<'acr'|'prompt'>} Fallback for ensureRegistryLogin
|
|
58
|
+
*/
|
|
59
|
+
/**
|
|
60
|
+
* An error that names the host it is about.
|
|
61
|
+
*
|
|
62
|
+
* Callers read `registryHost` off it to report which registry refused, so the property is
|
|
63
|
+
* attached in one place rather than at each throw.
|
|
64
|
+
*
|
|
65
|
+
* @param {string} message - Message
|
|
66
|
+
* @param {string} host - Registry host
|
|
67
|
+
* @returns {Error}
|
|
68
|
+
*/
|
|
69
|
+
function hostError(message, host) {
|
|
70
|
+
const err = new Error(message);
|
|
71
|
+
err.registryHost = host;
|
|
72
|
+
return err;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function buildRegistryAuthFallback(deps = {}) {
|
|
76
|
+
const interactive = deps.interactive === undefined
|
|
77
|
+
? Boolean(process.stdin.isTTY && process.stdout.isTTY)
|
|
78
|
+
: Boolean(deps.interactive);
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Try `az acr login` for an ACR host.
|
|
82
|
+
*
|
|
83
|
+
* Extracted so the fallback itself stays readable. A failure here is deliberately not
|
|
84
|
+
* fatal when a prompt is still possible: an expired or absent `az login` is an ordinary
|
|
85
|
+
* state, and a stored pair or an answered prompt can still authenticate the host.
|
|
86
|
+
*
|
|
87
|
+
* @param {string} host - Registry host
|
|
88
|
+
* @returns {Promise<boolean>} True when ACR login succeeded
|
|
89
|
+
*/
|
|
90
|
+
async function tryAcrLogin(host) {
|
|
91
|
+
if (!isAzureContainerRegistryHost(host)) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
const acrLogin = deps.acrLogin
|
|
95
|
+
|| ((target) => require('../deployment/push').authenticateACR(target));
|
|
96
|
+
try {
|
|
97
|
+
await acrLogin(host);
|
|
98
|
+
return true;
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (!interactive) {
|
|
101
|
+
throw hostError(`${nonInteractiveCredentialMessage(host)} (az acr login failed: ${error.message})`, host);
|
|
102
|
+
}
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return async function registryAuthFallback(host) {
|
|
108
|
+
if (await tryAcrLogin(host)) {
|
|
109
|
+
return 'acr';
|
|
110
|
+
}
|
|
111
|
+
if (!interactive) {
|
|
112
|
+
throw hostError(nonInteractiveCredentialMessage(host), host);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const promptCredentials = deps.promptCredentials || promptForRegistryCredentials;
|
|
116
|
+
const { username, password } = await promptCredentials(host);
|
|
117
|
+
if (!username || !password) {
|
|
118
|
+
throw new Error(`Registry credentials for ${host} are required: both a username and a password.`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const saveCredentials = deps.saveCredentials
|
|
122
|
+
|| ((target, user, pass) => require('./registry-credentials').saveRegistryCredentials(target, user, pass));
|
|
123
|
+
// Store before logging in, so the answer is not asked for again on the next command
|
|
124
|
+
// even if the login itself fails for an unrelated reason.
|
|
125
|
+
await saveCredentials(host, username, password);
|
|
126
|
+
|
|
127
|
+
const dockerLogin = deps.dockerLogin
|
|
128
|
+
|| ((target, user, pass) => require('../deployment/push').authenticateExternalRegistry(target, user, pass));
|
|
129
|
+
await dockerLogin(host, username, password);
|
|
130
|
+
return 'prompt';
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Ask for a registry username and password. The password is never echoed.
|
|
136
|
+
*
|
|
137
|
+
* @param {string} host - Registry host
|
|
138
|
+
* @returns {Promise<{ username: string, password: string }>}
|
|
139
|
+
*/
|
|
140
|
+
async function promptForRegistryCredentials(host) {
|
|
141
|
+
const inquirer = require('inquirer');
|
|
142
|
+
return inquirer.prompt([
|
|
143
|
+
{ type: 'input', name: 'username', message: `Username for ${host}:` },
|
|
144
|
+
{ type: 'password', name: 'password', mask: '*', message: `Password or token for ${host}:` }
|
|
145
|
+
]);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
module.exports = {
|
|
149
|
+
ACR_HOST_SUFFIX,
|
|
150
|
+
isAzureContainerRegistryHost,
|
|
151
|
+
nonInteractiveCredentialMessage,
|
|
152
|
+
buildRegistryAuthFallback,
|
|
153
|
+
promptForRegistryCredentials
|
|
154
|
+
};
|