@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
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build a no-duplicate upsert proof from author-owned scenario facts.
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Safe testPayload scaffolding for datasource capability upsert
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const { deepClone } = require('./copy-operations');
|
|
10
|
+
|
|
11
|
+
const RUN_TOKEN = /\{\{\s*testRunId\s*\}\}/i;
|
|
12
|
+
|
|
13
|
+
function scenarioInput(scenario) {
|
|
14
|
+
const input = scenario?.input;
|
|
15
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) return null;
|
|
16
|
+
const properties = input.properties;
|
|
17
|
+
return properties && typeof properties === 'object' && !Array.isArray(properties)
|
|
18
|
+
? properties
|
|
19
|
+
: input;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function scalar(value) {
|
|
23
|
+
return value !== null && ['string', 'number', 'boolean'].includes(typeof value);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function identityFromInput(input, identityFields) {
|
|
27
|
+
if (!input) return null;
|
|
28
|
+
const entries = identityFields.map((field) => [field, input[field]]);
|
|
29
|
+
if (entries.some(([, value]) => !scalar(value) || value === '')) return null;
|
|
30
|
+
return Object.fromEntries(entries);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function sameIdentity(first, second, identityFields) {
|
|
34
|
+
return identityFields.every((field) => first[field] === second[field]);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isCollisionSafe(identity) {
|
|
38
|
+
return Object.values(identity).some((value) =>
|
|
39
|
+
typeof value === 'string' && RUN_TOKEN.test(value)
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function profileFields(doc, targetKey) {
|
|
44
|
+
const profile = doc.exposed?.profiles?.[targetKey];
|
|
45
|
+
if (Array.isArray(profile)) return profile;
|
|
46
|
+
return Array.isArray(profile?.attributes) ? profile.attributes : [];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function changedPublishedField(doc, targetKey, first, second, identityFields) {
|
|
50
|
+
const identity = new Set(identityFields);
|
|
51
|
+
const published = new Set(profileFields(doc, targetKey));
|
|
52
|
+
return Object.keys(first).find((field) =>
|
|
53
|
+
!identity.has(field) && published.has(field) && scalar(first[field]) &&
|
|
54
|
+
scalar(second[field]) && first[field] !== second[field]
|
|
55
|
+
) || null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function analyzeWritePair(doc, targetKey, identityFields, firstRow, secondRow) {
|
|
59
|
+
const first = scenarioInput(firstRow);
|
|
60
|
+
const second = scenarioInput(secondRow);
|
|
61
|
+
const firstIdentity = identityFromInput(first, identityFields);
|
|
62
|
+
const secondIdentity = identityFromInput(second, identityFields);
|
|
63
|
+
if (!firstIdentity || !secondIdentity) return { reason: 'both writes must contain every business identity field' };
|
|
64
|
+
if (!sameIdentity(firstIdentity, secondIdentity, identityFields)) {
|
|
65
|
+
return { reason: 'the two writes must use the same business identity values' };
|
|
66
|
+
}
|
|
67
|
+
if (!isCollisionSafe(firstIdentity)) {
|
|
68
|
+
return { reason: 'the business identity must include {{testRunId}} to isolate each live run' };
|
|
69
|
+
}
|
|
70
|
+
const changedField = changedPublishedField(
|
|
71
|
+
doc, targetKey, first, second, identityFields
|
|
72
|
+
);
|
|
73
|
+
if (!changedField) {
|
|
74
|
+
return { reason: 'the second write must change one published non-identity scalar field' };
|
|
75
|
+
}
|
|
76
|
+
return { first, second, identity: firstIdentity, changedField };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function exactLookup(operation, input, expectedRows, identityKey) {
|
|
80
|
+
return {
|
|
81
|
+
operation,
|
|
82
|
+
input: deepClone(input),
|
|
83
|
+
identityKey,
|
|
84
|
+
matchMode: 'exactIdentities',
|
|
85
|
+
expectedRows: deepClone(expectedRows)
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function expectedRow(identity, changedField, value) {
|
|
90
|
+
return { ...deepClone(identity), [changedField]: value };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function hasExactProof(scenarios, index, targetKey, lookupKey, facts) {
|
|
94
|
+
const rows = scenarios.slice(index, index + 5);
|
|
95
|
+
if (rows.length !== 5) return false;
|
|
96
|
+
const operations = rows.map((row) => row?.operation);
|
|
97
|
+
if (operations.join('|') !== [lookupKey, targetKey, lookupKey, targetKey, lookupKey].join('|')) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
const [before, , afterFirst, , afterSecond] = rows;
|
|
101
|
+
return [before, afterFirst, afterSecond].every((row) =>
|
|
102
|
+
row?.matchMode === 'exactIdentities' && row?.identityKey === facts.changedField
|
|
103
|
+
) && Array.isArray(before.expectedRows) && before.expectedRows.length === 0 &&
|
|
104
|
+
afterFirst.expectedRows?.length === 1 && afterSecond.expectedRows?.length === 1 &&
|
|
105
|
+
afterFirst.expectedRows?.[0]?.[facts.changedField] === facts.first[facts.changedField] &&
|
|
106
|
+
afterSecond.expectedRows?.[0]?.[facts.changedField] === facts.second[facts.changedField];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function findExistingProof(doc, scenarios, targetKey, lookupKey, identityFields) {
|
|
110
|
+
for (let index = 0; index <= scenarios.length - 5; index += 1) {
|
|
111
|
+
const facts = analyzeWritePair(
|
|
112
|
+
doc, targetKey, identityFields, scenarios[index + 1], scenarios[index + 3]
|
|
113
|
+
);
|
|
114
|
+
if (!facts.reason && hasExactProof(scenarios, index, targetKey, lookupKey, facts)) return true;
|
|
115
|
+
}
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function findAdjacentWrites(doc, scenarios, targetKey, identityFields) {
|
|
120
|
+
let lastReason = 'two adjacent enabled upsert scenarios are required';
|
|
121
|
+
for (let index = 0; index < scenarios.length - 1; index += 1) {
|
|
122
|
+
const first = scenarios[index];
|
|
123
|
+
const second = scenarios[index + 1];
|
|
124
|
+
if (first?.operation !== targetKey || second?.operation !== targetKey) continue;
|
|
125
|
+
if (first.enabled === false || second.enabled === false) continue;
|
|
126
|
+
const facts = analyzeWritePair(doc, targetKey, identityFields, first, second);
|
|
127
|
+
if (!facts.reason) return { index, facts };
|
|
128
|
+
lastReason = facts.reason;
|
|
129
|
+
}
|
|
130
|
+
return { reason: lastReason };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function insufficientIssue(reason) {
|
|
134
|
+
return {
|
|
135
|
+
code: 'UPSERT-TEST-DATA-INSUFFICIENT',
|
|
136
|
+
severity: 'blocking',
|
|
137
|
+
ownership: 'test-payload',
|
|
138
|
+
aiFixable: false,
|
|
139
|
+
message: `Safe --test scaffolding was not generated: ${reason}; no values were fabricated`,
|
|
140
|
+
consequence: 'A weaker generated case could provide false no-duplicate confidence.',
|
|
141
|
+
remediation: ['Author two adjacent upsert inputs with the same isolated identity and one changed published field, then rerun --test.'],
|
|
142
|
+
prevention: 'Keep collision-safe two-write facts with the datasource test payload.',
|
|
143
|
+
residualRisk: 'Upsert remains unproven until the exact live chain passes.'
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function buildProofRows(lookupKey, facts) {
|
|
148
|
+
const firstExpected = expectedRow(
|
|
149
|
+
facts.identity, facts.changedField, facts.first[facts.changedField]
|
|
150
|
+
);
|
|
151
|
+
const secondExpected = expectedRow(
|
|
152
|
+
facts.identity, facts.changedField, facts.second[facts.changedField]
|
|
153
|
+
);
|
|
154
|
+
return [
|
|
155
|
+
exactLookup(lookupKey, facts.identity, [], facts.changedField),
|
|
156
|
+
exactLookup(lookupKey, facts.identity, [firstExpected], facts.changedField),
|
|
157
|
+
exactLookup(lookupKey, facts.identity, [secondExpected], facts.changedField)
|
|
158
|
+
];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function scaffoldUpsertTests(originalDoc, analysis, targetKey = 'upsert') {
|
|
162
|
+
const scenarios = originalDoc.testPayload?.scenarios;
|
|
163
|
+
const lookupKey = analysis?.selectedLookup?.operationKey;
|
|
164
|
+
const identities = Array.isArray(analysis?.identityFields) ? analysis.identityFields : [];
|
|
165
|
+
if (!Array.isArray(scenarios) || !lookupKey || identities.length === 0) {
|
|
166
|
+
return { changed: false, issue: insufficientIssue('required scenarios or exact lookup facts are missing') };
|
|
167
|
+
}
|
|
168
|
+
if (findExistingProof(originalDoc, scenarios, targetKey, lookupKey, identities)) {
|
|
169
|
+
return { changed: false, doc: originalDoc, patchOperations: [], updatedSections: [] };
|
|
170
|
+
}
|
|
171
|
+
const pair = findAdjacentWrites(originalDoc, scenarios, targetKey, identities);
|
|
172
|
+
if (pair.reason) return { changed: false, issue: insufficientIssue(pair.reason) };
|
|
173
|
+
const [before, afterFirst, afterSecond] = buildProofRows(lookupKey, pair.facts);
|
|
174
|
+
const doc = deepClone(originalDoc);
|
|
175
|
+
doc.testPayload.scenarios.splice(pair.index, 2,
|
|
176
|
+
before, scenarios[pair.index], afterFirst, scenarios[pair.index + 1], afterSecond);
|
|
177
|
+
return {
|
|
178
|
+
changed: true,
|
|
179
|
+
doc,
|
|
180
|
+
patchOperations: [{ op: 'replace', path: '/testPayload/scenarios', value: doc.testPayload.scenarios }],
|
|
181
|
+
updatedSections: ['testPayload.scenarios: upsert no-duplicate proof']
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
module.exports = {
|
|
186
|
+
scaffoldUpsertTests,
|
|
187
|
+
analyzeWritePair,
|
|
188
|
+
insufficientIssue
|
|
189
|
+
};
|
|
@@ -23,6 +23,8 @@ const { validateFieldReferences } = require('./field-reference-validator');
|
|
|
23
23
|
const { validateAbac } = require('./abac-validator');
|
|
24
24
|
const { buildDatasourceValidateSummary } = require('./datasource-validate-summary');
|
|
25
25
|
|
|
26
|
+
const { validateBinaryDocumentsContract } = require('./binary-documents-validator');
|
|
27
|
+
|
|
26
28
|
const DATASOURCE_CONFIG_EXTENSIONS = new Set(['.json', '.yaml', '.yml']);
|
|
27
29
|
const EXCLUDE_CONFIG_NAMES = new Set([
|
|
28
30
|
'application.json',
|
|
@@ -242,12 +244,14 @@ async function validateDatasourceFile(filePathOrKey) {
|
|
|
242
244
|
};
|
|
243
245
|
}
|
|
244
246
|
|
|
247
|
+
const binary = validateBinaryDocumentsContract(parsed);
|
|
248
|
+
const warnings = binary.warnings.map(issue => `${issue.code}: ${issue.message}`);
|
|
245
249
|
const postSchemaErrors = collectPostSchemaErrors(parsed);
|
|
246
250
|
if (postSchemaErrors.length > 0) {
|
|
247
251
|
return {
|
|
248
252
|
valid: false,
|
|
249
253
|
errors: postSchemaErrors,
|
|
250
|
-
warnings
|
|
254
|
+
warnings,
|
|
251
255
|
resolvedPath: filePath,
|
|
252
256
|
summary
|
|
253
257
|
};
|
|
@@ -256,7 +260,7 @@ async function validateDatasourceFile(filePathOrKey) {
|
|
|
256
260
|
return {
|
|
257
261
|
valid: true,
|
|
258
262
|
errors: [],
|
|
259
|
-
warnings
|
|
263
|
+
warnings,
|
|
260
264
|
resolvedPath: filePath,
|
|
261
265
|
summary
|
|
262
266
|
};
|
|
@@ -269,7 +273,8 @@ async function validateDatasourceFile(filePathOrKey) {
|
|
|
269
273
|
function collectPostSchemaErrors(parsed) {
|
|
270
274
|
return [
|
|
271
275
|
...validateFieldReferences(parsed),
|
|
272
|
-
...validateAbac(parsed)
|
|
276
|
+
...validateAbac(parsed),
|
|
277
|
+
...validateBinaryDocumentsContract(parsed).errors.map(issue => `${issue.code}: ${issue.message}`)
|
|
273
278
|
];
|
|
274
279
|
}
|
|
275
280
|
|
|
@@ -303,12 +308,14 @@ function validateDatasourceParsed(parsed) {
|
|
|
303
308
|
};
|
|
304
309
|
}
|
|
305
310
|
|
|
311
|
+
const binary = validateBinaryDocumentsContract(parsed);
|
|
312
|
+
const warnings = binary.warnings.map(issue => `${issue.code}: ${issue.message}`);
|
|
306
313
|
const postSchemaErrors = collectPostSchemaErrors(parsed);
|
|
307
314
|
if (postSchemaErrors.length > 0) {
|
|
308
315
|
return {
|
|
309
316
|
valid: false,
|
|
310
317
|
errors: postSchemaErrors,
|
|
311
|
-
warnings
|
|
318
|
+
warnings,
|
|
312
319
|
summary
|
|
313
320
|
};
|
|
314
321
|
}
|
|
@@ -316,7 +323,7 @@ function validateDatasourceParsed(parsed) {
|
|
|
316
323
|
return {
|
|
317
324
|
valid: true,
|
|
318
325
|
errors: [],
|
|
319
|
-
warnings
|
|
326
|
+
warnings,
|
|
320
327
|
summary
|
|
321
328
|
};
|
|
322
329
|
}
|
|
@@ -6,8 +6,14 @@
|
|
|
6
6
|
* All input validation happens before any filesystem, network or cloud mutation.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
const {
|
|
9
|
+
const {
|
|
10
|
+
resolveStageInput,
|
|
11
|
+
validateResourceGroupKey,
|
|
12
|
+
validateStage,
|
|
13
|
+
ENVIRONMENT_STAGES
|
|
14
|
+
} = require('./stage-input');
|
|
10
15
|
const { openWorkspace } = require('./workspace');
|
|
16
|
+
const { isLocalInstallationKey } = require('./local-installation');
|
|
11
17
|
|
|
12
18
|
/**
|
|
13
19
|
* Runs one installation workspace stage.
|
|
@@ -18,6 +24,13 @@ const { openWorkspace } = require('./workspace');
|
|
|
18
24
|
*/
|
|
19
25
|
async function deployInstallationStage(resourceGroupKey, options, deps = {}) {
|
|
20
26
|
const resourceGroup = validateResourceGroupKey(resourceGroupKey);
|
|
27
|
+
// A local installation owns its environments itself: no workspace, no Azure template and
|
|
28
|
+
// no Controller round trip. `register` still goes the shared route, because a local
|
|
29
|
+
// install registers with Mori exactly as any other does.
|
|
30
|
+
if (isLocalInstallationKey(resourceGroup) && ENVIRONMENT_STAGES.includes(validateStage(options.stage))) {
|
|
31
|
+
const { runLocalEnvironmentStage } = require('./local-environment-stage');
|
|
32
|
+
return runLocalEnvironmentStage(resourceGroup, validateStage(options.stage), deps);
|
|
33
|
+
}
|
|
21
34
|
const workspace = openWorkspace(resourceGroup, { root: deps.root });
|
|
22
35
|
const input = resolveStageInput(resourceGroup, options, workspace.infra?.vendor?.value);
|
|
23
36
|
const base = { stage: input.stage, vendor: input.vendor, workspaceDir: workspace.dir };
|
|
@@ -88,6 +88,7 @@ const PARAMETER_CATALOG = Object.freeze({
|
|
|
88
88
|
enablePostgresPublicAccess: { kind: 'allowMyIp', source: 'allowMyIp', description: 'Default off; enabled only while allowMyIp grants the validated caller IPv4 /32 for testing.' },
|
|
89
89
|
allowedIPs: { kind: 'allowMyIp', source: 'allowMyIpOrUser', description: 'Explicit CIDRs for PostgreSQL, Key Vault, and Storage; allowMyIp merges the resolved caller IPv4 /32.' },
|
|
90
90
|
adminPassword: { kind: 'secretEnv', env: 'ADMIN_PASSWORD', required: true, description: 'Installation administrator password.' },
|
|
91
|
+
adminEmail: { kind: 'file', source: 'user', required: true, description: 'Email of the initial platform administrator, used for the installation welcome. Required: the template sets no default and enforces minLength.' },
|
|
91
92
|
deploymentEnvironment: { kind: 'file', source: 'templateDefault', description: 'Vendor deployment profile.' },
|
|
92
93
|
keycloakVerifyAudience: { kind: 'file', source: 'templateDefault', description: 'Enable Keycloak audience verification.' },
|
|
93
94
|
encryptionKey: { kind: 'secretEnv', env: 'ENCRYPTION_KEY', required: true, description: 'Application/data encryption key.' },
|
|
@@ -99,11 +100,13 @@ const PARAMETER_CATALOG = Object.freeze({
|
|
|
99
100
|
deployRegistryRoleAssignment: { kind: 'parameterDefault', defaultFrom: 'deployRoleAssignments', source: 'templateDefault', description: 'Deploy the AcrPull grant on the container registry. False while credential image pull is in effect, because that grant lives in the registry resource group, which a customer installation cannot reach; restored for managed identity.' },
|
|
100
101
|
moriBaseUrl: { kind: 'file', source: 'registration', required: true, description: 'Mori service base URL.' },
|
|
101
102
|
moriLicensePublicKey: { kind: 'file', source: 'registration', required: false, description: 'Mori licence issuer public key (SPKI PEM); empty leaves the installation unpinned and it then refuses every licence.' },
|
|
103
|
+
moriPolicyPublicKey: { kind: 'file', source: 'registration', required: false, description: 'Mori policy signing anchor (SPKI PEM), a different pair from the licence key; empty leaves the policy bundle checkable only against the key it carries, which proves nothing.' },
|
|
102
104
|
moriLicenseIssuer: { kind: 'file', source: 'registration', required: false, description: 'Expected iss claim on a Mori licence.' },
|
|
103
105
|
entraIdClientId: { kind: 'deferred', description: 'Not collected or sent by infra; issued/configured during later Entra provider onboarding.' },
|
|
104
106
|
entraIdClientSecret: { kind: 'deferred', description: 'Not collected or sent by infra; issued/configured during later Entra provider onboarding.' },
|
|
105
107
|
entraIdTenantId: { kind: 'deferred', description: 'Customer Entra provider tenant; distinct from the Azure subscription directory tenant and configured later.' },
|
|
106
108
|
licenseEdition: { kind: 'license', source: 'cliOrFile', description: 'License requested from Mori during tenant activation: COMMUNITY, STANDARD or ENTERPRISE, or an Azure Marketplace subscription ID. Standard/Enterprise by name is a 30-day evaluation.' },
|
|
109
|
+
installationResourceModel: { kind: 'file', source: 'templateDefault', description: 'Initial installation topology: shared for Community and Standard, dedicated for Enterprise DEV. Defaults to shared and is protected by Azure application configuration. It records the topology the installation starts in and grants no later licence entitlement, so it is not a licence input.' },
|
|
107
110
|
onboardingMode: { kind: 'onboarding', source: 'workspacePolicy', description: 'Miso onboarding mode (ONBOARDING_MODE); Builder installations use saasDirect, Marketplace customer installations use customerOwned.' },
|
|
108
111
|
capacityProfile: { kind: 'capacity', source: 'workspacePreset', description: 'Infrastructure sizing profile (s, m, l, xl) selecting compute and PostgreSQL SKUs. Derived from the workspace preset so the two cannot drift; set it with --preset.' },
|
|
109
112
|
misoImageTag: { kind: 'file', source: 'templateDefault', description: 'Miso container image tag.' },
|
|
@@ -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
|
+
};
|