@aifabrix/builder 2.56.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/agent-kit/session-report.js +76 -0
- package/lib/agent-kit/start.js +124 -57
- package/lib/agent-kit/tmux-sessions.js +163 -0
- package/lib/api/environments.api.js +38 -0
- package/lib/api/marketplace-bootstrap.api.js +31 -0
- 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 +89 -19
- package/lib/commands/agent-kit-help.js +32 -15
- package/lib/commands/agent-kit.js +90 -8
- 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 +191 -0
- package/lib/deployment/installation/azure-preflight.js +147 -0
- package/lib/deployment/installation/azure-readiness.js +101 -0
- package/lib/deployment/installation/docker-register.js +112 -0
- package/lib/deployment/installation/environment-auth.js +76 -0
- package/lib/deployment/installation/environment-manifest.js +165 -0
- package/lib/deployment/installation/environment-stage.js +191 -0
- package/lib/deployment/installation/index.js +39 -0
- package/lib/deployment/installation/infra-catalog.js +211 -0
- package/lib/deployment/installation/infra-manifest.js +176 -0
- package/lib/deployment/installation/infra-projection.js +97 -0
- package/lib/deployment/installation/public-ip.js +65 -0
- package/lib/deployment/installation/registry-auth-mode.js +98 -0
- package/lib/deployment/installation/stage-input.js +184 -0
- package/lib/deployment/installation/stage-output.js +53 -0
- package/lib/deployment/installation/workspace-env.js +76 -0
- package/lib/deployment/installation/workspace-files.js +178 -0
- package/lib/deployment/installation/workspace-schema.js +40 -0
- package/lib/deployment/installation/workspace-secrets.js +66 -0
- package/lib/deployment/installation/workspace.js +128 -0
- package/lib/deployment/marketplace-environment.js +305 -0
- package/lib/generator/builders.js +12 -9
- package/lib/generator/helpers.js +55 -56
- package/lib/generator/split-variables.js +11 -5
- package/lib/internal/fs-real-sync.js +29 -0
- package/lib/role-assistant/package-validate.js +4 -0
- package/lib/role-assistant/test-runner-clarification-resubmit.js +149 -0
- package/lib/role-assistant/test-runner.js +14 -17
- package/lib/schema/application-schema.json +20 -2
- package/lib/schema/infra-parameter.schema.json +7 -0
- package/lib/schema/infra.parameter.yaml +26 -36
- package/lib/schema/infrastructure-schema.json +1531 -397
- package/lib/schema/installation-environment.schema.json +601 -0
- 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 +5 -4
- package/lib/validation/required-variables.js +41 -0
- package/lib/validation/validator.js +8 -5
- package/package.json +6 -1
- package/templates/README.md +4 -0
- package/templates/agent-kit/instructions/AGENTKIT.md +6 -6
- package/templates/agent-kit/skills/shared/hosts.md +10 -10
- package/templates/applications/dataplane/application.yaml +1 -1
- package/templates/applications/dataplane/env.template +1 -1
- package/templates/applications/miso-controller/application.yaml +3 -2
- package/templates/applications/miso-controller/env.template +15 -6
- package/templates/marketplace/createUiDefinition.json +77 -0
- package/templates/marketplace/install.parameters.example.json +9 -0
- package/templates/marketplace/main.json +4087 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview Projects `environment-infra.json` plus resolved workspace secrets into standard
|
|
5
|
+
* ARM deploymentParameters, and applies the default-off current-IP access option.
|
|
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.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { PARAMETER_CATALOG } = require('./infra-catalog');
|
|
11
|
+
const { sha256 } = require('./workspace-files');
|
|
12
|
+
|
|
13
|
+
const ARM_CREDENTIAL_PARAMS = Object.freeze(new Set(['acrAdminUsername', 'acrAdminPassword']));
|
|
14
|
+
|
|
15
|
+
function projectParameter(name, doc, context) {
|
|
16
|
+
const spec = PARAMETER_CATALOG[name];
|
|
17
|
+
switch (spec.kind) {
|
|
18
|
+
case 'deferred':
|
|
19
|
+
return undefined;
|
|
20
|
+
case 'secretEnv': {
|
|
21
|
+
const value = context.env[spec.env];
|
|
22
|
+
if (spec.required && (typeof value !== 'string' || value === '')) {
|
|
23
|
+
throw new Error(`Workspace .env is missing required ${spec.env}; rerun after the secret resolves`);
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
case 'acrLookup':
|
|
28
|
+
if (!context.acr || !context.acr[spec.field]) {
|
|
29
|
+
throw new Error(`ACR ${spec.field} was not resolved from acrName`);
|
|
30
|
+
}
|
|
31
|
+
return context.acr[spec.field];
|
|
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
|
+
}
|
|
39
|
+
return spec.value;
|
|
40
|
+
default:
|
|
41
|
+
return doc.parameters[name].value;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Builds the owner-only ARM parameters document and the non-secret input hash.
|
|
47
|
+
* @param {Object} doc - Infra manifest
|
|
48
|
+
* @param {Object} context - Resolution context
|
|
49
|
+
* @param {Object.<string,string>} context.env - Resolved workspace .env values
|
|
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)
|
|
52
|
+
* @param {string} context.templateHash - SHA-256 of the deployed template
|
|
53
|
+
* @returns {{ document: Object, inputHash: string }}
|
|
54
|
+
*/
|
|
55
|
+
function buildArmParameters(doc, context) {
|
|
56
|
+
const parameters = {};
|
|
57
|
+
const nonSecret = {};
|
|
58
|
+
for (const name of Object.keys(PARAMETER_CATALOG)) {
|
|
59
|
+
const value = projectParameter(name, doc, context);
|
|
60
|
+
if (value === undefined) continue;
|
|
61
|
+
parameters[name] = { value };
|
|
62
|
+
const catalog = PARAMETER_CATALOG[name];
|
|
63
|
+
if (catalog.kind !== 'secretEnv' && !ARM_CREDENTIAL_PARAMS.has(name)) nonSecret[name] = value;
|
|
64
|
+
}
|
|
65
|
+
nonSecret.acrAuth = doc.acrAuth?.effective || doc.acrAuth?.value || 'auto';
|
|
66
|
+
return {
|
|
67
|
+
document: {
|
|
68
|
+
$schema: 'https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#',
|
|
69
|
+
contentVersion: '1.0.0.0',
|
|
70
|
+
parameters
|
|
71
|
+
},
|
|
72
|
+
inputHash: sha256(JSON.stringify({ templateHash: context.templateHash, parameters: nonSecret }))
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Applies or revokes the generated caller `/32` rule. User-entered rules are preserved.
|
|
78
|
+
* @param {Object} doc - Infra manifest (mutated)
|
|
79
|
+
* @param {{ enabled: boolean, cidr?: string, resolvedAt?: string, resolver?: string }} access
|
|
80
|
+
*/
|
|
81
|
+
function applyAllowMyIp(doc, access) {
|
|
82
|
+
const previous = doc.allowMyIp.resolvedCidr;
|
|
83
|
+
const current = Array.isArray(doc.parameters.allowedIPs.value) ? doc.parameters.allowedIPs.value : [];
|
|
84
|
+
const userRules = current.filter(rule => rule !== previous);
|
|
85
|
+
doc.allowMyIp.value = access.enabled;
|
|
86
|
+
if (access.enabled) {
|
|
87
|
+
doc.parameters.allowedIPs.value = [...new Set([...userRules, access.cidr])];
|
|
88
|
+
doc.parameters.enablePostgresPublicAccess.value = true;
|
|
89
|
+
Object.assign(doc.allowMyIp, { resolvedCidr: access.cidr, resolvedAt: access.resolvedAt, resolver: access.resolver });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
doc.parameters.allowedIPs.value = userRules;
|
|
93
|
+
doc.parameters.enablePostgresPublicAccess.value = false;
|
|
94
|
+
Object.assign(doc.allowMyIp, { resolvedCidr: null, resolvedAt: null, resolver: null });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
module.exports = { buildArmParameters, applyAllowMyIp };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview Resolves the caller's single public IPv4 for default-off, invocation-specific
|
|
5
|
+
* infrastructure test access. Sends no credentials or cookies, follows no redirects and never
|
|
6
|
+
* retries against another host.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const net = require('net');
|
|
10
|
+
const { makeApiCall } = require('../../utils/api');
|
|
11
|
+
|
|
12
|
+
const PUBLIC_IP_RESOLVER = 'https://api.ipify.org?format=json';
|
|
13
|
+
const TIMEOUT_MS = 5000;
|
|
14
|
+
|
|
15
|
+
const NON_PUBLIC_IPV4 = [
|
|
16
|
+
['0.0.0.0', 8], ['10.0.0.0', 8], ['100.64.0.0', 10], ['127.0.0.0', 8], ['169.254.0.0', 16],
|
|
17
|
+
['172.16.0.0', 12], ['192.0.0.0', 24], ['192.0.2.0', 24], ['192.88.99.0', 24], ['192.168.0.0', 16],
|
|
18
|
+
['198.18.0.0', 15], ['198.51.100.0', 24], ['203.0.113.0', 24], ['224.0.0.0', 4], ['240.0.0.0', 4]
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
function ipv4ToInt(ip) {
|
|
22
|
+
return ip.split('.').reduce((acc, octet) => ((acc << 8) + Number(octet)) >>> 0, 0);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* True for one strictly formatted, globally routable IPv4 address.
|
|
27
|
+
* @param {string} ip - Candidate address
|
|
28
|
+
* @returns {boolean}
|
|
29
|
+
*/
|
|
30
|
+
function isPublicIpv4(ip) {
|
|
31
|
+
if (typeof ip !== 'string' || net.isIPv4(ip) !== true || !/^(\d{1,3})(\.\d{1,3}){3}$/.test(ip)) return false;
|
|
32
|
+
if (ip.split('.').some(octet => octet.length > 1 && octet.startsWith('0'))) return false;
|
|
33
|
+
const value = ipv4ToInt(ip);
|
|
34
|
+
return !NON_PUBLIC_IPV4.some(([base, bits]) => {
|
|
35
|
+
const mask = bits === 0 ? 0 : (~0 << (32 - bits)) >>> 0;
|
|
36
|
+
return ((value & mask) >>> 0) === ((ipv4ToInt(base) & mask) >>> 0);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Resolves the caller's public IPv4 as a `/32` rule.
|
|
42
|
+
* @param {Function} [call] - HTTP call (defaults to the shared API utility)
|
|
43
|
+
* @param {Function} [now] - Clock
|
|
44
|
+
* @returns {Promise<{ cidr: string, resolvedAt: string, resolver: string }>}
|
|
45
|
+
*/
|
|
46
|
+
async function resolvePublicIpv4(call = makeApiCall, now = () => new Date()) {
|
|
47
|
+
const response = await call(PUBLIC_IP_RESOLVER, {
|
|
48
|
+
method: 'GET',
|
|
49
|
+
headers: { Accept: 'application/json' },
|
|
50
|
+
redirect: 'error',
|
|
51
|
+
credentials: 'omit',
|
|
52
|
+
timeoutMs: TIMEOUT_MS
|
|
53
|
+
});
|
|
54
|
+
if (!response || !response.success) {
|
|
55
|
+
throw new Error('Could not resolve the caller public IPv4 for --allow-my-ip (resolver unavailable, redirected or timed out)');
|
|
56
|
+
}
|
|
57
|
+
const data = response.data;
|
|
58
|
+
const ip = data && typeof data === 'object' && !Array.isArray(data) ? data.ip : undefined;
|
|
59
|
+
if (!isPublicIpv4(ip)) {
|
|
60
|
+
throw new Error('Public IP resolver did not return exactly one public IPv4 address; --allow-my-ip was not applied');
|
|
61
|
+
}
|
|
62
|
+
return { cidr: `${ip}/32`, resolvedAt: now().toISOString(), resolver: PUBLIC_IP_RESOLVER };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = { PUBLIC_IP_RESOLVER, isPublicIpv4, resolvePublicIpv4 };
|
|
@@ -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 };
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview Workspace-mode `env deploy <resource-group> --stage <stage>` input validation:
|
|
5
|
+
* resource-group key, stage, vendor matrix and stage-scoped option checks. Runs before any
|
|
6
|
+
* filesystem, network or cloud mutation.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const { normalizeLicenseEdition, normalizeAcrAuth, normalizeAzureLocation } = require('./infra-catalog');
|
|
11
|
+
|
|
12
|
+
const WORKSPACE_STAGES = Object.freeze(['register', 'infra', 'dev', 'tst', 'pro']);
|
|
13
|
+
const ENVIRONMENT_STAGES = Object.freeze(['dev', 'tst', 'pro']);
|
|
14
|
+
const VENDORS = Object.freeze(['AZURE', 'DOCKER']);
|
|
15
|
+
const DEFAULT_VENDOR = 'AZURE';
|
|
16
|
+
const RESOURCE_GROUP_KEY = /^[a-z][a-z0-9-]{2,39}$/;
|
|
17
|
+
const ENV_VAR_NAME = /^[A-Z_][A-Z0-9_]*$/;
|
|
18
|
+
const LEGACY_MARKETPLACE_FLAGS = Object.freeze([
|
|
19
|
+
'marketplaceJson', 'parameters', 'subscription', 'resourceGroup', 'installId'
|
|
20
|
+
]);
|
|
21
|
+
const DOCKER_SETUP_GUIDANCE = 'DOCKER installations deploy through "aifabrix setup"; only --stage register is supported for DOCKER.';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* True when options select the deprecated explicit Marketplace/journal mode.
|
|
25
|
+
* @param {Object} options - Commander options
|
|
26
|
+
* @returns {boolean}
|
|
27
|
+
*/
|
|
28
|
+
function isLegacyMarketplaceMode(options) {
|
|
29
|
+
if (LEGACY_MARKETPLACE_FLAGS.some(flag => options[flag] !== undefined && options[flag] !== null)) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
return !options.stage && Boolean(options.controllerUrl);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* True when options select the resource-group installation workspace mode.
|
|
37
|
+
* @param {Object} options - Commander options
|
|
38
|
+
* @returns {boolean}
|
|
39
|
+
*/
|
|
40
|
+
function isWorkspaceMode(options) {
|
|
41
|
+
return Boolean(options.stage) && !isLegacyMarketplaceMode(options);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Validates the positional resource-group key before it is used as a path segment.
|
|
46
|
+
* @param {string} value - Positional value
|
|
47
|
+
* @returns {string} The validated key
|
|
48
|
+
* @throws {Error} On absolute paths, separators, traversal, normalization changes or bad format
|
|
49
|
+
*/
|
|
50
|
+
function validateResourceGroupKey(value) {
|
|
51
|
+
const raw = typeof value === 'string' ? value : '';
|
|
52
|
+
if (!raw || path.isAbsolute(raw) || /[\\/]/.test(raw) || raw.includes('..') ||
|
|
53
|
+
path.normalize(raw) !== raw || raw.normalize('NFC') !== raw || raw.trim() !== raw) {
|
|
54
|
+
throw new Error('Resource group must be a plain workspace key without paths, separators or traversal');
|
|
55
|
+
}
|
|
56
|
+
if (!RESOURCE_GROUP_KEY.test(raw)) {
|
|
57
|
+
throw new Error('Resource group must be a lowercase Azure-safe name (3-40 characters: a-z, 0-9, -; starts with a letter)');
|
|
58
|
+
}
|
|
59
|
+
return raw;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Normalizes a vendor value case-insensitively.
|
|
64
|
+
* @param {string|undefined} value - Raw vendor value
|
|
65
|
+
* @returns {string|undefined} Canonical vendor or undefined when absent
|
|
66
|
+
*/
|
|
67
|
+
function normalizeVendor(value) {
|
|
68
|
+
if (value === undefined || value === null || value === '') return undefined;
|
|
69
|
+
const vendor = String(value).trim().toUpperCase();
|
|
70
|
+
if (!VENDORS.includes(vendor)) {
|
|
71
|
+
throw new Error(`Unsupported vendor "${value}". Use one of: ${VENDORS.join(', ')}`);
|
|
72
|
+
}
|
|
73
|
+
return vendor;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function validateStage(stage) {
|
|
77
|
+
const value = String(stage || '').trim().toLowerCase();
|
|
78
|
+
if (!WORKSPACE_STAGES.includes(value)) {
|
|
79
|
+
throw new Error(`Unsupported stage "${stage}". Use one of: ${WORKSPACE_STAGES.join(', ')}`);
|
|
80
|
+
}
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function resolveVendor(stage, requested, persisted) {
|
|
85
|
+
if (requested && persisted && requested !== persisted) {
|
|
86
|
+
throw new Error(`Workspace vendor is ${persisted}; --vendor ${requested} conflicts with the existing installation`);
|
|
87
|
+
}
|
|
88
|
+
if (stage === 'register') {
|
|
89
|
+
if (requested !== 'DOCKER') {
|
|
90
|
+
throw new Error('--stage register requires explicit --vendor DOCKER; AZURE installations register with Mori during tenant activation in Miso');
|
|
91
|
+
}
|
|
92
|
+
return requested;
|
|
93
|
+
}
|
|
94
|
+
const vendor = requested || persisted || DEFAULT_VENDOR;
|
|
95
|
+
if (vendor === 'DOCKER') {
|
|
96
|
+
throw new Error(`--stage ${stage} is not supported for DOCKER. ${DOCKER_SETUP_GUIDANCE}`);
|
|
97
|
+
}
|
|
98
|
+
return vendor;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function assertInfraOnlyOptions(stage, options) {
|
|
102
|
+
if (options.allowMyIp && stage !== 'infra') {
|
|
103
|
+
throw new Error('--allow-my-ip is supported only with --stage infra for AZURE installations');
|
|
104
|
+
}
|
|
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);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function assertStageScopedOptions(stage, options) {
|
|
126
|
+
assertInfraOnlyOptions(stage, options);
|
|
127
|
+
const envOnly = [['controllerUrl', '--controller-url'], ['misoApiKeyEnv', '--miso-api-key-env']];
|
|
128
|
+
for (const [key, flag] of envOnly) {
|
|
129
|
+
if (options[key] !== undefined && !ENVIRONMENT_STAGES.includes(stage)) {
|
|
130
|
+
throw new Error(`${flag} is supported only with --stage dev, tst or pro`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (options.config !== undefined) {
|
|
134
|
+
throw new Error('--config is not supported with --stage; environment settings live in the workspace environment files');
|
|
135
|
+
}
|
|
136
|
+
if (options.preset !== undefined && stage === 'register') {
|
|
137
|
+
throw new Error('--preset is not supported with --stage register');
|
|
138
|
+
}
|
|
139
|
+
if (options.misoApiKeyEnv !== undefined && !ENV_VAR_NAME.test(String(options.misoApiKeyEnv))) {
|
|
140
|
+
throw new Error('--miso-api-key-env must be an environment-variable name (for example MISO_API_KEY), never a key value');
|
|
141
|
+
}
|
|
142
|
+
if (options.controllerUrl !== undefined && !/^https:\/\/[^/\s]+(\/[^\s]*)?$/.test(String(options.controllerUrl))) {
|
|
143
|
+
throw new Error('--controller-url must be an https:// URL');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Resolves validated workspace-mode input.
|
|
149
|
+
* @param {string} resourceGroupKey - Positional resource-group key
|
|
150
|
+
* @param {Object} options - Commander options
|
|
151
|
+
* @param {string|undefined} persistedVendor - Vendor recorded in an existing workspace
|
|
152
|
+
* @returns {{ resourceGroup: string, stage: string, vendor: string, allowMyIp: boolean, licenseEdition: string|undefined,
|
|
153
|
+
* acrAuth: string|undefined, location: string|undefined, preset: string|undefined, controllerUrl: string|undefined, misoApiKeyEnv: string|undefined }}
|
|
154
|
+
*/
|
|
155
|
+
function resolveStageInput(resourceGroupKey, options, persistedVendor) {
|
|
156
|
+
const resourceGroup = validateResourceGroupKey(resourceGroupKey);
|
|
157
|
+
const stage = validateStage(options.stage);
|
|
158
|
+
assertStageScopedOptions(stage, options);
|
|
159
|
+
const vendor = resolveVendor(stage, normalizeVendor(options.vendor), normalizeVendor(persistedVendor));
|
|
160
|
+
return {
|
|
161
|
+
resourceGroup,
|
|
162
|
+
stage,
|
|
163
|
+
vendor,
|
|
164
|
+
allowMyIp: options.allowMyIp === true,
|
|
165
|
+
licenseEdition: options.licenseEdition,
|
|
166
|
+
acrAuth: options.acrAuth,
|
|
167
|
+
location: options.location,
|
|
168
|
+
preset: options.preset,
|
|
169
|
+
controllerUrl: options.controllerUrl,
|
|
170
|
+
misoApiKeyEnv: options.misoApiKeyEnv
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
module.exports = {
|
|
175
|
+
WORKSPACE_STAGES,
|
|
176
|
+
ENVIRONMENT_STAGES,
|
|
177
|
+
VENDORS,
|
|
178
|
+
DEFAULT_VENDOR,
|
|
179
|
+
isLegacyMarketplaceMode,
|
|
180
|
+
isWorkspaceMode,
|
|
181
|
+
validateResourceGroupKey,
|
|
182
|
+
normalizeVendor,
|
|
183
|
+
resolveStageInput
|
|
184
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview Terminal summary lines for installation workspace stages (tty-summary profile).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
formatSuccessLine,
|
|
9
|
+
formatNextActions,
|
|
10
|
+
metadata
|
|
11
|
+
} = require('../../utils/cli-test-layout-chalk');
|
|
12
|
+
|
|
13
|
+
function registerLines(result, resourceGroup) {
|
|
14
|
+
return [
|
|
15
|
+
formatSuccessLine(result.alreadyRegistered
|
|
16
|
+
? `DOCKER installation ${resourceGroup} is already registered with Mori`
|
|
17
|
+
: `DOCKER installation ${resourceGroup} registered with Mori`),
|
|
18
|
+
formatNextActions(['Install locally with: aifabrix setup'])
|
|
19
|
+
];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function infraLines(result, resourceGroup) {
|
|
23
|
+
return [
|
|
24
|
+
formatSuccessLine(`Shared infrastructure ready: ${result.controllerUrl}`),
|
|
25
|
+
formatNextActions([
|
|
26
|
+
`Tenant activation is pending. Open ${result.activationUrl}, sign in and complete activation.`,
|
|
27
|
+
`Then deploy environments: af env deploy ${resourceGroup} --stage dev (or tst, pro)`
|
|
28
|
+
])
|
|
29
|
+
];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function environmentLines(result) {
|
|
33
|
+
return [
|
|
34
|
+
formatSuccessLine(`Environment ${result.envKey} ${result.status} (deployment ${result.deploymentId}${result.resumed ? ', resumed' : ''})`),
|
|
35
|
+
formatNextActions([`Deploy applications with: aifabrix deploy <app> (environment ${result.envKey})`])
|
|
36
|
+
];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {Object} result - Stage result
|
|
41
|
+
* @param {string} resourceGroup - Resource-group key
|
|
42
|
+
* @returns {string} Multi-line summary
|
|
43
|
+
*/
|
|
44
|
+
function formatStageResult(result, resourceGroup) {
|
|
45
|
+
const header = metadata(`Workspace: ${result.workspaceDir}`);
|
|
46
|
+
let lines;
|
|
47
|
+
if (result.stage === 'register') lines = registerLines(result, resourceGroup);
|
|
48
|
+
else if (result.stage === 'infra') lines = infraLines(result, resourceGroup);
|
|
49
|
+
else lines = environmentLines(result);
|
|
50
|
+
return `${[header, ...lines].join('\n')}\n`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = { formatStageResult };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview Owner-only workspace `.env` merge/remove. Never replaces the file as a whole:
|
|
5
|
+
* preserves unrelated values and comments, enforces mode 0600, and refuses to overwrite a
|
|
6
|
+
* conflicting user-set value. Error messages name keys only, never values.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Real-sync adapter: installation workspace state must not disappear under Jest fs mocks.
|
|
10
|
+
const fs = require('../../internal/fs-real-sync');
|
|
11
|
+
const { parseEnvContentToMap, mergeEnvMapIntoContent } = require('../../core/secrets-env-content');
|
|
12
|
+
const { writeFileAtomic, assertIgnoredSecretFile } = require('./workspace-files');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {string} envPath - Workspace .env path
|
|
16
|
+
* @returns {Object.<string, string>} Parsed values (empty when absent)
|
|
17
|
+
*/
|
|
18
|
+
function readWorkspaceEnv(envPath) {
|
|
19
|
+
if (!fs.existsSync(envPath)) return {};
|
|
20
|
+
return parseEnvContentToMap(fs.readFileSync(envPath, 'utf8'));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function findConflicts(existing, entries, replaceable) {
|
|
24
|
+
return Object.keys(entries).filter((key) => {
|
|
25
|
+
const current = existing[key];
|
|
26
|
+
return current !== undefined && current !== '' && current !== entries[key] && !replaceable.has(key);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Atomically merges owned keys into the workspace `.env`.
|
|
32
|
+
* @param {string} envPath - Workspace .env path
|
|
33
|
+
* @param {Object.<string, string>} entries - Keys to set
|
|
34
|
+
* @param {Object} context - Merge context
|
|
35
|
+
* @param {string} context.root - Workspace root (ignore check)
|
|
36
|
+
* @param {string[]} [context.replaceable] - Keys Builder may replace when they differ
|
|
37
|
+
* @throws {Error} When a user-set value conflicts
|
|
38
|
+
*/
|
|
39
|
+
function mergeWorkspaceEnv(envPath, entries, context) {
|
|
40
|
+
assertIgnoredSecretFile(envPath, context.root);
|
|
41
|
+
const content = fs.existsSync(envPath) ? fs.readFileSync(envPath, 'utf8') : '';
|
|
42
|
+
const existing = parseEnvContentToMap(content);
|
|
43
|
+
const conflicts = findConflicts(existing, entries, new Set(context.replaceable || []));
|
|
44
|
+
if (conflicts.length > 0) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`Workspace .env already sets a different value for ${conflicts.join(', ')}. ` +
|
|
47
|
+
'Remove or correct those lines, then rerun.'
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
const changed = Object.fromEntries(Object.entries(entries).filter(([key, value]) => existing[key] !== value));
|
|
51
|
+
if (Object.keys(changed).length === 0 && content) {
|
|
52
|
+
fs.chmodSync(envPath, 0o600);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const merged = mergeEnvMapIntoContent(content, changed);
|
|
56
|
+
writeFileAtomic(envPath, merged.endsWith('\n') ? merged : `${merged}\n`, 0o600);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Removes one resolved key line from the workspace `.env` (no-op when absent).
|
|
61
|
+
* @param {string} envPath - Workspace .env path
|
|
62
|
+
* @param {string} key - Variable name
|
|
63
|
+
*/
|
|
64
|
+
function removeWorkspaceEnvKey(envPath, key) {
|
|
65
|
+
if (!fs.existsSync(envPath)) return;
|
|
66
|
+
const lines = fs.readFileSync(envPath, 'utf8').split(/\r?\n/);
|
|
67
|
+
const kept = lines.filter((line) => {
|
|
68
|
+
const trimmed = line.trim();
|
|
69
|
+
return trimmed.startsWith('#') || trimmed.substring(0, trimmed.indexOf('=')).trim() !== key;
|
|
70
|
+
});
|
|
71
|
+
if (kept.length !== lines.length) {
|
|
72
|
+
writeFileAtomic(envPath, kept.join('\n'), 0o600);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = { readWorkspaceEnv, mergeWorkspaceEnv, removeWorkspaceEnvKey };
|