@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,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
|
-
|
package/lib/generator/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
+
};
|