@aifabrix/builder 2.57.0 → 2.59.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/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 +84 -0
- package/lib/agent-kit/start.js +4 -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/types/dev.types.js +3 -3
- package/lib/app/deploy.js +0 -102
- package/lib/app/push.js +98 -41
- package/lib/build/standard-docker-build.js +13 -11
- package/lib/cli/index.js +4 -0
- package/lib/cli/setup-app.js +16 -5
- package/lib/cli/setup-dev-path-commands.js +53 -2
- package/lib/cli/setup-dev.js +7 -4
- package/lib/cli/setup-environment.js +17 -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/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/dev-registry.js +155 -0
- package/lib/commands/dev-show-display.js +10 -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/setup-image-refresh.js +26 -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/config-attach-extensions.js +4 -1
- 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 +50 -9
- package/lib/deployment/installation/azure-preflight.js +63 -7
- package/lib/deployment/installation/azure-readiness.js +59 -7
- 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 +57 -5
- package/lib/deployment/installation/infra-manifest.js +22 -2
- package/lib/deployment/installation/infra-projection.js +14 -3
- package/lib/deployment/installation/registry-auth-mode.js +121 -0
- package/lib/deployment/installation/stage-input.js +21 -6
- package/lib/deployment/marketplace-environment.js +26 -1
- package/lib/generator/builders.js +8 -9
- package/lib/generator/index.js +23 -12
- 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/schema/environment-deploy-request.schema.json +1 -1
- package/lib/schema/external-datasource.schema.json +20 -5
- package/lib/schema/infra-parameter.schema.json +1 -1
- package/lib/schema/infra.parameter.yaml +2 -1
- package/lib/schema/infrastructure-schema.json +200 -3
- package/lib/schema/installation-environment.schema.json +1 -1
- package/lib/schema/wizard-config.schema.json +1 -1
- package/lib/utils/app-register-config.js +2 -4
- package/lib/utils/build-resolve-image.js +2 -21
- package/lib/utils/cli-utils.js +8 -1
- package/lib/utils/config-paths.js +24 -29
- package/lib/utils/config-registry-preference.js +113 -0
- 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 +53 -19
- package/lib/utils/docker-daemon-tls-ca.js +2 -2
- package/lib/utils/env-map.js +23 -6
- package/lib/utils/error-formatters/validation-errors.js +25 -5
- package/lib/utils/help-builder.js +4 -3
- package/lib/utils/image-tags.js +88 -0
- package/lib/utils/promote-dev-docker-image.js +6 -0
- package/lib/utils/registry-credentials.js +236 -0
- package/lib/utils/resolve-docker-image-ref.js +78 -9
- package/lib/utils/secrets-helpers.js +9 -1
- package/lib/utils/token-manager.js +29 -36
- package/lib/utils/variable-transformer.js +2 -4
- package/package.json +4 -2
- package/templates/agent-kit/instructions/AGENTKIT.md +39 -2
- package/templates/agent-kit/instructions/root.AGENTS.md +40 -1
- package/templates/agent-kit/skills/shared/hosts.md +23 -5
- package/templates/agent-kit/workspace/README.md +14 -1
- package/templates/applications/builder-api/application.yaml +1 -1
- package/templates/applications/dataplane/application.yaml +1 -1
- package/templates/applications/dataplane/env.template +1 -1
- package/templates/applications/miso-controller/application.yaml +1 -1
- package/templates/applications/miso-controller/env.template +1 -1
- package/templates/external-system/external-datasource.yaml.hbs +7 -6
- package/templates/marketplace/main.json +440 -82
|
@@ -74,17 +74,20 @@ function summarizeSyncLine(sync) {
|
|
|
74
74
|
if (!sync || typeof sync !== 'object') {
|
|
75
75
|
return 'Not configured';
|
|
76
76
|
}
|
|
77
|
-
const
|
|
77
|
+
const disabled = sync.enabled === false;
|
|
78
|
+
const parts = [disabled ? 'disabled' : sync.enabled === true ? 'enabled' : 'enabled (legacy default)'];
|
|
79
|
+
if (sync.schedule) {
|
|
80
|
+
parts.push(`${disabled ? 'retained schedule' : 'scheduled'} ${JSON.stringify(sync.schedule)}`);
|
|
81
|
+
} else {
|
|
82
|
+
parts.push('manual-only');
|
|
83
|
+
}
|
|
78
84
|
if (sync.mode) {
|
|
79
85
|
parts.push(String(sync.mode));
|
|
80
86
|
}
|
|
81
87
|
if (sync.batchSize !== undefined && sync.batchSize !== null) {
|
|
82
88
|
parts.push(`batch size ${sync.batchSize}`);
|
|
83
89
|
}
|
|
84
|
-
|
|
85
|
-
parts.push(`schedule ${JSON.stringify(sync.schedule)}`);
|
|
86
|
-
}
|
|
87
|
-
return parts.length ? parts.join(', ') : 'Present';
|
|
90
|
+
return parts.join(', ');
|
|
88
91
|
}
|
|
89
92
|
|
|
90
93
|
/**
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Datasource synchronization defaults for newly authored sync blocks.
|
|
3
|
+
* @fileoverview Keeps default creation separate from preservation decisions.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const DEFAULT_DATASOURCE_SYNC = Object.freeze({
|
|
9
|
+
enabled: true,
|
|
10
|
+
mode: 'pull',
|
|
11
|
+
schedule: '@monthly',
|
|
12
|
+
batchSize: 500
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const DATASOURCE_SYNC_MODES = new Set(['pull', 'push', 'bidirectional']);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {Record<string, unknown>} [overrides]
|
|
19
|
+
* @returns {Record<string, unknown>}
|
|
20
|
+
*/
|
|
21
|
+
function createDefaultDatasourceSync(overrides = {}) {
|
|
22
|
+
return { ...DEFAULT_DATASOURCE_SYNC, ...overrides };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Resolve sync for a newly materialized BID datasource.
|
|
27
|
+
* An explicit datasource block is preserved exactly, including no schedule.
|
|
28
|
+
* @param {Record<string, unknown>} datasource
|
|
29
|
+
* @param {Record<string, unknown>} bidSync
|
|
30
|
+
* @returns {unknown}
|
|
31
|
+
*/
|
|
32
|
+
function resolveAuthoredDatasourceSync(datasource, bidSync) {
|
|
33
|
+
if (Object.prototype.hasOwnProperty.call(datasource, 'sync')) {
|
|
34
|
+
const explicit = datasource.sync;
|
|
35
|
+
return explicit && typeof explicit === 'object' ? { ...explicit } : explicit;
|
|
36
|
+
}
|
|
37
|
+
const overrides = {};
|
|
38
|
+
for (const key of ['enabled', 'schedule', 'batchSize']) {
|
|
39
|
+
if (Object.prototype.hasOwnProperty.call(bidSync, key)) {
|
|
40
|
+
overrides[key] = bidSync[key];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (DATASOURCE_SYNC_MODES.has(String(bidSync.mode))) {
|
|
44
|
+
overrides.mode = bidSync.mode;
|
|
45
|
+
}
|
|
46
|
+
return createDefaultDatasourceSync(overrides);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = {
|
|
50
|
+
DEFAULT_DATASOURCE_SYNC,
|
|
51
|
+
createDefaultDatasourceSync,
|
|
52
|
+
resolveAuthoredDatasourceSync
|
|
53
|
+
};
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @fileoverview AZURE `--stage infra`: validates the workspace, resolves ACR and secrets, deploys
|
|
5
|
-
* the workspace Marketplace template snapshot, verifies
|
|
6
|
-
* applications started, and records outputs. Mori registration and
|
|
5
|
+
* the workspace Marketplace template snapshot, verifies image pull (managed identity or registry
|
|
6
|
+
* credentials) and that both applications started, and records outputs. Mori registration and
|
|
7
|
+
* tenant activation stay with Miso. Registry credentials are never written to the workspace.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
// Real-sync adapter: installation workspace state must not disappear under Jest fs mocks.
|
|
@@ -12,7 +13,7 @@ const os = require('os');
|
|
|
12
13
|
const path = require('path');
|
|
13
14
|
const marketplace = require('../marketplace-environment');
|
|
14
15
|
const { normalizePreset } = require('../environment-config');
|
|
15
|
-
const { normalizeLicenseEdition, ONBOARDING_MODES } = require('./infra-catalog');
|
|
16
|
+
const { normalizeLicenseEdition, normalizeAzureLocation, ONBOARDING_MODES } = require('./infra-catalog');
|
|
16
17
|
const { materializeWorkspace, saveInfra } = require('./workspace');
|
|
17
18
|
const { missingRequiredFields } = require('./infra-manifest');
|
|
18
19
|
const { buildArmParameters, applyAllowMyIp } = require('./infra-projection');
|
|
@@ -21,6 +22,7 @@ const { mergeWorkspaceEnv } = require('./workspace-env');
|
|
|
21
22
|
const { resolvePublicIpv4 } = require('./public-ip');
|
|
22
23
|
const preflight = require('./azure-preflight');
|
|
23
24
|
const readiness = require('./azure-readiness');
|
|
25
|
+
const registryAuth = require('./registry-auth-mode');
|
|
24
26
|
|
|
25
27
|
const INFRA_SECRETS = Object.freeze(['ADMIN_PASSWORD', 'ENCRYPTION_KEY', 'MORI_CALLBACK_API_KEY']);
|
|
26
28
|
|
|
@@ -48,6 +50,16 @@ async function applyAccessOptions(doc, input, deps) {
|
|
|
48
50
|
if (input.preset !== undefined) {
|
|
49
51
|
doc.preset = { ...doc.preset, value: normalizePreset(input.preset), source: 'cli' };
|
|
50
52
|
}
|
|
53
|
+
// The template sizes compute and PostgreSQL from capacityProfile, and the workspace already
|
|
54
|
+
// records a preset with the same values. Deriving one from the other keeps a single source:
|
|
55
|
+
// an installation cannot end up recording preset 'm' while deploying 's'.
|
|
56
|
+
if (doc.parameters.capacityProfile) {
|
|
57
|
+
doc.parameters.capacityProfile = {
|
|
58
|
+
...doc.parameters.capacityProfile,
|
|
59
|
+
value: normalizePreset(doc.preset.value),
|
|
60
|
+
source: 'workspacePreset'
|
|
61
|
+
};
|
|
62
|
+
}
|
|
51
63
|
applyLicenseOptions(doc, input);
|
|
52
64
|
const enabled = input.allowMyIp || doc.allowMyIp.value === true;
|
|
53
65
|
if (!enabled) {
|
|
@@ -111,18 +123,41 @@ async function verifyAndRecordReadiness(workspace, doc, deps) {
|
|
|
111
123
|
return controllerUrl;
|
|
112
124
|
}
|
|
113
125
|
|
|
126
|
+
function applyLocation(doc, input) {
|
|
127
|
+
if (input.location === undefined) return;
|
|
128
|
+
doc.parameters.location = { ...doc.parameters.location, value: normalizeAzureLocation(input.location), source: 'cli' };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function resolvePullAuth(doc, acr, deps, az) {
|
|
132
|
+
const loadRegistryCredentials = deps.loadRegistryCredentials ||
|
|
133
|
+
require('../../utils/registry-credentials').loadRegistryCredentials;
|
|
134
|
+
const resolved = await registryAuth.resolveRegistryAuth(doc, acr, az, {
|
|
135
|
+
loadRegistryCredentials,
|
|
136
|
+
resolveAcrAdminCredentials: preflight.resolveAcrAdminCredentials
|
|
137
|
+
});
|
|
138
|
+
registryAuth.applyEffectiveAcrAuth(doc, resolved.mode);
|
|
139
|
+
return resolved.credentials;
|
|
140
|
+
}
|
|
141
|
+
|
|
114
142
|
async function prepareInfra(workspace, input, deps, az) {
|
|
115
143
|
const { doc, templateHash } = materializeWorkspace(workspace, input, deps);
|
|
144
|
+
applyLocation(doc, input);
|
|
116
145
|
const missing = missingRequiredFields(doc);
|
|
117
|
-
if (missing.length > 0)
|
|
146
|
+
if (missing.length > 0) {
|
|
147
|
+
if (input.location !== undefined) saveInfra(workspace, doc);
|
|
148
|
+
throw missingInputError(workspace.paths.infra, missing);
|
|
149
|
+
}
|
|
150
|
+
registryAuth.applyRequestedAcrAuth(doc, input);
|
|
118
151
|
preflight.assertActiveAccount(doc, az);
|
|
119
152
|
const acr = preflight.resolveAcr(doc, az);
|
|
120
|
-
preflight.assertDeploymentAuthority(doc, acr, az);
|
|
121
153
|
doc.parameters.acrLoginServer.value = acr.loginServer;
|
|
122
154
|
doc.parameters.acrResourceId.value = acr.id;
|
|
155
|
+
// Local pair first (a partial pair fails closed); Azure admin lookup only for explicit credentials.
|
|
156
|
+
const registryCredentials = await resolvePullAuth(doc, acr, deps, az);
|
|
157
|
+
preflight.assertDeploymentAuthority(doc, acr, az);
|
|
123
158
|
await applyAccessOptions(doc, input, deps);
|
|
124
159
|
saveInfra(workspace, doc);
|
|
125
|
-
return { doc, templateHash, acr };
|
|
160
|
+
return { doc, templateHash, acr, registryCredentials };
|
|
126
161
|
}
|
|
127
162
|
|
|
128
163
|
function deployWithStatus(workspace, doc, context) {
|
|
@@ -141,17 +176,23 @@ function deployWithStatus(workspace, doc, context) {
|
|
|
141
176
|
* Runs the AZURE infra stage.
|
|
142
177
|
* @param {Object} workspace - Opened workspace
|
|
143
178
|
* @param {Object} input - Validated stage input
|
|
144
|
-
* @param {Object} [deps] - Injected dependencies (az, probe, resolvePublicIpv4, secretsApi, now, templatePath
|
|
179
|
+
* @param {Object} [deps] - Injected dependencies (az, probe, resolvePublicIpv4, secretsApi, now, templatePath,
|
|
180
|
+
* loadRegistryCredentials)
|
|
145
181
|
* @returns {Promise<{ doc: Object, controllerUrl: string, activationUrl: string }>}
|
|
146
182
|
*/
|
|
147
183
|
async function runAzureInfraStage(workspace, input, deps = {}) {
|
|
148
184
|
const context = { az: deps.az || marketplace.runAz, now: deps.now || (() => new Date()), probe: deps.probe };
|
|
149
|
-
const { doc, templateHash, acr } = await prepareInfra(workspace, input, deps, context.az);
|
|
185
|
+
const { doc, templateHash, acr, registryCredentials } = await prepareInfra(workspace, input, deps, context.az);
|
|
150
186
|
const env = await materializeWorkspaceSecrets({
|
|
151
187
|
vendor: 'AZURE', envPath: workspace.paths.env, root: workspace.root, names: INFRA_SECRETS, secretsApi: deps.secretsApi
|
|
152
188
|
});
|
|
153
|
-
const record = deployWithStatus(workspace, doc, {
|
|
189
|
+
const record = deployWithStatus(workspace, doc, {
|
|
190
|
+
env, acr, templateHash, az: context.az, registryCredentials
|
|
191
|
+
});
|
|
154
192
|
recordDeployment(doc, record, context.now);
|
|
193
|
+
// Never persist registry credentials in the workspace manifest.
|
|
194
|
+
doc.parameters.acrAdminUsername = { ...doc.parameters.acrAdminUsername, value: '', source: 'managedIdentityMode' };
|
|
195
|
+
doc.parameters.acrAdminPassword = { ...doc.parameters.acrAdminPassword, value: '', source: 'managedIdentityMode' };
|
|
155
196
|
saveInfra(workspace, doc);
|
|
156
197
|
const controllerUrl = await verifyAndRecordReadiness(workspace, doc, context);
|
|
157
198
|
return { doc, controllerUrl, activationUrl: doc.tenantActivation.url.value };
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* @fileoverview AZURE infra preflight before any Azure mutation: active account equals the
|
|
5
5
|
* workspace subscription/tenant, `acrName` resolves inside that subscription, and the caller can
|
|
6
|
-
* create deployments
|
|
7
|
-
*
|
|
6
|
+
* create deployments. Managed-identity mode also requires role-assignment and AcrPull authority;
|
|
7
|
+
* password mode fetches ACR admin credentials instead. Uses only the operator's Azure CLI session.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
const ROLE_ASSIGNMENT_WRITE = 'Microsoft.Authorization/roleAssignments/write';
|
|
@@ -19,6 +19,14 @@ function parseJson(text, label) {
|
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* @param {Object} doc - Infra manifest
|
|
24
|
+
* @returns {boolean} True when image pull uses registry username/password (effective mode)
|
|
25
|
+
*/
|
|
26
|
+
function isAdminAcrAuth(doc) {
|
|
27
|
+
return doc.acrAuth?.effective === 'credentials';
|
|
28
|
+
}
|
|
29
|
+
|
|
22
30
|
/**
|
|
23
31
|
* @param {Object} doc - Infra manifest
|
|
24
32
|
* @param {Function} az - Azure CLI runner
|
|
@@ -41,7 +49,7 @@ function assertActiveAccount(doc, az) {
|
|
|
41
49
|
* Resolves `acrName` to its resource ID and login server within the workspace subscription.
|
|
42
50
|
* @param {Object} doc - Infra manifest
|
|
43
51
|
* @param {Function} az - Azure CLI runner
|
|
44
|
-
* @returns {{ id: string, loginServer: string }}
|
|
52
|
+
* @returns {{ id: string, loginServer: string, adminUserEnabled: boolean }}
|
|
45
53
|
*/
|
|
46
54
|
function resolveAcr(doc, az) {
|
|
47
55
|
const subscription = String(doc.subscriptionId.value);
|
|
@@ -54,7 +62,35 @@ function resolveAcr(doc, az) {
|
|
|
54
62
|
if (!acr.id.toLowerCase().startsWith(`/subscriptions/${subscription.toLowerCase()}/`)) {
|
|
55
63
|
throw new Error(`Registry ${doc.acrName.value} is outside the workspace subscription; use a registry in ${subscription}`);
|
|
56
64
|
}
|
|
57
|
-
return { id: acr.id, loginServer: acr.loginServer };
|
|
65
|
+
return { id: acr.id, loginServer: acr.loginServer, adminUserEnabled: acr.adminUserEnabled === true };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Loads ACR admin username/password for password registry auth. Never persists the password.
|
|
70
|
+
* @param {Object} doc - Infra manifest
|
|
71
|
+
* @param {{ adminUserEnabled?: boolean }} acr - Resolved registry metadata
|
|
72
|
+
* @param {Function} az - Azure CLI runner
|
|
73
|
+
* @returns {{ username: string, password: string }}
|
|
74
|
+
*/
|
|
75
|
+
function resolveAcrAdminCredentials(doc, acr, az) {
|
|
76
|
+
if (acr.adminUserEnabled !== true) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`Registry ${doc.acrName.value} does not have the admin user enabled; enable admin user on the ACR ` +
|
|
79
|
+
'or save a local pair with "aifabrix dev set-registry"'
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
const creds = parseJson(az([
|
|
83
|
+
'acr', 'credential', 'show', '--name', doc.acrName.value,
|
|
84
|
+
'--subscription', String(doc.subscriptionId.value), '--output', 'json'
|
|
85
|
+
]), 'registry credentials');
|
|
86
|
+
const username = typeof creds.username === 'string' ? creds.username : '';
|
|
87
|
+
const password = Array.isArray(creds.passwords)
|
|
88
|
+
? (creds.passwords.find(item => item && item.value)?.value || '')
|
|
89
|
+
: '';
|
|
90
|
+
if (!username || !password) {
|
|
91
|
+
throw new Error(`Registry ${doc.acrName.value} did not return admin credentials; check ACR admin user access`);
|
|
92
|
+
}
|
|
93
|
+
return { username, password };
|
|
58
94
|
}
|
|
59
95
|
|
|
60
96
|
function actionMatches(pattern, action) {
|
|
@@ -84,19 +120,39 @@ function hasPermission(az, scope, action) {
|
|
|
84
120
|
* @param {Object} doc - Infra manifest
|
|
85
121
|
* @param {{ id: string }} acr - Resolved registry
|
|
86
122
|
* @param {Function} az - Azure CLI runner
|
|
87
|
-
* @throws {Error} When deployment
|
|
123
|
+
* @throws {Error} When deployment or (mi) role-assignment authority is missing
|
|
88
124
|
*/
|
|
89
125
|
function assertDeploymentAuthority(doc, acr, az) {
|
|
90
126
|
const subscriptionScope = `/subscriptions/${doc.subscriptionId.value}`;
|
|
91
127
|
if (!hasPermission(az, subscriptionScope, DEPLOYMENT_WRITE)) {
|
|
92
128
|
throw new Error(`The signed-in Azure account cannot create deployments in subscription ${doc.subscriptionId.value}`);
|
|
93
129
|
}
|
|
130
|
+
// Checked separately, because they are different permissions on different resources.
|
|
131
|
+
//
|
|
132
|
+
// Credential mode used to return here and skip both. That was wrong for the first one: the
|
|
133
|
+
// installation still needs Contributor granted in the resource group it is creating, and
|
|
134
|
+
// skipping the check hid the fact that the grant was being skipped too. The controller then
|
|
135
|
+
// could not create anything, and the failure surfaced three layers away as "Environment 'dev'
|
|
136
|
+
// has no recorded placement".
|
|
94
137
|
if (doc.parameters.deployRoleAssignments.value !== false && !hasPermission(az, subscriptionScope, ROLE_ASSIGNMENT_WRITE)) {
|
|
95
138
|
throw new Error(`The signed-in Azure account cannot create role assignments in subscription ${doc.subscriptionId.value}; ask for Owner or User Access Administrator, or set parameters.deployRoleAssignments.value to false when roles already exist`);
|
|
96
139
|
}
|
|
140
|
+
// The AcrPull grant reaches into the registry's own resource group, which a customer
|
|
141
|
+
// installation cannot touch at all. Credential mode does not request it, so do not require
|
|
142
|
+
// permission for it.
|
|
143
|
+
if (isAdminAcrAuth(doc)) return;
|
|
144
|
+
if (doc.parameters.deployRegistryRoleAssignment?.value === false) return;
|
|
97
145
|
if (!hasPermission(az, acr.id, ROLE_ASSIGNMENT_WRITE)) {
|
|
98
|
-
throw new Error(`The signed-in Azure account cannot assign AcrPull on registry ${doc.acrName.value}; ask for Owner or User Access Administrator on that registry`);
|
|
146
|
+
throw new Error(`The signed-in Azure account cannot assign AcrPull on registry ${doc.acrName.value}; ask for Owner or User Access Administrator on that registry, or use --acr-auth credentials with a pair saved by "aifabrix dev set-registry"`);
|
|
99
147
|
}
|
|
100
148
|
}
|
|
101
149
|
|
|
102
|
-
module.exports = {
|
|
150
|
+
module.exports = {
|
|
151
|
+
assertActiveAccount,
|
|
152
|
+
resolveAcr,
|
|
153
|
+
resolveAcrAdminCredentials,
|
|
154
|
+
isAdminAcrAuth,
|
|
155
|
+
hasPermission,
|
|
156
|
+
actionMatches,
|
|
157
|
+
assertDeploymentAuthority
|
|
158
|
+
};
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* @fileoverview AZURE infra readiness evidence:
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* @fileoverview AZURE infra readiness evidence: image pull authorization for both web apps, then
|
|
5
|
+
* HTTPS probes. Managed-identity mode requires AcrPull + acrUseManagedIdentityCreds; password mode
|
|
6
|
+
* requires registry credentials on the web app and acrUseManagedIdentityCreds false. Both modes
|
|
7
|
+
* then require the applications to start (proof that the image was pulled). Local
|
|
8
|
+
* `az acr login` success is never treated as readiness.
|
|
7
9
|
*/
|
|
8
10
|
|
|
11
|
+
const { isAdminAcrAuth } = require('./azure-preflight');
|
|
12
|
+
|
|
9
13
|
const APP_OUTPUTS = Object.freeze([
|
|
10
14
|
{ name: 'webAppName', label: 'Miso' },
|
|
11
15
|
{ name: 'keycloakWebAppName', label: 'Keycloak' }
|
|
@@ -15,7 +19,7 @@ function scoped(args, doc) {
|
|
|
15
19
|
return [...args, '--subscription', doc.subscriptionId.value];
|
|
16
20
|
}
|
|
17
21
|
|
|
18
|
-
function
|
|
22
|
+
function verifyManagedIdentityPull(doc, app, acrId, az) {
|
|
19
23
|
const group = doc.resourceGroup.value;
|
|
20
24
|
const principalId = az(scoped([
|
|
21
25
|
'webapp', 'identity', 'show', '--resource-group', group, '--name', app.value,
|
|
@@ -38,8 +42,34 @@ function verifyAppImagePull(doc, app, acrId, az) {
|
|
|
38
42
|
}
|
|
39
43
|
}
|
|
40
44
|
|
|
45
|
+
function verifyPasswordPull(doc, app, az) {
|
|
46
|
+
const group = doc.resourceGroup.value;
|
|
47
|
+
const managedPull = az(scoped([
|
|
48
|
+
'webapp', 'config', 'show', '--resource-group', group, '--name', app.value,
|
|
49
|
+
'--query', 'acrUseManagedIdentityCreds', '--output', 'tsv'
|
|
50
|
+
], doc));
|
|
51
|
+
if (String(managedPull).toLowerCase() === 'true') {
|
|
52
|
+
throw new Error(`${app.label} web app still uses managed-identity image pull; expected registry username/password`);
|
|
53
|
+
}
|
|
54
|
+
const username = az(scoped([
|
|
55
|
+
'webapp', 'config', 'appsettings', 'list', '--resource-group', group, '--name', app.value,
|
|
56
|
+
'--query', '[?name==\'DOCKER_REGISTRY_SERVER_USERNAME\'].value | [0]', '--output', 'tsv'
|
|
57
|
+
], doc));
|
|
58
|
+
if (!username) {
|
|
59
|
+
throw new Error(`${app.label} web app is missing DOCKER_REGISTRY_SERVER_USERNAME for password registry auth`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function verifyAppImagePull(doc, app, acrId, az) {
|
|
64
|
+
if (isAdminAcrAuth(doc)) {
|
|
65
|
+
verifyPasswordPull(doc, app, az);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
verifyManagedIdentityPull(doc, app, acrId, az);
|
|
69
|
+
}
|
|
70
|
+
|
|
41
71
|
/**
|
|
42
|
-
* Verifies
|
|
72
|
+
* Verifies image-pull authorization for both web apps per acrAuth mode.
|
|
43
73
|
* @param {Object} doc - Infra manifest with outputs
|
|
44
74
|
* @param {Function} az - Azure CLI runner
|
|
45
75
|
*/
|
|
@@ -52,14 +82,36 @@ function verifyRuntimeImagePull(doc, az) {
|
|
|
52
82
|
}
|
|
53
83
|
}
|
|
54
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
|
+
|
|
55
102
|
/**
|
|
56
103
|
* Waits for Miso and Keycloak to start after pulling their images.
|
|
57
104
|
* @param {Object} doc - Infra manifest with outputs
|
|
58
|
-
* @param {Function} probe - probeHttps(baseUrl, path, label)
|
|
105
|
+
* @param {Function} probe - probeHttps(baseUrl, path, label, attempts)
|
|
59
106
|
* @returns {Promise<void>}
|
|
60
107
|
*/
|
|
61
108
|
async function waitForApplications(doc, probe) {
|
|
62
|
-
await probe(
|
|
109
|
+
await probe(
|
|
110
|
+
doc.outputs.webAppUrl.value,
|
|
111
|
+
'/api/v1/health/readiness',
|
|
112
|
+
'Miso',
|
|
113
|
+
MISO_READINESS_ATTEMPTS
|
|
114
|
+
);
|
|
63
115
|
const realmUrl = doc.outputs.keycloakRealmUrl.value;
|
|
64
116
|
if (typeof realmUrl !== 'string' || !realmUrl.startsWith('https://')) {
|
|
65
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);
|