@aifabrix/builder 2.60.0 → 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/README.md +14 -11
- package/docs/README.md +80 -0
- package/docs/builder-help/evidence-patterns.json +155 -0
- package/docs/builder-help/golden-examples/crm-company.json +30 -0
- package/docs/builder-help/golden-examples/crm-deal.json +29 -0
- package/docs/builder-help/golden-examples/document-storage-keyed-get.json +85 -0
- package/docs/builder-help/golden-examples/document-storage.json +30 -0
- package/docs/builder-help/golden-examples/meeting-transcript.json +29 -0
- package/docs/builder-help/golden-examples/repository-template.json +29 -0
- package/docs/builder-help/golden-examples/service-ticket.json +29 -0
- package/docs/builder-help/platform-roles.json +98 -0
- package/docs/builder-help/resource-type-catalog.json +402 -0
- package/lib/api/applications.api.js +22 -2
- package/lib/api/bootstrap-snapshot.api.js +115 -0
- package/lib/api/configuration.api.js +131 -0
- package/lib/api/system-secrets.api.js +72 -0
- package/lib/app/deploy.js +14 -5
- package/lib/app/run-docker-fallback.js +6 -1
- package/lib/app/run-env-compose.js +5 -6
- package/lib/app/run-helpers.js +2 -1
- package/lib/app/run-parameter-sync.js +142 -0
- package/lib/app/run.js +6 -4
- package/lib/build/docker-build-args.js +5 -2
- package/lib/build/index.js +3 -2
- package/lib/build/standard-docker-build.js +6 -2
- 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-app.js +5 -0
- package/lib/cli/setup-environment.js +156 -0
- 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/datasource-capability-upsert-cli.js +112 -0
- package/lib/commands/datasource-capability.js +4 -2
- package/lib/commands/env-secret-context.js +113 -0
- package/lib/commands/env-secret-list.js +200 -0
- package/lib/commands/env-secret-push-confirm.js +54 -0
- package/lib/commands/env-secret-push-run.js +227 -0
- package/lib/commands/env-secret-push.js +168 -0
- package/lib/commands/login.js +4 -0
- package/lib/commands/repair-datasource-apply.js +5 -1
- package/lib/commands/repair-datasource-keyed-document.js +122 -0
- package/lib/commands/repair-datasource-legacy.js +94 -0
- package/lib/commands/repair-datasource-run.js +1 -0
- package/lib/commands/setup-image-refresh.js +26 -0
- package/lib/commands/setup-modes.js +8 -9
- package/lib/commands/setup-prompts-platform-mode.js +34 -0
- package/lib/commands/setup-prompts.js +2 -180
- 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/verify-operations-skip-e2e.js +25 -1
- package/lib/commands/verify-operations-steps.js +13 -1
- package/lib/commands/wizard-config-normalizer.js +12 -5
- package/lib/commands/wizard-core.js +5 -157
- package/lib/commands/wizard-file-saving.js +163 -0
- package/lib/core/admin-secrets-env-overlay.js +8 -3
- package/lib/core/env-platform-expand.js +5 -1
- 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 +17 -10
- package/lib/core/secrets-env-write.js +11 -4
- package/lib/core/secrets-load.js +4 -2
- package/lib/datasource/binary-documents-validator.js +190 -0
- package/lib/datasource/capability/capability-manifest.js +1 -0
- package/lib/datasource/capability/run-capability-upsert.js +202 -0
- package/lib/datasource/capability/upsert-ingredients.js +291 -0
- package/lib/datasource/capability/upsert-operations.js +138 -0
- package/lib/datasource/capability/upsert-test-scaffold.js +189 -0
- package/lib/datasource/validate.js +12 -5
- 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 +3 -0
- package/lib/generator/split-variables.js +2 -2
- package/lib/lifecycle/product-model.js +4 -3
- package/lib/lifecycle/report-display.js +3 -2
- package/lib/parameters/database-adoption.js +125 -0
- package/lib/parameters/infra-parameter-catalog.js +1 -1
- 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/builder-help-enterprise-sync-fabrix.js +1 -1
- package/lib/programmatic/builder-help-governance.js +1 -1
- package/lib/programmatic/builder-help.js +1 -1
- 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/role-assistant/test-runner-workhub-answers.js +4 -1
- package/lib/role-assistant/test-runner-workhub-missing-fields.js +42 -0
- package/lib/role-assistant/test-runner-workhub-wait-stop.js +94 -0
- package/lib/role-assistant/test-runner-workhub.js +28 -19
- package/lib/schema/application-schema.json +14 -2
- package/lib/schema/external-datasource.schema.json +23 -3
- package/lib/schema/infra.parameter.yaml +440 -56
- package/lib/schema/infrastructure-schema.json +213 -120
- package/lib/utils/compose-generate-docker-compose.js +9 -14
- 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/datasource-binary-evidence.js +92 -0
- package/lib/utils/datasource-test-run-capability-scope.js +44 -1
- package/lib/utils/datasource-test-run-debug-display.js +2 -0
- package/lib/utils/datasource-test-run-display.js +8 -2
- package/lib/utils/datasource-test-run-issue-guidance.js +176 -0
- package/lib/utils/datasource-test-run-tty-log.js +2 -0
- package/lib/utils/declarative-url-ports.js +3 -0
- package/lib/utils/env-copy.js +11 -10
- package/lib/utils/env-environment-file-paths.js +105 -0
- package/lib/utils/environment-scoped-resources.js +48 -14
- package/lib/utils/external-system-system-test-tty.js +3 -2
- package/lib/utils/image-tags.js +2 -2
- package/lib/utils/paths-system-builder-keys.js +7 -1
- package/lib/utils/paths.js +7 -2
- package/lib/utils/platform-kv-ref.js +1 -1
- package/lib/utils/postgres-platform-bootstrap.js +35 -1
- package/lib/utils/prepare-local-data-mount.js +58 -0
- 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 -8
- 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/system-secret-mapping.js +125 -0
- package/lib/utils/test-log-writer.js +2 -1
- package/lib/utils/token-manager.js +14 -8
- package/lib/utils/url-public-path-prefix.js +6 -10
- package/lib/validation/external-manifest-validator.js +5 -0
- package/lib/validation/openapi-contract-surface-validator.js +3 -1
- package/lib/validation/validate-external-file.js +5 -1
- package/package.json +6 -4
- package/templates/agent-kit/agent-kit.yaml +1 -1
- package/templates/agent-kit/skills/aifabrix-connected-system/SKILL.md +2 -1
- package/templates/agent-kit/skills/aifabrix-connected-system/references/delivery-gates.md +15 -0
- package/templates/agent-kit/skills/aifabrix-plan/SKILL.md +3 -2
- package/templates/agent-kit/skills/aifabrix-prove/SKILL.md +6 -2
- package/templates/agent-kit/skills/aifabrix-prove/references/evidence-lifecycle.md +22 -0
- package/templates/agent-kit/skills/aifabrix-role-assistant/SKILL.md +7 -1
- package/templates/agent-kit/skills/aifabrix-role-assistant/references/testing-playbook.md +117 -0
- package/templates/agent-kit/skills/shared/interaction.md +83 -0
- package/templates/agent-kit/skills/shared/status.md +3 -12
- package/templates/applications/builder-api/application.yaml +1 -1
- package/templates/applications/builder-api/env.template +5 -1
- package/templates/applications/dataplane/application.yaml +2 -2
- package/templates/applications/dataplane/env.template +10 -12
- package/templates/applications/integration-server/application.yaml +91 -0
- package/templates/applications/integration-server/env.template +215 -0
- package/templates/applications/keycloak/application.yaml +6 -1
- package/templates/applications/mirrored-templates.json +52 -0
- package/templates/applications/miso-controller/application.yaml +116 -2
- package/templates/applications/miso-controller/env.template +62 -17
- package/templates/applications/miso-controller/rbac.yaml +17 -0
- package/templates/external-system/external-datasource.yaml.hbs +7 -1
- package/templates/marketplace/createUiDefinition.json +52 -4
- package/templates/marketplace/main.json +88 -10
- package/templates/python/docker-compose.hbs +1 -1
- package/templates/agent-kit/skills/aifabrix-plan/references/interaction.md +0 -41
- /package/{lib/programmatic/help-content → docs/builder-help/content}/channel-onboarding.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/cip-overview.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/connected-system-ui.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/dimensions-guide.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/enterprise-sync-fabrix.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/overview.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/subscription-guide.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/workflow.md +0 -0
|
@@ -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,7 @@ 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 { mergeAdditionalPublishedPorts } = require('../utils/docker-manifest-public-port');
|
|
25
26
|
|
|
26
27
|
/**
|
|
27
28
|
* Generates deployment JSON from application configuration files
|
|
@@ -247,6 +248,7 @@ async function buildDeploymentManifestInMemory(appName, options = {}) {
|
|
|
247
248
|
? parseInt(deployment.port, 10) : (typeof deployment.port === 'number' ? deployment.port : effectivePort);
|
|
248
249
|
}
|
|
249
250
|
const envVarMap = await buildEnvVarMap('docker', null, null, { appPort: effectivePort });
|
|
251
|
+
await mergeAdditionalPublishedPorts(envVarMap, variablesWithVersion);
|
|
250
252
|
substituteEnvVarsInDeployment(deployment, envVarMap);
|
|
251
253
|
_validator.validateNoUnresolvedVariablesInDeployment(deployment);
|
|
252
254
|
return { deployment, appPath };
|
|
@@ -300,6 +302,7 @@ async function writeRegularDeployJson(appName, appPath, appType, options = {}) {
|
|
|
300
302
|
};
|
|
301
303
|
const effectivePort = getEffectivePortForSubstitution(variablesWithVersion, 3000);
|
|
302
304
|
const envVarMap = await buildEnvVarMap('docker', null, null, { appPort: effectivePort });
|
|
305
|
+
await mergeAdditionalPublishedPorts(envVarMap, variablesWithVersion);
|
|
303
306
|
const deployment = buildAndValidateDeployment(appName, variablesWithVersion, envTemplate, rbac, {
|
|
304
307
|
envVarMap,
|
|
305
308
|
target: options.target
|
|
@@ -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);
|
|
@@ -134,6 +134,7 @@ function computeOperationalReadinessPercent(checks) {
|
|
|
134
134
|
let earned = 0;
|
|
135
135
|
let total = 0;
|
|
136
136
|
for (const [key, weight] of Object.entries(OPERATIONS_WEIGHTS)) {
|
|
137
|
+
if (checks[key] === null) continue;
|
|
137
138
|
total += weight;
|
|
138
139
|
if (checks[key] === true) {
|
|
139
140
|
earned += weight;
|
|
@@ -159,9 +160,9 @@ function buildOperationsChecksFromSteps(steps) {
|
|
|
159
160
|
authorization: integration || e2e,
|
|
160
161
|
openApi: validation,
|
|
161
162
|
mappings: validation,
|
|
162
|
-
sync: e2e,
|
|
163
|
-
crud: e2e,
|
|
164
|
-
execution: e2e,
|
|
163
|
+
sync: steps.keyedDocumentGet === true ? null : e2e,
|
|
164
|
+
crud: steps.keyedDocumentGet === true ? steps.documentPersistence === true : e2e,
|
|
165
|
+
execution: steps.keyedDocumentGet === true ? steps.documentPersistence === true : e2e,
|
|
165
166
|
validation,
|
|
166
167
|
unitTests: unit,
|
|
167
168
|
integrationTests: integration,
|
|
@@ -132,8 +132,9 @@ function displayOperationsVerificationSteps(detailsGrouped) {
|
|
|
132
132
|
for (const [groupName, keys] of groups) {
|
|
133
133
|
logger.log(chalk.white(groupName));
|
|
134
134
|
for (const key of keys) {
|
|
135
|
-
const
|
|
136
|
-
const
|
|
135
|
+
const group = detailsGrouped[groupName.toLowerCase()];
|
|
136
|
+
const passed = group && Object.hasOwn(group, key) ? group[key] : detailsGrouped[key];
|
|
137
|
+
const glyph = passed === null ? chalk.gray('⏭ Not applicable') : passed ? successGlyph() : failureGlyph();
|
|
137
138
|
logger.log(` ${labels[key] || key}`.padEnd(24) + glyph);
|
|
138
139
|
}
|
|
139
140
|
}
|
|
@@ -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
|
+
};
|
|
@@ -135,7 +135,7 @@ function createCatalogApi(doc, exact, patterns) {
|
|
|
135
135
|
const entry = findEntryForKey(key);
|
|
136
136
|
const gen = entry && entry.generator;
|
|
137
137
|
// emptyAllowed: e.g. Redis password when no requirepass.
|
|
138
|
-
// emptyString: optional user-supplied keys
|
|
138
|
+
// emptyString: optional user-supplied catalog keys — absent resolves like ''.
|
|
139
139
|
return Boolean(gen && (gen.type === 'emptyAllowed' || gen.type === 'emptyString'));
|
|
140
140
|
}
|
|
141
141
|
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The physical secret name for a catalog key (plan 210.0, mirroring Miso 227.4).
|
|
3
|
+
*
|
|
4
|
+
* Builder resolved `tst` and `pro` secrets by prefixing the logical key: `pro-<key>`. That is
|
|
5
|
+
* right for an environment-scoped parameter and **wrong for the other two cases**, which
|
|
6
|
+
* Miso's naming rule (`services/parameters/parameter-naming.ts`) states as:
|
|
7
|
+
*
|
|
8
|
+
* ```text
|
|
9
|
+
* system <key> no prefix, deliberately
|
|
10
|
+
* environment-scoped dev-<key>
|
|
11
|
+
* application-scoped dev-<app>-<key>
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* So the database password an installation actually holds is
|
|
15
|
+
* `pro-dataplane-databases-0-passwordKeyVault`, not `pro-databases-dataplane-0-passwordKeyVault`
|
|
16
|
+
* — the application segment sits before the key, not after the environment alone. And system
|
|
17
|
+
* names such as `keycloak-*` and `acr-*` must not move at all, because the Marketplace
|
|
18
|
+
* template writes them and every installation-level reader expects them where they are.
|
|
19
|
+
*
|
|
20
|
+
* Rather than restate that rule, this resolves the name the way Miso's `computeVaultSecretName`
|
|
21
|
+
* does: from the catalog entry's own `azure.vaultSecretName` or `azure.vaultSecretNamePattern`.
|
|
22
|
+
* The catalog is the Miso master and Builder mirrors it byte for byte, so the pattern is the
|
|
23
|
+
* contract. A key with no pattern is installation-wide by declaration, not by omission.
|
|
24
|
+
*
|
|
25
|
+
* This is why no caller may build a prefix itself: the three cases are not distinguishable
|
|
26
|
+
* from the key alone.
|
|
27
|
+
*
|
|
28
|
+
* @fileoverview Catalog-driven physical secret names, mirroring Miso's computeVaultSecretName
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
'use strict';
|
|
32
|
+
|
|
33
|
+
/** Database catalog keys carry both the owning application and the index in the key. */
|
|
34
|
+
const DATABASE_KEY_PARTS = /^databases-([a-z0-9-]+)-(\d+)-(?:url|password)KeyVault$/;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {string} key - Logical catalog key
|
|
38
|
+
* @returns {string} The index, or empty when the key is not a database key
|
|
39
|
+
*/
|
|
40
|
+
function extractDatabaseIndex(key) {
|
|
41
|
+
const match = String(key ?? '').match(DATABASE_KEY_PARTS);
|
|
42
|
+
return match ? match[2] : '';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The application a database key belongs to.
|
|
47
|
+
*
|
|
48
|
+
* Most database entries match the catalog's generic `keyPattern` rather than an exact key, so
|
|
49
|
+
* they carry no `ownerAppKey` to read — the owner is in the key. Without this the `{appKey}`
|
|
50
|
+
* placeholder resolves empty and the name collapses to `pro--databases-0-…`, a name nothing
|
|
51
|
+
* writes and nothing reads.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} key - Logical catalog key
|
|
54
|
+
* @returns {string} The application key, or empty when the key is not a database key
|
|
55
|
+
*/
|
|
56
|
+
function extractDatabaseAppKey(key) {
|
|
57
|
+
const match = String(key ?? '').match(DATABASE_KEY_PARTS);
|
|
58
|
+
return match ? match[1] : '';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Substitute the placeholders a vault name pattern may carry.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} pattern - From `azure.vaultSecretNamePattern`
|
|
65
|
+
* @param {string} key - Logical catalog key
|
|
66
|
+
* @param {{ environment?: string, serviceName?: string, appKey?: string }} context
|
|
67
|
+
* @returns {string}
|
|
68
|
+
*/
|
|
69
|
+
function applyVaultNamePattern(pattern, key, context = {}) {
|
|
70
|
+
return String(pattern)
|
|
71
|
+
.replace(/\{key\}/g, key)
|
|
72
|
+
.replace(/\{environment\}/g, context.environment ?? '')
|
|
73
|
+
.replace(/\{serviceName\}/g, context.serviceName ?? '')
|
|
74
|
+
.replace(/\{appKey\}/g, context.appKey ?? '')
|
|
75
|
+
.replace(/\{index\}/g, extractDatabaseIndex(key));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The physical name of the secret holding a logical key's value.
|
|
80
|
+
*
|
|
81
|
+
* @param {string} key - Logical catalog key
|
|
82
|
+
* @param {Object} [context] - Resolution context
|
|
83
|
+
* @param {string} [context.environment] - Environment key
|
|
84
|
+
* @param {string} [context.serviceName] - Installation service name
|
|
85
|
+
* @param {Object} [context.catalog] - Catalog override, for tests
|
|
86
|
+
* @returns {string} Physical secret name, or the key unchanged when the catalog says so
|
|
87
|
+
*/
|
|
88
|
+
function computePhysicalSecretName(key, context = {}) {
|
|
89
|
+
const logicalKey = String(key ?? '').trim();
|
|
90
|
+
if (!logicalKey) {
|
|
91
|
+
return logicalKey;
|
|
92
|
+
}
|
|
93
|
+
const entry = findCatalogEntry(logicalKey, context.catalog);
|
|
94
|
+
const azure = entry && entry.azure ? entry.azure : null;
|
|
95
|
+
if (!azure) {
|
|
96
|
+
return logicalKey;
|
|
97
|
+
}
|
|
98
|
+
if (azure.vaultSecretName) {
|
|
99
|
+
return String(azure.vaultSecretName);
|
|
100
|
+
}
|
|
101
|
+
if (!azure.vaultSecretNamePattern) {
|
|
102
|
+
return logicalKey;
|
|
103
|
+
}
|
|
104
|
+
// The owning application comes from the entry or from the key, never from the caller: an
|
|
105
|
+
// application-scoped parameter names its own owner, and a caller passing a different one
|
|
106
|
+
// would silently address another application's secret.
|
|
107
|
+
return applyVaultNamePattern(azure.vaultSecretNamePattern, logicalKey, {
|
|
108
|
+
environment: context.environment,
|
|
109
|
+
serviceName: context.serviceName,
|
|
110
|
+
appKey: entry.ownerAppKey || extractDatabaseAppKey(logicalKey)
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* @param {string} key - Logical catalog key
|
|
116
|
+
* @param {Object} [override] - Catalog override, for tests
|
|
117
|
+
* @returns {Object|null}
|
|
118
|
+
*/
|
|
119
|
+
function findCatalogEntry(key, override) {
|
|
120
|
+
const catalog = override || loadCatalogQuietly();
|
|
121
|
+
if (!catalog || typeof catalog.findEntryForKey !== 'function') {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
return catalog.findEntryForKey(key) || null;
|
|
126
|
+
} catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The catalog, or nothing.
|
|
133
|
+
*
|
|
134
|
+
* A key resolves to itself when the catalog cannot be read, which is the same answer an
|
|
135
|
+
* absent entry gives. Failing here instead would turn an unreadable catalog into "no secret
|
|
136
|
+
* resolves", and the catalog is already validated by its own checks.
|
|
137
|
+
*
|
|
138
|
+
* @returns {Object|null}
|
|
139
|
+
*/
|
|
140
|
+
function loadCatalogQuietly() {
|
|
141
|
+
try {
|
|
142
|
+
return require('./infra-parameter-catalog').getInfraParameterCatalog();
|
|
143
|
+
} catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The name an environment-isolated installation must read.
|
|
150
|
+
*
|
|
151
|
+
* `computePhysicalSecretName` answers what the catalog declares. This answers what an
|
|
152
|
+
* environment may actually use, and the two differ in one place that matters.
|
|
153
|
+
*
|
|
154
|
+
* **An application-scoped secret is never shared between environments.** Each environment's
|
|
155
|
+
* application holds its own credential. Some catalog entries still pin a literal from before
|
|
156
|
+
* environments existed — `dataplane-client-secretKeyVault` names itself, with no
|
|
157
|
+
* `{environment}` anywhere — and honouring that in `pro` would hand `pro` the credential
|
|
158
|
+
* `dev` is using. One leaked client secret would then authenticate as the other environment's
|
|
159
|
+
* application, which is the whole isolation gone.
|
|
160
|
+
*
|
|
161
|
+
* So an app-scoped name that does not carry its environment gets one. System names
|
|
162
|
+
* (`keycloak-*`, `acr-*`) are left exactly as they are: there is one Keycloak and one
|
|
163
|
+
* registry per platform, and those secrets are shared on purpose.
|
|
164
|
+
*
|
|
165
|
+
* @param {string} key - Logical catalog key
|
|
166
|
+
* @param {string} environment - Environment key
|
|
167
|
+
* @returns {string} The secret name this environment must read
|
|
168
|
+
*/
|
|
169
|
+
function resolveStrictSecretName(key, environment) {
|
|
170
|
+
const logicalKey = String(key ?? '').trim();
|
|
171
|
+
const env = String(environment ?? '').trim().toLowerCase();
|
|
172
|
+
if (!logicalKey || !env) {
|
|
173
|
+
return logicalKey;
|
|
174
|
+
}
|
|
175
|
+
const physical = computePhysicalSecretName(logicalKey, { environment: env });
|
|
176
|
+
if (physical.startsWith(`${env}-`)) {
|
|
177
|
+
return physical;
|
|
178
|
+
}
|
|
179
|
+
const entry = findCatalogEntry(logicalKey);
|
|
180
|
+
// Unplaced keys are this installation's own and are scoped by environment; `system`,
|
|
181
|
+
// `infra` and `shared` entries are platform-wide and keep their names.
|
|
182
|
+
if (!entry || entry.scope === 'app') {
|
|
183
|
+
return `${env}-${physical}`;
|
|
184
|
+
}
|
|
185
|
+
return physical;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
module.exports = {
|
|
189
|
+
extractDatabaseIndex,
|
|
190
|
+
extractDatabaseAppKey,
|
|
191
|
+
applyVaultNamePattern,
|
|
192
|
+
computePhysicalSecretName,
|
|
193
|
+
resolveStrictSecretName
|
|
194
|
+
};
|