@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
|
@@ -1,15 +1,18 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `aifabrix agent-kit` command group.
|
|
3
|
-
* @fileoverview Install, init, status, update, validate, remote create, start
|
|
4
|
-
*/
|
|
1
|
+
/** `aifabrix agent-kit` command group. */
|
|
5
2
|
|
|
6
3
|
'use strict';
|
|
7
4
|
|
|
8
5
|
const logger = require('../utils/logger');
|
|
9
6
|
const { handleCommandError } = require('../utils/cli-utils');
|
|
10
|
-
const {
|
|
7
|
+
const {
|
|
8
|
+
formatSuccessLine,
|
|
9
|
+
formatBlockingError,
|
|
10
|
+
formatWarningLine
|
|
11
|
+
} = require('../utils/cli-layout-chalk');
|
|
11
12
|
const { runInstall, runUpdate } = require('../agent-kit/run');
|
|
12
13
|
const { runStart } = require('../agent-kit/start');
|
|
14
|
+
const { runList, runKill } = require('../agent-kit/tmux-sessions');
|
|
15
|
+
const { formatSessionList, formatMissingKillName } = require('../agent-kit/session-report');
|
|
13
16
|
const path = require('path');
|
|
14
17
|
const { runInit, runRemoteCreate } = require('../agent-kit/init');
|
|
15
18
|
const { enrichInitHostOptions, enrichInitRemoteOptions, enrichRemoteOptions, enrichApproveAllOptions } = require('../agent-kit/remote-prompts');
|
|
@@ -30,6 +33,8 @@ const {
|
|
|
30
33
|
REMOTE_GROUP_HELP,
|
|
31
34
|
REMOTE_HELP,
|
|
32
35
|
START_HELP,
|
|
36
|
+
LIST_HELP,
|
|
37
|
+
KILL_HELP,
|
|
33
38
|
HOST_OPTION_HELP,
|
|
34
39
|
APPROVE_ALL_OPTION_HELP
|
|
35
40
|
} = require('./agent-kit-help');
|
|
@@ -110,6 +115,8 @@ function emitStartReport(json, asJson) {
|
|
|
110
115
|
logger.log(`Session ${json.session}`);
|
|
111
116
|
if (json.remoteControlName) {
|
|
112
117
|
logger.log(`Remote ${json.remoteControlName}`);
|
|
118
|
+
} else if (json.remoteControl) {
|
|
119
|
+
logger.log('Remote Codex Remote Control');
|
|
113
120
|
}
|
|
114
121
|
logger.log(`Path ${json.path}`);
|
|
115
122
|
logger.log(`Launch ${json.launch.join(' ')}`);
|
|
@@ -124,6 +131,32 @@ function emitStartReport(json, asJson) {
|
|
|
124
131
|
logger.log(formatSuccessLine(json.dryRun ? 'Dry-run complete' : 'Ready'));
|
|
125
132
|
}
|
|
126
133
|
|
|
134
|
+
/**
|
|
135
|
+
* @param {object} json
|
|
136
|
+
* @param {boolean} asJson
|
|
137
|
+
* @returns {void}
|
|
138
|
+
*/
|
|
139
|
+
function emitListReport(json, asJson) {
|
|
140
|
+
if (asJson) {
|
|
141
|
+
logger.log(JSON.stringify(json, null, 2));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
logger.log(formatSessionList(json));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* @param {object} json
|
|
149
|
+
* @param {boolean} asJson
|
|
150
|
+
* @returns {void}
|
|
151
|
+
*/
|
|
152
|
+
function emitKillReport(json, asJson) {
|
|
153
|
+
if (asJson) {
|
|
154
|
+
logger.log(JSON.stringify(json, null, 2));
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
logger.log(formatSuccessLine(`Stopped tmux session ${json.session}`));
|
|
158
|
+
}
|
|
159
|
+
|
|
127
160
|
/**
|
|
128
161
|
* @param {Function} runner
|
|
129
162
|
* @param {object} options
|
|
@@ -159,6 +192,8 @@ function setupAgentKitCommands(program) {
|
|
|
159
192
|
addValidateCommand(group);
|
|
160
193
|
addRemoteCommand(group);
|
|
161
194
|
addStartCommand(group);
|
|
195
|
+
addListCommand(group);
|
|
196
|
+
addKillCommand(group);
|
|
162
197
|
}
|
|
163
198
|
|
|
164
199
|
/**
|
|
@@ -387,8 +422,8 @@ async function runValidateLeaf(options) {
|
|
|
387
422
|
function addStartCommand(group) {
|
|
388
423
|
group
|
|
389
424
|
.command('start <app>')
|
|
390
|
-
.description('Start Claude Code or Codex
|
|
391
|
-
.option('--approve-all', 'Write auto-approve config for
|
|
425
|
+
.description('Start Claude Code or Codex with Remote Control in named tmux')
|
|
426
|
+
.option('--approve-all', 'Write auto-approve config (default for start)')
|
|
392
427
|
.option('--skip-approve-all', 'Do not write auto-approve configs')
|
|
393
428
|
.option('--yes', 'Same as --approve-all')
|
|
394
429
|
.option('--detach', 'Create the tmux session in the background (no attach)')
|
|
@@ -398,8 +433,10 @@ function addStartCommand(group) {
|
|
|
398
433
|
)
|
|
399
434
|
.option(
|
|
400
435
|
'--remote-control',
|
|
401
|
-
'
|
|
436
|
+
'Enable native Remote Control for Claude or Codex (default)',
|
|
437
|
+
true
|
|
402
438
|
)
|
|
439
|
+
.option('--no-remote-control', 'Start a local tmux-only session')
|
|
403
440
|
.option('--dry-run', 'Print the launch command without starting it')
|
|
404
441
|
.option('--json', 'Machine-readable output')
|
|
405
442
|
.addHelpText('after', START_HELP)
|
|
@@ -414,4 +451,49 @@ function addStartCommand(group) {
|
|
|
414
451
|
});
|
|
415
452
|
}
|
|
416
453
|
|
|
454
|
+
/**
|
|
455
|
+
* @param {import('commander').Command} group
|
|
456
|
+
*/
|
|
457
|
+
function addListCommand(group) {
|
|
458
|
+
group
|
|
459
|
+
.command('list')
|
|
460
|
+
.description('List active tmux sessions for the current user')
|
|
461
|
+
.option('--json', 'Machine-readable output')
|
|
462
|
+
.addHelpText('after', LIST_HELP)
|
|
463
|
+
.action(async(options) => {
|
|
464
|
+
try {
|
|
465
|
+
emitListReport(runList(options), Boolean(options.json));
|
|
466
|
+
} catch (error) {
|
|
467
|
+
handleCommandError(error, 'agent-kit list');
|
|
468
|
+
process.exitCode = 1;
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* @param {import('commander').Command} group
|
|
475
|
+
*/
|
|
476
|
+
function addKillCommand(group) {
|
|
477
|
+
group
|
|
478
|
+
.command('kill [name]')
|
|
479
|
+
.description('Stop one tmux session by exact name')
|
|
480
|
+
.option('--json', 'Machine-readable output')
|
|
481
|
+
.addHelpText('after', KILL_HELP)
|
|
482
|
+
.action(async(name, options) => {
|
|
483
|
+
if (!name) {
|
|
484
|
+
const report = formatMissingKillName(Boolean(options.json));
|
|
485
|
+
logger.error(report.stderr);
|
|
486
|
+
if (report.stdout) logger.log(report.stdout);
|
|
487
|
+
process.exitCode = 1;
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
try {
|
|
491
|
+
emitKillReport(runKill({ ...options, name }), Boolean(options.json));
|
|
492
|
+
} catch (error) {
|
|
493
|
+
handleCommandError(error, 'agent-kit kill');
|
|
494
|
+
process.exitCode = 1;
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
|
|
417
499
|
module.exports = { setupAgentKitCommands };
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `aifabrix dev set-registry` / `dev print-registry` logic.
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Installation registry preference and host-scoped credential management
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 1.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const config = require('../core/config');
|
|
12
|
+
const logger = require('../utils/logger');
|
|
13
|
+
const { formatSuccessLine, formatWarningLine } = require('../utils/cli-test-layout-chalk');
|
|
14
|
+
const { normalizeRegistryHost } = require('../utils/config-registry-preference');
|
|
15
|
+
const registryCredentials = require('../utils/registry-credentials');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Reads the whole of stdin (for --password-stdin).
|
|
19
|
+
* @param {NodeJS.ReadableStream} [stream] - Input stream
|
|
20
|
+
* @returns {Promise<string>}
|
|
21
|
+
*/
|
|
22
|
+
function readAllStdin(stream = process.stdin) {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
let data = '';
|
|
25
|
+
stream.setEncoding('utf8');
|
|
26
|
+
stream.on('data', (chunk) => {
|
|
27
|
+
data += chunk;
|
|
28
|
+
});
|
|
29
|
+
stream.on('end', () => resolve(data));
|
|
30
|
+
stream.on('error', reject);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function hasOption(value) {
|
|
35
|
+
return value !== undefined && value !== null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function assertModeOptions(registry, opts) {
|
|
39
|
+
const credentialOption = hasOption(opts.username) || hasOption(opts.password) || opts.passwordStdin === true;
|
|
40
|
+
if (opts.clear && opts.clearCredentials) {
|
|
41
|
+
throw new Error('Use either --clear or --clear-credentials, not both');
|
|
42
|
+
}
|
|
43
|
+
if ((opts.clear || opts.clearCredentials) && credentialOption) {
|
|
44
|
+
throw new Error('--clear and --clear-credentials cannot be combined with --username or a password option');
|
|
45
|
+
}
|
|
46
|
+
if (opts.clear && hasOption(registry)) {
|
|
47
|
+
throw new Error('--clear removes the saved registry and takes no registry argument');
|
|
48
|
+
}
|
|
49
|
+
if (!opts.clear && !(typeof registry === 'string' && registry.trim())) {
|
|
50
|
+
throw new Error('Registry is required (for example: aifabrix dev set-registry myregistry.azurecr.io)');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function assertCredentialOptions(opts) {
|
|
55
|
+
if (hasOption(opts.password) && opts.passwordStdin === true) {
|
|
56
|
+
throw new Error('Use either --password or --password-stdin, not both');
|
|
57
|
+
}
|
|
58
|
+
const passwordGiven = hasOption(opts.password) || opts.passwordStdin === true;
|
|
59
|
+
if (hasOption(opts.username) !== passwordGiven) {
|
|
60
|
+
throw new Error('--username and a password (--password or --password-stdin) must be supplied together');
|
|
61
|
+
}
|
|
62
|
+
if (hasOption(opts.username) && !String(opts.username).trim()) {
|
|
63
|
+
throw new Error('--username must not be empty');
|
|
64
|
+
}
|
|
65
|
+
if (hasOption(opts.password) && String(opts.password) === '') {
|
|
66
|
+
throw new Error('--password must not be empty');
|
|
67
|
+
}
|
|
68
|
+
return passwordGiven;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function resolvePassword(opts, readStdin) {
|
|
72
|
+
if (opts.passwordStdin !== true) return String(opts.password);
|
|
73
|
+
const password = (await readStdin()).replace(/\r?\n$/, '');
|
|
74
|
+
if (!password) {
|
|
75
|
+
throw new Error('--password-stdin received an empty password');
|
|
76
|
+
}
|
|
77
|
+
return password;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function saveWithCredentials(host, username, password) {
|
|
81
|
+
const saved = await registryCredentials.saveRegistryCredentials(host, username, password);
|
|
82
|
+
try {
|
|
83
|
+
await config.setRegistry(host);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
saved.rollback();
|
|
86
|
+
throw new Error(`Could not save the registry preference; credentials for ${host} were restored (${error.message})`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function runClearMode(registry, opts) {
|
|
91
|
+
if (opts.clear) {
|
|
92
|
+
await config.clearRegistry();
|
|
93
|
+
logger.log(formatSuccessLine('Registry cleared (application image.registry applies again)'));
|
|
94
|
+
return { action: 'clear', registry: null, credentials: false };
|
|
95
|
+
}
|
|
96
|
+
const host = normalizeRegistryHost(registry);
|
|
97
|
+
const removed = registryCredentials.removeRegistryCredentials(host);
|
|
98
|
+
logger.log(formatSuccessLine(removed ? `Credentials removed for ${host}` : `No stored credentials for ${host}`));
|
|
99
|
+
return { action: 'clear-credentials', registry: host, credentials: false };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Runs `dev set-registry`.
|
|
104
|
+
* @param {string|undefined} registry - Positional registry
|
|
105
|
+
* @param {Object} opts - Commander options
|
|
106
|
+
* @param {Object} [deps] - `readStdin` override (tests)
|
|
107
|
+
* @returns {Promise<{ action: string, registry: string|null, credentials: boolean }>}
|
|
108
|
+
*/
|
|
109
|
+
async function runSetRegistry(registry, opts = {}, deps = {}) {
|
|
110
|
+
assertModeOptions(registry, opts);
|
|
111
|
+
if (opts.clear || opts.clearCredentials) return runClearMode(registry, opts);
|
|
112
|
+
const host = normalizeRegistryHost(registry);
|
|
113
|
+
const withCredentials = assertCredentialOptions(opts);
|
|
114
|
+
if (!withCredentials) {
|
|
115
|
+
await config.setRegistry(host);
|
|
116
|
+
logger.log(formatSuccessLine(`Registry set to ${host}`));
|
|
117
|
+
return { action: 'set', registry: host, credentials: false };
|
|
118
|
+
}
|
|
119
|
+
const password = await resolvePassword(opts, deps.readStdin || readAllStdin);
|
|
120
|
+
await saveWithCredentials(host, String(opts.username).trim(), password);
|
|
121
|
+
logger.log(formatSuccessLine(`Registry set to ${host}`));
|
|
122
|
+
logger.log(formatSuccessLine(`Credentials saved for ${host} in your local secrets file (encrypted)`));
|
|
123
|
+
if (opts.passwordStdin !== true) {
|
|
124
|
+
logger.log(formatWarningLine('--password can appear in shell history and process listings; prefer --password-stdin'));
|
|
125
|
+
}
|
|
126
|
+
return { action: 'set', registry: host, credentials: true };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* `dev print-registry`: the saved value plus a newline, or an empty line. Stdout only.
|
|
131
|
+
* @returns {Promise<void>}
|
|
132
|
+
*/
|
|
133
|
+
async function runPrintRegistry() {
|
|
134
|
+
const registry = await config.getRegistry();
|
|
135
|
+
process.stdout.write(`${registry || ''}\n`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Safe credential status label for `dev show` (never decrypts).
|
|
140
|
+
* @param {string|null} registry - Saved registry
|
|
141
|
+
* @returns {string|null}
|
|
142
|
+
*/
|
|
143
|
+
function registryCredentialLabel(registry) {
|
|
144
|
+
if (!registry) return null;
|
|
145
|
+
try {
|
|
146
|
+
const status = registryCredentials.getRegistryCredentialStatus(registry);
|
|
147
|
+
if (status === 'complete') return 'configured';
|
|
148
|
+
if (status === 'absent') return 'not configured';
|
|
149
|
+
return `incomplete (run: aifabrix dev set-registry ${registry} --clear-credentials)`;
|
|
150
|
+
} catch {
|
|
151
|
+
return 'unknown';
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
module.exports = { runSetRegistry, runPrintRegistry, registryCredentialLabel, readAllStdin };
|
|
@@ -336,6 +336,8 @@ function logConfigurationSection(p) {
|
|
|
336
336
|
logRow('Environment', p.environment);
|
|
337
337
|
logRow('Controller', p.controller);
|
|
338
338
|
logRow('Format', p.formatVal);
|
|
339
|
+
logRow('Registry', p.registry);
|
|
340
|
+
if (p.registry) logRow('Registry login', p.registryCredentials);
|
|
339
341
|
logRow('Platform mode', p.setupPlatformMode);
|
|
340
342
|
logRow('Traefik', p.traefikEnabled ? 'on' : 'off');
|
|
341
343
|
logRow('Scoped resources', p.scopedResourcesLabel);
|
|
@@ -389,14 +391,16 @@ async function loadDevShowData(devIdStr) {
|
|
|
389
391
|
const ports = devConfig.getDevPorts(Number.isNaN(devIdNum) ? 0 : devIdNum);
|
|
390
392
|
const [
|
|
391
393
|
environment, tlsEnabled, controller, formatRaw, useScoped, secretsRaw, setupPlatformMode, traefikEnabled,
|
|
392
|
-
remoteServer, dockerEndpoint, dockerTlsSkipVerify, mutagenFolder, syncSshUser, syncSshHost
|
|
394
|
+
remoteServer, dockerEndpoint, dockerTlsSkipVerify, mutagenFolder, syncSshUser, syncSshHost, registry
|
|
393
395
|
] = await Promise.all([
|
|
394
396
|
config.getCurrentEnvironment(), config.getTlsEnabled(), config.getControllerUrl(),
|
|
395
397
|
config.getFormat(), config.getUseEnvironmentScopedResources(), config.getAifabrixSecretsPath(),
|
|
396
398
|
config.getSetupPlatformMode(), config.getTraefikEnabled(),
|
|
397
399
|
config.getRemoteServer(), config.getDockerEndpoint(), config.getDockerTlsSkipVerify(),
|
|
398
|
-
config.getUserMutagenFolder(), config.getSyncSshUser(), config.getSyncSshHost()
|
|
400
|
+
config.getUserMutagenFolder(), config.getSyncSshUser(), config.getSyncSshHost(),
|
|
401
|
+
config.getRegistry ? config.getRegistry() : null
|
|
399
402
|
]);
|
|
403
|
+
const { registryCredentialLabel } = require('./dev-registry');
|
|
400
404
|
const urlsLocalPath = getUrlsLocalYamlPath();
|
|
401
405
|
const scopedResourcesLabel = useScoped ? 'on' : 'off (default)';
|
|
402
406
|
const workResolved = paths.getAifabrixWork();
|
|
@@ -412,6 +416,8 @@ async function loadDevShowData(devIdStr) {
|
|
|
412
416
|
tlsEnabled,
|
|
413
417
|
controller,
|
|
414
418
|
formatVal: formatRaw ?? null,
|
|
419
|
+
registry: registry || null,
|
|
420
|
+
registryCredentials: registryCredentialLabel(registry || null),
|
|
415
421
|
scopedResourcesLabel,
|
|
416
422
|
setupPlatformMode,
|
|
417
423
|
traefikEnabled,
|
|
@@ -451,6 +457,8 @@ function renderDevShow(data) {
|
|
|
451
457
|
tlsEnabled: data.tlsEnabled,
|
|
452
458
|
controller: data.controller,
|
|
453
459
|
formatVal: data.formatVal,
|
|
460
|
+
registry: data.registry,
|
|
461
|
+
registryCredentials: data.registryCredentials,
|
|
454
462
|
setupPlatformMode: data.setupPlatformMode,
|
|
455
463
|
traefikEnabled: data.traefikEnabled,
|
|
456
464
|
scopedResourcesLabel: data.scopedResourcesLabel,
|
|
@@ -21,9 +21,30 @@ const infraHelpers = require('../infrastructure/helpers');
|
|
|
21
21
|
const upCommon = require('./up-common');
|
|
22
22
|
const { sectionTitle, formatDatasourceListRow } = require('../utils/cli-test-layout-chalk');
|
|
23
23
|
const { startSpinner, stopSpinnerSuccess } = require('./setup-spinners');
|
|
24
|
+
const { ensureLoginForImageRefs } = require('../utils/registry-credentials');
|
|
24
25
|
|
|
25
26
|
const PLATFORM_APPS = ['keycloak', 'miso-controller', 'dataplane'];
|
|
26
27
|
|
|
28
|
+
/**
|
|
29
|
+
* `image:` values of a compose file (plain scan; variables are left unresolved and skipped).
|
|
30
|
+
* @param {string} composePath - Compose file path
|
|
31
|
+
* @returns {string[]}
|
|
32
|
+
*/
|
|
33
|
+
function readComposeImageRefs(composePath) {
|
|
34
|
+
let text = '';
|
|
35
|
+
try {
|
|
36
|
+
text = String(fs.readFileSync(composePath, 'utf8') || '');
|
|
37
|
+
} catch {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
const refs = [];
|
|
41
|
+
for (const line of text.split(/\r?\n/)) {
|
|
42
|
+
const match = line.match(/^\s*image:\s*["']?([^"'\s#]+)["']?/);
|
|
43
|
+
if (match && !match[1].includes('$')) refs.push(match[1]);
|
|
44
|
+
}
|
|
45
|
+
return refs;
|
|
46
|
+
}
|
|
47
|
+
|
|
27
48
|
/**
|
|
28
49
|
* Pull infra-compose images (postgres, redis, optional pgadmin/redis-commander/traefik).
|
|
29
50
|
*
|
|
@@ -42,6 +63,8 @@ async function pullInfraImages(opts = {}) {
|
|
|
42
63
|
}
|
|
43
64
|
return;
|
|
44
65
|
}
|
|
66
|
+
// Authenticate once per private registry host before compose pulls (stored pairs only).
|
|
67
|
+
await ensureLoginForImageRefs(readComposeImageRefs(composePath));
|
|
45
68
|
const project = infraHelpers.getInfraProjectName(devId);
|
|
46
69
|
const composeCmd = await dockerUtils.getComposeCommand();
|
|
47
70
|
if (!quiet) {
|
|
@@ -91,6 +114,8 @@ async function pullPlatformAppImage(appName, opts = {}) {
|
|
|
91
114
|
}
|
|
92
115
|
return false;
|
|
93
116
|
}
|
|
117
|
+
// A partial stored credential pair fails closed here instead of degrading to a warning.
|
|
118
|
+
await ensureLoginForImageRefs([imageRef]);
|
|
94
119
|
try {
|
|
95
120
|
if (!quiet) {
|
|
96
121
|
const spin = startSpinner(`Pulling ${appName} image...`);
|
|
@@ -142,5 +167,6 @@ module.exports = {
|
|
|
142
167
|
PLATFORM_APPS,
|
|
143
168
|
pullInfraImages,
|
|
144
169
|
pullPlatformImages,
|
|
170
|
+
readComposeImageRefs,
|
|
145
171
|
runSetupImageRefresh
|
|
146
172
|
};
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview AZURE `--stage infra`: validates the workspace, resolves ACR and secrets, deploys
|
|
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.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// Real-sync adapter: installation workspace state must not disappear under Jest fs mocks.
|
|
11
|
+
const fs = require('../../internal/fs-real-sync');
|
|
12
|
+
const os = require('os');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const marketplace = require('../marketplace-environment');
|
|
15
|
+
const { normalizePreset } = require('../environment-config');
|
|
16
|
+
const { normalizeLicenseEdition, normalizeAzureLocation, ONBOARDING_MODES } = require('./infra-catalog');
|
|
17
|
+
const { materializeWorkspace, saveInfra } = require('./workspace');
|
|
18
|
+
const { missingRequiredFields } = require('./infra-manifest');
|
|
19
|
+
const { buildArmParameters, applyAllowMyIp } = require('./infra-projection');
|
|
20
|
+
const { materializeWorkspaceSecrets } = require('./workspace-secrets');
|
|
21
|
+
const { mergeWorkspaceEnv } = require('./workspace-env');
|
|
22
|
+
const { resolvePublicIpv4 } = require('./public-ip');
|
|
23
|
+
const preflight = require('./azure-preflight');
|
|
24
|
+
const readiness = require('./azure-readiness');
|
|
25
|
+
const registryAuth = require('./registry-auth-mode');
|
|
26
|
+
|
|
27
|
+
const INFRA_SECRETS = Object.freeze(['ADMIN_PASSWORD', 'ENCRYPTION_KEY', 'MORI_CALLBACK_API_KEY']);
|
|
28
|
+
|
|
29
|
+
function missingInputError(file, fields) {
|
|
30
|
+
const error = new Error(
|
|
31
|
+
`Installation workspace needs input before infra can run. Set these fields in ${file}, then rerun:\n - ${fields.join('\n - ')}`
|
|
32
|
+
);
|
|
33
|
+
error.code = 'WORKSPACE_INPUT_REQUIRED';
|
|
34
|
+
return error;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function applyLicenseOptions(doc, input) {
|
|
38
|
+
const license = doc.parameters.licenseEdition;
|
|
39
|
+
if (input.licenseEdition !== undefined) {
|
|
40
|
+
doc.parameters.licenseEdition = { ...license, value: normalizeLicenseEdition(input.licenseEdition), source: 'cli' };
|
|
41
|
+
} else {
|
|
42
|
+
doc.parameters.licenseEdition = { ...license, value: normalizeLicenseEdition(license.value) };
|
|
43
|
+
}
|
|
44
|
+
if (!ONBOARDING_MODES.includes(doc.parameters.onboardingMode.value)) {
|
|
45
|
+
throw new Error(`parameters.onboardingMode.value must be one of ${ONBOARDING_MODES.join(', ')}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function applyAccessOptions(doc, input, deps) {
|
|
50
|
+
if (input.preset !== undefined) {
|
|
51
|
+
doc.preset = { ...doc.preset, value: normalizePreset(input.preset), source: 'cli' };
|
|
52
|
+
}
|
|
53
|
+
applyLicenseOptions(doc, input);
|
|
54
|
+
const enabled = input.allowMyIp || doc.allowMyIp.value === true;
|
|
55
|
+
if (!enabled) {
|
|
56
|
+
applyAllowMyIp(doc, { enabled: false });
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const access = await (deps.resolvePublicIpv4 || resolvePublicIpv4)();
|
|
60
|
+
applyAllowMyIp(doc, { enabled: true, ...access });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function withTemporaryParameters(document, action) {
|
|
64
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'aifabrix-arm-'));
|
|
65
|
+
const file = path.join(dir, 'parameters.json');
|
|
66
|
+
try {
|
|
67
|
+
fs.chmodSync(dir, 0o700);
|
|
68
|
+
fs.writeFileSync(file, JSON.stringify(document), { mode: 0o600 });
|
|
69
|
+
return action(file);
|
|
70
|
+
} finally {
|
|
71
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function recordDeployment(doc, record, now) {
|
|
76
|
+
doc.deployment = {
|
|
77
|
+
...doc.deployment,
|
|
78
|
+
status: { ...doc.deployment.status, value: 'succeeded' },
|
|
79
|
+
name: record.deploymentName,
|
|
80
|
+
id: record.deploymentId,
|
|
81
|
+
correlationId: record.correlationId || null,
|
|
82
|
+
inputHash: record.inputHash,
|
|
83
|
+
completedAt: record.recordedAt || now().toISOString()
|
|
84
|
+
};
|
|
85
|
+
for (const name of Object.keys(doc.outputs)) {
|
|
86
|
+
doc.outputs[name].value = record.outputs[name] ?? null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function deployTemplate(workspace, doc, context) {
|
|
91
|
+
const { document, inputHash } = buildArmParameters(doc, context);
|
|
92
|
+
return withTemporaryParameters(document, parametersFile => marketplace.deployInfra({
|
|
93
|
+
installId: doc.installId.value,
|
|
94
|
+
envKey: null,
|
|
95
|
+
subscription: doc.subscriptionId.value,
|
|
96
|
+
resourceGroup: doc.resourceGroup.value,
|
|
97
|
+
location: doc.parameters.location.value,
|
|
98
|
+
template: workspace.paths.template,
|
|
99
|
+
parameters: parametersFile,
|
|
100
|
+
templateHash: context.templateHash,
|
|
101
|
+
inputHash
|
|
102
|
+
}, context.az));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function verifyAndRecordReadiness(workspace, doc, deps) {
|
|
106
|
+
readiness.verifyRuntimeImagePull(doc, deps.az);
|
|
107
|
+
await readiness.waitForApplications(doc, deps.probe || marketplace.probeHttps);
|
|
108
|
+
const controllerUrl = `${String(doc.outputs.webAppUrl.value).replace(/\/+$/, '')}/miso`;
|
|
109
|
+
doc.controllerUrl = { ...doc.controllerUrl, value: controllerUrl };
|
|
110
|
+
doc.tenantActivation.url = { ...doc.tenantActivation.url, value: controllerUrl };
|
|
111
|
+
saveInfra(workspace, doc);
|
|
112
|
+
mergeWorkspaceEnv(workspace.paths.env, { CONTROLLER_URL: controllerUrl }, { root: workspace.root, replaceable: ['CONTROLLER_URL'] });
|
|
113
|
+
return controllerUrl;
|
|
114
|
+
}
|
|
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
|
+
|
|
132
|
+
async function prepareInfra(workspace, input, deps, az) {
|
|
133
|
+
const { doc, templateHash } = materializeWorkspace(workspace, input, deps);
|
|
134
|
+
applyLocation(doc, input);
|
|
135
|
+
const missing = missingRequiredFields(doc);
|
|
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);
|
|
141
|
+
preflight.assertActiveAccount(doc, az);
|
|
142
|
+
const acr = preflight.resolveAcr(doc, az);
|
|
143
|
+
doc.parameters.acrLoginServer.value = acr.loginServer;
|
|
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);
|
|
148
|
+
await applyAccessOptions(doc, input, deps);
|
|
149
|
+
saveInfra(workspace, doc);
|
|
150
|
+
return { doc, templateHash, acr, registryCredentials };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function deployWithStatus(workspace, doc, context) {
|
|
154
|
+
doc.deployment.status = { ...doc.deployment.status, value: 'running' };
|
|
155
|
+
saveInfra(workspace, doc);
|
|
156
|
+
try {
|
|
157
|
+
return deployTemplate(workspace, doc, context);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
doc.deployment.status = { ...doc.deployment.status, value: 'failed' };
|
|
160
|
+
saveInfra(workspace, doc);
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Runs the AZURE infra stage.
|
|
167
|
+
* @param {Object} workspace - Opened workspace
|
|
168
|
+
* @param {Object} input - Validated stage input
|
|
169
|
+
* @param {Object} [deps] - Injected dependencies (az, probe, resolvePublicIpv4, secretsApi, now, templatePath,
|
|
170
|
+
* loadRegistryCredentials)
|
|
171
|
+
* @returns {Promise<{ doc: Object, controllerUrl: string, activationUrl: string }>}
|
|
172
|
+
*/
|
|
173
|
+
async function runAzureInfraStage(workspace, input, deps = {}) {
|
|
174
|
+
const context = { az: deps.az || marketplace.runAz, now: deps.now || (() => new Date()), probe: deps.probe };
|
|
175
|
+
const { doc, templateHash, acr, registryCredentials } = await prepareInfra(workspace, input, deps, context.az);
|
|
176
|
+
const env = await materializeWorkspaceSecrets({
|
|
177
|
+
vendor: 'AZURE', envPath: workspace.paths.env, root: workspace.root, names: INFRA_SECRETS, secretsApi: deps.secretsApi
|
|
178
|
+
});
|
|
179
|
+
const record = deployWithStatus(workspace, doc, {
|
|
180
|
+
env, acr, templateHash, az: context.az, registryCredentials
|
|
181
|
+
});
|
|
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' };
|
|
186
|
+
saveInfra(workspace, doc);
|
|
187
|
+
const controllerUrl = await verifyAndRecordReadiness(workspace, doc, context);
|
|
188
|
+
return { doc, controllerUrl, activationUrl: doc.tenantActivation.url.value };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
module.exports = { runAzureInfraStage, INFRA_SECRETS };
|