@aifabrix/builder 2.57.0 → 2.58.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/app/push.js +98 -41
- package/lib/build/standard-docker-build.js +13 -11
- package/lib/cli/setup-app.js +16 -5
- package/lib/cli/setup-dev-path-commands.js +49 -2
- package/lib/cli/setup-environment.js +17 -4
- package/lib/commands/dev-registry.js +155 -0
- package/lib/commands/dev-show-display.js +10 -2
- package/lib/commands/setup-image-refresh.js +26 -0
- package/lib/core/config-attach-extensions.js +4 -1
- package/lib/deployment/installation/azure-infra-stage.js +40 -9
- package/lib/deployment/installation/azure-preflight.js +53 -8
- package/lib/deployment/installation/azure-readiness.js +35 -5
- package/lib/deployment/installation/infra-catalog.js +50 -4
- package/lib/deployment/installation/infra-manifest.js +17 -2
- package/lib/deployment/installation/infra-projection.js +14 -3
- package/lib/deployment/installation/registry-auth-mode.js +98 -0
- package/lib/deployment/installation/stage-input.js +21 -6
- package/lib/generator/builders.js +8 -9
- package/lib/schema/infrastructure-schema.json +55 -2
- 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-registry-preference.js +106 -0
- package/lib/utils/docker-build.js +53 -19
- package/lib/utils/env-map.js +23 -6
- 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/variable-transformer.js +2 -4
- package/package.json +2 -1
- package/templates/marketplace/main.json +2 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Attach token, path, format,
|
|
2
|
+
* Attach token, path, format, scoped-resources, and registry helpers to config exports.
|
|
3
3
|
*
|
|
4
4
|
* @fileoverview Keeps lib/core/config.js under max-lines
|
|
5
5
|
* @author AI Fabrix Team
|
|
@@ -42,6 +42,9 @@ function attachConfigExtensions(exportsObj, deps) {
|
|
|
42
42
|
const { createScopedResourcesPreferenceFunctions } = require('../utils/config-scoped-resources-preference');
|
|
43
43
|
Object.assign(exportsObj, createScopedResourcesPreferenceFunctions(getConfig, saveConfig));
|
|
44
44
|
|
|
45
|
+
const { createRegistryPreferenceFunctions } = require('../utils/config-registry-preference');
|
|
46
|
+
Object.assign(exportsObj, createRegistryPreferenceFunctions(getConfig, saveConfig));
|
|
47
|
+
|
|
45
48
|
const { createSecretsPathFunctions } = require('../utils/config-secrets-path');
|
|
46
49
|
Object.assign(exportsObj, createSecretsPathFunctions(getConfig, saveConfig));
|
|
47
50
|
}
|
|
@@ -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
|
|
|
@@ -111,18 +113,41 @@ async function verifyAndRecordReadiness(workspace, doc, deps) {
|
|
|
111
113
|
return controllerUrl;
|
|
112
114
|
}
|
|
113
115
|
|
|
116
|
+
function applyLocation(doc, input) {
|
|
117
|
+
if (input.location === undefined) return;
|
|
118
|
+
doc.parameters.location = { ...doc.parameters.location, value: normalizeAzureLocation(input.location), source: 'cli' };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function resolvePullAuth(doc, acr, deps, az) {
|
|
122
|
+
const loadRegistryCredentials = deps.loadRegistryCredentials ||
|
|
123
|
+
require('../../utils/registry-credentials').loadRegistryCredentials;
|
|
124
|
+
const resolved = await registryAuth.resolveRegistryAuth(doc, acr, az, {
|
|
125
|
+
loadRegistryCredentials,
|
|
126
|
+
resolveAcrAdminCredentials: preflight.resolveAcrAdminCredentials
|
|
127
|
+
});
|
|
128
|
+
registryAuth.applyEffectiveAcrAuth(doc, resolved.mode);
|
|
129
|
+
return resolved.credentials;
|
|
130
|
+
}
|
|
131
|
+
|
|
114
132
|
async function prepareInfra(workspace, input, deps, az) {
|
|
115
133
|
const { doc, templateHash } = materializeWorkspace(workspace, input, deps);
|
|
134
|
+
applyLocation(doc, input);
|
|
116
135
|
const missing = missingRequiredFields(doc);
|
|
117
|
-
if (missing.length > 0)
|
|
136
|
+
if (missing.length > 0) {
|
|
137
|
+
if (input.location !== undefined) saveInfra(workspace, doc);
|
|
138
|
+
throw missingInputError(workspace.paths.infra, missing);
|
|
139
|
+
}
|
|
140
|
+
registryAuth.applyRequestedAcrAuth(doc, input);
|
|
118
141
|
preflight.assertActiveAccount(doc, az);
|
|
119
142
|
const acr = preflight.resolveAcr(doc, az);
|
|
120
|
-
preflight.assertDeploymentAuthority(doc, acr, az);
|
|
121
143
|
doc.parameters.acrLoginServer.value = acr.loginServer;
|
|
122
144
|
doc.parameters.acrResourceId.value = acr.id;
|
|
145
|
+
// Local pair first (a partial pair fails closed); Azure admin lookup only for explicit credentials.
|
|
146
|
+
const registryCredentials = await resolvePullAuth(doc, acr, deps, az);
|
|
147
|
+
preflight.assertDeploymentAuthority(doc, acr, az);
|
|
123
148
|
await applyAccessOptions(doc, input, deps);
|
|
124
149
|
saveInfra(workspace, doc);
|
|
125
|
-
return { doc, templateHash, acr };
|
|
150
|
+
return { doc, templateHash, acr, registryCredentials };
|
|
126
151
|
}
|
|
127
152
|
|
|
128
153
|
function deployWithStatus(workspace, doc, context) {
|
|
@@ -141,17 +166,23 @@ function deployWithStatus(workspace, doc, context) {
|
|
|
141
166
|
* Runs the AZURE infra stage.
|
|
142
167
|
* @param {Object} workspace - Opened workspace
|
|
143
168
|
* @param {Object} input - Validated stage input
|
|
144
|
-
* @param {Object} [deps] - Injected dependencies (az, probe, resolvePublicIpv4, secretsApi, now, templatePath
|
|
169
|
+
* @param {Object} [deps] - Injected dependencies (az, probe, resolvePublicIpv4, secretsApi, now, templatePath,
|
|
170
|
+
* loadRegistryCredentials)
|
|
145
171
|
* @returns {Promise<{ doc: Object, controllerUrl: string, activationUrl: string }>}
|
|
146
172
|
*/
|
|
147
173
|
async function runAzureInfraStage(workspace, input, deps = {}) {
|
|
148
174
|
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);
|
|
175
|
+
const { doc, templateHash, acr, registryCredentials } = await prepareInfra(workspace, input, deps, context.az);
|
|
150
176
|
const env = await materializeWorkspaceSecrets({
|
|
151
177
|
vendor: 'AZURE', envPath: workspace.paths.env, root: workspace.root, names: INFRA_SECRETS, secretsApi: deps.secretsApi
|
|
152
178
|
});
|
|
153
|
-
const record = deployWithStatus(workspace, doc, {
|
|
179
|
+
const record = deployWithStatus(workspace, doc, {
|
|
180
|
+
env, acr, templateHash, az: context.az, registryCredentials
|
|
181
|
+
});
|
|
154
182
|
recordDeployment(doc, record, context.now);
|
|
183
|
+
// Never persist registry credentials in the workspace manifest.
|
|
184
|
+
doc.parameters.acrAdminUsername = { ...doc.parameters.acrAdminUsername, value: '', source: 'managedIdentityMode' };
|
|
185
|
+
doc.parameters.acrAdminPassword = { ...doc.parameters.acrAdminPassword, value: '', source: 'managedIdentityMode' };
|
|
155
186
|
saveInfra(workspace, doc);
|
|
156
187
|
const controllerUrl = await verifyAndRecordReadiness(workspace, doc, context);
|
|
157
188
|
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,28 @@ 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
|
+
if (isAdminAcrAuth(doc)) return;
|
|
94
131
|
if (doc.parameters.deployRoleAssignments.value !== false && !hasPermission(az, subscriptionScope, ROLE_ASSIGNMENT_WRITE)) {
|
|
95
|
-
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`);
|
|
132
|
+
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, or use --acr-auth credentials with a pair saved by "aifabrix dev set-registry"`);
|
|
96
133
|
}
|
|
97
134
|
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`);
|
|
135
|
+
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
136
|
}
|
|
100
137
|
}
|
|
101
138
|
|
|
102
|
-
module.exports = {
|
|
139
|
+
module.exports = {
|
|
140
|
+
assertActiveAccount,
|
|
141
|
+
resolveAcr,
|
|
142
|
+
resolveAcrAdminCredentials,
|
|
143
|
+
isAdminAcrAuth,
|
|
144
|
+
hasPermission,
|
|
145
|
+
actionMatches,
|
|
146
|
+
assertDeploymentAuthority
|
|
147
|
+
};
|
|
@@ -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
|
*/
|
|
@@ -20,6 +20,9 @@ const LICENSE_EDITIONS = Object.freeze(['COMMUNITY', 'STANDARD', 'ENTERPRISE']);
|
|
|
20
20
|
const DEFAULT_LICENSE_EDITION = 'COMMUNITY';
|
|
21
21
|
const ONBOARDING_MODES = Object.freeze(['saasDirect', 'customerOwned']);
|
|
22
22
|
const DEFAULT_ONBOARDING_MODE = 'saasDirect';
|
|
23
|
+
const ACR_AUTH_MODES = Object.freeze(['auto', 'mi', 'credentials']);
|
|
24
|
+
const DEFAULT_ACR_AUTH = 'auto';
|
|
25
|
+
const AZURE_LOCATION = /^[a-z0-9]{2,40}$/;
|
|
23
26
|
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
24
27
|
|
|
25
28
|
/**
|
|
@@ -36,6 +39,45 @@ function normalizeLicenseEdition(value) {
|
|
|
36
39
|
throw new Error(`License edition must be one of ${LICENSE_EDITIONS.join(', ')} or an Azure Marketplace subscription ID`);
|
|
37
40
|
}
|
|
38
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Normalizes requested ACR image-pull auth: auto (default), mi, or credentials.
|
|
44
|
+
* `admin` (and its spellings) is a deprecated alias for `credentials`.
|
|
45
|
+
* @param {string} value - Raw value
|
|
46
|
+
* @returns {string} Canonical mode
|
|
47
|
+
*/
|
|
48
|
+
function normalizeAcrAuth(value) {
|
|
49
|
+
const raw = String(value ?? '').trim();
|
|
50
|
+
const aliases = {
|
|
51
|
+
auto: 'auto',
|
|
52
|
+
mi: 'mi',
|
|
53
|
+
'managed-identity': 'mi',
|
|
54
|
+
managedidentity: 'mi',
|
|
55
|
+
credentials: 'credentials',
|
|
56
|
+
admin: 'credentials',
|
|
57
|
+
adminuser: 'credentials',
|
|
58
|
+
'admin-user': 'credentials',
|
|
59
|
+
password: 'credentials'
|
|
60
|
+
};
|
|
61
|
+
const mode = aliases[raw.toLowerCase()];
|
|
62
|
+
if (!mode) {
|
|
63
|
+
throw new Error(`ACR auth must be one of ${ACR_AUTH_MODES.join(', ')} (deprecated alias: admin = credentials)`);
|
|
64
|
+
}
|
|
65
|
+
return mode;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Validates an Azure region name (for example swedencentral).
|
|
70
|
+
* @param {string} value - Raw value
|
|
71
|
+
* @returns {string} Lower-case region
|
|
72
|
+
*/
|
|
73
|
+
function normalizeAzureLocation(value) {
|
|
74
|
+
const location = String(value ?? '').trim().toLowerCase();
|
|
75
|
+
if (!AZURE_LOCATION.test(location)) {
|
|
76
|
+
throw new Error(`--location must be an Azure region name such as swedencentral (letters and digits only); got "${value}"`);
|
|
77
|
+
}
|
|
78
|
+
return location;
|
|
79
|
+
}
|
|
80
|
+
|
|
39
81
|
const PARAMETER_CATALOG = Object.freeze({
|
|
40
82
|
infrastructureNameOverride: { kind: 'file', source: 'user', description: 'Optional infrastructure-name override; empty uses generated naming.' },
|
|
41
83
|
location: { kind: 'file', source: 'user', required: true, description: 'Azure region for shared infrastructure.' },
|
|
@@ -50,10 +92,10 @@ const PARAMETER_CATALOG = Object.freeze({
|
|
|
50
92
|
keycloakVerifyAudience: { kind: 'file', source: 'templateDefault', description: 'Enable Keycloak audience verification.' },
|
|
51
93
|
encryptionKey: { kind: 'secretEnv', env: 'ENCRYPTION_KEY', required: true, description: 'Application/data encryption key.' },
|
|
52
94
|
acrLoginServer: { kind: 'acrLookup', field: 'loginServer', description: 'Resolved from acrName; not entered separately.' },
|
|
53
|
-
acrAdminUsername: { kind: 'fixed', value: '', source: 'managedIdentityMode', description: '
|
|
54
|
-
acrAdminPassword: { kind: 'fixed', value: '', source: 'managedIdentityMode', description: '
|
|
55
|
-
acrResourceId: { kind: 'acrLookup', field: 'id', description: 'Resolved from acrName;
|
|
56
|
-
deployRoleAssignments: { kind: 'file', source: 'templateDefault', description: 'Deploy required Azure role assignments.' },
|
|
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
|
+
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
|
+
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 required Azure role assignments (false while credential image pull is in effect; restored for managed identity).' },
|
|
57
99
|
moriBaseUrl: { kind: 'file', source: 'registration', required: true, description: 'Mori service base URL.' },
|
|
58
100
|
moriCallbackApiKey: { kind: 'secretEnv', env: 'MORI_CALLBACK_API_KEY', required: true, description: 'Mori callback authentication key.' },
|
|
59
101
|
entraCallbackStateSecret: { kind: 'deferred', description: 'Not collected or sent by infra; issued/configured during later Entra provider onboarding.' },
|
|
@@ -159,7 +201,11 @@ module.exports = {
|
|
|
159
201
|
DEFAULT_LICENSE_EDITION,
|
|
160
202
|
ONBOARDING_MODES,
|
|
161
203
|
DEFAULT_ONBOARDING_MODE,
|
|
204
|
+
ACR_AUTH_MODES,
|
|
205
|
+
DEFAULT_ACR_AUTH,
|
|
162
206
|
normalizeLicenseEdition,
|
|
207
|
+
normalizeAcrAuth,
|
|
208
|
+
normalizeAzureLocation,
|
|
163
209
|
buildEnvTemplate,
|
|
164
210
|
assertTemplateCoverage
|
|
165
211
|
};
|
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
|
|
9
9
|
const crypto = require('crypto');
|
|
10
10
|
const {
|
|
11
|
-
PARAMETER_CATALOG, OUTPUT_CATALOG, DEFAULT_LICENSE_EDITION, DEFAULT_ONBOARDING_MODE
|
|
11
|
+
PARAMETER_CATALOG, OUTPUT_CATALOG, DEFAULT_LICENSE_EDITION, DEFAULT_ONBOARDING_MODE, DEFAULT_ACR_AUTH
|
|
12
12
|
} = require('./infra-catalog');
|
|
13
13
|
|
|
14
|
+
const ACR_AUTH_DESCRIPTION = 'Image pull auth: auto (default; local dev set-registry pair for the ACR login server, else managed identity), mi (AcrPull), or credentials (local pair, then authorized ACR admin lookup). Credentials are never committed.';
|
|
14
15
|
const INFRA_SCHEMA_VERSION = '2.0.0';
|
|
15
16
|
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
16
17
|
|
|
@@ -46,12 +47,24 @@ function parameterEntry(name, template) {
|
|
|
46
47
|
}
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
/** Sources written by Builder workflows that must survive regeneration. */
|
|
51
|
+
const STICKY_SOURCES = Object.freeze(['cli', 'acrAuthCredentials', 'acrAuthAdmin']);
|
|
52
|
+
|
|
53
|
+
function preservedState(previous) {
|
|
54
|
+
const kept = { value: previous.value };
|
|
55
|
+
if (STICKY_SOURCES.includes(previous.source)) kept.source = previous.source;
|
|
56
|
+
for (const key of ['restoreValue', 'restoreSource']) {
|
|
57
|
+
if (previous[key] !== undefined) kept[key] = previous[key];
|
|
58
|
+
}
|
|
59
|
+
return kept;
|
|
60
|
+
}
|
|
61
|
+
|
|
49
62
|
function buildParameters(template, existing = {}) {
|
|
50
63
|
const parameters = {};
|
|
51
64
|
for (const name of Object.keys(PARAMETER_CATALOG)) {
|
|
52
65
|
const fresh = parameterEntry(name, template);
|
|
53
66
|
const workflowOwned = ['fixed', 'deferred', 'secretEnv'].includes(PARAMETER_CATALOG[name].kind);
|
|
54
|
-
parameters[name] = existing[name] && !workflowOwned ? { ...fresh,
|
|
67
|
+
parameters[name] = existing[name] && !workflowOwned ? { ...fresh, ...preservedState(existing[name]) } : fresh;
|
|
55
68
|
}
|
|
56
69
|
return parameters;
|
|
57
70
|
}
|
|
@@ -64,6 +77,7 @@ function buildOutputs(existing = {}) {
|
|
|
64
77
|
function azureSections(template, existing) {
|
|
65
78
|
return {
|
|
66
79
|
acrName: existing.acrName || entry('', 'user', 'Required private ACR name. Builder resolves and persists its login server and resource ID using the authenticated Azure account.', { required: true }),
|
|
80
|
+
acrAuth: existing.acrAuth || entry(DEFAULT_ACR_AUTH, 'cliOrFile', ACR_AUTH_DESCRIPTION),
|
|
67
81
|
allowMyIp: existing.allowMyIp || entry(false, 'cliOrFile', 'Default-off internal test access. When enabled, Builder resolves and audits one public IPv4 /32.', { resolvedCidr: null, resolvedAt: null, resolver: null }),
|
|
68
82
|
preset: existing.preset || entry('s', 'cliOrFile', 'Shared infrastructure sizing preset.'),
|
|
69
83
|
parameters: buildParameters(template, existing.parameters),
|
|
@@ -156,6 +170,7 @@ function missingRequiredFields(doc) {
|
|
|
156
170
|
|
|
157
171
|
module.exports = {
|
|
158
172
|
INFRA_SCHEMA_VERSION,
|
|
173
|
+
ACR_AUTH_DESCRIPTION,
|
|
159
174
|
buildInfraDocument,
|
|
160
175
|
missingRequiredFields
|
|
161
176
|
};
|
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* @fileoverview Projects `environment-infra.json` plus resolved workspace secrets into standard
|
|
5
5
|
* ARM deploymentParameters, and applies the default-off current-IP access option.
|
|
6
|
-
* Deferred tenant-activation parameters are never projected.
|
|
6
|
+
* Deferred tenant-activation parameters are never projected. ACR admin credentials (password mode)
|
|
7
|
+
* are injected only into the temporary parameters file and never enter the input hash.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
const { PARAMETER_CATALOG } = require('./infra-catalog');
|
|
10
11
|
const { sha256 } = require('./workspace-files');
|
|
11
12
|
|
|
13
|
+
const ARM_CREDENTIAL_PARAMS = Object.freeze(new Set(['acrAdminUsername', 'acrAdminPassword']));
|
|
14
|
+
|
|
12
15
|
function projectParameter(name, doc, context) {
|
|
13
16
|
const spec = PARAMETER_CATALOG[name];
|
|
14
17
|
switch (spec.kind) {
|
|
@@ -27,6 +30,12 @@ function projectParameter(name, doc, context) {
|
|
|
27
30
|
}
|
|
28
31
|
return context.acr[spec.field];
|
|
29
32
|
case 'fixed':
|
|
33
|
+
if (name === 'acrAdminUsername' && context.registryCredentials) {
|
|
34
|
+
return context.registryCredentials.username;
|
|
35
|
+
}
|
|
36
|
+
if (name === 'acrAdminPassword' && context.registryCredentials) {
|
|
37
|
+
return context.registryCredentials.password;
|
|
38
|
+
}
|
|
30
39
|
return spec.value;
|
|
31
40
|
default:
|
|
32
41
|
return doc.parameters[name].value;
|
|
@@ -39,6 +48,7 @@ function projectParameter(name, doc, context) {
|
|
|
39
48
|
* @param {Object} context - Resolution context
|
|
40
49
|
* @param {Object.<string,string>} context.env - Resolved workspace .env values
|
|
41
50
|
* @param {{ id: string, loginServer: string }} context.acr - Resolved registry
|
|
51
|
+
* @param {{ username: string, password: string }|null|undefined} context.registryCredentials - Credential-mode pull creds (never hashed)
|
|
42
52
|
* @param {string} context.templateHash - SHA-256 of the deployed template
|
|
43
53
|
* @returns {{ document: Object, inputHash: string }}
|
|
44
54
|
*/
|
|
@@ -49,9 +59,10 @@ function buildArmParameters(doc, context) {
|
|
|
49
59
|
const value = projectParameter(name, doc, context);
|
|
50
60
|
if (value === undefined) continue;
|
|
51
61
|
parameters[name] = { value };
|
|
52
|
-
|
|
53
|
-
if (
|
|
62
|
+
const catalog = PARAMETER_CATALOG[name];
|
|
63
|
+
if (catalog.kind !== 'secretEnv' && !ARM_CREDENTIAL_PARAMS.has(name)) nonSecret[name] = value;
|
|
54
64
|
}
|
|
65
|
+
nonSecret.acrAuth = doc.acrAuth?.effective || doc.acrAuth?.value || 'auto';
|
|
55
66
|
return {
|
|
56
67
|
document: {
|
|
57
68
|
$schema: 'https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#',
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview Image-pull authentication for CLI-managed AZURE infra. The requested mode
|
|
5
|
+
* (`auto|mi|credentials`, deprecated `admin` = credentials) resolves to an effective `mi` or
|
|
6
|
+
* `credentials` mode. Credential values come from the primary user's `dev set-registry` pair for the
|
|
7
|
+
* resolved ACR login server, or (explicit credentials only) from an authorized ACR admin lookup.
|
|
8
|
+
* They live only in memory and in the temporary ARM parameters file.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { normalizeAcrAuth } = require('./infra-catalog');
|
|
12
|
+
const { ACR_AUTH_DESCRIPTION } = require('./infra-manifest');
|
|
13
|
+
|
|
14
|
+
const FORCED_ROLE_SOURCES = Object.freeze(['acrAuthCredentials', 'acrAuthAdmin']);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Normalizes and records the requested mode (CLI wins over the workspace value).
|
|
18
|
+
* @param {Object} doc - Infra manifest (mutated)
|
|
19
|
+
* @param {Object} input - Stage input (`acrAuth` from CLI)
|
|
20
|
+
* @returns {string} Requested mode
|
|
21
|
+
*/
|
|
22
|
+
function applyRequestedAcrAuth(doc, input) {
|
|
23
|
+
const fromCli = input.acrAuth !== undefined;
|
|
24
|
+
const mode = normalizeAcrAuth(fromCli ? input.acrAuth : (doc.acrAuth?.value || 'auto'));
|
|
25
|
+
doc.acrAuth = {
|
|
26
|
+
...(doc.acrAuth || {}),
|
|
27
|
+
value: mode,
|
|
28
|
+
source: fromCli ? 'cli' : (doc.acrAuth?.source || 'cliOrFile'),
|
|
29
|
+
description: ACR_AUTH_DESCRIPTION
|
|
30
|
+
};
|
|
31
|
+
return mode;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function localSetupHint(host) {
|
|
35
|
+
return `Run "aifabrix dev set-registry ${host} --username <username> --password-stdin", or use --acr-auth mi`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function resolveAzureFallback(doc, acr, az, fetchAdmin) {
|
|
39
|
+
try {
|
|
40
|
+
return fetchAdmin(doc, acr, az);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
throw new Error(`No registry credentials are available for ${acr.loginServer}: ${error.message}. ${localSetupHint(acr.loginServer)}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Resolves the effective pull mode and ephemeral credentials. A partial local pair always fails.
|
|
48
|
+
* @param {Object} doc - Infra manifest with a normalized `acrAuth`
|
|
49
|
+
* @param {{ loginServer: string, adminUserEnabled?: boolean }} acr - Resolved registry
|
|
50
|
+
* @param {Function} az - Azure CLI runner
|
|
51
|
+
* @param {Object} deps - `loadRegistryCredentials(host)` and `resolveAcrAdminCredentials(doc, acr, az)`
|
|
52
|
+
* @returns {Promise<{ mode: 'mi'|'credentials', credentials: ({username: string, password: string}|null), source: string }>}
|
|
53
|
+
*/
|
|
54
|
+
async function resolveRegistryAuth(doc, acr, az, deps) {
|
|
55
|
+
const requested = normalizeAcrAuth(doc.acrAuth?.value || 'auto');
|
|
56
|
+
if (requested === 'mi') return { mode: 'mi', credentials: null, source: 'managedIdentity' };
|
|
57
|
+
const local = await deps.loadRegistryCredentials(acr.loginServer);
|
|
58
|
+
if (local) return { mode: 'credentials', credentials: local, source: 'localRegistryCredentials' };
|
|
59
|
+
if (requested === 'auto') return { mode: 'mi', credentials: null, source: 'managedIdentity' };
|
|
60
|
+
const azure = await resolveAzureFallback(doc, acr, az, deps.resolveAcrAdminCredentials);
|
|
61
|
+
return { mode: 'credentials', credentials: azure, source: 'acrAdminLookup' };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Records the effective mode and keeps role assignments consistent with it. Credential mode turns
|
|
66
|
+
* ARM role assignments off; returning to managed identity restores the value it replaced.
|
|
67
|
+
* @param {Object} doc - Infra manifest (mutated)
|
|
68
|
+
* @param {'mi'|'credentials'} mode - Effective mode
|
|
69
|
+
*/
|
|
70
|
+
function applyEffectiveAcrAuth(doc, mode) {
|
|
71
|
+
doc.acrAuth = { ...doc.acrAuth, effective: mode };
|
|
72
|
+
const roles = doc.parameters.deployRoleAssignments;
|
|
73
|
+
const forced = FORCED_ROLE_SOURCES.includes(roles.source);
|
|
74
|
+
if (mode === 'credentials') {
|
|
75
|
+
if (forced) return;
|
|
76
|
+
doc.parameters.deployRoleAssignments = {
|
|
77
|
+
...roles, value: false, source: 'acrAuthCredentials', restoreValue: roles.value, restoreSource: roles.source
|
|
78
|
+
};
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (!forced) return;
|
|
82
|
+
const { restoreValue, restoreSource, ...rest } = roles;
|
|
83
|
+
doc.parameters.deployRoleAssignments = {
|
|
84
|
+
...rest,
|
|
85
|
+
value: restoreValue === undefined ? true : restoreValue,
|
|
86
|
+
source: restoreSource || 'templateDefault'
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* @param {Object} doc - Infra manifest
|
|
92
|
+
* @returns {boolean} True when the last resolved pull mode uses registry credentials
|
|
93
|
+
*/
|
|
94
|
+
function usesRegistryCredentials(doc) {
|
|
95
|
+
return doc.acrAuth?.effective === 'credentials';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = { applyRequestedAcrAuth, resolveRegistryAuth, applyEffectiveAcrAuth, usesRegistryCredentials };
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
const path = require('path');
|
|
10
|
-
const { normalizeLicenseEdition } = require('./infra-catalog');
|
|
10
|
+
const { normalizeLicenseEdition, normalizeAcrAuth, normalizeAzureLocation } = require('./infra-catalog');
|
|
11
11
|
|
|
12
12
|
const WORKSPACE_STAGES = Object.freeze(['register', 'infra', 'dev', 'tst', 'pro']);
|
|
13
13
|
const ENVIRONMENT_STAGES = Object.freeze(['dev', 'tst', 'pro']);
|
|
@@ -102,11 +102,24 @@ function assertInfraOnlyOptions(stage, options) {
|
|
|
102
102
|
if (options.allowMyIp && stage !== 'infra') {
|
|
103
103
|
throw new Error('--allow-my-ip is supported only with --stage infra for AZURE installations');
|
|
104
104
|
}
|
|
105
|
-
if (options.licenseEdition
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
if (options.licenseEdition !== undefined) {
|
|
106
|
+
if (stage !== 'infra') {
|
|
107
|
+
throw new Error('--license-edition is supported only with --stage infra for AZURE installations');
|
|
108
|
+
}
|
|
109
|
+
normalizeLicenseEdition(options.licenseEdition);
|
|
110
|
+
}
|
|
111
|
+
if (options.acrAuth !== undefined) {
|
|
112
|
+
if (stage !== 'infra') {
|
|
113
|
+
throw new Error('--acr-auth is supported only with --stage infra for AZURE installations');
|
|
114
|
+
}
|
|
115
|
+
normalizeAcrAuth(options.acrAuth);
|
|
116
|
+
}
|
|
117
|
+
if (options.location !== undefined) {
|
|
118
|
+
if (stage !== 'infra') {
|
|
119
|
+
throw new Error('--location is supported only with --stage infra (later environments inherit the saved region)');
|
|
120
|
+
}
|
|
121
|
+
normalizeAzureLocation(options.location);
|
|
108
122
|
}
|
|
109
|
-
normalizeLicenseEdition(options.licenseEdition);
|
|
110
123
|
}
|
|
111
124
|
|
|
112
125
|
function assertStageScopedOptions(stage, options) {
|
|
@@ -137,7 +150,7 @@ function assertStageScopedOptions(stage, options) {
|
|
|
137
150
|
* @param {Object} options - Commander options
|
|
138
151
|
* @param {string|undefined} persistedVendor - Vendor recorded in an existing workspace
|
|
139
152
|
* @returns {{ resourceGroup: string, stage: string, vendor: string, allowMyIp: boolean, licenseEdition: string|undefined,
|
|
140
|
-
* preset: string|undefined, controllerUrl: string|undefined, misoApiKeyEnv: string|undefined }}
|
|
153
|
+
* acrAuth: string|undefined, location: string|undefined, preset: string|undefined, controllerUrl: string|undefined, misoApiKeyEnv: string|undefined }}
|
|
141
154
|
*/
|
|
142
155
|
function resolveStageInput(resourceGroupKey, options, persistedVendor) {
|
|
143
156
|
const resourceGroup = validateResourceGroupKey(resourceGroupKey);
|
|
@@ -150,6 +163,8 @@ function resolveStageInput(resourceGroupKey, options, persistedVendor) {
|
|
|
150
163
|
vendor,
|
|
151
164
|
allowMyIp: options.allowMyIp === true,
|
|
152
165
|
licenseEdition: options.licenseEdition,
|
|
166
|
+
acrAuth: options.acrAuth,
|
|
167
|
+
location: options.location,
|
|
153
168
|
preset: options.preset,
|
|
154
169
|
controllerUrl: options.controllerUrl,
|
|
155
170
|
misoApiKeyEnv: options.misoApiKeyEnv
|
|
@@ -41,16 +41,15 @@ function filterConfigurationByRegistryMode(configuration, _registryMode) {
|
|
|
41
41
|
return Array.isArray(configuration) ? configuration : [];
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Deployment image reference using the shared registry precedence
|
|
46
|
+
* (installation `registry` in config.yaml, then manifest `image.registry`).
|
|
47
|
+
* @param {Object} variables - Application variables
|
|
48
|
+
* @returns {string}
|
|
49
|
+
*/
|
|
44
50
|
function buildImageReference(variables) {
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
const tag = variables.image?.tag || 'latest';
|
|
48
|
-
|
|
49
|
-
if (registry) {
|
|
50
|
-
return `${registry}/${imageName}:${tag}`;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return `${imageName}:${tag}`;
|
|
51
|
+
const { buildManifestImageReference } = require('../utils/resolve-docker-image-ref');
|
|
52
|
+
return buildManifestImageReference(variables, 'app');
|
|
54
53
|
}
|
|
55
54
|
|
|
56
55
|
/**
|