@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.
Files changed (90) hide show
  1. package/lib/api/applications.api.js +22 -2
  2. package/lib/api/bootstrap-snapshot.api.js +115 -0
  3. package/lib/api/onboarding-status.api.js +117 -0
  4. package/lib/app/deploy.js +14 -5
  5. package/lib/app/register.js +24 -2
  6. package/lib/app/run-env-compose.js +44 -8
  7. package/lib/app/run-env-recovery.js +1 -0
  8. package/lib/app/run-helpers.js +1 -1
  9. package/lib/app/run.js +6 -4
  10. package/lib/build/standard-docker-build.js +4 -0
  11. package/lib/channels/channel-artifact-generator.js +13 -3
  12. package/lib/cli/infra-guided-footers.js +10 -8
  13. package/lib/cli/infra-guided.js +60 -12
  14. package/lib/cli/setup-infra-up-platform-action.js +14 -1
  15. package/lib/cli/setup-platform.js +20 -3
  16. package/lib/commands/auth-config.js +2 -0
  17. package/lib/commands/login.js +4 -0
  18. package/lib/commands/repair-datasource-apply.js +3 -1
  19. package/lib/commands/repair-datasource-legacy.js +94 -0
  20. package/lib/commands/setup-image-refresh.js +26 -0
  21. package/lib/commands/setup-modes.js +9 -2
  22. package/lib/commands/setup-onboarding-readiness.js +83 -0
  23. package/lib/commands/setup-prompts-platform-mode.js +34 -0
  24. package/lib/commands/setup.js +35 -10
  25. package/lib/commands/up-builder-api.js +24 -8
  26. package/lib/commands/up-common.js +16 -1
  27. package/lib/commands/up-dataplane-bootstrap.js +149 -16
  28. package/lib/commands/up-dataplane-credentials.js +21 -0
  29. package/lib/commands/up-dataplane.js +26 -34
  30. package/lib/commands/up-integration-server.js +112 -0
  31. package/lib/commands/wizard-config-normalizer.js +5 -1
  32. package/lib/core/admin-secrets-env-overlay.js +8 -3
  33. package/lib/core/environment-access-policy.js +121 -0
  34. package/lib/core/environment-mode-policy.js +177 -0
  35. package/lib/core/local-env-overrides.js +149 -0
  36. package/lib/core/secrets-ensure.js +4 -2
  37. package/lib/core/secrets-env-content.js +12 -9
  38. package/lib/core/secrets-env-write.js +1 -1
  39. package/lib/datasource/capability/capability-manifest.js +1 -0
  40. package/lib/deployment/installation/index.js +14 -1
  41. package/lib/deployment/installation/infra-catalog.js +3 -0
  42. package/lib/deployment/installation/local-environment-stage.js +285 -0
  43. package/lib/deployment/installation/local-installation.js +76 -0
  44. package/lib/deployment/installation/stage-input.js +5 -1
  45. package/lib/generator/builders.js +3 -1
  46. package/lib/generator/index.js +32 -9
  47. package/lib/generator/split-variables.js +2 -2
  48. package/lib/parameters/database-adoption.js +125 -0
  49. package/lib/parameters/physical-database-naming.js +136 -0
  50. package/lib/parameters/physical-secret-name.js +194 -0
  51. package/lib/parameters/rsa-secret-values.js +165 -0
  52. package/lib/programmatic/run-channel-install-artifacts.js +28 -2
  53. package/lib/programmatic/run-channel-package.js +3 -1
  54. package/lib/programmatic/workspace-context.js +37 -2
  55. package/lib/schema/infra.parameter.yaml +61 -27
  56. package/lib/schema/infrastructure-schema.json +213 -120
  57. package/lib/utils/compose-generate-docker-compose.js +2 -2
  58. package/lib/utils/compose-generator.js +4 -7
  59. package/lib/utils/compose-miso-env.js +2 -2
  60. package/lib/utils/compose-traefik-ingress-base.js +3 -4
  61. package/lib/utils/dataplane-dimension-abac-setting.js +120 -0
  62. package/lib/utils/declarative-url-ports.js +3 -0
  63. package/lib/utils/env-environment-file-paths.js +105 -0
  64. package/lib/utils/environment-scoped-resources.js +48 -14
  65. package/lib/utils/log-redaction.js +4 -1
  66. package/lib/utils/paths-system-builder-keys.js +7 -1
  67. package/lib/utils/paths.js +7 -2
  68. package/lib/utils/postgres-platform-bootstrap.js +35 -1
  69. package/lib/utils/redis-env-scope.js +21 -1
  70. package/lib/utils/registry-auth-sources.js +154 -0
  71. package/lib/utils/registry-credentials.js +131 -14
  72. package/lib/utils/secrets-generator.js +30 -21
  73. package/lib/utils/secrets-helpers.js +5 -7
  74. package/lib/utils/secrets-kv-scope.js +127 -28
  75. package/lib/utils/secrets-materialize-local.js +9 -0
  76. package/lib/utils/secrets-missing-error.js +35 -1
  77. package/lib/utils/token-manager.js +14 -8
  78. package/lib/utils/url-public-path-prefix.js +6 -10
  79. package/package.json +4 -2
  80. package/templates/README.md +6 -0
  81. package/templates/applications/builder-api/application.yaml +1 -1
  82. package/templates/applications/dataplane/application.yaml +2 -2
  83. package/templates/applications/dataplane/env.template +1 -1
  84. package/templates/applications/integration-server/application.yaml +91 -0
  85. package/templates/applications/integration-server/env.template +215 -0
  86. package/templates/applications/mirrored-templates.json +52 -0
  87. package/templates/applications/miso-controller/application.yaml +43 -1
  88. package/templates/applications/miso-controller/env.template +45 -39
  89. package/templates/marketplace/createUiDefinition.json +52 -4
  90. package/templates/marketplace/main.json +85 -7
