@aifabrix/builder 2.48.0 → 2.49.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/.temp/jest-protection-convert/run-IFN2nv/.protection/hubspot-companies.json +1 -0
- package/.temp/jest-protection-convert/run-IvmKLO/.protection/hubspot-companies.yaml +17 -0
- package/.temp/jest-protection-convert/run-SdvblF/.protection/hubspot-companies.json +1 -0
- package/.temp/jest-protection-convert/run-b51TsW/.protection/hubspot-companies.json +1 -0
- package/.temp/jest-protection-convert/run-fiLmb1/.protection/hubspot-companies.json +1 -0
- package/.temp/jest-protection-convert/run-qYGHMU/.protection/hubspot-companies.yaml +17 -0
- package/.temp/jest-protection-fixtures/hubspot-Mq5re8/hubspot-companies.yaml +17 -0
- package/.temp/jest-protection-fixtures/hubspot-f3u4YL/hubspot-companies.yaml +17 -0
- package/.temp/jest-protection-fixtures/hubspot-iQtd1O/hubspot-companies.yaml +17 -0
- package/.temp/jest-protection-fixtures/hubspot-mdmz5M/hubspot-companies.yaml +17 -0
- package/.temp/jest-protection-fixtures/hubspot-qbKYxQ/hubspot-companies.yaml +17 -0
- package/.temp/jest-protection-fixtures/hubspot-yzlJy6/hubspot-companies.yaml +17 -0
- package/.temp/validation/summary.txt +5 -0
- package/jest.isolated-projects.js +3 -0
- package/jest.projects.js +12 -0
- package/lib/api/applications.api.js +23 -0
- package/lib/api/dev.api.js +1 -1
- package/lib/api/index.js +5 -2
- package/lib/api/types/dev.types.js +3 -2
- package/lib/app/deploy-config.js +6 -4
- package/lib/app/register.js +1 -1
- package/lib/app/run-container-start.js +0 -9
- package/lib/app/run-helpers.js +8 -4
- package/lib/app/run-reload-sync.js +16 -18
- package/lib/app/run-stop-containers.js +48 -5
- package/lib/app/run.js +6 -3
- package/lib/app/show.js +2 -2
- package/lib/cli/infra-guided-auth.js +42 -6
- package/lib/cli/infra-guided-footers.js +164 -0
- package/lib/cli/infra-guided-spinners.js +57 -0
- package/lib/cli/infra-guided.js +41 -179
- package/lib/cli/setup-app.js +1 -1
- package/lib/cli/setup-dev.js +3 -2
- package/lib/cli/setup-infra.js +3 -2
- package/lib/cli/setup-platform.js +3 -0
- package/lib/commands/login-device.js +6 -2
- package/lib/commands/login.js +4 -1
- package/lib/commands/setup-modes.js +19 -3
- package/lib/commands/setup-platform-auth.js +25 -0
- package/lib/commands/setup-prompts-admin.js +9 -0
- package/lib/commands/setup.js +60 -8
- package/lib/commands/up-dataplane-bootstrap.js +149 -0
- package/lib/commands/up-dataplane-credentials.js +19 -4
- package/lib/commands/up-dataplane.js +74 -11
- package/lib/core/secrets-env-declarative-expand.js +35 -23
- package/lib/core/setup-platform-mode.js +43 -0
- package/lib/deployment/deployer-status.js +26 -3
- package/lib/deployment/deployer.js +7 -14
- package/lib/external-system/download-helpers.js +10 -1
- package/lib/infrastructure/compose.js +40 -12
- package/lib/infrastructure/services.js +15 -3
- package/lib/infrastructure/traefik-tls-dynamic.js +88 -0
- package/lib/utils/app-bootstrap-api.js +53 -0
- package/lib/utils/app-run-containers.js +14 -3
- package/lib/utils/applications-config-defaults.js +19 -3
- package/lib/utils/compose-generate-docker-compose.js +16 -5
- package/lib/utils/compose-generator.js +34 -15
- package/lib/utils/compose-traefik-ingress-base.js +13 -14
- package/lib/utils/config-paths.js +6 -1
- package/lib/utils/dev-user-groups.js +1 -0
- package/lib/utils/docker-daemon-tls-ca.js +94 -57
- package/lib/utils/docker-reload-mount.js +12 -1
- package/lib/utils/external-env-template.js +53 -18
- package/lib/utils/health-check-poll-ui.js +84 -0
- package/lib/utils/health-check-url.js +54 -34
- package/lib/utils/health-check.js +43 -16
- package/lib/utils/infra-optional-service-flags.js +31 -2
- package/lib/utils/mutagen.js +7 -4
- package/lib/utils/platform-controller-url.js +23 -1
- package/lib/utils/refresh-remote-settings.js +67 -0
- package/lib/utils/remote-host-for-front-door.js +43 -0
- package/lib/utils/run-cli-flags.js +37 -1
- package/lib/utils/url-declarative-public-base.js +11 -4
- package/lib/utils/url-declarative-resolve-build-urls.js +11 -2
- package/lib/utils/url-declarative-resolve-expand-token.js +12 -1
- package/lib/utils/url-declarative-resolve-load-doc.js +30 -11
- package/lib/utils/url-declarative-resolve.js +2 -0
- package/lib/utils/urls-local-registry-scan.js +95 -10
- package/lib/utils/urls-local-registry.js +24 -1
- package/lib/utils/worker-sidecars.js +2 -2
- package/lib/validation/external-system-auth-rules.js +45 -7
- package/package.json +2 -1
- package/scripts/test-dataplane-bootstrap.js +331 -0
- package/templates/applications/dataplane/env.template +22 -5
- package/templates/applications/dataplane/rbac.yaml +11 -0
- package/templates/applications/miso-controller/application.yaml +1 -1
- package/templates/applications/miso-controller/env.template +3 -0
- package/templates/infra/compose.yaml.hbs +13 -9
- package/templates/python/docker-compose.hbs +3 -0
- package/templates/typescript/docker-compose.hbs +3 -0
|
@@ -29,6 +29,8 @@ What this command does:
|
|
|
29
29
|
2) Wipe data — drop every database and DB user, then up-infra + up-platform --force.
|
|
30
30
|
3) Clean installation files — remove user-local secrets, then up-infra + up-platform --force.
|
|
31
31
|
4) Update images — pull the latest infra and platform images, then up-infra + up-platform.
|
|
32
|
+
- With --mode fresh: first-time install when no infra is running (non-interactive; use with --admin-email and --admin-password).
|
|
33
|
+
- With --mode update-images: refresh images when infra is already running (non-interactive).
|
|
32
34
|
|
|
33
35
|
AI tool keys are read from the user-local file (~/.aifabrix/secrets.local.yaml) merged with the shared aifabrix-secrets file. If either source already provides keys, the AI tool prompt is skipped.
|
|
34
36
|
|
|
@@ -59,6 +61,7 @@ function registerSetup(program) {
|
|
|
59
61
|
.option('--pro-password-mode <mode>', 'Pro profile only: autogen or manual')
|
|
60
62
|
.option('--admin-email <email>', 'Admin email (non-interactive fresh install)')
|
|
61
63
|
.option('--admin-password <password>', 'Admin password for dev or pro manual-single (non-interactive)')
|
|
64
|
+
.option('--mode <mode>', 'Non-interactive mode: fresh (no infra) or update-images (infra running)')
|
|
62
65
|
.option('-y, --yes', 'Skip destructive confirmation prompts (re-install, wipe data, teardown)')
|
|
63
66
|
.addHelpText('after', SETUP_HELP_AFTER)
|
|
64
67
|
.action(async(options) => {
|
|
@@ -96,7 +96,7 @@ async function saveTokenAndDisplaySuccess(controllerUrl, token, refreshToken, ex
|
|
|
96
96
|
logger.log(chalk.gray(`Environment: ${envKey}`));
|
|
97
97
|
}
|
|
98
98
|
logger.log(chalk.gray('Token stored securely in ~/.aifabrix/config.yaml\n'));
|
|
99
|
-
} else {
|
|
99
|
+
} else if (!opts.suppressSuccessLine && !opts.guidedInstall) {
|
|
100
100
|
logger.log(formatSuccessLine('Authentication successful'));
|
|
101
101
|
}
|
|
102
102
|
}
|
|
@@ -116,8 +116,10 @@ async function pollAndSaveDeviceCodeToken(controllerUrl, deviceCode, interval, e
|
|
|
116
116
|
const spinner = useSpinner
|
|
117
117
|
? ora({ text: 'Waiting for approval', spinner: 'dots' }).start()
|
|
118
118
|
: null;
|
|
119
|
-
if (!useSpinner) {
|
|
119
|
+
if (!useSpinner && !opts.onProgressText) {
|
|
120
120
|
logger.log('Waiting for approval...');
|
|
121
|
+
} else if (opts.onProgressText) {
|
|
122
|
+
opts.onProgressText('Waiting for approval...');
|
|
121
123
|
}
|
|
122
124
|
|
|
123
125
|
let pollCount = 0;
|
|
@@ -125,6 +127,8 @@ async function pollAndSaveDeviceCodeToken(controllerUrl, deviceCode, interval, e
|
|
|
125
127
|
pollCount++;
|
|
126
128
|
if (spinner) {
|
|
127
129
|
spinner.text = `Waiting for approval (attempt ${pollCount})...`;
|
|
130
|
+
} else if (opts.onProgressText) {
|
|
131
|
+
opts.onProgressText(`Waiting for approval (attempt ${pollCount})...`);
|
|
128
132
|
}
|
|
129
133
|
};
|
|
130
134
|
|
package/lib/commands/login.js
CHANGED
|
@@ -163,7 +163,10 @@ async function handleDeviceCodeLoginFlow(controllerUrl, environment, options) {
|
|
|
163
163
|
return await handleDeviceCodeLogin(controllerUrl, environment, options.online, options.scope, {
|
|
164
164
|
compact: Boolean(options.compact),
|
|
165
165
|
exitOnFailure: options.exitOnFailure !== false,
|
|
166
|
-
tokenControllerUrl: options.tokenControllerUrl
|
|
166
|
+
tokenControllerUrl: options.tokenControllerUrl,
|
|
167
|
+
guidedInstall: options.guidedInstall === true,
|
|
168
|
+
onProgressText: options.onProgressText,
|
|
169
|
+
suppressSuccessLine: options.suppressSuccessLine === true
|
|
167
170
|
});
|
|
168
171
|
}
|
|
169
172
|
|
|
@@ -42,11 +42,19 @@ const postgresBootstrap = require('../utils/postgres-platform-bootstrap');
|
|
|
42
42
|
const appLib = require('../app');
|
|
43
43
|
const setupPrompts = require('./setup-prompts');
|
|
44
44
|
const { ensureSetupPlatformAuth, syncPlatformControllerUrlsInConfig } = require('./setup-platform-auth');
|
|
45
|
+
const {
|
|
46
|
+
refreshRemoteSettingsFromBuilderIfPossible
|
|
47
|
+
} = require('../utils/refresh-remote-settings');
|
|
45
48
|
const { handleLogin } = require('./login');
|
|
46
49
|
const infraGuided = require('../cli/infra-guided');
|
|
50
|
+
const {
|
|
51
|
+
applyPlatformTopologyFlagsToConfig,
|
|
52
|
+
platformTopologyNeedsReconcile
|
|
53
|
+
} = require('../core/setup-platform-mode');
|
|
47
54
|
const {
|
|
48
55
|
computeEffectiveInfraOptionalFlags,
|
|
49
|
-
persistMissingInfraOptionalServiceFlags
|
|
56
|
+
persistMissingInfraOptionalServiceFlags,
|
|
57
|
+
topologyFlagsForConfig
|
|
50
58
|
} = require('../utils/infra-optional-service-flags');
|
|
51
59
|
|
|
52
60
|
/** Builder app keys touched by `up-platform --force`. */
|
|
@@ -197,11 +205,19 @@ async function maybePromptForExistingBuilderDir({ force, skipBuilderConflictProm
|
|
|
197
205
|
*/
|
|
198
206
|
async function startInfraFromConfig(overrides = {}) {
|
|
199
207
|
await config.ensureSecretsEncryptionKey();
|
|
200
|
-
|
|
208
|
+
await refreshRemoteSettingsFromBuilderIfPossible({ quiet: true });
|
|
209
|
+
let cfg = await config.getConfig();
|
|
210
|
+
if (platformTopologyNeedsReconcile(cfg)) {
|
|
211
|
+
applyPlatformTopologyFlagsToConfig(cfg, cfg.setupPlatformMode);
|
|
212
|
+
await config.saveConfig(cfg);
|
|
213
|
+
cfg = await config.getConfig();
|
|
214
|
+
}
|
|
201
215
|
const emailOverride = String(overrides.adminEmail || '').trim();
|
|
202
216
|
const emailFromConfig = typeof cfg.adminEmail === 'string' ? cfg.adminEmail.trim() : '';
|
|
203
217
|
const adminEmailMerged = emailOverride || emailFromConfig || undefined;
|
|
204
218
|
const effective = computeEffectiveInfraOptionalFlags(cfg, {});
|
|
219
|
+
const topology = topologyFlagsForConfig(cfg);
|
|
220
|
+
const tlsEnabled = topology ? topology.tlsEnabled : cfg.tlsEnabled === true;
|
|
205
221
|
const legacyPwd =
|
|
206
222
|
overrides.passwordBundle && overrides.passwordBundle.mode === 'single'
|
|
207
223
|
? overrides.passwordBundle.password
|
|
@@ -215,7 +231,7 @@ async function startInfraFromConfig(overrides = {}) {
|
|
|
215
231
|
adminPwd: legacyPwd,
|
|
216
232
|
adminEmail: adminEmailMerged,
|
|
217
233
|
passwordBundle: overrides.passwordBundle,
|
|
218
|
-
tlsEnabled
|
|
234
|
+
tlsEnabled
|
|
219
235
|
});
|
|
220
236
|
});
|
|
221
237
|
await persistMissingInfraOptionalServiceFlags(cfg, effective);
|
|
@@ -14,6 +14,14 @@ const { resolvePlatformControllerUrl } = require('../utils/platform-controller-u
|
|
|
14
14
|
const { isPlatformAuthValidForController } = require('../utils/controller-url');
|
|
15
15
|
const { infoLine } = require('../utils/cli-test-layout-chalk');
|
|
16
16
|
const logger = require('../utils/logger');
|
|
17
|
+
const chalk = require('chalk');
|
|
18
|
+
const {
|
|
19
|
+
refreshRemoteSettingsFromBuilderIfPossible
|
|
20
|
+
} = require('../utils/refresh-remote-settings');
|
|
21
|
+
const {
|
|
22
|
+
applyPlatformTopologyFlagsToConfig,
|
|
23
|
+
platformTopologyNeedsReconcile
|
|
24
|
+
} = require('../core/setup-platform-mode');
|
|
17
25
|
|
|
18
26
|
/**
|
|
19
27
|
* Recompute `controller` and `platform-controller` from traefik / setupPlatformMode / front-door config.
|
|
@@ -24,6 +32,23 @@ const logger = require('../utils/logger');
|
|
|
24
32
|
* @returns {Promise<string>} Normalized platform controller URL
|
|
25
33
|
*/
|
|
26
34
|
async function syncPlatformControllerUrlsInConfig() {
|
|
35
|
+
const refresh = await refreshRemoteSettingsFromBuilderIfPossible({ quiet: true });
|
|
36
|
+
const cfg = await config.getConfig();
|
|
37
|
+
if (platformTopologyNeedsReconcile(cfg)) {
|
|
38
|
+
applyPlatformTopologyFlagsToConfig(cfg, cfg.setupPlatformMode);
|
|
39
|
+
await config.saveConfig(cfg);
|
|
40
|
+
}
|
|
41
|
+
const remoteServer = await config.getRemoteServer();
|
|
42
|
+
const rootDomain = await config.getDeveloperRootDomain();
|
|
43
|
+
if (remoteServer && !rootDomain && !refresh.refreshed) {
|
|
44
|
+
logger.warn(
|
|
45
|
+
chalk.yellow(
|
|
46
|
+
'Platform URLs may be wrong: developer-root-domain is missing. ' +
|
|
47
|
+
'Run "aifabrix dev refresh" (or "aifabrix dev init") to sync from the Builder Server.'
|
|
48
|
+
)
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
27
52
|
const platformControllerUrl = await resolvePlatformControllerUrl();
|
|
28
53
|
await config.setPlatformControllerUrl(platformControllerUrl);
|
|
29
54
|
await config.setControllerUrl(platformControllerUrl);
|
|
@@ -225,6 +225,15 @@ async function promptAdminCredentialsWithProfile(options = {}) {
|
|
|
225
225
|
await config.setSetupInstallationProfile(profile);
|
|
226
226
|
|
|
227
227
|
const adminEmail = await promptAdminEmail(options);
|
|
228
|
+
const cliPassword = String(options.adminPassword || '').trim();
|
|
229
|
+
if (cliPassword) {
|
|
230
|
+
validatePassword(cliPassword);
|
|
231
|
+
const passwordBundle = isProInstallationProfile(profile)
|
|
232
|
+
? singlePasswordBundle(cliPassword)
|
|
233
|
+
: singlePasswordBundle(cliPassword);
|
|
234
|
+
return { adminEmail, passwordBundle, profile };
|
|
235
|
+
}
|
|
236
|
+
|
|
228
237
|
const passwordBundle = isProInstallationProfile(profile)
|
|
229
238
|
? await promptProPasswordBundle(options)
|
|
230
239
|
: await promptDevPasswordBundle();
|
package/lib/commands/setup.js
CHANGED
|
@@ -29,7 +29,9 @@ const {
|
|
|
29
29
|
|
|
30
30
|
const setupPrompts = require('./setup-prompts');
|
|
31
31
|
const setupPlatformModePrompts = require('./setup-prompts-platform-mode');
|
|
32
|
-
const {
|
|
32
|
+
const {
|
|
33
|
+
applyPlatformTopologyFlagsToConfig
|
|
34
|
+
} = require('../core/setup-platform-mode');
|
|
33
35
|
const setupModes = require('./setup-modes');
|
|
34
36
|
const { syncPlatformControllerUrlsInConfig } = require('./setup-platform-auth');
|
|
35
37
|
const installationLog = require('../utils/installation-log');
|
|
@@ -37,6 +39,11 @@ const { beginSetupCommandFlow, endSetupCommandFlow } = require('./setup-run-cont
|
|
|
37
39
|
|
|
38
40
|
const MODE = setupPrompts.MODE;
|
|
39
41
|
|
|
42
|
+
const CLI_SETUP_MODES = Object.freeze({
|
|
43
|
+
FRESH: 'fresh',
|
|
44
|
+
UPDATE_IMAGES: 'update-images'
|
|
45
|
+
});
|
|
46
|
+
|
|
40
47
|
const SEPARATOR = '────────────────────────────────────────';
|
|
41
48
|
|
|
42
49
|
function title(text) {
|
|
@@ -58,15 +65,53 @@ async function maybePinDeveloperIdForFreshInstall(rawDeveloper) {
|
|
|
58
65
|
logger.log(infoLine(`Developer ID pinned to ${dev}.`));
|
|
59
66
|
}
|
|
60
67
|
|
|
68
|
+
/**
|
|
69
|
+
* @param {Object} options
|
|
70
|
+
* @returns {'fresh'|'update-images'|null}
|
|
71
|
+
*/
|
|
72
|
+
function normalizeCliMode(options) {
|
|
73
|
+
const raw = options.mode ?? options.setupMode;
|
|
74
|
+
if (raw === undefined || raw === null || String(raw).trim() === '') {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const mode = String(raw).trim().toLowerCase();
|
|
78
|
+
if (mode === CLI_SETUP_MODES.FRESH || mode === CLI_SETUP_MODES.UPDATE_IMAGES) {
|
|
79
|
+
return mode;
|
|
80
|
+
}
|
|
81
|
+
throw new Error(`Invalid --mode "${raw}". Use fresh or update-images.`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Non-interactive path when --mode is set (af-server automation).
|
|
86
|
+
* @param {Object} options
|
|
87
|
+
* @param {'fresh'|'update-images'} cliMode
|
|
88
|
+
* @returns {Promise<{ outcome: string, setupMode: string }>}
|
|
89
|
+
*/
|
|
90
|
+
async function runSetupWithCliMode(options, cliMode) {
|
|
91
|
+
logSetupHeader();
|
|
92
|
+
logger.log(formatProgress('Detecting current installation state...'));
|
|
93
|
+
const running = await isInfraRunning();
|
|
94
|
+
if (cliMode === CLI_SETUP_MODES.FRESH) {
|
|
95
|
+
if (running) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
'Infrastructure is already running; use --mode update-images or run teardown first.'
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
await runFreshInstallFlow(options);
|
|
101
|
+
return { outcome: 'success', setupMode: CLI_SETUP_MODES.FRESH };
|
|
102
|
+
}
|
|
103
|
+
if (!running) {
|
|
104
|
+
throw new Error('No infrastructure running; use --mode fresh for first-time install.');
|
|
105
|
+
}
|
|
106
|
+
await dispatchMode(MODE.UPDATE_IMAGES);
|
|
107
|
+
logger.log(formatSuccessParagraph(`aifabrix setup complete (mode: ${cliMode}).`));
|
|
108
|
+
return { outcome: 'success', setupMode: cliMode };
|
|
109
|
+
}
|
|
110
|
+
|
|
61
111
|
async function applySetupPlatformMode(options) {
|
|
62
112
|
const mode = await setupPlatformModePrompts.promptSetupPlatformMode(options);
|
|
63
|
-
await config.setSetupPlatformMode(mode);
|
|
64
|
-
const derived = flagsForSetupPlatformMode(mode);
|
|
65
|
-
// These are the user gates that control full-mode behavior.
|
|
66
113
|
const cfg = await config.getConfig();
|
|
67
|
-
cfg
|
|
68
|
-
cfg.tlsEnabled = derived.tlsEnabled;
|
|
69
|
-
cfg.useEnvironmentScopedResources = derived.useEnvironmentScopedResources;
|
|
114
|
+
applyPlatformTopologyFlagsToConfig(cfg, mode);
|
|
70
115
|
await config.saveConfig(cfg);
|
|
71
116
|
await syncPlatformControllerUrlsInConfig();
|
|
72
117
|
return mode;
|
|
@@ -150,6 +195,11 @@ async function dispatchMode(mode) {
|
|
|
150
195
|
* @returns {Promise<{ outcome: string, setupMode: string|null }>}
|
|
151
196
|
*/
|
|
152
197
|
async function runSetupMainPath(options, assumeYes) {
|
|
198
|
+
const cliMode = normalizeCliMode(options);
|
|
199
|
+
if (cliMode) {
|
|
200
|
+
return runSetupWithCliMode(options, cliMode);
|
|
201
|
+
}
|
|
202
|
+
|
|
153
203
|
logSetupHeader();
|
|
154
204
|
logger.log(formatProgress('Detecting current installation state...'));
|
|
155
205
|
const running = await isInfraRunning();
|
|
@@ -273,5 +323,7 @@ async function handleSetup(options = {}) {
|
|
|
273
323
|
module.exports = {
|
|
274
324
|
handleSetup,
|
|
275
325
|
isInfraRunning,
|
|
276
|
-
dispatchMode
|
|
326
|
+
dispatchMode,
|
|
327
|
+
normalizeCliMode,
|
|
328
|
+
CLI_SETUP_MODES
|
|
277
329
|
};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Headless dataplane bootstrap provisioning (plan 197.0).
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview up-dataplane bootstrap path when Docker + ENCRYPTION_KEY available
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 1.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const chalk = require('chalk');
|
|
12
|
+
const logger = require('../utils/logger');
|
|
13
|
+
const config = require('../core/config');
|
|
14
|
+
const dockerUtils = require('../utils/docker');
|
|
15
|
+
const { loadSecrets } = require('../core/secrets-load');
|
|
16
|
+
const { callBootstrapApi } = require('../utils/app-bootstrap-api');
|
|
17
|
+
const {
|
|
18
|
+
loadVariablesYaml,
|
|
19
|
+
extractAppConfiguration
|
|
20
|
+
} = require('../utils/app-register-config');
|
|
21
|
+
const { buildRegistrationData, saveLocalCredentials, applyLocalhostPortAdjustment } = require('../app/register');
|
|
22
|
+
const { loadDataplanePipelineCredentialsFromSecretsFile } = require('./up-dataplane-credentials');
|
|
23
|
+
const { formatSuccessLine } = require('../utils/cli-test-layout-chalk');
|
|
24
|
+
|
|
25
|
+
const ENCRYPTION_VAULT_KEYS = [
|
|
26
|
+
'miso-controller-secrets-encryptionKeyVault',
|
|
27
|
+
'secrets-encryptionKeyVault'
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {Record<string, unknown>} secrets
|
|
32
|
+
* @returns {string|null}
|
|
33
|
+
*/
|
|
34
|
+
function pickBootstrapEncryptionKeyFromSecrets(secrets) {
|
|
35
|
+
if (!secrets || typeof secrets !== 'object') {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
for (const vaultKey of ENCRYPTION_VAULT_KEYS) {
|
|
39
|
+
const raw = secrets[vaultKey];
|
|
40
|
+
if (raw === undefined || raw === null) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const trimmed = String(raw).trim();
|
|
44
|
+
if (trimmed) {
|
|
45
|
+
return trimmed;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Resolve shared ENCRYPTION_KEY for bootstrap header (never log).
|
|
53
|
+
* Uses the same merged secrets store as kv:// resolution (local + aifabrix-secrets / remote API).
|
|
54
|
+
* Must match miso-controller `ENCRYPTION_KEY` (kv://miso-controller-secrets-encryptionKeyVault),
|
|
55
|
+
* not `secrets-encryption` in config.yaml (CLI file-encryption key only).
|
|
56
|
+
*
|
|
57
|
+
* @async
|
|
58
|
+
* @returns {Promise<string|null>}
|
|
59
|
+
*/
|
|
60
|
+
async function resolveBootstrapEncryptionKey() {
|
|
61
|
+
await config.ensureSecretsEncryptionKey();
|
|
62
|
+
const secrets = await loadSecrets(undefined);
|
|
63
|
+
return pickBootstrapEncryptionKeyFromSecrets(secrets);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* True when bootstrap path is eligible (dev + Docker + encryption key).
|
|
68
|
+
* @async
|
|
69
|
+
* @param {Object} [options]
|
|
70
|
+
* @returns {Promise<boolean>}
|
|
71
|
+
*/
|
|
72
|
+
async function canUseBootstrapProvision(options = {}) {
|
|
73
|
+
if (options.allowNonDev === true) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
const cfg = await config.getConfig();
|
|
77
|
+
const environment = cfg && cfg.environment ? cfg.environment : 'dev';
|
|
78
|
+
if (environment !== 'dev') {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
await dockerUtils.ensureDockerAndCompose();
|
|
83
|
+
} catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
const encryptionKey = await resolveBootstrapEncryptionKey();
|
|
87
|
+
return Boolean(encryptionKey);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Register dataplane via bootstrap endpoint (idempotent).
|
|
92
|
+
* @async
|
|
93
|
+
* @param {Object} options - up-dataplane options
|
|
94
|
+
* @param {string} controllerUrl
|
|
95
|
+
* @param {string} environmentKey
|
|
96
|
+
* @param {Function} buildDataplaneImageRefFn
|
|
97
|
+
* @returns {Promise<void>}
|
|
98
|
+
*/
|
|
99
|
+
async function bootstrapDataplaneApplication(options, controllerUrl, environmentKey, buildDataplaneImageRefFn) {
|
|
100
|
+
const encryptionKey = await resolveBootstrapEncryptionKey();
|
|
101
|
+
if (!encryptionKey) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
'Bootstrap encryption key is not available. Ensure miso-controller-secrets-encryptionKeyVault ' +
|
|
104
|
+
'exists in merged secrets (local secrets.local.yaml or aifabrix-secrets remote store) after up-miso resolve.'
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const { variables } = await loadVariablesYaml('dataplane');
|
|
109
|
+
const appConfig = await extractAppConfiguration(variables, 'dataplane', options);
|
|
110
|
+
await applyLocalhostPortAdjustment(appConfig, controllerUrl, options);
|
|
111
|
+
|
|
112
|
+
const imageOverride = options.image || buildDataplaneImageRefFn(options);
|
|
113
|
+
const registerOpts = {
|
|
114
|
+
imageOverride,
|
|
115
|
+
image: imageOverride,
|
|
116
|
+
registryMode: options.registryMode,
|
|
117
|
+
envKey: environmentKey
|
|
118
|
+
};
|
|
119
|
+
const registrationData = buildRegistrationData(appConfig, registerOpts);
|
|
120
|
+
|
|
121
|
+
logger.log(chalk.blue('Provisioning dataplane via bootstrap (no device login)...'));
|
|
122
|
+
const responseData = await callBootstrapApi(
|
|
123
|
+
controllerUrl,
|
|
124
|
+
encryptionKey,
|
|
125
|
+
environmentKey,
|
|
126
|
+
registrationData
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
if (responseData.credentials && responseData.credentials.clientSecret) {
|
|
130
|
+
await saveLocalCredentials(responseData, controllerUrl);
|
|
131
|
+
logger.log(formatSuccessLine('Dataplane bootstrap credentials saved locally'));
|
|
132
|
+
} else if (responseData.status === 'already_provisioned') {
|
|
133
|
+
logger.log(formatSuccessLine('Dataplane already provisioned (bootstrap idempotent)'));
|
|
134
|
+
const localCredentials = await loadDataplanePipelineCredentialsFromSecretsFile();
|
|
135
|
+
if (!localCredentials) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
'Dataplane is registered on the controller but pipeline credentials are missing locally. ' +
|
|
138
|
+
'Run fresh setup with volume wipe, or: aifabrix login && aifabrix app rotate-secret dataplane'
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
module.exports = {
|
|
145
|
+
canUseBootstrapProvision,
|
|
146
|
+
resolveBootstrapEncryptionKey,
|
|
147
|
+
bootstrapDataplaneApplication,
|
|
148
|
+
pickBootstrapEncryptionKeyFromSecrets
|
|
149
|
+
};
|
|
@@ -26,6 +26,12 @@ const MISALIGN_ERROR =
|
|
|
26
26
|
'Dataplane pipeline credentials are not aligned with the controller. ' +
|
|
27
27
|
'Run: aifabrix app rotate-secret dataplane && aifabrix run dataplane';
|
|
28
28
|
|
|
29
|
+
const BOOTSTRAP_MISALIGN_ERROR =
|
|
30
|
+
'Dataplane bootstrap credentials are not accepted by the controller after deploy. ' +
|
|
31
|
+
'Ensure miso-controller has BOOTSTRAP_APPKEYS=dataplane and ENCRYPTION_KEY set, then re-run: aifabrix up-dataplane';
|
|
32
|
+
|
|
33
|
+
const BOOTSTRAP_PROBE_RETRY = { maxAttempts: 15, delayMs: 3000 };
|
|
34
|
+
|
|
29
35
|
/**
|
|
30
36
|
* @param {string} controllerUrl
|
|
31
37
|
* @returns {string}
|
|
@@ -120,9 +126,10 @@ async function credentialsAcceptTokenWithRetry(controllerUrl, maxAttempts = 5, d
|
|
|
120
126
|
|
|
121
127
|
function resolveCredentialProbeRetry(options = {}) {
|
|
122
128
|
const retry = options.credentialProbeRetry || {};
|
|
129
|
+
const bootstrapDefaults = options.useBootstrap === true ? BOOTSTRAP_PROBE_RETRY : { maxAttempts: 5, delayMs: 2000 };
|
|
123
130
|
return {
|
|
124
|
-
maxAttempts: Number.isFinite(retry.maxAttempts) ? retry.maxAttempts :
|
|
125
|
-
delayMs: Number.isFinite(retry.delayMs) ? retry.delayMs :
|
|
131
|
+
maxAttempts: Number.isFinite(retry.maxAttempts) ? retry.maxAttempts : bootstrapDefaults.maxAttempts,
|
|
132
|
+
delayMs: Number.isFinite(retry.delayMs) ? retry.delayMs : bootstrapDefaults.delayMs
|
|
126
133
|
};
|
|
127
134
|
}
|
|
128
135
|
|
|
@@ -155,7 +162,12 @@ async function credentialsAcceptToken(controllerUrl) {
|
|
|
155
162
|
*/
|
|
156
163
|
async function alignDataplaneCredentialsAfterDeploy(options, controllerUrl) {
|
|
157
164
|
const quiet = options.platformInstall === true || options.quiet === true;
|
|
158
|
-
const
|
|
165
|
+
const bootstrapPath = options.useBootstrap === true;
|
|
166
|
+
const { maxAttempts, delayMs } = resolveCredentialProbeRetry(options);
|
|
167
|
+
|
|
168
|
+
const aligned = bootstrapPath
|
|
169
|
+
? await credentialsAcceptTokenWithRetry(controllerUrl, maxAttempts, delayMs)
|
|
170
|
+
: await credentialsAcceptToken(controllerUrl);
|
|
159
171
|
if (aligned) {
|
|
160
172
|
if (!quiet) {
|
|
161
173
|
logger.log(formatSuccessLine('Dataplane pipeline credentials verified.'));
|
|
@@ -163,12 +175,15 @@ async function alignDataplaneCredentialsAfterDeploy(options, controllerUrl) {
|
|
|
163
175
|
return;
|
|
164
176
|
}
|
|
165
177
|
|
|
178
|
+
if (bootstrapPath) {
|
|
179
|
+
throw new Error(BOOTSTRAP_MISALIGN_ERROR);
|
|
180
|
+
}
|
|
181
|
+
|
|
166
182
|
if (!quiet) {
|
|
167
183
|
logger.log(chalk.blue('Aligning dataplane pipeline credentials with controller...'));
|
|
168
184
|
}
|
|
169
185
|
await rotateSecret(DATAPLANE_APP_KEY, { ...options, quiet, exitOnFailure: false });
|
|
170
186
|
|
|
171
|
-
const { maxAttempts, delayMs } = resolveCredentialProbeRetry(options);
|
|
172
187
|
const stillMisaligned = !(await credentialsAcceptTokenWithRetry(controllerUrl, maxAttempts, delayMs));
|
|
173
188
|
if (stillMisaligned) {
|
|
174
189
|
throw new Error(MISALIGN_ERROR);
|
|
@@ -35,6 +35,10 @@ const {
|
|
|
35
35
|
refreshUrlsLocalRegistryForCurrentProject
|
|
36
36
|
} = require('./up-common');
|
|
37
37
|
const { alignDataplaneCredentialsAfterDeploy } = require('./up-dataplane-credentials');
|
|
38
|
+
const {
|
|
39
|
+
canUseBootstrapProvision,
|
|
40
|
+
bootstrapDataplaneApplication
|
|
41
|
+
} = require('./up-dataplane-bootstrap');
|
|
38
42
|
|
|
39
43
|
const CONTROLLER_HEALTH_PATH = '/health';
|
|
40
44
|
|
|
@@ -120,7 +124,9 @@ async function resolveUpDataplaneAuthContext(options, environmentKey) {
|
|
|
120
124
|
if (options.allowNonDev !== true) {
|
|
121
125
|
assertDevEnvironmentForDataplane(cfg);
|
|
122
126
|
}
|
|
123
|
-
|
|
127
|
+
if (options.platformInstall !== true) {
|
|
128
|
+
logger.log(formatSuccessLine(`Logged in (environment: ${environmentKey})`));
|
|
129
|
+
}
|
|
124
130
|
return { controllerUrl, authConfig };
|
|
125
131
|
}
|
|
126
132
|
|
|
@@ -223,6 +229,57 @@ async function computeDataplaneRunOverrides(options) {
|
|
|
223
229
|
return { runEnvKey, portOverride };
|
|
224
230
|
}
|
|
225
231
|
|
|
232
|
+
/**
|
|
233
|
+
* Resolve controller URL and auth for bootstrap vs device-login paths.
|
|
234
|
+
* @async
|
|
235
|
+
* @param {Object} options
|
|
236
|
+
* @param {string} environmentKey
|
|
237
|
+
* @returns {Promise<{ controllerUrl: string, authConfig: Object|null, useBootstrap: boolean }>}
|
|
238
|
+
*/
|
|
239
|
+
async function resolveUpDataplaneControllerContext(options, environmentKey) {
|
|
240
|
+
const useBootstrap = await canUseBootstrapProvision(options);
|
|
241
|
+
if (useBootstrap) {
|
|
242
|
+
if (options.skipInfraCheck !== true) {
|
|
243
|
+
await assertDevInfraUp();
|
|
244
|
+
}
|
|
245
|
+
const controllerUrl = await resolveControllerUrlWithHealthCheck();
|
|
246
|
+
const cfg = await config.getConfig();
|
|
247
|
+
if (options.allowNonDev !== true) {
|
|
248
|
+
assertDevEnvironmentForDataplane(cfg);
|
|
249
|
+
}
|
|
250
|
+
if (options.platformInstall !== true) {
|
|
251
|
+
logger.log(formatSuccessLine('Using bootstrap provision (no device login)'));
|
|
252
|
+
}
|
|
253
|
+
return { controllerUrl, authConfig: null, useBootstrap: true };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const authContext = await resolveUpDataplaneAuthContext(options, environmentKey);
|
|
257
|
+
return {
|
|
258
|
+
controllerUrl: authContext.controllerUrl,
|
|
259
|
+
authConfig: authContext.authConfig,
|
|
260
|
+
useBootstrap: false
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Register/bootstrap, deploy manifest, align credentials.
|
|
266
|
+
* @async
|
|
267
|
+
*/
|
|
268
|
+
async function provisionAndDeployDataplane(options, environmentKey, controllerUrl, useBootstrap, authConfig) {
|
|
269
|
+
if (useBootstrap) {
|
|
270
|
+
await bootstrapDataplaneApplication(options, controllerUrl, environmentKey, buildDataplaneImageRef);
|
|
271
|
+
} else {
|
|
272
|
+
await registerOrRotateDataplane(options, controllerUrl, environmentKey, authConfig);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
await deployDataplaneToController({
|
|
276
|
+
...options,
|
|
277
|
+
envKey: environmentKey,
|
|
278
|
+
...(useBootstrap ? { deploymentAuth: 'client-credentials' } : {})
|
|
279
|
+
});
|
|
280
|
+
logger.log('');
|
|
281
|
+
}
|
|
282
|
+
|
|
226
283
|
/**
|
|
227
284
|
* Handle up-dataplane command: ensure logged in, environment dev, ensure dataplane,
|
|
228
285
|
* register or rotate (if already registered), deploy (send manifest to controller),
|
|
@@ -239,30 +296,34 @@ async function computeDataplaneRunOverrides(options) {
|
|
|
239
296
|
* @throws {Error} If infra not up, controller unavailable, not logged in, environment not dev, or any step fails
|
|
240
297
|
*/
|
|
241
298
|
async function handleUpDataplane(options = {}) {
|
|
242
|
-
|
|
299
|
+
if (options.platformInstall !== true) {
|
|
300
|
+
logger.log(chalk.blue('Starting up-dataplane (register/rotate, deploy, then run dataplane locally)...\n'));
|
|
301
|
+
}
|
|
243
302
|
|
|
244
303
|
const environmentKey =
|
|
245
304
|
options && options.envKey
|
|
246
305
|
? String(options.envKey).trim().toLowerCase()
|
|
247
306
|
: await resolveEnvironment();
|
|
248
|
-
|
|
307
|
+
|
|
308
|
+
const { controllerUrl, authConfig, useBootstrap } = await resolveUpDataplaneControllerContext(
|
|
309
|
+
options,
|
|
310
|
+
environmentKey
|
|
311
|
+
);
|
|
249
312
|
|
|
250
313
|
await ensureDataplaneAppsFromTemplate();
|
|
251
|
-
// If envOutputPath target folder does not exist, set envOutputPath to null
|
|
252
314
|
validateEnvOutputPathFolderOrNull('dataplane');
|
|
253
|
-
|
|
254
315
|
refreshUrlsLocalRegistryForCurrentProject();
|
|
255
316
|
|
|
256
317
|
if (options.platformInstall !== true) {
|
|
257
318
|
emitDataplaneManifestLineFromBuilder();
|
|
258
319
|
}
|
|
259
320
|
|
|
260
|
-
await
|
|
321
|
+
await provisionAndDeployDataplane(options, environmentKey, controllerUrl, useBootstrap, authConfig);
|
|
261
322
|
|
|
262
|
-
await
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
323
|
+
await alignDataplaneCredentialsAfterDeploy(
|
|
324
|
+
{ ...options, useBootstrap },
|
|
325
|
+
controllerUrl
|
|
326
|
+
);
|
|
266
327
|
|
|
267
328
|
const { runEnvKey, portOverride } = await computeDataplaneRunOverrides(options);
|
|
268
329
|
|
|
@@ -275,7 +336,9 @@ async function handleUpDataplane(options = {}) {
|
|
|
275
336
|
skipManifestMetadataLine: true
|
|
276
337
|
});
|
|
277
338
|
|
|
278
|
-
|
|
339
|
+
if (options.platformInstall !== true) {
|
|
340
|
+
logger.log(formatSuccessParagraph('up-dataplane complete. Dataplane is registered, deployed in dev, and running locally.'));
|
|
341
|
+
}
|
|
279
342
|
}
|
|
280
343
|
|
|
281
344
|
module.exports = { handleUpDataplane, buildDataplaneImageRef };
|