@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
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who may act on which environment (plan 210.0).
|
|
3
|
+
*
|
|
4
|
+
* Cross-environment management is gated by the **signed-in user's** access, never by a
|
|
5
|
+
* standing service identity. The distinction matters because the bootstrap credential that
|
|
6
|
+
* makes a silent `dev` install possible would otherwise also reach `pro`: one shared secret
|
|
7
|
+
* on a developer host would become a route into production, and nothing in the audit trail
|
|
8
|
+
* would name a person.
|
|
9
|
+
*
|
|
10
|
+
* The entitled set therefore has to come from the user's own token. That is a property of
|
|
11
|
+
* the caller rather than of this module, so the rule is made mechanical: a caller states
|
|
12
|
+
* where the list came from, and anything other than a signed-in user is refused here rather
|
|
13
|
+
* than trusted to have checked.
|
|
14
|
+
*
|
|
15
|
+
* **A request touching several environments is all-or-nothing, decided before anything
|
|
16
|
+
* runs.** A partly applied cross-environment operation leaves the installation in a state
|
|
17
|
+
* nobody asked for, and the exit code does not say which half happened; refusing the whole
|
|
18
|
+
* request is recoverable, half-doing it is not.
|
|
19
|
+
*
|
|
20
|
+
* Pure: no I/O, no token parsing. The caller supplies what it learned.
|
|
21
|
+
*
|
|
22
|
+
* @fileoverview Environment entitlement checks for cross-environment commands
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
'use strict';
|
|
26
|
+
|
|
27
|
+
/** The only acceptable provenance for an entitled-environment list. */
|
|
28
|
+
const USER_ACCESS_SOURCE = 'user';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {unknown} value - Environment key
|
|
32
|
+
* @returns {string}
|
|
33
|
+
*/
|
|
34
|
+
function normalizeEnvKey(value) {
|
|
35
|
+
return String(value ?? '').trim().toLowerCase();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Normalise an entitled-environment list to comparable keys.
|
|
40
|
+
*
|
|
41
|
+
* @param {Array<unknown>|null|undefined} entitled - Environments the user may act on
|
|
42
|
+
* @returns {string[]} Unique, normalised, in the order given
|
|
43
|
+
*/
|
|
44
|
+
function normalizeEntitledEnvironments(entitled) {
|
|
45
|
+
if (!Array.isArray(entitled)) {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
const seen = new Set();
|
|
49
|
+
const out = [];
|
|
50
|
+
for (const item of entitled) {
|
|
51
|
+
const key = normalizeEnvKey(typeof item === 'object' && item ? item.key ?? item.name : item);
|
|
52
|
+
if (!key || seen.has(key)) continue;
|
|
53
|
+
seen.add(key);
|
|
54
|
+
out.push(key);
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Split a request into what this user may and may not act on.
|
|
61
|
+
*
|
|
62
|
+
* @param {Array<unknown>} requested - Environments the command would touch
|
|
63
|
+
* @param {Array<unknown>} entitled - Environments the user may act on
|
|
64
|
+
* @returns {{ allowed: string[], refused: string[] }}
|
|
65
|
+
*/
|
|
66
|
+
function partitionByEntitlement(requested, entitled) {
|
|
67
|
+
const permitted = new Set(normalizeEntitledEnvironments(entitled));
|
|
68
|
+
const allowed = [];
|
|
69
|
+
const refused = [];
|
|
70
|
+
for (const item of Array.isArray(requested) ? requested : [requested]) {
|
|
71
|
+
const key = normalizeEnvKey(item);
|
|
72
|
+
if (!key) continue;
|
|
73
|
+
(permitted.has(key) ? allowed : refused).push(key);
|
|
74
|
+
}
|
|
75
|
+
return { allowed, refused };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Refuse a request that reaches an environment this user is not entitled to.
|
|
80
|
+
*
|
|
81
|
+
* @param {Array<unknown>|string} requested - Environments the command would touch
|
|
82
|
+
* @param {Object} access - What the caller learned about the signed-in user
|
|
83
|
+
* @param {Array<unknown>} access.entitled - Environments the user may act on
|
|
84
|
+
* @param {string} access.source - Provenance; must be `user`
|
|
85
|
+
* @returns {string[]} The requested environments, normalised, when every one is permitted
|
|
86
|
+
* @throws {Error} When the list is not user-scoped, or any environment is not permitted
|
|
87
|
+
*/
|
|
88
|
+
function assertEnvironmentsEntitled(requested, access = {}) {
|
|
89
|
+
if (normalizeEnvKey(access.source) !== USER_ACCESS_SOURCE) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
'Environment access must come from the signed-in user, not a service identity. ' +
|
|
92
|
+
'Run "aifabrix login" and retry.'
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
const entitled = normalizeEntitledEnvironments(access.entitled);
|
|
96
|
+
const { allowed, refused } = partitionByEntitlement(requested, access.entitled);
|
|
97
|
+
|
|
98
|
+
if (entitled.length === 0) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
'You are signed in but entitled to no environment. Ask an administrator for access, ' +
|
|
101
|
+
'or check the controller with: aifabrix env secret list'
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
if (refused.length > 0) {
|
|
105
|
+
// Named in full, and nothing is run. Reporting only the first would hide the rest
|
|
106
|
+
// behind a retry loop, and running the entitled ones first would half-apply the request.
|
|
107
|
+
throw new Error(
|
|
108
|
+
`Not entitled to ${refused.join(', ')}. You may act on ${entitled.join(', ')}.\n` +
|
|
109
|
+
'Nothing was changed: a request spanning environments is refused as a whole so it ' +
|
|
110
|
+
'cannot be left half applied.'
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
return allowed;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
module.exports = {
|
|
117
|
+
USER_ACCESS_SOURCE,
|
|
118
|
+
normalizeEntitledEnvironments,
|
|
119
|
+
partitionByEntitlement,
|
|
120
|
+
assertEnvironmentsEntitled
|
|
121
|
+
};
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which environments a local installation may build in and select (plan 210.0).
|
|
3
|
+
*
|
|
4
|
+
* Two rules, kept together because they describe one position: `tst` and `pro` are
|
|
5
|
+
* **promotion targets**, not working environments.
|
|
6
|
+
*
|
|
7
|
+
* **Build is a dev activity.** `tst` and `pro` receive a promoted image — the same image run
|
|
8
|
+
* as another container under an environment-suffixed name. Building into them would mean the
|
|
9
|
+
* artifact in production was never the artifact that was tested, which is the property
|
|
10
|
+
* promotion exists to guarantee. Miso does not police this; a build leaves no trace of where
|
|
11
|
+
* it happened, so the refusal has to be here.
|
|
12
|
+
*
|
|
13
|
+
* **`tst` and `pro` need the front door.** They exist only in `full` platform mode, where
|
|
14
|
+
* Traefik routes each environment under its own path. `single-traefik` is a front door for
|
|
15
|
+
* **one** environment, and `single` is direct ports: several environments on one host collide
|
|
16
|
+
* on ports there. Allowing the selection in either would produce an environment that is
|
|
17
|
+
* unreachable rather than one that failed to start, so it is refused up front.
|
|
18
|
+
*
|
|
19
|
+
* Pure except for `resolveEffectiveBuildEnvironment`, which reads the configured environment.
|
|
20
|
+
*
|
|
21
|
+
* @fileoverview Build and platform-mode environment policy for local installations
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
'use strict';
|
|
25
|
+
|
|
26
|
+
const {
|
|
27
|
+
SETUP_PLATFORM_MODE,
|
|
28
|
+
normalizeSetupPlatformMode
|
|
29
|
+
} = require('./setup-platform-mode');
|
|
30
|
+
|
|
31
|
+
/** Environments that receive a promoted artifact and are never built into. */
|
|
32
|
+
const PROMOTION_ONLY_ENVIRONMENTS = Object.freeze(['tst', 'pro']);
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {unknown} envKey - Environment key
|
|
36
|
+
* @returns {string} Lowercase trimmed key
|
|
37
|
+
*/
|
|
38
|
+
function normalizeEnvKey(envKey) {
|
|
39
|
+
return String(envKey ?? '').trim().toLowerCase();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {unknown} envKey - Environment key
|
|
44
|
+
* @returns {boolean} True when the environment only ever receives a promoted artifact
|
|
45
|
+
*/
|
|
46
|
+
function isPromotionOnlyEnvironment(envKey) {
|
|
47
|
+
return PROMOTION_ONLY_ENVIRONMENTS.includes(normalizeEnvKey(envKey));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Refuse a build that would target a promotion-only environment.
|
|
52
|
+
*
|
|
53
|
+
* `--env` has no default, so an omitted flag silently takes the configured environment. The
|
|
54
|
+
* message distinguishes the two cases: being told "build cannot target pro" after typing no
|
|
55
|
+
* environment at all is not actionable on its own.
|
|
56
|
+
*
|
|
57
|
+
* @param {unknown} envKey - Resolved environment key
|
|
58
|
+
* @param {Object} [opts] - Context
|
|
59
|
+
* @param {boolean} [opts.explicit] - Whether the caller passed --env
|
|
60
|
+
* @throws {Error} When the environment is tst or pro
|
|
61
|
+
*/
|
|
62
|
+
function assertBuildEnvironmentAllowed(envKey, opts = {}) {
|
|
63
|
+
const key = normalizeEnvKey(envKey);
|
|
64
|
+
if (!isPromotionOnlyEnvironment(key)) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const cause = opts.explicit
|
|
68
|
+
? `--env ${key}`
|
|
69
|
+
: `the current environment is ${key}`;
|
|
70
|
+
throw new Error(
|
|
71
|
+
`Cannot build into ${key} (${cause}). ${key} receives a promoted image, never a locally ` +
|
|
72
|
+
'built one, so that what runs there is what was tested in dev.\n' +
|
|
73
|
+
'Build in dev with: aifabrix build <appKey> --env dev\n' +
|
|
74
|
+
`Then promote into ${key} with: aifabrix env`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Refuse selecting a promotion-only environment outside the front-door platform mode.
|
|
80
|
+
*
|
|
81
|
+
* @param {unknown} envKey - Environment key being selected
|
|
82
|
+
* @param {unknown} mode - setupPlatformMode value
|
|
83
|
+
* @throws {Error} When the environment needs full mode and the install is not in it
|
|
84
|
+
*/
|
|
85
|
+
function assertEnvironmentAllowedForPlatformMode(envKey, mode) {
|
|
86
|
+
const key = normalizeEnvKey(envKey);
|
|
87
|
+
if (!isPromotionOnlyEnvironment(key)) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const normalized = normalizeSetupPlatformMode(mode);
|
|
91
|
+
if (normalized === SETUP_PLATFORM_MODE.FULL) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const reason =
|
|
95
|
+
normalized === SETUP_PLATFORM_MODE.SINGLE_TRAEFIK
|
|
96
|
+
? 'single-traefik is a front door for one environment only'
|
|
97
|
+
: 'single mode publishes direct ports, and several environments on one host collide on ports';
|
|
98
|
+
throw new Error(
|
|
99
|
+
`Environment ${key} requires platform mode "full" (HTTPS front door with per-environment ` +
|
|
100
|
+
`paths). This installation is "${normalized}": ${reason}.\n` +
|
|
101
|
+
'Switch with: aifabrix setup --platform full'
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Refuse a promotion-only environment against the installation's configured mode.
|
|
107
|
+
*
|
|
108
|
+
* @param {unknown} envKey - Environment key being selected
|
|
109
|
+
* @returns {Promise<void>}
|
|
110
|
+
* @throws {Error} When the environment needs full mode and the install is not in it
|
|
111
|
+
*/
|
|
112
|
+
async function assertEnvironmentAllowedForCurrentMode(envKey) {
|
|
113
|
+
if (!isPromotionOnlyEnvironment(envKey)) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const config = require('./config');
|
|
117
|
+
const mode =
|
|
118
|
+
typeof config.getSetupPlatformMode === 'function'
|
|
119
|
+
? await config.getSetupPlatformMode()
|
|
120
|
+
: SETUP_PLATFORM_MODE.SINGLE;
|
|
121
|
+
assertEnvironmentAllowedForPlatformMode(envKey, mode);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The environment a build will actually target.
|
|
126
|
+
*
|
|
127
|
+
* Mirrors how the secret path resolves it (`buildScopedKvContext`): an omitted `--env` falls
|
|
128
|
+
* through to the configured environment, so a guard that only inspected the flag would let
|
|
129
|
+
* `aifabrix build` run against `pro` on a machine configured for it.
|
|
130
|
+
*
|
|
131
|
+
* @param {Object} [options] - Build options carrying `env`
|
|
132
|
+
* @returns {Promise<{ envKey: string, explicit: boolean }>}
|
|
133
|
+
*/
|
|
134
|
+
async function resolveEffectiveBuildEnvironment(options = {}) {
|
|
135
|
+
if (options && options.env) {
|
|
136
|
+
return { envKey: normalizeEnvKey(options.env), explicit: true };
|
|
137
|
+
}
|
|
138
|
+
const config = require('./config');
|
|
139
|
+
if (typeof config.getCurrentEnvironment !== 'function') {
|
|
140
|
+
return { envKey: 'dev', explicit: false };
|
|
141
|
+
}
|
|
142
|
+
let current;
|
|
143
|
+
try {
|
|
144
|
+
current = await config.getCurrentEnvironment();
|
|
145
|
+
} catch {
|
|
146
|
+
// An unreadable config is a different failure, reported properly by whatever next needs
|
|
147
|
+
// it — secrets resolution cannot proceed without it either. Turning it into "cannot
|
|
148
|
+
// build" would stop every build on a machine whose config has an unrelated problem, and
|
|
149
|
+
// it protects nothing: reaching tst or pro requires either an explicit --env, which
|
|
150
|
+
// never reads config, or a readable config naming one.
|
|
151
|
+
return { envKey: 'dev', explicit: false };
|
|
152
|
+
}
|
|
153
|
+
return { envKey: normalizeEnvKey(current) || 'dev', explicit: false };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Resolve the build environment and refuse it when it is promotion-only.
|
|
158
|
+
*
|
|
159
|
+
* @param {Object} [options] - Build options carrying `env`
|
|
160
|
+
* @returns {Promise<string>} The allowed environment key
|
|
161
|
+
* @throws {Error} When the environment is tst or pro
|
|
162
|
+
*/
|
|
163
|
+
async function assertBuildEnvironmentForOptions(options = {}) {
|
|
164
|
+
const { envKey, explicit } = await resolveEffectiveBuildEnvironment(options);
|
|
165
|
+
assertBuildEnvironmentAllowed(envKey, { explicit });
|
|
166
|
+
return envKey;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
module.exports = {
|
|
170
|
+
PROMOTION_ONLY_ENVIRONMENTS,
|
|
171
|
+
isPromotionOnlyEnvironment,
|
|
172
|
+
assertBuildEnvironmentAllowed,
|
|
173
|
+
assertEnvironmentAllowedForPlatformMode,
|
|
174
|
+
assertEnvironmentAllowedForCurrentMode,
|
|
175
|
+
resolveEffectiveBuildEnvironment,
|
|
176
|
+
assertBuildEnvironmentForOptions
|
|
177
|
+
};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builder-owned env values for locally installed platform applications.
|
|
3
|
+
*
|
|
4
|
+
* `templates/applications/{miso-controller,builder-api,keycloak}/env.template` are
|
|
5
|
+
* byte-identical mirrors of the Miso master, and `dataplane` mirrors the dataplane
|
|
6
|
+
* repository. An edit to a mirror survives until the next `chore(templates): sync`
|
|
7
|
+
* commit and then disappears without a diff anyone reads, so a value Builder decides
|
|
8
|
+
* cannot live there. It is applied here instead, after the template is resolved.
|
|
9
|
+
*
|
|
10
|
+
* @fileoverview Local-install env overrides owned by Builder, not by a mirrored template
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
/** Applications the CLI registers itself through `.../applications/bootstrap`. */
|
|
16
|
+
const CLI_BOOTSTRAPPED_APP_KEYS = Object.freeze(['dataplane']);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Controller onboarding owns the miso-only platform applications. Dataplane remains a
|
|
20
|
+
* CLI-owned workload and must never be inserted into this list.
|
|
21
|
+
*/
|
|
22
|
+
const DEFAULT_LOCAL_ONBOARDING_APPS = Object.freeze(['builder-api']);
|
|
23
|
+
|
|
24
|
+
/** Config key an installation may set to choose its own onboarding list. */
|
|
25
|
+
const ONBOARDING_APPS_CONFIG_KEY = 'onboardingApps';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Keys Builder's mirror carried that the Miso master does not.
|
|
29
|
+
*
|
|
30
|
+
* They were edits to a mirrored file, which is the thing this module exists to prevent: the
|
|
31
|
+
* mirror has since fallen 57 lines behind its master, and re-syncing it would have dropped
|
|
32
|
+
* them silently. They are declared here instead, so the next sync cannot lose them.
|
|
33
|
+
*
|
|
34
|
+
* - `INFRASTRUCTURE_NAME` is the only one carrying a value, used by controller and onboarding
|
|
35
|
+
* flows. Losing it would change behaviour, not merely remove a blank line.
|
|
36
|
+
* - The other three shipped empty. They are kept so a local install's env has exactly the keys
|
|
37
|
+
* it had before this sync: an absent key and an empty one are usually the same thing, and
|
|
38
|
+
* "usually" is not a basis for changing every installation at once.
|
|
39
|
+
*
|
|
40
|
+
* `ACR_LOGIN_SERVER` is deliberately empty. Miso removed it with the note that its proper home
|
|
41
|
+
* is Builder's run tooling, which knows the per-developer answer — so an installation that
|
|
42
|
+
* needs it should have it injected there rather than shipped one test installation's registry
|
|
43
|
+
* to everybody, which is what happened the last time it carried a value.
|
|
44
|
+
*/
|
|
45
|
+
const MISO_CONTROLLER_LOCAL_ONLY_ENV = Object.freeze({
|
|
46
|
+
INFRASTRUCTURE_NAME: '${DEV_USERNAME}.${DEV_ROOT_DOMAIN}',
|
|
47
|
+
KEYCLOAK_PUBLIC_KEY: '',
|
|
48
|
+
OTEL_SERVICE_VERSION: '',
|
|
49
|
+
ACR_LOGIN_SERVER: ''
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
function normalizeAppKeyList(value) {
|
|
53
|
+
const raw = Array.isArray(value) ? value : String(value ?? '').split(',');
|
|
54
|
+
const seen = new Set();
|
|
55
|
+
for (const entry of raw) {
|
|
56
|
+
const key = String(entry ?? '').trim().toLowerCase();
|
|
57
|
+
if (key) {
|
|
58
|
+
seen.add(key);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return [...seen];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Applications controller onboarding may register on this installation.
|
|
66
|
+
*
|
|
67
|
+
* Builder API is always retained because Controller onboarding owns that miso-only
|
|
68
|
+
* application. This also migrates older explicit-empty setup values safely.
|
|
69
|
+
*
|
|
70
|
+
* @param {Object} [cfg] - Parsed `~/.aifabrix/config.yaml`
|
|
71
|
+
* @returns {string[]} Application keys, never including one the CLI bootstraps
|
|
72
|
+
*/
|
|
73
|
+
function resolveLocalOnboardingApps(cfg) {
|
|
74
|
+
const configured = cfg ? cfg[ONBOARDING_APPS_CONFIG_KEY] : undefined;
|
|
75
|
+
const list = configured === undefined || configured === null
|
|
76
|
+
? [...DEFAULT_LOCAL_ONBOARDING_APPS]
|
|
77
|
+
: normalizeAppKeyList(configured);
|
|
78
|
+
const allowed = list.filter((key) => !CLI_BOOTSTRAPPED_APP_KEYS.includes(key));
|
|
79
|
+
return ['builder-api', ...allowed.filter((key) => key !== 'builder-api')];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Env values Builder owns for a locally installed application.
|
|
84
|
+
*
|
|
85
|
+
* @param {string} appName - Application key
|
|
86
|
+
* @param {Object} [cfg] - Parsed `~/.aifabrix/config.yaml`
|
|
87
|
+
* @returns {Object.<string, string>} Map to merge into resolved env content; empty when none apply
|
|
88
|
+
*/
|
|
89
|
+
function localEnvOverridesForApp(appName, cfg) {
|
|
90
|
+
const key = String(appName ?? '').trim().toLowerCase();
|
|
91
|
+
if (key === 'miso-controller') {
|
|
92
|
+
return {
|
|
93
|
+
ONBOARDING_APPS: resolveLocalOnboardingApps(cfg).join(','),
|
|
94
|
+
...MISO_CONTROLLER_LOCAL_ONLY_ENV
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
if (key === 'builder-api' || key === 'integration-server') {
|
|
98
|
+
return { MISO_ENVIRONMENT: 'miso' };
|
|
99
|
+
}
|
|
100
|
+
return {};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Merge Builder-owned values into resolved env content for a locally installed application.
|
|
105
|
+
*
|
|
106
|
+
* Applied after the template is resolved, so the installation's choice is the authority for
|
|
107
|
+
* these keys. The merge function is passed in rather than imported, because the only caller
|
|
108
|
+
* owns it and importing it here would close a require cycle.
|
|
109
|
+
*
|
|
110
|
+
* @param {string} content - Resolved env content
|
|
111
|
+
* @param {string} appName - Application key
|
|
112
|
+
* @param {(content: string, map: Object) => string} mergeEnvMapIntoContent - Merge helper
|
|
113
|
+
* @returns {Promise<string>} Content with overrides merged, unchanged when none apply
|
|
114
|
+
*/
|
|
115
|
+
async function applyLocalEnvOverrides(content, appName, mergeEnvMapIntoContent) {
|
|
116
|
+
let cfg = null;
|
|
117
|
+
try {
|
|
118
|
+
cfg = await require('./config').getConfig();
|
|
119
|
+
} catch {
|
|
120
|
+
// No readable config (fresh machine, or resolve outside an installation): use defaults.
|
|
121
|
+
}
|
|
122
|
+
const overrides = localEnvOverridesForApp(appName, cfg);
|
|
123
|
+
if (!overrides || Object.keys(overrides).length === 0) {
|
|
124
|
+
return content;
|
|
125
|
+
}
|
|
126
|
+
return mergeEnvMapIntoContent(content, overrides);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The onboarding list for an installation that does or does not carry the integration
|
|
131
|
+
* server. Written to config so the choice survives, rather than being asked on every run.
|
|
132
|
+
*
|
|
133
|
+
* @param {boolean} integrationServer - Whether the installation carries it
|
|
134
|
+
* @returns {string[]} Value for the `onboardingApps` config key
|
|
135
|
+
*/
|
|
136
|
+
function onboardingAppsForIntegrationServer(integrationServer) {
|
|
137
|
+
return integrationServer ? ['builder-api', 'integration-server'] : ['builder-api'];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = {
|
|
141
|
+
CLI_BOOTSTRAPPED_APP_KEYS,
|
|
142
|
+
DEFAULT_LOCAL_ONBOARDING_APPS,
|
|
143
|
+
ONBOARDING_APPS_CONFIG_KEY,
|
|
144
|
+
MISO_CONTROLLER_LOCAL_ONLY_ENV,
|
|
145
|
+
resolveLocalOnboardingApps,
|
|
146
|
+
localEnvOverridesForApp,
|
|
147
|
+
applyLocalEnvOverrides,
|
|
148
|
+
onboardingAppsForIntegrationServer
|
|
149
|
+
};
|
|
@@ -29,6 +29,7 @@ const {
|
|
|
29
29
|
saveSecretsFile
|
|
30
30
|
} = require('../utils/secrets-generator');
|
|
31
31
|
const { encryptSecret } = require('../utils/secrets-encryption');
|
|
32
|
+
const { generateRsaSecretValues } = require('../parameters/rsa-secret-values');
|
|
32
33
|
const { loadEnvTemplate } = require('../utils/secrets-helpers');
|
|
33
34
|
const {
|
|
34
35
|
buildInfraPlaceholderContext,
|
|
@@ -287,12 +288,13 @@ async function ensureSecretsForKeys(keys, options = {}) {
|
|
|
287
288
|
if (toAdd.length === 0) return [];
|
|
288
289
|
|
|
289
290
|
const encryptionKey = await config.getSecretsEncryptionKey();
|
|
291
|
+
const rsaValues = emptyForCredentials ? {} : generateRsaSecretValues(toAdd, { ...existing, ...suggested }, encryptionKey);
|
|
290
292
|
const added = [];
|
|
291
293
|
|
|
292
294
|
return addSecretsToFile({
|
|
293
295
|
filePath: target.filePath,
|
|
294
|
-
toAdd,
|
|
295
|
-
suggested,
|
|
296
|
+
toAdd: [...new Set([...toAdd, ...Object.keys(rsaValues)])],
|
|
297
|
+
suggested: { ...rsaValues, ...suggested },
|
|
296
298
|
emptyForCredentials,
|
|
297
299
|
encryptionKey,
|
|
298
300
|
added,
|
|
@@ -12,6 +12,7 @@ const fs = require('fs');
|
|
|
12
12
|
const path = require('path');
|
|
13
13
|
const { resolveApplicationConfigPath } = require('../utils/app-config-resolver');
|
|
14
14
|
const config = require('./config');
|
|
15
|
+
const { applyLocalEnvOverrides } = require('./local-env-overrides');
|
|
15
16
|
const {
|
|
16
17
|
interpolateEnvVars,
|
|
17
18
|
collectMissingSecrets,
|
|
@@ -40,8 +41,9 @@ const secretsEnsure = require('./secrets-ensure');
|
|
|
40
41
|
const { resolveSecretsPath, getActualSecretsPath } = require('../utils/secrets-path');
|
|
41
42
|
const pathsUtil = require('../utils/paths');
|
|
42
43
|
const { readAppEnvironmentScopedFlagForAppPath } = require('../utils/app-scoped-config');
|
|
43
|
-
const { computeEffectiveEnvironmentScopedResources
|
|
44
|
-
const {
|
|
44
|
+
const { computeEffectiveEnvironmentScopedResources } = require('../utils/environment-scoped-resources');
|
|
45
|
+
const { isStrictScopedEnvironment } = require('../utils/secrets-kv-scope');
|
|
46
|
+
const { applyScopedRedisDbIndex } = require('../utils/redis-env-scope');
|
|
45
47
|
const { expandDeclarativeUrlsIfPresent } = require('./secrets-env-declarative-expand');
|
|
46
48
|
const {
|
|
47
49
|
mergeAdditionalPublishedPorts,
|
|
@@ -106,7 +108,8 @@ async function resolveKvReferences(
|
|
|
106
108
|
secretsFilePaths,
|
|
107
109
|
appName,
|
|
108
110
|
envTemplatePath,
|
|
109
|
-
envTemplate
|
|
111
|
+
envTemplate,
|
|
112
|
+
scopedKv
|
|
110
113
|
})
|
|
111
114
|
);
|
|
112
115
|
}
|
|
@@ -237,12 +240,14 @@ async function generateEnvContent(appName, secretsPath, environment = 'local', f
|
|
|
237
240
|
const variablesPath = (options && options.envOnly) ? null : resolveApplicationConfigPath(appPath);
|
|
238
241
|
const template = loadEnvTemplate(templatePath);
|
|
239
242
|
const secretsPaths = await getActualSecretsPath(secretsPath, appName);
|
|
240
|
-
|
|
243
|
+
const { runEnvKey, effective } = await buildScopedKvContext(appPath, options);
|
|
244
|
+
// --force backfills a missing secret by generating one. A strict environment must not be
|
|
245
|
+
// handed a value the Controller has never seen, so it errors on the missing key instead.
|
|
246
|
+
if (force && !isStrictScopedEnvironment(runEnvKey)) {
|
|
241
247
|
const preferredPath = secretsPath ? resolveSecretsPath(secretsPath) : secretsPaths.userPath;
|
|
242
248
|
await secretsEnsure.ensureSecretsFromEnvTemplate(templatePath, { preferredFilePath: preferredPath });
|
|
243
249
|
}
|
|
244
250
|
const secrets = await loadSecrets(secretsPath, appName);
|
|
245
|
-
const { runEnvKey, effective } = await buildScopedKvContext(appPath, options);
|
|
246
251
|
let resolved = await resolveKvReferences(template, secrets, environment, secretsPaths, appName, {
|
|
247
252
|
envKey: runEnvKey,
|
|
248
253
|
effective,
|
|
@@ -262,10 +267,8 @@ async function generateEnvContent(appName, secretsPath, environment = 'local', f
|
|
|
262
267
|
Boolean(options.envOnly)
|
|
263
268
|
);
|
|
264
269
|
resolved = await applyEnvironmentTransformations(resolved, environment, variablesPath);
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
resolved = applyRedisDbIndexToEnvContent(resolved, idx);
|
|
268
|
-
}
|
|
270
|
+
resolved = applyScopedRedisDbIndex(resolved, runEnvKey, effective);
|
|
271
|
+
resolved = await applyLocalEnvOverrides(resolved, appName, mergeEnvMapIntoContent);
|
|
269
272
|
|
|
270
273
|
return resolved;
|
|
271
274
|
}
|
|
@@ -208,7 +208,7 @@ async function resolveAndGetEnvMap(appName, options = {}) {
|
|
|
208
208
|
const adminSecrets = require('./admin-secrets');
|
|
209
209
|
const { applyAdminSecretsPlatformOverlay } = require('./admin-secrets-env-overlay');
|
|
210
210
|
const adminObj = await adminSecrets.readAndDecryptAdminSecrets();
|
|
211
|
-
applyAdminSecretsPlatformOverlay(map, adminObj);
|
|
211
|
+
applyAdminSecretsPlatformOverlay(map, adminObj, appName);
|
|
212
212
|
} catch {
|
|
213
213
|
// admin-secrets.env optional until up-infra/setup
|
|
214
214
|
}
|
|
@@ -6,8 +6,14 @@
|
|
|
6
6
|
* All input validation happens before any filesystem, network or cloud mutation.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
const {
|
|
9
|
+
const {
|
|
10
|
+
resolveStageInput,
|
|
11
|
+
validateResourceGroupKey,
|
|
12
|
+
validateStage,
|
|
13
|
+
ENVIRONMENT_STAGES
|
|
14
|
+
} = require('./stage-input');
|
|
10
15
|
const { openWorkspace } = require('./workspace');
|
|
16
|
+
const { isLocalInstallationKey } = require('./local-installation');
|
|
11
17
|
|
|
12
18
|
/**
|
|
13
19
|
* Runs one installation workspace stage.
|
|
@@ -18,6 +24,13 @@ const { openWorkspace } = require('./workspace');
|
|
|
18
24
|
*/
|
|
19
25
|
async function deployInstallationStage(resourceGroupKey, options, deps = {}) {
|
|
20
26
|
const resourceGroup = validateResourceGroupKey(resourceGroupKey);
|
|
27
|
+
// A local installation owns its environments itself: no workspace, no Azure template and
|
|
28
|
+
// no Controller round trip. `register` still goes the shared route, because a local
|
|
29
|
+
// install registers with Mori exactly as any other does.
|
|
30
|
+
if (isLocalInstallationKey(resourceGroup) && ENVIRONMENT_STAGES.includes(validateStage(options.stage))) {
|
|
31
|
+
const { runLocalEnvironmentStage } = require('./local-environment-stage');
|
|
32
|
+
return runLocalEnvironmentStage(resourceGroup, validateStage(options.stage), deps);
|
|
33
|
+
}
|
|
21
34
|
const workspace = openWorkspace(resourceGroup, { root: deps.root });
|
|
22
35
|
const input = resolveStageInput(resourceGroup, options, workspace.infra?.vendor?.value);
|
|
23
36
|
const base = { stage: input.stage, vendor: input.vendor, workspaceDir: workspace.dir };
|
|
@@ -88,6 +88,7 @@ const PARAMETER_CATALOG = Object.freeze({
|
|
|
88
88
|
enablePostgresPublicAccess: { kind: 'allowMyIp', source: 'allowMyIp', description: 'Default off; enabled only while allowMyIp grants the validated caller IPv4 /32 for testing.' },
|
|
89
89
|
allowedIPs: { kind: 'allowMyIp', source: 'allowMyIpOrUser', description: 'Explicit CIDRs for PostgreSQL, Key Vault, and Storage; allowMyIp merges the resolved caller IPv4 /32.' },
|
|
90
90
|
adminPassword: { kind: 'secretEnv', env: 'ADMIN_PASSWORD', required: true, description: 'Installation administrator password.' },
|
|
91
|
+
adminEmail: { kind: 'file', source: 'user', required: true, description: 'Email of the initial platform administrator, used for the installation welcome. Required: the template sets no default and enforces minLength.' },
|
|
91
92
|
deploymentEnvironment: { kind: 'file', source: 'templateDefault', description: 'Vendor deployment profile.' },
|
|
92
93
|
keycloakVerifyAudience: { kind: 'file', source: 'templateDefault', description: 'Enable Keycloak audience verification.' },
|
|
93
94
|
encryptionKey: { kind: 'secretEnv', env: 'ENCRYPTION_KEY', required: true, description: 'Application/data encryption key.' },
|
|
@@ -99,11 +100,13 @@ const PARAMETER_CATALOG = Object.freeze({
|
|
|
99
100
|
deployRegistryRoleAssignment: { kind: 'parameterDefault', defaultFrom: 'deployRoleAssignments', source: 'templateDefault', description: 'Deploy the AcrPull grant on the container registry. False while credential image pull is in effect, because that grant lives in the registry resource group, which a customer installation cannot reach; restored for managed identity.' },
|
|
100
101
|
moriBaseUrl: { kind: 'file', source: 'registration', required: true, description: 'Mori service base URL.' },
|
|
101
102
|
moriLicensePublicKey: { kind: 'file', source: 'registration', required: false, description: 'Mori licence issuer public key (SPKI PEM); empty leaves the installation unpinned and it then refuses every licence.' },
|
|
103
|
+
moriPolicyPublicKey: { kind: 'file', source: 'registration', required: false, description: 'Mori policy signing anchor (SPKI PEM), a different pair from the licence key; empty leaves the policy bundle checkable only against the key it carries, which proves nothing.' },
|
|
102
104
|
moriLicenseIssuer: { kind: 'file', source: 'registration', required: false, description: 'Expected iss claim on a Mori licence.' },
|
|
103
105
|
entraIdClientId: { kind: 'deferred', description: 'Not collected or sent by infra; issued/configured during later Entra provider onboarding.' },
|
|
104
106
|
entraIdClientSecret: { kind: 'deferred', description: 'Not collected or sent by infra; issued/configured during later Entra provider onboarding.' },
|
|
105
107
|
entraIdTenantId: { kind: 'deferred', description: 'Customer Entra provider tenant; distinct from the Azure subscription directory tenant and configured later.' },
|
|
106
108
|
licenseEdition: { kind: 'license', source: 'cliOrFile', description: 'License requested from Mori during tenant activation: COMMUNITY, STANDARD or ENTERPRISE, or an Azure Marketplace subscription ID. Standard/Enterprise by name is a 30-day evaluation.' },
|
|
109
|
+
installationResourceModel: { kind: 'file', source: 'templateDefault', description: 'Initial installation topology: shared for Community and Standard, dedicated for Enterprise DEV. Defaults to shared and is protected by Azure application configuration. It records the topology the installation starts in and grants no later licence entitlement, so it is not a licence input.' },
|
|
107
110
|
onboardingMode: { kind: 'onboarding', source: 'workspacePolicy', description: 'Miso onboarding mode (ONBOARDING_MODE); Builder installations use saasDirect, Marketplace customer installations use customerOwned.' },
|
|
108
111
|
capacityProfile: { kind: 'capacity', source: 'workspacePreset', description: 'Infrastructure sizing profile (s, m, l, xl) selecting compute and PostgreSQL SKUs. Derived from the workspace preset so the two cannot drift; set it with --preset.' },
|
|
109
112
|
misoImageTag: { kind: 'file', source: 'templateDefault', description: 'Miso container image tag.' },
|