@aifabrix/builder 2.48.0 → 2.49.1
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/.cursor/skills/README.md +14 -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 +50 -12
- package/lib/cli/infra-guided-footers.js +164 -0
- package/lib/cli/infra-guided-spinners.js +57 -0
- package/lib/cli/infra-guided.js +52 -181
- 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/teardown.js +3 -5
- package/lib/commands/up-common.js +8 -11
- package/lib/commands/up-dataplane-bootstrap.js +149 -0
- package/lib/commands/up-dataplane-credentials.js +19 -4
- package/lib/commands/up-dataplane.js +155 -24
- package/lib/commands/up-miso.js +6 -2
- 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 +119 -0
- package/lib/utils/health-check-url.js +54 -34
- package/lib/utils/health-check.js +91 -35
- package/lib/utils/infra-optional-service-flags.js +31 -2
- package/lib/utils/mutagen.js +7 -4
- package/lib/utils/paths.js +26 -36
- 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/application.yaml +1 -1
- package/templates/applications/dataplane/env.template +24 -6
- package/templates/applications/dataplane/rbac.yaml +11 -0
- package/templates/applications/miso-controller/application.yaml +1 -1
- package/templates/applications/miso-controller/env.template +5 -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
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
|
};
|
package/lib/commands/teardown.js
CHANGED
|
@@ -120,17 +120,15 @@ function cleanAifabrixSystemDir() {
|
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
/**
|
|
123
|
-
* Remove materialized `builder/` trees
|
|
124
|
-
*
|
|
125
|
-
* the config directory.
|
|
123
|
+
* Remove materialized `builder/` trees under home/work and config dir only.
|
|
124
|
+
* Does not remove `cwd/builder` in product repos (e.g. aifabrix-dataplane/builder/dataplane).
|
|
126
125
|
*
|
|
127
126
|
* @returns {{ removed: string[], failed: string[] }}
|
|
128
127
|
*/
|
|
129
128
|
function cleanMaterializedBuilderTrees() {
|
|
130
129
|
const candidates = [
|
|
131
130
|
pathsUtil.getBuilderRoot(),
|
|
132
|
-
path.join(pathsUtil.getAifabrixSystemDir(), 'builder')
|
|
133
|
-
path.join(process.cwd(), 'builder')
|
|
131
|
+
path.join(pathsUtil.getAifabrixSystemDir(), 'builder')
|
|
134
132
|
];
|
|
135
133
|
const seen = new Set();
|
|
136
134
|
const removed = [];
|
|
@@ -242,26 +242,21 @@ function isUnderAllowedBuilderRoot(resolvedAppPath, allowedRoots) {
|
|
|
242
242
|
}
|
|
243
243
|
|
|
244
244
|
/**
|
|
245
|
-
* Removes
|
|
246
|
-
*
|
|
247
|
-
* Uses {@link pathsUtil.getBuilderPath} for each app (system apps may resolve under the config dir).
|
|
245
|
+
* Removes platform app directories under the home/work materialized `builder/` root only.
|
|
246
|
+
* Never deletes `cwd/builder` trees in product repos (e.g. aifabrix-miso/builder/miso-controller).
|
|
248
247
|
*
|
|
249
248
|
* @param {string[]} appNames - Application names (e.g. ['keycloak', 'miso-controller', 'dataplane'])
|
|
250
249
|
* @returns {Promise<void>}
|
|
251
|
-
* @throws {Error} If any path is outside
|
|
250
|
+
* @throws {Error} If any path is outside the home builder root (path traversal attempt)
|
|
252
251
|
*/
|
|
253
252
|
async function cleanBuilderAppDirs(appNames, opts = {}) {
|
|
254
253
|
const silent = Boolean(opts.silent);
|
|
255
254
|
if (!Array.isArray(appNames) || appNames.length === 0) return;
|
|
256
|
-
const allowedRoots = uniqueResolvedRoots([
|
|
257
|
-
path.join(process.cwd(), 'builder'),
|
|
258
|
-
pathsUtil.getBuilderRoot(),
|
|
259
|
-
pathsUtil.getSystemBuilderRoot()
|
|
260
|
-
]);
|
|
255
|
+
const allowedRoots = uniqueResolvedRoots([pathsUtil.getSystemBuilderRoot()]);
|
|
261
256
|
const cleaned = [];
|
|
262
257
|
for (const appName of appNames) {
|
|
263
258
|
if (!appName || typeof appName !== 'string') continue;
|
|
264
|
-
const appPath = path.resolve(pathsUtil.
|
|
259
|
+
const appPath = path.resolve(pathsUtil.getHomeBuilderAppPath(appName));
|
|
265
260
|
if (!isUnderAllowedBuilderRoot(appPath, allowedRoots)) {
|
|
266
261
|
throw new Error(
|
|
267
262
|
`Path ${appPath} is outside allowed builder roots (${allowedRoots.join(', ')}); refusing to clean`
|
|
@@ -303,7 +298,9 @@ async function ensureAppFromTemplate(appName, opts = {}) {
|
|
|
303
298
|
}
|
|
304
299
|
const silent = Boolean(opts.silent);
|
|
305
300
|
|
|
306
|
-
const appPath = pathsUtil.
|
|
301
|
+
const appPath = pathsUtil.isSystemBuilderAppName(appName)
|
|
302
|
+
? pathsUtil.getHomeBuilderAppPath(appName)
|
|
303
|
+
: pathsUtil.getBuilderPath(appName);
|
|
307
304
|
const primaryCopied = await ensureTemplateAtPath(appName, appPath);
|
|
308
305
|
if (primaryCopied && !silent) {
|
|
309
306
|
logger.log(chalk.blue(`Creating builder/${appName} from template...`));
|
|
@@ -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);
|