@@ -0,0 +1,285 @@
1
+ /**
2
+ * `env deploy local --stage dev|tst|pro` — the local environment stage (plan 210.0).
3
+ *
4
+ * The Azure path creates an environment in the Miso Controller for an Azure installation.
5
+ * This is the same sentence spoken locally: create this environment's resources inside the
6
+ * local installation. Increment 1 covers the databases and roles; the promoted container and
7
+ * the front-door routing follow.
8
+ *
9
+ * Every environment shares one Postgres server here, so the names carry the environment —
10
+ * `{env}_{appKey}_{logical}` with a `_user` role, mirroring Miso 227.3 exactly rather than
11
+ * inventing a local scheme. See `lib/parameters/physical-database-naming.js`.
12
+ *
13
+ * @fileoverview Local environment provisioning for env deploy
14
+ */
15
+
16
+ 'use strict';
17
+
18
+ const logger = require('../../utils/logger');
19
+ const { resolveDatabaseNamesWithAdoption } = require('../../parameters/database-adoption');
20
+ const { parseDatabaseSecretKey } = require('../../parameters/database-secret-values');
21
+ const { resolveLocalInstallationName } = require('./local-installation');
22
+
23
+ /**
24
+ * Applications that exist **once per environment**.
25
+ *
26
+ * The dataplane and nothing else. Keycloak, the Miso Controller, builder-api and
27
+ * integration-server are **platform services**: one per platform, living in `miso`, shared by
28
+ * every environment. There is one ACR and one Keycloak, not one each per environment.
29
+ *
30
+ * This is why the list also bounds the databases. Creating `pro_keycloak_keycloak` would
31
+ * stand up a second Keycloak's database for a Keycloak that does not exist, and nothing
32
+ * would ever connect to it.
33
+ */
34
+ const ENVIRONMENT_WORKLOAD_APP_KEYS = Object.freeze(['dataplane']);
35
+
36
+ /**
37
+ * The databases this environment needs, with their physical names resolved together.
38
+ *
39
+ * Resolved as one list so a caller cannot take the database name and leave the role
40
+ * verbatim, which is the shape of the defect 227.3 fixed.
41
+ *
42
+ * Platform-service databases are **excluded**: they belong to the single shared platform in
43
+ * `miso`, which `setup` installs, not to a workload environment.
44
+ *
45
+ * An existing unprefixed database is **adopted** rather than renamed: renaming on a live
46
+ * server means dropping and recreating, which means losing the data. Only `dev` may adopt,
47
+ * because everything a pre-environment install created belongs to it.
48
+ *
49
+ * @param {string} envKey - Environment key
50
+ * @param {{ dbName: string, secretKey: string }[]} slots - Platform database slots
51
+ * @param {Iterable<string>} [existingDatabaseNames] - Databases the server already has
52
+ * @returns {{ logicalName: string, appKey: string, databaseName: string, roleName: string, secretKey: string, origin: string }[]}
53
+ * @throws {Error} When a slot's secret key cannot be parsed for its owning application
54
+ */
55
+ function resolveEnvironmentDatabasePlan(envKey, slots, existingDatabaseNames) {
56
+ const forEnvironment = slots.filter((slot) => {
57
+ const parsed = parseDatabaseSecretKey(slot.secretKey);
58
+ return parsed && ENVIRONMENT_WORKLOAD_APP_KEYS.includes(parsed.appKey);
59
+ });
60
+ return forEnvironment.map((slot) => {
61
+ const parsed = parseDatabaseSecretKey(slot.secretKey);
62
+ if (!parsed) {
63
+ throw new Error(
64
+ `Cannot determine the owning application for database "${slot.dbName}" from secret ` +
65
+ `key "${slot.secretKey}".`
66
+ );
67
+ }
68
+ const names = resolveDatabaseNamesWithAdoption({
69
+ logicalName: slot.dbName,
70
+ appKey: parsed.appKey,
71
+ environment: envKey,
72
+ existingDatabaseNames
73
+ });
74
+ return {
75
+ logicalName: names.logicalName,
76
+ appKey: parsed.appKey,
77
+ databaseName: names.databaseName,
78
+ roleName: names.roleName,
79
+ origin: names.origin,
80
+ // The catalog key, which the strict resolver already scopes per environment. Not
81
+ // `names.passwordSecretKey`: which grammar names this secret is open with Miso, and
82
+ // the catalog key is the one every existing consumer reads.
83
+ secretKey: slot.secretKey
84
+ };
85
+ });
86
+ }
87
+
88
+ /**
89
+ * Create the databases and roles for one environment.
90
+ *
91
+ * @async
92
+ * @param {string} envKey - Environment key
93
+ * @param {Object} deps - Injected collaborators
94
+ * @returns {Promise<string[]>} Database names ensured
95
+ */
96
+ async function ensureEnvironmentDatabases(envKey, deps) {
97
+ const bootstrap = deps.bootstrap || require('../../utils/postgres-platform-bootstrap');
98
+ const { container, adminPassword, secrets } = await bootstrap.resolveBootstrapContext();
99
+ const existing = await bootstrap.listExistingDatabaseNames(container, adminPassword);
100
+ const plan = resolveEnvironmentDatabasePlan(envKey, bootstrap.PLATFORM_DATABASE_SLOTS, existing);
101
+ const ensured = [];
102
+ for (const item of plan) {
103
+ const password = bootstrap.extractPasswordFromUrlOrValue(secrets[item.secretKey]);
104
+ if (!password || !password.trim()) {
105
+ // Name the physical secret, which for an application-scoped key carries the owning
106
+ // application before the key. Quoting `<env>-<key>` here would send someone to create
107
+ // a name nothing reads.
108
+ const { computePhysicalSecretName } = require('../../parameters/physical-secret-name');
109
+ const physical = computePhysicalSecretName(item.secretKey, { environment: envKey });
110
+ throw new Error(
111
+ `Secret ${item.secretKey} is missing or empty for environment ${envKey}. ` +
112
+ `Add it with: aifabrix secret set ${physical} <value>`
113
+ );
114
+ }
115
+ await bootstrap.ensureRoleAndDatabase(
116
+ container,
117
+ item.databaseName,
118
+ item.roleName,
119
+ password.trim(),
120
+ adminPassword
121
+ );
122
+ if (item.origin === 'adopted') {
123
+ logger.log(`Adopted existing database ${item.databaseName} for ${envKey}`);
124
+ }
125
+ ensured.push(item.databaseName);
126
+ }
127
+ return ensured;
128
+ }
129
+
130
+ /**
131
+ * Ask Miso whether this environment may exist, before anything is created.
132
+ *
133
+ * **Editions are Miso's, not Builder's.** Whether a licence permits `tst` or `pro` is not a
134
+ * question this repository answers or caches: it asks, and Miso accepts or declines. Builder
135
+ * holding its own copy of the entitlement rules would be a second answer to drift from the
136
+ * licence, and a local one at that — the easiest possible thing to edit.
137
+ *
138
+ * This runs **first**. A decline after the databases exist would leave an environment's
139
+ * objects behind for an environment that is not licensed, and nothing would later remove
140
+ * them.
141
+ *
142
+ * @async
143
+ * @param {string} envKey - Environment key
144
+ * @param {Object} deps - Injected collaborators
145
+ * @returns {Promise<void>}
146
+ * @throws {Error} When Miso declines, or cannot be reached to ask
147
+ */
148
+ async function authorizeEnvironmentWithController(envKey, deps) {
149
+ if (typeof deps.authorizeEnvironment === 'function') {
150
+ await deps.authorizeEnvironment(envKey);
151
+ return;
152
+ }
153
+ const { resolveControllerUrl } = require('../../utils/controller-url');
154
+ const { getOrRefreshDeviceToken } = require('../../utils/token-manager');
155
+ const { createEnvironment } = require('../../api/environments.api');
156
+
157
+ const controllerUrl = await resolveControllerUrl();
158
+ if (!controllerUrl) {
159
+ throw new Error(
160
+ `Cannot create ${envKey}: no controller is configured, so whether your licence ` +
161
+ 'permits this environment cannot be checked. Run "aifabrix login" first.'
162
+ );
163
+ }
164
+ const deviceToken = await getOrRefreshDeviceToken(controllerUrl);
165
+ if (!deviceToken || !deviceToken.token) {
166
+ throw new Error(
167
+ `Cannot create ${envKey}: not signed in, so whether your licence permits this ` +
168
+ 'environment cannot be checked. Run "aifabrix login" first.'
169
+ );
170
+ }
171
+ const authConfig = {
172
+ type: 'device',
173
+ token: deviceToken.token,
174
+ controller: deviceToken.controller || controllerUrl
175
+ };
176
+ try {
177
+ await createEnvironment(controllerUrl, authConfig, { key: envKey, environment: envKey });
178
+ } catch (error) {
179
+ // Miso owns the edition rules, so its refusal is reported as given rather than
180
+ // reinterpreted here. Guessing at the reason would put a second, stale explanation of
181
+ // the licence in front of the operator.
182
+ throw new Error(
183
+ `Miso declined environment ${envKey}: ${error.message}\n` +
184
+ 'Editions are licensed in Miso. Nothing was created locally.'
185
+ );
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Workloads `env deploy` promotes into a promotion-only environment.
191
+ *
192
+ * The same set that owns the environment's databases: an application exists per environment
193
+ * or it does not, and its containers and its databases cannot disagree about which.
194
+ */
195
+ const PROMOTED_WORKLOAD_APP_KEYS = ENVIRONMENT_WORKLOAD_APP_KEYS;
196
+
197
+ /**
198
+ * Run the promoted image as this environment's container.
199
+ *
200
+ * `base: true` is the whole point. Without it the run path prefers a local developer-scoped
201
+ * tag, which is the image someone happens to have built — the opposite of promotion, where
202
+ * what runs in `pro` must be the artifact that was promoted out of dev.
203
+ *
204
+ * Nothing is built here, and image resolution stays with the run path rather than being
205
+ * recomputed: a second answer to "which image" is how the promoted artifact and the running
206
+ * one come apart.
207
+ *
208
+ * @async
209
+ * @param {string} envKey - Environment key
210
+ * @param {Object} deps - Injected collaborators
211
+ * @returns {Promise<string[]>} Application keys promoted
212
+ */
213
+ async function promoteWorkloadsForEnvironment(envKey, deps) {
214
+ const policy = deps.policy || require('../../core/environment-mode-policy');
215
+ // dev is a working environment: its containers come from `setup` and the `up-*` commands,
216
+ // and promoting over them would replace what a developer is working on.
217
+ if (!policy.isPromotionOnlyEnvironment(envKey)) {
218
+ return [];
219
+ }
220
+ const runApp = deps.runApp || require('../../app').runApp;
221
+ const promoted = [];
222
+ for (const appKey of PROMOTED_WORKLOAD_APP_KEYS) {
223
+ try {
224
+ await runApp(appKey, { env: envKey, base: true });
225
+ } catch (error) {
226
+ throw new Error(
227
+ `Could not promote ${appKey} into ${envKey}: ${error.message}\n` +
228
+ `${envKey} runs a promoted image and never builds one. Produce it in dev first ` +
229
+ `with: aifabrix build ${appKey} --base --from-dev`
230
+ );
231
+ }
232
+ promoted.push(appKey);
233
+ }
234
+ return promoted;
235
+ }
236
+
237
+ /**
238
+ * Run the local environment stage.
239
+ *
240
+ * @async
241
+ * @param {string} installationKey - Positional installation key (`local` or `local-…`)
242
+ * @param {string} envKey - Validated stage, one of dev|tst|pro
243
+ * @param {Object} [deps] - Injected collaborators
244
+ * @returns {Promise<{ stage: string, vendor: string, installation: string, databases: string[], promoted: string[] }>}
245
+ */
246
+ async function runLocalEnvironmentStage(installationKey, envKey, deps = {}) {
247
+ const config = deps.config || require('../../core/config');
248
+ const policy = deps.policy || require('../../core/environment-mode-policy');
249
+
250
+ // Miso first: it owns editions, and it accepts or declines this environment. Everything
251
+ // below creates something, so the licence answer has to come before any of it.
252
+ await authorizeEnvironmentWithController(envKey, deps);
253
+
254
+ // Then the local precondition. tst and pro are unreachable outside the front-door mode,
255
+ // so creating their databases would leave resources behind for an environment that cannot
256
+ // serve a request.
257
+ await policy.assertEnvironmentAllowedForCurrentMode(envKey);
258
+
259
+ const developerId = await config.getDeveloperId();
260
+ const installation = resolveLocalInstallationName(installationKey, developerId);
261
+
262
+ logger.log(`Deploying environment ${envKey} into local installation ${installation}`);
263
+
264
+ // Databases before containers: a workload that starts without its database fails on its
265
+ // first request rather than at startup, which is the harder failure to read.
266
+ const databases = await ensureEnvironmentDatabases(envKey, deps);
267
+ logger.log(`Ensured ${databases.length} database(s) for ${envKey}: ${databases.join(', ')}`);
268
+
269
+ const promoted = await promoteWorkloadsForEnvironment(envKey, deps);
270
+ if (promoted.length > 0) {
271
+ logger.log(`Promoted into ${envKey}: ${promoted.join(', ')}`);
272
+ }
273
+
274
+ return { stage: envKey, vendor: 'DOCKER', installation, databases, promoted };
275
+ }
276
+
277
+ module.exports = {
278
+ ENVIRONMENT_WORKLOAD_APP_KEYS,
279
+ PROMOTED_WORKLOAD_APP_KEYS,
280
+ authorizeEnvironmentWithController,
281
+ resolveEnvironmentDatabasePlan,
282
+ ensureEnvironmentDatabases,
283
+ promoteWorkloadsForEnvironment,
284
+ runLocalEnvironmentStage
285
+ };
@@ -0,0 +1,76 @@
1
+ /**
2
+ * The `local` installation key for `env deploy` (plan 210.0).
3
+ *
4
+ * `env deploy` is dual-mode, and the installation name is what chooses the mode. Miso
5
+ * already names a local installation `local-dev{NN}`, so Builder mirrors that rather than
6
+ * inventing a second convention. The operator types the bare word `local`; the CLI knows its
7
+ * own developer id and supplies the rest.
8
+ *
9
+ * Naming the mode in the argument, rather than inferring it from machine state, means the
10
+ * positional means the same thing on both paths — an installation — and the command reads
11
+ * the same whichever installation it names.
12
+ *
13
+ * @fileoverview Local installation key detection and name derivation
14
+ */
15
+
16
+ 'use strict';
17
+
18
+ /** The bare key an operator types. */
19
+ const LOCAL_INSTALLATION_KEY = 'local';
20
+
21
+ /** Prefix of a fully qualified local installation name. */
22
+ const LOCAL_INSTALLATION_PREFIX = 'local-';
23
+
24
+ /**
25
+ * Whether this installation key names a local installation.
26
+ *
27
+ * @param {unknown} key - Positional installation key
28
+ * @returns {boolean}
29
+ */
30
+ function isLocalInstallationKey(key) {
31
+ const value = String(key ?? '').trim().toLowerCase();
32
+ return value === LOCAL_INSTALLATION_KEY || value.startsWith(LOCAL_INSTALLATION_PREFIX);
33
+ }
34
+
35
+ /**
36
+ * The installation name for a local key.
37
+ *
38
+ * `local` expands to `local-dev{developerId}`; an already-qualified `local-…` is kept as
39
+ * given, so an operator can name an installation whose id is not this machine's.
40
+ *
41
+ * **The developer id must be the stored string, never a parsed number.** It is persisted
42
+ * zero-padded (`'03'`), and `config.js` preserves that string deliberately. Surrounding code
43
+ * carries `parseInt(developerId, 10)` for numeric comparisons such as `idNum === 0`; deriving
44
+ * the name from that would produce `local-dev3`, which disagrees with Miso's `local-dev03`
45
+ * and surfaces as a missing installation rather than as a naming bug.
46
+ *
47
+ * @param {string} key - Positional installation key
48
+ * @param {string|number} developerId - Stored developer id
49
+ * @returns {string} Qualified installation name
50
+ * @throws {Error} When the key is not a local key, or the id is unusable
51
+ */
52
+ function resolveLocalInstallationName(key, developerId) {
53
+ const value = String(key ?? '').trim().toLowerCase();
54
+ if (!isLocalInstallationKey(value)) {
55
+ throw new Error(`Installation "${key}" is not a local installation key`);
56
+ }
57
+ if (value !== LOCAL_INSTALLATION_KEY) {
58
+ return value;
59
+ }
60
+ const id = String(developerId ?? '').trim();
61
+ if (!id || !/^\d+$/.test(id)) {
62
+ throw new Error(
63
+ 'Cannot derive the local installation name: no developer id is configured. ' +
64
+ 'Set one with "aifabrix setup", or name the installation explicitly, for example ' +
65
+ '"aifabrix env deploy local-dev03".'
66
+ );
67
+ }
68
+ return `${LOCAL_INSTALLATION_PREFIX}dev${id}`;
69
+ }
70
+
71
+ module.exports = {
72
+ LOCAL_INSTALLATION_KEY,
73
+ LOCAL_INSTALLATION_PREFIX,
74
+ isLocalInstallationKey,
75
+ resolveLocalInstallationName
76
+ };
@@ -18,7 +18,10 @@ const ENV_VAR_NAME = /^[A-Z_][A-Z0-9_]*$/;
18
18
  const LEGACY_MARKETPLACE_FLAGS = Object.freeze([
19
19
  'marketplaceJson', 'parameters', 'subscription', 'resourceGroup', 'installId'
20
20
  ]);
21
- const DOCKER_SETUP_GUIDANCE = 'DOCKER installations deploy through "aifabrix setup"; only --stage register is supported for DOCKER.';
21
+ const DOCKER_SETUP_GUIDANCE =
22
+ 'Install a DOCKER platform with "aifabrix setup", and deploy its environments with ' +
23
+ '"aifabrix env deploy local --stage <env>". Only --stage register is supported for a ' +
24
+ 'DOCKER vendor on a non-local installation.';
22
25
 
23
26
  /**
24
27
  * True when options select the deprecated explicit Marketplace/journal mode.
@@ -174,6 +177,7 @@ function resolveStageInput(resourceGroupKey, options, persistedVendor) {
174
177
  module.exports = {
175
178
  WORKSPACE_STAGES,
176
179
  ENVIRONMENT_STAGES,
180
+ validateStage,
177
181
  VENDORS,
178
182
  DEFAULT_VENDOR,
179
183
  isLegacyMarketplaceMode,
@@ -343,6 +343,9 @@ function addValidatedConfigSections(deployment, variables) {
343
343
  * @param {Object} variables - Variables configuration
344
344
  */
345
345
  function addSimpleOptionalFields(deployment, variables) {
346
+ if (variables.webSocketsEnabled !== undefined) {
347
+ deployment.webSocketsEnabled = variables.webSocketsEnabled;
348
+ }
346
349
  if (variables.startupCommand) {
347
350
  deployment.startupCommand = variables.startupCommand;
348
351
  }
@@ -419,4 +422,3 @@ module.exports = {
419
422
  sanitizeAuthType,
420
423
  validateBuildFields
421
424
  };
422
-
@@ -22,6 +22,22 @@ const { generateControllerManifest } = require('./external-controller-manifest')
22
22
  const { resolveVersionForApp } = require('../utils/image-version');
23
23
  const { getContainerPort } = require('../utils/port-resolver');
24
24
  const { buildEnvVarMap } = require('../utils/env-map');
25
+ const config = require('../core/config');
26
+ const {
27
+ resolveDimensionAbacSetting,
28
+ applyDimensionAbacToConfiguration
29
+ } = require('../utils/dataplane-dimension-abac-setting');
30
+
31
+ async function resolveDimensionAbacValue(appName) {
32
+ if (appName !== 'dataplane') return undefined;
33
+ const userConfig = await config.getConfig();
34
+ return resolveDimensionAbacSetting(userConfig).value;
35
+ }
36
+
37
+ async function applyResolvedDimensionAbacToConfiguration(appName, configuration) {
38
+ const value = await resolveDimensionAbacValue(appName);
39
+ if (value !== undefined) applyDimensionAbacToConfiguration(appName, configuration, value);
40
+ }
25
41
  const { mergeAdditionalPublishedPorts } = require('../utils/docker-manifest-public-port');
26
42
 
27
43
  /**
@@ -115,6 +131,13 @@ function substitutePortInDeployment(obj, portNumber) {
115
131
  }
116
132
  }
117
133
 
134
+ function normalizeDeploymentPort(deployment, effectivePort) {
135
+ if (deployment.port === undefined) return;
136
+ deployment.port = typeof deployment.port === 'string' && /^\d+$/.test(deployment.port)
137
+ ? parseInt(deployment.port, 10)
138
+ : (typeof deployment.port === 'number' ? deployment.port : effectivePort);
139
+ }
140
+
118
141
  /** Regex to find ${VAR} placeholders for env substitution */
119
142
  const ENV_VAR_PLACEHOLDER_REGEX = /\$\{([^}]+)\}/g;
120
143
 
@@ -179,6 +202,9 @@ function buildAndValidateDeployment(appName, variables, envTemplate, rbac, optio
179
202
  // Parse environment variables from template and merge portalInput from application config.
180
203
  // A known deployment target resolves manifest {NAME} values; otherwise they stay for the Controller.
181
204
  const configuration = parseEnvironmentVariables(envTemplate, variables, options && options.target);
205
+ if (options && options.dimensionAbacValue !== undefined) {
206
+ applyDimensionAbacToConfiguration(appName, configuration, options.dimensionAbacValue);
207
+ }
182
208
 
183
209
  // Build deployment manifest (Controller computes deploymentKey from schema)
184
210
  const deployment = builders.buildManifestStructure(appName, variables, configuration, rbac);
@@ -193,10 +219,7 @@ function buildAndValidateDeployment(appName, variables, envTemplate, rbac, optio
193
219
  // Replace ${PORT} with port from application.yaml so manifest deploys correctly
194
220
  const effectivePort = getEffectivePortForSubstitution(variables, 3000);
195
221
  substitutePortInDeployment(deployment, effectivePort);
196
- if (deployment.port !== undefined) {
197
- deployment.port = typeof deployment.port === 'string' && /^\d+$/.test(deployment.port)
198
- ? parseInt(deployment.port, 10) : (typeof deployment.port === 'number' ? deployment.port : effectivePort);
199
- }
222
+ normalizeDeploymentPort(deployment, effectivePort);
200
223
 
201
224
  // Resolve ${REDIS_HOST}, ${DB_HOST}, etc. from env-config.yaml so manifest has no unresolved vars
202
225
  const envVarMap = options && options.envVarMap;
@@ -239,14 +262,12 @@ async function buildDeploymentManifestInMemory(appName, options = {}) {
239
262
  app: { ...variables.app, version: resolved.version }
240
263
  };
241
264
  const configuration = parseEnvironmentVariables(envTemplate, variables, options && options.target);
265
+ await applyResolvedDimensionAbacToConfiguration(appName, configuration);
242
266
  const deployment = builders.buildManifestStructure(appName, variablesWithVersion, configuration, rbac);
243
267
 
244
268
  const effectivePort = getEffectivePortForSubstitution(variablesWithVersion, 3000);
245
269
  substitutePortInDeployment(deployment, effectivePort);
246
- if (deployment.port !== undefined) {
247
- deployment.port = typeof deployment.port === 'string' && /^\d+$/.test(deployment.port)
248
- ? parseInt(deployment.port, 10) : (typeof deployment.port === 'number' ? deployment.port : effectivePort);
249
- }
270
+ normalizeDeploymentPort(deployment, effectivePort);
250
271
  const envVarMap = await buildEnvVarMap('docker', null, null, { appPort: effectivePort });
251
272
  await mergeAdditionalPublishedPorts(envVarMap, variablesWithVersion);
252
273
  substituteEnvVarsInDeployment(deployment, envVarMap);
@@ -302,10 +323,12 @@ async function writeRegularDeployJson(appName, appPath, appType, options = {}) {
302
323
  };
303
324
  const effectivePort = getEffectivePortForSubstitution(variablesWithVersion, 3000);
304
325
  const envVarMap = await buildEnvVarMap('docker', null, null, { appPort: effectivePort });
326
+ const dimensionAbacValue = await resolveDimensionAbacValue(appName);
305
327
  await mergeAdditionalPublishedPorts(envVarMap, variablesWithVersion);
306
328
  const deployment = buildAndValidateDeployment(appName, variablesWithVersion, envTemplate, rbac, {
307
329
  envVarMap,
308
- target: options.target
330
+ target: options.target,
331
+ dimensionAbacValue
309
332
  });
310
333
  const jsonContent = JSON.stringify(deployment, null, 2);
311
334
  fs.writeFileSync(jsonPath, jsonContent, { mode: 0o644 });
@@ -46,7 +46,7 @@ function extractRequirementsSection(deployment) {
46
46
  }
47
47
 
48
48
  function extractOptionalSection(deployment, sectionName, optional) {
49
- if (!deployment[sectionName]) return;
49
+ if (!deployment[sectionName] && !(sectionName === 'webSocketsEnabled' && deployment[sectionName] === false)) return;
50
50
  optional[sectionName] = sectionName === 'authentication'
51
51
  ? { ...deployment[sectionName] }
52
52
  : deployment[sectionName];
@@ -57,7 +57,7 @@ function extractOptionalSections(deployment) {
57
57
  const names = [
58
58
  'healthCheck', 'authentication', 'build', 'repository', 'deployment',
59
59
  'startupCommand', 'runtimeVersion', 'scaling', 'frontDoorRouting',
60
- 'environmentScopedResources', 'required'
60
+ 'environmentScopedResources', 'required', 'webSocketsEnabled'
61
61
  ];
62
62
  for (const sectionName of names) {
63
63
  extractOptionalSection(deployment, sectionName, optional);
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Adopting the databases an existing installation already has (plan 210.0, Miso 227.3).
3
+ *
4
+ * Before environments were encoded in the name, a local install created `keycloak`, `miso`
5
+ * and `dataplane` flat. Encoding the environment renames every one of them, and a rename on
6
+ * a live Postgres means dropping and recreating — which means losing the data. 227.3 required
7
+ * the same thing for Azure and answered it the same way: **adopt the database under the name
8
+ * it already has** rather than move it.
9
+ *
10
+ * Miso keeps old names resolvable (`legacyVaultSecretNames`) and adopts a bare legacy
11
+ * database in `physical-database-adoption.ts`. This is that rule, locally.
12
+ *
13
+ * ## The one thing adoption must never do
14
+ *
15
+ * Only the environment the legacy install *was* may adopt its databases. A local install
16
+ * before this plan was `dev` and nothing else, so `dev` adopting `dataplane` continues what
17
+ * was already running. **`pro` adopting `dataplane` would point production at the database a
18
+ * developer has been working in** — the exact collision environment-encoded names exist to
19
+ * prevent, arriving through the mechanism meant to prevent it. So adoption is refused for
20
+ * every environment except the legacy one, and a fresh install adopts nothing because there
21
+ * is nothing to adopt.
22
+ *
23
+ * Pure: no I/O. The caller supplies the names the server already has.
24
+ *
25
+ * @fileoverview Adopt existing unprefixed databases instead of renaming them
26
+ */
27
+
28
+ 'use strict';
29
+
30
+ const { resolvePhysicalDatabaseNames } = require('./physical-database-naming');
31
+
32
+ /**
33
+ * The environment a pre-environment installation implicitly was.
34
+ *
35
+ * Everything an older local install created belongs to `dev`: there was no other environment
36
+ * to have created it.
37
+ */
38
+ const LEGACY_ENVIRONMENT = 'dev';
39
+
40
+ /** How a name was arrived at. */
41
+ const NAME_ORIGIN = Object.freeze({ CONTRACT: 'contract', ADOPTED: 'adopted' });
42
+
43
+ /**
44
+ * The role that owns a database, derived the way the server already derived it.
45
+ *
46
+ * An adopted database keeps its existing role, and that role was built from the database name
47
+ * with hyphens flattened. Re-deriving it the same way is what makes adoption a no-op rather
48
+ * than a second role on an existing database.
49
+ *
50
+ * @param {string} databaseName - Physical database name
51
+ * @returns {string}
52
+ */
53
+ function roleNameForDatabase(databaseName) {
54
+ return `${String(databaseName).replace(/-/g, '_')}_user`;
55
+ }
56
+
57
+ /**
58
+ * Whether this environment may adopt a legacy database.
59
+ *
60
+ * @param {string} environment - Environment key
61
+ * @returns {boolean}
62
+ */
63
+ function canAdoptInEnvironment(environment) {
64
+ return String(environment ?? '').trim().toLowerCase() === LEGACY_ENVIRONMENT;
65
+ }
66
+
67
+ /**
68
+ * Resolve the names to use for one database, adopting an existing one where that applies.
69
+ *
70
+ * @param {Object} params - Inputs
71
+ * @param {string} params.logicalName - Logical database name from the manifest
72
+ * @param {string} params.appKey - Owning application key
73
+ * @param {string} params.environment - Environment key
74
+ * @param {Iterable<string>} [params.existingDatabaseNames] - Databases the server already has
75
+ * @returns {{ logicalName: string, databaseName: string, roleName: string, origin: string }}
76
+ */
77
+ function resolveDatabaseNamesWithAdoption(params = {}) {
78
+ const contract = resolvePhysicalDatabaseNames({
79
+ logicalName: params.logicalName,
80
+ appKey: params.appKey,
81
+ environment: params.environment
82
+ });
83
+ const existing = new Set(params.existingDatabaseNames || []);
84
+
85
+ // The contract name already being there is the normal case after the first run: adopting it
86
+ // and resolving it are the same answer, so this is reported as `contract` rather than as an
87
+ // adoption of something this plan created itself.
88
+ if (existing.has(contract.databaseName) || !canAdoptInEnvironment(params.environment)) {
89
+ return { ...stripSecretKey(contract), origin: NAME_ORIGIN.CONTRACT };
90
+ }
91
+
92
+ const legacyName = String(params.logicalName ?? '').trim();
93
+ if (!legacyName || !existing.has(legacyName)) {
94
+ return { ...stripSecretKey(contract), origin: NAME_ORIGIN.CONTRACT };
95
+ }
96
+ return {
97
+ logicalName: contract.logicalName,
98
+ databaseName: legacyName,
99
+ roleName: roleNameForDatabase(legacyName),
100
+ origin: NAME_ORIGIN.ADOPTED
101
+ };
102
+ }
103
+
104
+ /**
105
+ * `passwordSecretKey` is deliberately not carried forward: which grammar names that secret is
106
+ * Miso's, and the catalog key is what every consumer reads.
107
+ *
108
+ * @param {Object} names - From resolvePhysicalDatabaseNames
109
+ * @returns {{ logicalName: string, databaseName: string, roleName: string }}
110
+ */
111
+ function stripSecretKey(names) {
112
+ return {
113
+ logicalName: names.logicalName,
114
+ databaseName: names.databaseName,
115
+ roleName: names.roleName
116
+ };
117
+ }
118
+
119
+ module.exports = {
120
+ LEGACY_ENVIRONMENT,
121
+ NAME_ORIGIN,
122
+ roleNameForDatabase,
123
+ canAdoptInEnvironment,
124
+ resolveDatabaseNamesWithAdoption
125
+ };