@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
|
@@ -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 =
|
|
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
|
-
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Physical database, role and password-secret names for a local installation.
|
|
3
|
+
*
|
|
4
|
+
* Local runs every entitled environment on one PostgreSQL server, which is 227.0's shared
|
|
5
|
+
* model. Without an environment segment two environments declaring the same logical
|
|
6
|
+
* database name collide on the database, the role and the secret — the defect Miso 227.3
|
|
7
|
+
* fixed for Azure. The local path had none of it: keys are derived as
|
|
8
|
+
* `databases-{appKey}-{i}-*` with no environment in them.
|
|
9
|
+
*
|
|
10
|
+
* This mirrors Miso's `resolvePhysicalDatabaseNames`
|
|
11
|
+
* (`services/infra-parameters/physical-database-naming.ts`) deliberately. Matching it makes
|
|
12
|
+
* a local environment and an Azure environment the same object at a different address,
|
|
13
|
+
* which is the point of validating a near-Azure stack in Docker. A second scheme would be
|
|
14
|
+
* two isolation rules to keep in step, and the Azure one already ships.
|
|
15
|
+
*
|
|
16
|
+
* Pure: no I/O. The environment comes from the caller, never from `process.env` and never
|
|
17
|
+
* from a manifest.
|
|
18
|
+
*
|
|
19
|
+
* @fileoverview Environment-encoded PostgreSQL naming for shared local infrastructure
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
/** Canonical environment keys. Matches the Azure and database vocabulary, not licence slots. */
|
|
25
|
+
const PHYSICAL_NAME_ENVIRONMENTS = Object.freeze(['miso', 'dev', 'tst', 'pro']);
|
|
26
|
+
|
|
27
|
+
/** PostgreSQL truncates identifiers at 63 bytes, silently, which would reintroduce collisions. */
|
|
28
|
+
const POSTGRES_IDENTIFIER_LIMIT = 63;
|
|
29
|
+
|
|
30
|
+
class UnknownPhysicalEnvironmentError extends Error {
|
|
31
|
+
constructor(environment) {
|
|
32
|
+
super(
|
|
33
|
+
`Unknown environment '${environment}'. Expected one of: ${PHYSICAL_NAME_ENVIRONMENTS.join(', ')}.`
|
|
34
|
+
);
|
|
35
|
+
this.name = 'UnknownPhysicalEnvironmentError';
|
|
36
|
+
this.environment = environment;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
class PhysicalNameTooLongError extends Error {
|
|
41
|
+
constructor(resolvedName, limit) {
|
|
42
|
+
super(
|
|
43
|
+
`Resolved physical name '${resolvedName}' is ${resolvedName.length} characters, over the ` +
|
|
44
|
+
`${limit} character limit. Shorten the application key or the logical database name.`
|
|
45
|
+
);
|
|
46
|
+
this.name = 'PhysicalNameTooLongError';
|
|
47
|
+
this.resolvedName = resolvedName;
|
|
48
|
+
this.limit = limit;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Normalise a segment to the PostgreSQL identifier character set.
|
|
54
|
+
*
|
|
55
|
+
* @param {string} value - Raw segment
|
|
56
|
+
* @returns {string}
|
|
57
|
+
*/
|
|
58
|
+
function normalizeSegment(value) {
|
|
59
|
+
return String(value ?? '')
|
|
60
|
+
.trim()
|
|
61
|
+
.toLowerCase()
|
|
62
|
+
.replace(/[^a-z0-9_]/g, '_')
|
|
63
|
+
.replace(/_+/g, '_')
|
|
64
|
+
.replace(/^_|_$/g, '');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Reject an unknown environment instead of defaulting to one.
|
|
69
|
+
*
|
|
70
|
+
* Defaulting is how a production database ends up on a development server, so this fails
|
|
71
|
+
* closed.
|
|
72
|
+
*
|
|
73
|
+
* @param {string} environment - Environment key
|
|
74
|
+
* @returns {string} Normalised environment key
|
|
75
|
+
* @throws {UnknownPhysicalEnvironmentError}
|
|
76
|
+
*/
|
|
77
|
+
function assertPhysicalEnvironment(environment) {
|
|
78
|
+
const normalized = String(environment ?? '').trim().toLowerCase();
|
|
79
|
+
if (!PHYSICAL_NAME_ENVIRONMENTS.includes(normalized)) {
|
|
80
|
+
throw new UnknownPhysicalEnvironmentError(environment);
|
|
81
|
+
}
|
|
82
|
+
return normalized;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Resolve the database, role and password-secret names together.
|
|
87
|
+
*
|
|
88
|
+
* Resolved as one call so a caller cannot take two of the three and leave the third
|
|
89
|
+
* verbatim, which is the shape of the original defect.
|
|
90
|
+
*
|
|
91
|
+
* @param {Object} params - Inputs
|
|
92
|
+
* @param {string} params.logicalName - Logical database name from the manifest
|
|
93
|
+
* @param {string} params.appKey - Application key
|
|
94
|
+
* @param {string} params.environment - Logical environment
|
|
95
|
+
* @returns {{ logicalName: string, databaseName: string, roleName: string, passwordSecretKey: string }}
|
|
96
|
+
* @throws {UnknownPhysicalEnvironmentError|PhysicalNameTooLongError|Error}
|
|
97
|
+
*/
|
|
98
|
+
function resolvePhysicalDatabaseNames(params = {}) {
|
|
99
|
+
const environment = assertPhysicalEnvironment(params.environment);
|
|
100
|
+
const logicalName = String(params.logicalName ?? '').trim();
|
|
101
|
+
const app = normalizeSegment(params.appKey);
|
|
102
|
+
const logical = normalizeSegment(logicalName);
|
|
103
|
+
|
|
104
|
+
if (!app) {
|
|
105
|
+
throw new Error('appKey is required to resolve a physical database name');
|
|
106
|
+
}
|
|
107
|
+
if (!logical) {
|
|
108
|
+
throw new Error('logicalName is required to resolve a physical database name');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const databaseName = `${environment}_${app}_${logical}`;
|
|
112
|
+
const roleName = `${databaseName}_user`;
|
|
113
|
+
|
|
114
|
+
// The role is the longest derived identifier, so checking it covers the database too.
|
|
115
|
+
if (roleName.length > POSTGRES_IDENTIFIER_LIMIT) {
|
|
116
|
+
throw new PhysicalNameTooLongError(roleName, POSTGRES_IDENTIFIER_LIMIT);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
logicalName,
|
|
121
|
+
databaseName,
|
|
122
|
+
roleName,
|
|
123
|
+
// Hyphenated because the vault naming contract rejects underscores.
|
|
124
|
+
passwordSecretKey: `${databaseName.replace(/_/g, '-')}-password`
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = {
|
|
129
|
+
PHYSICAL_NAME_ENVIRONMENTS,
|
|
130
|
+
POSTGRES_IDENTIFIER_LIMIT,
|
|
131
|
+
UnknownPhysicalEnvironmentError,
|
|
132
|
+
PhysicalNameTooLongError,
|
|
133
|
+
normalizeSegment,
|
|
134
|
+
assertPhysicalEnvironment,
|
|
135
|
+
resolvePhysicalDatabaseNames
|
|
136
|
+
};
|