@aifabrix/builder 2.58.0 → 2.60.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/agent-kit/codex-app-server.js +44 -0
- package/lib/agent-kit/git-identity.js +180 -0
- package/lib/agent-kit/init.js +4 -17
- package/lib/agent-kit/session-report.js +15 -10
- package/lib/agent-kit/setup-auth.js +131 -0
- package/lib/agent-kit/setup-context.js +118 -0
- package/lib/agent-kit/setup-github.js +70 -0
- package/lib/agent-kit/setup-managed.js +79 -0
- package/lib/agent-kit/setup-process.js +42 -0
- package/lib/agent-kit/setup-session.js +53 -0
- package/lib/agent-kit/setup-state.js +52 -0
- package/lib/agent-kit/setup-workspace.js +113 -0
- package/lib/agent-kit/setup.js +87 -0
- package/lib/agent-kit/start.js +48 -3
- package/lib/agent-kit/tmux-sessions.js +20 -3
- package/lib/api/dev.api.js +1 -1
- package/lib/api/miso-health.api.js +26 -0
- package/lib/api/role-assistant-test-job.api.js +60 -0
- package/lib/api/types/dev.types.js +3 -3
- package/lib/api/work-search.api.js +13 -6
- package/lib/app/deploy.js +8 -103
- package/lib/app/show-display.js +1 -0
- package/lib/app/show-online.js +15 -0
- package/lib/build/standard-docker-build.js +4 -1
- package/lib/cli/index.js +4 -0
- package/lib/cli/setup-app.js +14 -1
- package/lib/cli/setup-dev-path-commands.js +4 -0
- package/lib/cli/setup-dev.js +7 -4
- package/lib/cli/setup-help.js +86 -0
- package/lib/cli/setup-onboarding.js +37 -0
- package/lib/cli/setup-utility-repair.js +7 -5
- package/lib/cli/setup-utility.js +24 -1
- package/lib/commands/agent-kit-help.js +21 -7
- package/lib/commands/agent-kit-sessions.js +126 -0
- package/lib/commands/agent-kit-setup.js +44 -0
- package/lib/commands/agent-kit.js +5 -79
- package/lib/commands/dev-init.js +21 -23
- package/lib/commands/dev-refresh.js +2 -2
- package/lib/commands/governance-verify-external.js +12 -5
- package/lib/commands/repair-datasource.js +9 -9
- package/lib/commands/repair.js +45 -8
- package/lib/commands/role-assistant.js +7 -0
- package/lib/commands/upload-qualification-client.js +56 -11
- package/lib/commands/upload-qualification-poll.js +51 -22
- package/lib/commands/wizard-dataplane.js +16 -52
- package/lib/core/env-platform-expand.js +93 -0
- package/lib/core/secrets-env-content.js +37 -3
- package/lib/datasource/datasource-validate-summary.js +8 -5
- package/lib/datasource/sync-defaults.js +53 -0
- package/lib/deployment/installation/azure-infra-stage.js +13 -1
- package/lib/deployment/installation/azure-preflight.js +13 -2
- package/lib/deployment/installation/azure-readiness.js +24 -2
- package/lib/deployment/installation/environment-auth.js +60 -20
- package/lib/deployment/installation/environment-stage.js +9 -5
- package/lib/deployment/installation/infra-catalog.js +10 -7
- package/lib/deployment/installation/infra-manifest.js +5 -0
- package/lib/deployment/installation/registry-auth-mode.js +33 -10
- package/lib/deployment/marketplace-environment.js +26 -1
- package/lib/generator/builders.js +17 -0
- package/lib/generator/helpers.js +23 -2
- package/lib/generator/index.js +33 -18
- package/lib/integration-definition/apply.js +2 -2
- package/lib/internal/fs-real-sync.js +4 -1
- package/lib/programmatic/bearer-auth.js +3 -62
- package/lib/programmatic/builder-api-startup.js +51 -0
- package/lib/programmatic/builder-miso-runtime.js +167 -0
- package/lib/programmatic/openapi-descriptions.js +3 -3
- package/lib/programmatic/openapi-schema-descriptions-core.js +3 -3
- package/lib/programmatic/openapi-spec.js +2 -1
- package/lib/programmatic/route-handler-map.js +3 -2
- package/lib/role-assistant/test-cases-search.js +3 -0
- package/lib/role-assistant/test-job-runner.js +125 -0
- package/lib/role-assistant/test-runner-search.js +44 -2
- package/lib/schema/application-schema.json +193 -2
- package/lib/schema/environment-deploy-request.schema.json +1 -1
- package/lib/schema/external-datasource.schema.json +20 -5
- package/lib/schema/infra-parameter.schema.json +140 -34
- package/lib/schema/infra.parameter.yaml +30 -29
- package/lib/schema/infrastructure-schema.json +133 -14
- package/lib/schema/installation-environment.schema.json +1 -1
- package/lib/schema/wizard-config.schema.json +1 -1
- package/lib/utils/compose-generate-docker-compose.js +12 -0
- package/lib/utils/config-paths.js +24 -29
- package/lib/utils/config-registry-preference.js +13 -6
- package/lib/utils/dev-init-ssh-merge.js +5 -4
- package/lib/utils/dev-user-groups.js +2 -1
- package/lib/utils/docker-build.js +29 -8
- package/lib/utils/docker-daemon-tls-ca.js +2 -2
- package/lib/utils/docker-manifest-public-port.js +36 -0
- package/lib/utils/error-formatters/validation-errors.js +25 -5
- package/lib/utils/help-builder.js +4 -3
- package/lib/utils/platform-resolution.js +226 -0
- package/lib/utils/resolve-docker-image-ref.js +8 -3
- package/lib/utils/token-manager.js +29 -36
- package/package.json +4 -3
- package/templates/README.md +2 -1
- package/templates/agent-kit/agent-kit.yaml +4 -1
- package/templates/agent-kit/instructions/AGENTKIT.md +40 -3
- package/templates/agent-kit/instructions/root.AGENTS.md +42 -1
- package/templates/agent-kit/skills/aifabrix-connected-system/SKILL.md +33 -1
- package/templates/agent-kit/skills/aifabrix-connected-system/scripts/delivery-verdict.js +176 -0
- package/templates/agent-kit/skills/aifabrix-plan/SKILL.md +3 -0
- package/templates/agent-kit/skills/aifabrix-plan/references/interaction.md +10 -3
- package/templates/agent-kit/skills/aifabrix-prove/SKILL.md +22 -0
- package/templates/agent-kit/skills/aifabrix-role-assistant/SKILL.md +21 -0
- package/templates/agent-kit/skills/shared/feedback.md +31 -0
- package/templates/agent-kit/skills/shared/hosts.md +35 -7
- package/templates/agent-kit/skills/shared/status.md +85 -0
- package/templates/agent-kit/workspace/BUILDER_IMPROVEMENT_FINDINGS.md +27 -0
- package/templates/agent-kit/workspace/README.md +14 -1
- package/templates/applications/builder-api/application.yaml +1 -1
- package/templates/applications/dataplane/application.yaml +30 -2
- package/templates/applications/dataplane/env.template +37 -4
- package/templates/applications/miso-controller/application.yaml +24 -1
- package/templates/applications/miso-controller/env.template +22 -35
- package/templates/external-system/external-datasource.yaml.hbs +7 -6
- package/templates/marketplace/main.json +281 -75
- package/templates/python/Dockerfile.hbs +2 -0
- package/templates/typescript/Dockerfile.hbs +2 -0
- package/templates/typescript/docker-compose.hbs +12 -0
|
@@ -82,14 +82,36 @@ function verifyRuntimeImagePull(doc, az) {
|
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Attempts allowed for Miso readiness, at 10-second intervals.
|
|
87
|
+
*
|
|
88
|
+
* Miso runs first-time onboarding inside startup, and on Azure that onboarding is normally
|
|
89
|
+
* interrupted once — the template applies post-Key-Vault app settings and restarts the site — so
|
|
90
|
+
* the retry runs too. A clean installation was measured taking 720 seconds to pass its warm-up
|
|
91
|
+
* probe, and ce-10 was failed at 300 by the old default of 30 attempts.
|
|
92
|
+
*
|
|
93
|
+
* Reporting `readiness did not pass` for a healthy installation is worse than waiting: it leaves
|
|
94
|
+
* `controllerUrl` unrecorded, and `--stage dev` then refuses with "Shared infrastructure is not
|
|
95
|
+
* deployed for this workspace" on infrastructure that is deployed and serving.
|
|
96
|
+
*
|
|
97
|
+
* 180 attempts is 30 minutes, matching WEBSITES_CONTAINER_START_TIME_LIMIT. Keycloak keeps the
|
|
98
|
+
* shorter default: it does not run onboarding, and it is already up by the time Miso answers.
|
|
99
|
+
*/
|
|
100
|
+
const MISO_READINESS_ATTEMPTS = 180;
|
|
101
|
+
|
|
85
102
|
/**
|
|
86
103
|
* Waits for Miso and Keycloak to start after pulling their images.
|
|
87
104
|
* @param {Object} doc - Infra manifest with outputs
|
|
88
|
-
* @param {Function} probe - probeHttps(baseUrl, path, label)
|
|
105
|
+
* @param {Function} probe - probeHttps(baseUrl, path, label, attempts)
|
|
89
106
|
* @returns {Promise<void>}
|
|
90
107
|
*/
|
|
91
108
|
async function waitForApplications(doc, probe) {
|
|
92
|
-
await probe(
|
|
109
|
+
await probe(
|
|
110
|
+
doc.outputs.webAppUrl.value,
|
|
111
|
+
'/api/v1/health/readiness',
|
|
112
|
+
'Miso',
|
|
113
|
+
MISO_READINESS_ATTEMPTS
|
|
114
|
+
);
|
|
93
115
|
const realmUrl = doc.outputs.keycloakRealmUrl.value;
|
|
94
116
|
if (typeof realmUrl !== 'string' || !realmUrl.startsWith('https://')) {
|
|
95
117
|
throw new Error('ARM output keycloakRealmUrl is not a valid HTTPS URL');
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @fileoverview Controller URL and Bearer authentication for dev/tst/pro workspace stages.
|
|
5
|
-
* Order: explicit
|
|
6
|
-
* `.env`/secret system
|
|
5
|
+
* Order: exclusive explicit selector, otherwise Controller login, process key, then
|
|
6
|
+
* workspace `.env`/secret system. Key values never appear in output.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
const { validateControllerUrl } = require('../../utils/deployment-validation');
|
|
@@ -40,37 +40,77 @@ async function workspaceApiKey(workspace, secretsApi) {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
|
-
*
|
|
44
|
-
* @param {Object} params -
|
|
45
|
-
* @
|
|
46
|
-
* @param {string} params.controllerUrl - Controller URL
|
|
47
|
-
* @param {string} [params.keyEnvName] - Explicit environment-variable selector
|
|
48
|
-
* @param {Object} [params.deps] - Injected getDeviceToken/secretsApi/env
|
|
49
|
-
* @returns {Promise<{ authConfig: Object, source: string }>}
|
|
43
|
+
* Lazily yield credentials without resolving lower-priority secrets unnecessarily.
|
|
44
|
+
* @param {Object} params - Workspace, controller URL, selector and dependencies
|
|
45
|
+
* @yields {Object} Authentication candidate
|
|
50
46
|
*/
|
|
51
|
-
async function
|
|
47
|
+
async function* environmentAuthCandidates({ workspace, controllerUrl, keyEnvName, deps = {} }) {
|
|
52
48
|
const processEnv = deps.env || process.env;
|
|
53
49
|
if (keyEnvName) {
|
|
54
50
|
const value = processEnv[keyEnvName];
|
|
55
51
|
if (!value) throw new Error(`Environment variable ${keyEnvName} selected by --miso-api-key-env is not set`);
|
|
56
|
-
|
|
52
|
+
yield { authConfig: { type: 'bearer', token: value }, source: 'selector' };
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const getDeviceToken = deps.getDeviceToken || require('../../utils/token-manager').getOrRefreshDeviceToken;
|
|
56
|
+
const device = await getDeviceToken(controllerUrl);
|
|
57
|
+
if (device?.token) {
|
|
58
|
+
yield { authConfig: { type: 'bearer', token: device.token, controller: controllerUrl }, source: 'login' };
|
|
57
59
|
}
|
|
58
60
|
if (processEnv.MISO_API_KEY) {
|
|
59
|
-
|
|
61
|
+
yield { authConfig: { type: 'bearer', token: processEnv.MISO_API_KEY }, source: 'process' };
|
|
60
62
|
}
|
|
61
63
|
const fromWorkspace = await workspaceApiKey(workspace, deps.secretsApi);
|
|
62
|
-
if (fromWorkspace) {
|
|
63
|
-
|
|
64
|
+
if (fromWorkspace) yield { authConfig: { type: 'bearer', token: fromWorkspace }, source: 'workspace' };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Normalize preflight failures without exposing server-echoed credentials.
|
|
69
|
+
* @param {Function} validateAuth - Initial read-only request
|
|
70
|
+
* @param {Object} auth - Candidate auth config
|
|
71
|
+
* @returns {Promise<Object>} API response (only 401 permits fallback)
|
|
72
|
+
*/
|
|
73
|
+
async function validateCandidate(validateAuth, auth) {
|
|
74
|
+
let response;
|
|
75
|
+
try {
|
|
76
|
+
response = await validateAuth(auth);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
const status = error?.status ?? error?.statusCode;
|
|
79
|
+
response = { success: false, status: Number.isInteger(status) ? status : undefined };
|
|
64
80
|
}
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
81
|
+
if (response?.success === true || response?.status === 401) return response;
|
|
82
|
+
const status = Number.isInteger(response?.status) ? response.status : undefined;
|
|
83
|
+
const error = new Error(`Miso tenant status failed${status ? ` (HTTP ${status})` : ''}. Check Controller connectivity and permissions.`);
|
|
84
|
+
error.status = status;
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Resolve auth, optionally selecting it through the initial read-only preflight.
|
|
90
|
+
* @param {Object} params - Workspace, controllerUrl, keyEnvName and injected deps
|
|
91
|
+
* @param {Function} [params.validateAuth] - Read-only request returning an API response
|
|
92
|
+
* @returns {Promise<Object>} Selected auth/source and optional validationResponse
|
|
93
|
+
*/
|
|
94
|
+
async function resolveEnvironmentAuth(params) {
|
|
95
|
+
const seen = new Set();
|
|
96
|
+
for await (const candidate of environmentAuthCandidates(params)) {
|
|
97
|
+
if (seen.has(candidate.authConfig.token)) continue;
|
|
98
|
+
seen.add(candidate.authConfig.token);
|
|
99
|
+
if (!params.validateAuth) return candidate;
|
|
100
|
+
const response = await validateCandidate(params.validateAuth, candidate.authConfig);
|
|
101
|
+
if (response.success === true) return { ...candidate, validationResponse: response };
|
|
102
|
+
if (candidate.source === 'selector') {
|
|
103
|
+
const error = new Error('Explicit Controller key rejected (HTTP 401). Check --miso-api-key-env; no other credential was tried.');
|
|
104
|
+
error.status = 401;
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
69
107
|
}
|
|
70
|
-
|
|
71
|
-
`No Controller authentication for ${controllerUrl}. Run "aifabrix login -c ${controllerUrl}", ` +
|
|
108
|
+
const error = new Error(
|
|
109
|
+
`No Controller authentication for ${params.controllerUrl}. Run "aifabrix login -c ${params.controllerUrl}", ` +
|
|
72
110
|
'or set MISO_API_KEY (or --miso-api-key-env <NAME>) to a temporary installation key.'
|
|
73
111
|
);
|
|
112
|
+
error.status = 401;
|
|
113
|
+
throw error;
|
|
74
114
|
}
|
|
75
115
|
|
|
76
116
|
module.exports = { resolveControllerUrl, resolveEnvironmentAuth };
|
|
@@ -14,6 +14,9 @@ const { buildEnvironmentDocument, buildMisoRequests, refreshVariableCatalog } =
|
|
|
14
14
|
const { readWorkspaceEnv, removeWorkspaceEnvKey } = require('./workspace-env');
|
|
15
15
|
const { resolveControllerUrl, resolveEnvironmentAuth } = require('./environment-auth');
|
|
16
16
|
|
|
17
|
+
const logger = require('../../utils/logger');
|
|
18
|
+
const { metadata } = require('../../utils/cli-test-layout-chalk');
|
|
19
|
+
|
|
17
20
|
const FAILED_STATES = Object.freeze(['failed', 'error', 'cancelled']);
|
|
18
21
|
|
|
19
22
|
function defaultApi() {
|
|
@@ -34,8 +37,7 @@ function apiFailure(step, response) {
|
|
|
34
37
|
return error;
|
|
35
38
|
}
|
|
36
39
|
|
|
37
|
-
async function assertTenantActive(ctx) {
|
|
38
|
-
const response = await ctx.api.getTenantStatus(ctx.controllerUrl, ctx.auth);
|
|
40
|
+
async function assertTenantActive(ctx, response) {
|
|
39
41
|
if (!response.success) throw apiFailure('tenant status', response);
|
|
40
42
|
const status = String(payload(response)?.status || 'UNKNOWN').toUpperCase();
|
|
41
43
|
const recorded = ['PENDING', 'ACTIVE', 'INACTIVE'].includes(status) ? status.toLowerCase() : 'unknown';
|
|
@@ -169,11 +171,13 @@ async function runEnvironmentStage(workspace, input, deps = {}) {
|
|
|
169
171
|
const { envDoc, requests } = prepareEnvironmentDocument(ctx, input, env);
|
|
170
172
|
ctx.controllerUrl = resolveControllerUrl({ envDoc, infra: ctx.infra, env, override: input.controllerUrl });
|
|
171
173
|
saveEnvironment(ctx.workspace, ctx.envKey, envDoc);
|
|
172
|
-
const { authConfig, source } = await resolveEnvironmentAuth({
|
|
173
|
-
workspace, controllerUrl: ctx.controllerUrl, keyEnvName: input.misoApiKeyEnv, deps: { ...deps, env: ctx.processEnv }
|
|
174
|
+
const { authConfig, source, validationResponse } = await resolveEnvironmentAuth({
|
|
175
|
+
workspace, controllerUrl: ctx.controllerUrl, keyEnvName: input.misoApiKeyEnv, deps: { ...deps, env: ctx.processEnv },
|
|
176
|
+
validateAuth: auth => ctx.api.getTenantStatus(ctx.controllerUrl, auth)
|
|
174
177
|
});
|
|
175
178
|
ctx.auth = authConfig;
|
|
176
|
-
await assertTenantActive(ctx);
|
|
179
|
+
await assertTenantActive(ctx, validationResponse);
|
|
180
|
+
logger.log(metadata(`Controller authentication source: ${source}`));
|
|
177
181
|
const change = await createOrUpdate(ctx, requests);
|
|
178
182
|
await validateRequest(ctx, requests.deploy);
|
|
179
183
|
const { deploymentId, resumed } = await submitDeployment(ctx, requests.deploy);
|
|
@@ -95,15 +95,17 @@ const PARAMETER_CATALOG = Object.freeze({
|
|
|
95
95
|
acrAdminUsername: { kind: 'fixed', value: '', source: 'managedIdentityMode', description: 'Empty in the workspace; credential mode fills it only in the temporary ARM parameters file (never committed).' },
|
|
96
96
|
acrAdminPassword: { kind: 'fixed', value: '', source: 'managedIdentityMode', description: 'Empty in the workspace; credential mode fills it only in the temporary ARM parameters file (never committed).' },
|
|
97
97
|
acrResourceId: { kind: 'acrLookup', field: 'id', description: 'Resolved from acrName; mi mode grants AcrPull to both web app identities.' },
|
|
98
|
-
deployRoleAssignments: { kind: 'file', source: 'templateDefault', description: 'Deploy
|
|
98
|
+
deployRoleAssignments: { kind: 'file', source: 'templateDefault', description: 'Deploy Contributor role assignments in the installation resource group. Needed in every mode: without it the controller cannot create the environment resources its second stage deploys.' },
|
|
99
|
+
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.' },
|
|
99
100
|
moriBaseUrl: { kind: 'file', source: 'registration', required: true, description: 'Mori service base URL.' },
|
|
100
|
-
|
|
101
|
-
|
|
101
|
+
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.' },
|
|
102
|
+
moriLicenseIssuer: { kind: 'file', source: 'registration', required: false, description: 'Expected iss claim on a Mori licence.' },
|
|
102
103
|
entraIdClientId: { kind: 'deferred', description: 'Not collected or sent by infra; issued/configured during later Entra provider onboarding.' },
|
|
103
104
|
entraIdClientSecret: { kind: 'deferred', description: 'Not collected or sent by infra; issued/configured during later Entra provider onboarding.' },
|
|
104
105
|
entraIdTenantId: { kind: 'deferred', description: 'Customer Entra provider tenant; distinct from the Azure subscription directory tenant and configured later.' },
|
|
105
106
|
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.' },
|
|
106
107
|
onboardingMode: { kind: 'onboarding', source: 'workspacePolicy', description: 'Miso onboarding mode (ONBOARDING_MODE); Builder installations use saasDirect, Marketplace customer installations use customerOwned.' },
|
|
108
|
+
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.' },
|
|
107
109
|
misoImageTag: { kind: 'file', source: 'templateDefault', description: 'Miso container image tag.' },
|
|
108
110
|
keycloakImageTag: { kind: 'file', source: 'templateDefault', description: 'Keycloak container image tag.' }
|
|
109
111
|
});
|
|
@@ -127,7 +129,9 @@ const OUTPUT_CATALOG = Object.freeze({
|
|
|
127
129
|
keycloakWebAppUrl: 'Keycloak web application URL.',
|
|
128
130
|
keycloakRealmUrl: 'Keycloak realm URL.',
|
|
129
131
|
keycloakAdminUser: 'Keycloak administrator user name.',
|
|
130
|
-
portalUrl: 'Azure portal link for the installation resource group.'
|
|
132
|
+
portalUrl: 'Azure portal link for the installation resource group.',
|
|
133
|
+
azureOpenAiCreated: 'Whether Phase-1 created the Azure OpenAI account in the installation region.',
|
|
134
|
+
azureOpenAiSkipReason: 'Why Azure OpenAI was not created; empty when it was. An unsupported region skips the optional account instead of relocating it.'
|
|
131
135
|
});
|
|
132
136
|
|
|
133
137
|
/** Mori service key name written by DOCKER registration. */
|
|
@@ -140,14 +144,13 @@ const MORI_SERVICE_KEY = 'mori-controller-api-keyKeyVault';
|
|
|
140
144
|
const SECRET_BINDINGS = Object.freeze({
|
|
141
145
|
ADMIN_PASSWORD: { kv: 'miso-controller-admin-passwordKeyVault', required: true, generate: false, comment: 'Required installation administrator password; same canonical secret used by Miso onboarding.' },
|
|
142
146
|
ENCRYPTION_KEY: { kv: 'miso-controller-secrets-encryptionKeyVault', required: true, generate: true, comment: 'Application/data encryption key.' },
|
|
143
|
-
MORI_CALLBACK_API_KEY: { kv: 'mori-callback-api-keyKeyVault', required: true, generate: true, comment: 'Mori callback credential.' },
|
|
144
147
|
MISO_API_KEY: { kv: 'miso-controller-secrets-apiKeyVault', required: false, generate: false, comment: 'Temporary Miso deployment key; an existing login may be used instead.' },
|
|
145
148
|
MORI_API_KEY: { kv: MORI_SERVICE_KEY, required: true, generate: false, comment: 'Mori installation service key stored by DOCKER registration.' }
|
|
146
149
|
});
|
|
147
150
|
|
|
148
151
|
const ENV_TEMPLATE_KEYS = Object.freeze({
|
|
149
|
-
AZURE: ['ADMIN_PASSWORD', 'ENCRYPTION_KEY', '
|
|
150
|
-
DOCKER: ['ADMIN_PASSWORD', 'ENCRYPTION_KEY', '
|
|
152
|
+
AZURE: ['ADMIN_PASSWORD', 'ENCRYPTION_KEY', 'MISO_API_KEY'],
|
|
153
|
+
DOCKER: ['ADMIN_PASSWORD', 'ENCRYPTION_KEY', 'MORI_API_KEY']
|
|
151
154
|
});
|
|
152
155
|
|
|
153
156
|
/**
|
|
@@ -42,6 +42,11 @@ function parameterEntry(name, template) {
|
|
|
42
42
|
return entry(DEFAULT_LICENSE_EDITION, spec.source, spec.description);
|
|
43
43
|
case 'onboarding':
|
|
44
44
|
return entry(DEFAULT_ONBOARDING_MODE, spec.source, spec.description);
|
|
45
|
+
case 'capacity':
|
|
46
|
+
// Mirrors the workspace preset; azure-infra-stage keeps them in step on every run.
|
|
47
|
+
return entry('s', spec.source, spec.description);
|
|
48
|
+
case 'parameterDefault':
|
|
49
|
+
return entry(templateDefault(template, spec.defaultFrom), spec.source, spec.description);
|
|
45
50
|
default:
|
|
46
51
|
return entry(templateDefault(template, name), spec.source, spec.description, spec.required ? { required: true } : {});
|
|
47
52
|
}
|
|
@@ -13,6 +13,22 @@ const { ACR_AUTH_DESCRIPTION } = require('./infra-manifest');
|
|
|
13
13
|
|
|
14
14
|
const FORCED_ROLE_SOURCES = Object.freeze(['acrAuthCredentials', 'acrAuthAdmin']);
|
|
15
15
|
|
|
16
|
+
function restoreForcedRole(roles) {
|
|
17
|
+
const { restoreValue, restoreSource, ...rest } = roles;
|
|
18
|
+
return {
|
|
19
|
+
...rest,
|
|
20
|
+
value: restoreValue === undefined ? true : restoreValue,
|
|
21
|
+
source: restoreSource || 'templateDefault'
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function restoreLegacyContributorRole(doc) {
|
|
26
|
+
const roles = doc.parameters.deployRoleAssignments;
|
|
27
|
+
if (roles && FORCED_ROLE_SOURCES.includes(roles.source)) {
|
|
28
|
+
doc.parameters.deployRoleAssignments = restoreForcedRole(roles);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
16
32
|
/**
|
|
17
33
|
* Normalizes and records the requested mode (CLI wins over the workspace value).
|
|
18
34
|
* @param {Object} doc - Infra manifest (mutated)
|
|
@@ -62,29 +78,36 @@ async function resolveRegistryAuth(doc, acr, az, deps) {
|
|
|
62
78
|
}
|
|
63
79
|
|
|
64
80
|
/**
|
|
65
|
-
* Records the effective mode and keeps role
|
|
66
|
-
*
|
|
81
|
+
* Records the effective mode and keeps the registry role assignment consistent with it.
|
|
82
|
+
*
|
|
83
|
+
* Credential mode turns off only `deployRegistryRoleAssignment` -- the AcrPull grant, which lives
|
|
84
|
+
* in the registry's own resource group and, for a customer installation, in the vendor's tenant
|
|
85
|
+
* where Azure RBAC cannot reach. It deliberately leaves `deployRoleAssignments` alone: those are
|
|
86
|
+
* Contributor grants in the resource group the installation is creating, which the deployer owns.
|
|
87
|
+
*
|
|
88
|
+
* Turning both off is what ce-12 and ce-13 did, and it broke the installation in a way that took
|
|
89
|
+
* two attempts to see. Without Contributor on its own resource group the controller cannot create
|
|
90
|
+
* anything, so its second stage failed every module, no resource bindings were recorded, and both
|
|
91
|
+
* application deployments refused with "Environment 'dev' has no recorded placement".
|
|
92
|
+
*
|
|
67
93
|
* @param {Object} doc - Infra manifest (mutated)
|
|
68
94
|
* @param {'mi'|'credentials'} mode - Effective mode
|
|
69
95
|
*/
|
|
70
96
|
function applyEffectiveAcrAuth(doc, mode) {
|
|
71
97
|
doc.acrAuth = { ...doc.acrAuth, effective: mode };
|
|
72
|
-
|
|
98
|
+
restoreLegacyContributorRole(doc);
|
|
99
|
+
const roles = doc.parameters.deployRegistryRoleAssignment
|
|
100
|
+
|| doc.parameters.deployRoleAssignments;
|
|
73
101
|
const forced = FORCED_ROLE_SOURCES.includes(roles.source);
|
|
74
102
|
if (mode === 'credentials') {
|
|
75
103
|
if (forced) return;
|
|
76
|
-
doc.parameters.
|
|
104
|
+
doc.parameters.deployRegistryRoleAssignment = {
|
|
77
105
|
...roles, value: false, source: 'acrAuthCredentials', restoreValue: roles.value, restoreSource: roles.source
|
|
78
106
|
};
|
|
79
107
|
return;
|
|
80
108
|
}
|
|
81
109
|
if (!forced) return;
|
|
82
|
-
|
|
83
|
-
doc.parameters.deployRoleAssignments = {
|
|
84
|
-
...rest,
|
|
85
|
-
value: restoreValue === undefined ? true : restoreValue,
|
|
86
|
-
source: restoreSource || 'templateDefault'
|
|
87
|
-
};
|
|
110
|
+
doc.parameters.deployRegistryRoleAssignment = restoreForcedRole(roles);
|
|
88
111
|
}
|
|
89
112
|
|
|
90
113
|
/**
|
|
@@ -9,6 +9,7 @@ const crypto = require('crypto');
|
|
|
9
9
|
const { spawnSync } = require('child_process');
|
|
10
10
|
const config = require('../core/config');
|
|
11
11
|
const { getRuntimeConfigDir } = require('../core/config-runtime-paths');
|
|
12
|
+
const logger = require('../utils/logger');
|
|
12
13
|
|
|
13
14
|
const SHIPPED_TEMPLATE = path.resolve(__dirname, '../../templates/marketplace/main.json');
|
|
14
15
|
// Resolved per call (not a module-load constant) so it honors AIFABRIX_HOME/AIFABRIX_CONFIG
|
|
@@ -273,8 +274,28 @@ async function deployMarketplaceEnvironment(envKey, options, dependencies = {})
|
|
|
273
274
|
return deployPlatform({ ...input, controllerUrl: options.controllerUrl }, dependencies);
|
|
274
275
|
}
|
|
275
276
|
|
|
277
|
+
/**
|
|
278
|
+
* Attempts allowed for the controller, at 10-second intervals.
|
|
279
|
+
*
|
|
280
|
+
* The controller runs first-time onboarding inside startup, and on Azure that onboarding is
|
|
281
|
+
* normally interrupted once — the template applies post-Key-Vault app settings and restarts the
|
|
282
|
+
* site — so the retry runs too. A clean installation was measured taking 720 seconds to pass its
|
|
283
|
+
* warm-up probe. The old 30 attempts gave 5 minutes and reported `readiness did not pass` for
|
|
284
|
+
* installations that were healthy, which is worse than a slow wait: it leaves `controllerUrl`
|
|
285
|
+
* unrecorded, so `--stage dev` then refuses with "Shared infrastructure is not deployed".
|
|
286
|
+
*
|
|
287
|
+
* 180 attempts is 30 minutes, matching WEBSITES_CONTAINER_START_TIME_LIMIT. Waiting longer than
|
|
288
|
+
* App Service itself would is pointless; waiting less than it declares is what caused this.
|
|
289
|
+
*/
|
|
290
|
+
const CONTROLLER_READINESS_ATTEMPTS = 180;
|
|
291
|
+
|
|
276
292
|
async function probeController(controllerUrl) {
|
|
277
|
-
return probeHttps(
|
|
293
|
+
return probeHttps(
|
|
294
|
+
controllerUrl,
|
|
295
|
+
'/api/v1/health/readiness',
|
|
296
|
+
'Controller',
|
|
297
|
+
CONTROLLER_READINESS_ATTEMPTS
|
|
298
|
+
);
|
|
278
299
|
}
|
|
279
300
|
|
|
280
301
|
/**
|
|
@@ -297,6 +318,10 @@ async function probeHttps(baseUrl, pathname, label, attempts = 30) {
|
|
|
297
318
|
} catch (_) {
|
|
298
319
|
// App Service startup can lag successful ARM provisioning.
|
|
299
320
|
}
|
|
321
|
+
// A wait measured in minutes should say so, or it is indistinguishable from a hang.
|
|
322
|
+
if (attempt > 0 && attempt % 6 === 0) {
|
|
323
|
+
logger.log(` waiting for ${label} readiness… ${attempt * 10}s elapsed`);
|
|
324
|
+
}
|
|
300
325
|
await new Promise(resolve => setTimeout(resolve, 10000));
|
|
301
326
|
}
|
|
302
327
|
throw new Error(`${label} readiness did not pass at ${url.origin}; inspect App Service before resuming`);
|
|
@@ -362,6 +362,23 @@ function addSimpleOptionalFields(deployment, variables) {
|
|
|
362
362
|
if (variables.required !== undefined && !(Array.isArray(variables.required) && variables.required.length === 0)) {
|
|
363
363
|
deployment.required = variables.required;
|
|
364
364
|
}
|
|
365
|
+
addPlatformSections(deployment, variables);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Carries the platform/environment declarations into the deployment manifest so the Controller can
|
|
370
|
+
* resolve `{NAME}` values per installation, which is the case Builder cannot resolve: one uploaded
|
|
371
|
+
* application serving many customer resource groups.
|
|
372
|
+
* @param {Object} deployment - Deployment manifest (mutated)
|
|
373
|
+
* @param {Object} variables - Application manifest
|
|
374
|
+
*/
|
|
375
|
+
function addPlatformSections(deployment, variables) {
|
|
376
|
+
if (Array.isArray(variables.platforms) && variables.platforms.length > 0) {
|
|
377
|
+
deployment.platforms = variables.platforms;
|
|
378
|
+
}
|
|
379
|
+
if (Array.isArray(variables.environments) && variables.environments.length > 0) {
|
|
380
|
+
deployment.environments = variables.environments;
|
|
381
|
+
}
|
|
365
382
|
}
|
|
366
383
|
|
|
367
384
|
function buildOptionalFields(deployment, variables, rbac) {
|
package/lib/generator/helpers.js
CHANGED
|
@@ -243,6 +243,27 @@ function determineVariableLocation(value) {
|
|
|
243
243
|
return value.startsWith('kv://') ? 'keyvault' : 'variable';
|
|
244
244
|
}
|
|
245
245
|
|
|
246
|
+
/**
|
|
247
|
+
* Expands manifest `{NAME}` values into the env template before it becomes configuration items.
|
|
248
|
+
* Lenient by design: with no deployment target the tokens stay in the manifest for the Controller
|
|
249
|
+
* to resolve per installation, which is what the SaaS case needs. A known target resolves fully.
|
|
250
|
+
* @param {string} envTemplate - Raw env template
|
|
251
|
+
* @param {Object|null} variablesConfig - Application manifest
|
|
252
|
+
* @param {Object|null} target - `environment` and `resourceGroup` when known
|
|
253
|
+
* @returns {string}
|
|
254
|
+
*/
|
|
255
|
+
function expandManifestNames(envTemplate, variablesConfig, target) {
|
|
256
|
+
const { expandPlatformNamesIfPresent } = require('../core/env-platform-expand');
|
|
257
|
+
const known = target && (target.resourceGroup || target.environment);
|
|
258
|
+
if (!known) return envTemplate;
|
|
259
|
+
return expandPlatformNamesIfPresent(envTemplate, {
|
|
260
|
+
manifest: variablesConfig,
|
|
261
|
+
environment: target.environment,
|
|
262
|
+
resourceGroup: target.resourceGroup,
|
|
263
|
+
strict: Boolean(target.resourceGroup)
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
246
267
|
/**
|
|
247
268
|
* Creates a configuration item from parsed variable
|
|
248
269
|
* @function createConfigItem
|
|
@@ -282,9 +303,9 @@ function createConfigItem(key, value, location, applicationConfigMap) {
|
|
|
282
303
|
return configItem;
|
|
283
304
|
}
|
|
284
305
|
|
|
285
|
-
function parseEnvironmentVariables(envTemplate, variablesConfig = null) {
|
|
306
|
+
function parseEnvironmentVariables(envTemplate, variablesConfig = null, target = null) {
|
|
286
307
|
const configuration = [];
|
|
287
|
-
const lines = envTemplate.split('\n');
|
|
308
|
+
const lines = expandManifestNames(envTemplate, variablesConfig, target).split('\n');
|
|
288
309
|
const applicationConfigMap = createApplicationConfigMap(variablesConfig);
|
|
289
310
|
for (const line of lines) {
|
|
290
311
|
const parsed = parseEnvironmentVariableLine(line);
|
package/lib/generator/index.js
CHANGED
|
@@ -117,35 +117,46 @@ function substitutePortInDeployment(obj, portNumber) {
|
|
|
117
117
|
/** Regex to find ${VAR} placeholders for env substitution */
|
|
118
118
|
const ENV_VAR_PLACEHOLDER_REGEX = /\$\{([^}]+)\}/g;
|
|
119
119
|
|
|
120
|
+
function replaceDeploymentEnvString(value, envVarMap, fieldPath) {
|
|
121
|
+
if (fieldPath === 'frontDoorRouting.host') return value;
|
|
122
|
+
return value.replace(ENV_VAR_PLACEHOLDER_REGEX, (match, varName) =>
|
|
123
|
+
Object.prototype.hasOwnProperty.call(envVarMap, varName) ? String(envVarMap[varName]) : match);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function substituteEnvVarsInArray(items, envVarMap, fieldPath) {
|
|
127
|
+
for (let i = 0; i < items.length; i++) {
|
|
128
|
+
const itemPath = `${fieldPath}[${i}]`;
|
|
129
|
+
if (typeof items[i] === 'string') {
|
|
130
|
+
items[i] = replaceDeploymentEnvString(items[i], envVarMap, itemPath);
|
|
131
|
+
} else if (typeof items[i] === 'object' && items[i] !== null) {
|
|
132
|
+
substituteEnvVarsInDeployment(items[i], envVarMap, itemPath);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
120
137
|
/**
|
|
121
138
|
* Recursively replaces ${VAR} with envVarMap[VAR] in all string values of obj (in-place).
|
|
122
139
|
* Only substitutes when VAR is a key in envVarMap (from env-config.yaml / config).
|
|
123
140
|
*
|
|
124
141
|
* @param {Object} obj - Deployment manifest or any nested object
|
|
125
142
|
* @param {Object} envVarMap - Flat map of variable names to values (e.g. from buildEnvVarMap('docker'))
|
|
143
|
+
* @param {string} [fieldPath=''] - Internal traversal path used to preserve the portable front door host template
|
|
126
144
|
*/
|
|
127
|
-
function substituteEnvVarsInDeployment(obj, envVarMap) {
|
|
145
|
+
function substituteEnvVarsInDeployment(obj, envVarMap, fieldPath = '') {
|
|
128
146
|
if (!envVarMap || typeof envVarMap !== 'object') return;
|
|
129
147
|
if (obj === null || obj === undefined) return;
|
|
130
148
|
if (Array.isArray(obj)) {
|
|
131
|
-
|
|
132
|
-
if (typeof obj[i] === 'string') {
|
|
133
|
-
obj[i] = obj[i].replace(ENV_VAR_PLACEHOLDER_REGEX, (match, varName) =>
|
|
134
|
-
Object.prototype.hasOwnProperty.call(envVarMap, varName) ? String(envVarMap[varName]) : match);
|
|
135
|
-
} else if (typeof obj[i] === 'object' && obj[i] !== null) {
|
|
136
|
-
substituteEnvVarsInDeployment(obj[i], envVarMap);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
149
|
+
substituteEnvVarsInArray(obj, envVarMap, fieldPath);
|
|
139
150
|
return;
|
|
140
151
|
}
|
|
141
152
|
if (typeof obj === 'object') {
|
|
142
153
|
for (const key of Object.keys(obj)) {
|
|
143
154
|
const value = obj[key];
|
|
155
|
+
const childPath = fieldPath ? `${fieldPath}.${key}` : key;
|
|
144
156
|
if (typeof value === 'string') {
|
|
145
|
-
obj[key] = value
|
|
146
|
-
Object.prototype.hasOwnProperty.call(envVarMap, varName) ? String(envVarMap[varName]) : match);
|
|
157
|
+
obj[key] = replaceDeploymentEnvString(value, envVarMap, childPath);
|
|
147
158
|
} else if (typeof value === 'object' && value !== null) {
|
|
148
|
-
substituteEnvVarsInDeployment(value, envVarMap);
|
|
159
|
+
substituteEnvVarsInDeployment(value, envVarMap, childPath);
|
|
149
160
|
}
|
|
150
161
|
}
|
|
151
162
|
}
|
|
@@ -164,8 +175,9 @@ function substituteEnvVarsInDeployment(obj, envVarMap) {
|
|
|
164
175
|
* @throws {Error} If validation fails
|
|
165
176
|
*/
|
|
166
177
|
function buildAndValidateDeployment(appName, variables, envTemplate, rbac, options = null) {
|
|
167
|
-
// Parse environment variables from template and merge portalInput from application config
|
|
168
|
-
|
|
178
|
+
// Parse environment variables from template and merge portalInput from application config.
|
|
179
|
+
// A known deployment target resolves manifest {NAME} values; otherwise they stay for the Controller.
|
|
180
|
+
const configuration = parseEnvironmentVariables(envTemplate, variables, options && options.target);
|
|
169
181
|
|
|
170
182
|
// Build deployment manifest (Controller computes deploymentKey from schema)
|
|
171
183
|
const deployment = builders.buildManifestStructure(appName, variables, configuration, rbac);
|
|
@@ -225,7 +237,7 @@ async function buildDeploymentManifestInMemory(appName, options = {}) {
|
|
|
225
237
|
...variables,
|
|
226
238
|
app: { ...variables.app, version: resolved.version }
|
|
227
239
|
};
|
|
228
|
-
const configuration = parseEnvironmentVariables(envTemplate, variables);
|
|
240
|
+
const configuration = parseEnvironmentVariables(envTemplate, variables, options && options.target);
|
|
229
241
|
const deployment = builders.buildManifestStructure(appName, variablesWithVersion, configuration, rbac);
|
|
230
242
|
|
|
231
243
|
const effectivePort = getEffectivePortForSubstitution(variablesWithVersion, 3000);
|
|
@@ -279,7 +291,7 @@ async function writeExternalDeployJson(appName, appPath, options) {
|
|
|
279
291
|
* @param {string} appType - Application type
|
|
280
292
|
* @returns {Promise<string>} Path to written deploy JSON
|
|
281
293
|
*/
|
|
282
|
-
async function writeRegularDeployJson(appName, appPath, appType) {
|
|
294
|
+
async function writeRegularDeployJson(appName, appPath, appType, options = {}) {
|
|
283
295
|
const { variables, envTemplate, rbac, jsonPath } = loadDeploymentConfigFiles(appPath, appType, appName);
|
|
284
296
|
const resolved = await resolveVersionForApp(appName, variables, { updateBuilder: false });
|
|
285
297
|
const variablesWithVersion = {
|
|
@@ -288,7 +300,10 @@ async function writeRegularDeployJson(appName, appPath, appType) {
|
|
|
288
300
|
};
|
|
289
301
|
const effectivePort = getEffectivePortForSubstitution(variablesWithVersion, 3000);
|
|
290
302
|
const envVarMap = await buildEnvVarMap('docker', null, null, { appPort: effectivePort });
|
|
291
|
-
const deployment = buildAndValidateDeployment(appName, variablesWithVersion, envTemplate, rbac, {
|
|
303
|
+
const deployment = buildAndValidateDeployment(appName, variablesWithVersion, envTemplate, rbac, {
|
|
304
|
+
envVarMap,
|
|
305
|
+
target: options.target
|
|
306
|
+
});
|
|
292
307
|
const jsonContent = JSON.stringify(deployment, null, 2);
|
|
293
308
|
fs.writeFileSync(jsonPath, jsonContent, { mode: 0o644 });
|
|
294
309
|
return jsonPath;
|
|
@@ -322,7 +337,7 @@ async function generateDeployJson(appName, options = {}) {
|
|
|
322
337
|
if (isExternal) {
|
|
323
338
|
return writeExternalDeployJson(appName, appPath, options);
|
|
324
339
|
}
|
|
325
|
-
return await writeRegularDeployJson(appName, appPath, appType);
|
|
340
|
+
return await writeRegularDeployJson(appName, appPath, appType, options);
|
|
326
341
|
}
|
|
327
342
|
|
|
328
343
|
async function generateDeployJsonWithValidation(appName, options = {}) {
|
|
@@ -18,6 +18,7 @@ const { applyStaticAndEnterprise } = require('./apply-static-enterprise');
|
|
|
18
18
|
const { applyViewpointDefaults } = require('./apply-viewpoint-defaults');
|
|
19
19
|
const { stampCredentialTestEndpoint } = require('./apply-test-endpoint');
|
|
20
20
|
const { buildSystemFile } = require('./apply-system-file');
|
|
21
|
+
const { resolveAuthoredDatasourceSync } = require('../datasource/sync-defaults');
|
|
21
22
|
|
|
22
23
|
const STAMP_FILE = '.aifabrix-integration-definition.json';
|
|
23
24
|
|
|
@@ -160,8 +161,7 @@ function buildDatasourceFile(ds, systemKey, capabilities, sync, enrichOptions =
|
|
|
160
161
|
...(ds.storageType ? { storageType: ds.storageType } : {}),
|
|
161
162
|
...(ds.dimensions && typeof ds.dimensions === 'object' ? { dimensions: ds.dimensions } : {}),
|
|
162
163
|
capabilities: buildDatasourceCapabilities(caps, key),
|
|
163
|
-
|
|
164
|
-
sync: { mode: 'pull', schedule: /** @type {any} */ (sync).schedule || '0 */6 * * *', batchSize: 100 },
|
|
164
|
+
sync: resolveAuthoredDatasourceSync(ds, sync),
|
|
165
165
|
primaryKey: ['id'],
|
|
166
166
|
labelKey: [labelField],
|
|
167
167
|
...buildDatasourceSchema(labelField)
|
|
@@ -178,5 +178,8 @@ module.exports = {
|
|
|
178
178
|
rmSync,
|
|
179
179
|
unlinkSync,
|
|
180
180
|
copyFileSync,
|
|
181
|
-
appendFileSync
|
|
181
|
+
appendFileSync,
|
|
182
|
+
openSync: (...args) => delegate('openSync', args),
|
|
183
|
+
closeSync: (...args) => delegate('closeSync', args),
|
|
184
|
+
realpathSync: (...args) => delegate('realpathSync', args)
|
|
182
185
|
};
|