@aifabrix/builder 2.47.4 → 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/jest.isolated-projects.js +7 -0
- package/jest.projects.js +16 -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 +4 -3
- package/lib/app/deploy-config.js +6 -4
- package/lib/app/register.js +21 -19
- package/lib/app/rotate-secret.js +39 -16
- 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/build/index.js +12 -49
- package/lib/build/integration-docker-staging.js +35 -0
- package/lib/build/standard-docker-build.js +113 -0
- 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 +5 -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/dev-init.js +18 -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-prompts-platform-mode.js +11 -1
- package/lib/commands/setup.js +60 -7
- package/lib/commands/up-dataplane-bootstrap.js +149 -0
- package/lib/commands/up-dataplane-credentials.js +104 -6
- 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 +59 -9
- 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/parameters/infra-kv-discovery.js +4 -3
- package/lib/utils/aifabrix-runtime-config-dir.js +2 -1
- 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 +18 -6
- package/lib/utils/compose-generator.js +34 -15
- package/lib/utils/compose-traefik-ingress-base.js +13 -14
- package/lib/utils/config-paths.js +8 -3
- package/lib/utils/dev-ca-docker-augment.js +54 -0
- package/lib/utils/dev-user-groups.js +1 -0
- package/lib/utils/docker-daemon-tls-ca.js +363 -0
- package/lib/utils/docker-reload-mount.js +12 -1
- package/lib/utils/ensure-dev-certs-for-remote-docker.js +5 -0
- 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/promote-dev-docker-image.js +170 -0
- package/lib/utils/refresh-remote-settings.js +67 -0
- package/lib/utils/remote-docker-env.js +22 -7
- package/lib/utils/remote-host-for-front-door.js +43 -0
- package/lib/utils/run-cli-flags.js +37 -1
- package/lib/utils/secrets-helpers.js +9 -14
- package/lib/utils/secrets-kv-refs.js +47 -3
- package/lib/utils/secrets-missing-error.js +1 -9
- package/lib/utils/token-manager-client-credentials.js +11 -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 +28 -1
- 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
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,14 +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.useEnvironmentScopedResources = derived.useEnvironmentScopedResources;
|
|
114
|
+
applyPlatformTopologyFlagsToConfig(cfg, mode);
|
|
69
115
|
await config.saveConfig(cfg);
|
|
70
116
|
await syncPlatformControllerUrlsInConfig();
|
|
71
117
|
return mode;
|
|
@@ -149,6 +195,11 @@ async function dispatchMode(mode) {
|
|
|
149
195
|
* @returns {Promise<{ outcome: string, setupMode: string|null }>}
|
|
150
196
|
*/
|
|
151
197
|
async function runSetupMainPath(options, assumeYes) {
|
|
198
|
+
const cliMode = normalizeCliMode(options);
|
|
199
|
+
if (cliMode) {
|
|
200
|
+
return runSetupWithCliMode(options, cliMode);
|
|
201
|
+
}
|
|
202
|
+
|
|
152
203
|
logSetupHeader();
|
|
153
204
|
logger.log(formatProgress('Detecting current installation state...'));
|
|
154
205
|
const running = await isInfraRunning();
|
|
@@ -272,5 +323,7 @@ async function handleSetup(options = {}) {
|
|
|
272
323
|
module.exports = {
|
|
273
324
|
handleSetup,
|
|
274
325
|
isInfraRunning,
|
|
275
|
-
dispatchMode
|
|
326
|
+
dispatchMode,
|
|
327
|
+
normalizeCliMode,
|
|
328
|
+
CLI_SETUP_MODES
|
|
276
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
|
+
};
|
|
@@ -16,12 +16,22 @@ const { loadClientCredentials } = require('../utils/token-manager');
|
|
|
16
16
|
const { rotateSecret } = require('../app/rotate-secret');
|
|
17
17
|
const logger = require('../utils/logger');
|
|
18
18
|
const { formatSuccessLine } = require('../utils/cli-test-layout-chalk');
|
|
19
|
+
const {
|
|
20
|
+
resolveLocalhostPlatformControllerUrl,
|
|
21
|
+
resolveCliPlatformControllerUrl
|
|
22
|
+
} = require('../utils/platform-controller-url');
|
|
19
23
|
|
|
20
24
|
const DATAPLANE_APP_KEY = 'dataplane';
|
|
21
25
|
const MISALIGN_ERROR =
|
|
22
26
|
'Dataplane pipeline credentials are not aligned with the controller. ' +
|
|
23
27
|
'Run: aifabrix app rotate-secret dataplane && aifabrix run dataplane';
|
|
24
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
|
+
|
|
25
35
|
/**
|
|
26
36
|
* @param {string} controllerUrl
|
|
27
37
|
* @returns {string}
|
|
@@ -30,6 +40,31 @@ function normalizeControllerUrl(controllerUrl) {
|
|
|
30
40
|
return String(controllerUrl || '').replace(/\/+$/, '');
|
|
31
41
|
}
|
|
32
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Controller bases to try for token probe (public Traefik, CLI-resolved, localhost).
|
|
45
|
+
* @async
|
|
46
|
+
* @param {string} controllerUrl
|
|
47
|
+
* @returns {Promise<string[]>}
|
|
48
|
+
*/
|
|
49
|
+
async function resolveControllerUrlsForCredentialProbe(controllerUrl) {
|
|
50
|
+
const urls = [];
|
|
51
|
+
const add = (url) => {
|
|
52
|
+
const normalized = normalizeControllerUrl(url);
|
|
53
|
+
if (normalized && !urls.includes(normalized)) {
|
|
54
|
+
urls.push(normalized);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
add(controllerUrl);
|
|
59
|
+
try {
|
|
60
|
+
add(await resolveCliPlatformControllerUrl());
|
|
61
|
+
add(await resolveLocalhostPlatformControllerUrl());
|
|
62
|
+
} catch {
|
|
63
|
+
// optional fallbacks only
|
|
64
|
+
}
|
|
65
|
+
return urls;
|
|
66
|
+
}
|
|
67
|
+
|
|
33
68
|
/**
|
|
34
69
|
* Exchange dataplane client id/secret for a controller token (probe only).
|
|
35
70
|
* @async
|
|
@@ -52,17 +87,69 @@ async function probeDataplaneClientToken(controllerUrl, credentials) {
|
|
|
52
87
|
return response.success === true;
|
|
53
88
|
}
|
|
54
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Load dataplane pipeline credentials from secrets.local.yaml only (ignore process env).
|
|
92
|
+
* @async
|
|
93
|
+
* @returns {Promise<{ clientId: string, clientSecret: string }|null>}
|
|
94
|
+
*/
|
|
95
|
+
async function loadDataplanePipelineCredentialsFromSecretsFile() {
|
|
96
|
+
return loadClientCredentials(DATAPLANE_APP_KEY, { secretsFileOnly: true });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @param {number} ms
|
|
101
|
+
* @returns {Promise<void>}
|
|
102
|
+
*/
|
|
103
|
+
function sleep(ms) {
|
|
104
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Probe token endpoint with retries (controller Redis may briefly serve stale pipeline config).
|
|
109
|
+
* @async
|
|
110
|
+
* @param {string} controllerUrl
|
|
111
|
+
* @param {number} [maxAttempts]
|
|
112
|
+
* @param {number} [delayMs]
|
|
113
|
+
* @returns {Promise<boolean>}
|
|
114
|
+
*/
|
|
115
|
+
async function credentialsAcceptTokenWithRetry(controllerUrl, maxAttempts = 5, delayMs = 2000) {
|
|
116
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
117
|
+
if (await credentialsAcceptToken(controllerUrl)) {
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
if (attempt < maxAttempts - 1 && delayMs > 0) {
|
|
121
|
+
await sleep(delayMs);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function resolveCredentialProbeRetry(options = {}) {
|
|
128
|
+
const retry = options.credentialProbeRetry || {};
|
|
129
|
+
const bootstrapDefaults = options.useBootstrap === true ? BOOTSTRAP_PROBE_RETRY : { maxAttempts: 5, delayMs: 2000 };
|
|
130
|
+
return {
|
|
131
|
+
maxAttempts: Number.isFinite(retry.maxAttempts) ? retry.maxAttempts : bootstrapDefaults.maxAttempts,
|
|
132
|
+
delayMs: Number.isFinite(retry.delayMs) ? retry.delayMs : bootstrapDefaults.delayMs
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
55
136
|
/**
|
|
56
137
|
* @async
|
|
57
138
|
* @param {string} controllerUrl
|
|
58
139
|
* @returns {Promise<boolean>}
|
|
59
140
|
*/
|
|
60
141
|
async function credentialsAcceptToken(controllerUrl) {
|
|
61
|
-
const credentials = await
|
|
142
|
+
const credentials = await loadDataplanePipelineCredentialsFromSecretsFile();
|
|
62
143
|
if (!credentials) {
|
|
63
144
|
return false;
|
|
64
145
|
}
|
|
65
|
-
|
|
146
|
+
const urls = await resolveControllerUrlsForCredentialProbe(controllerUrl);
|
|
147
|
+
for (const url of urls) {
|
|
148
|
+
if (await probeDataplaneClientToken(url, credentials)) {
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return false;
|
|
66
153
|
}
|
|
67
154
|
|
|
68
155
|
/**
|
|
@@ -75,7 +162,12 @@ async function credentialsAcceptToken(controllerUrl) {
|
|
|
75
162
|
*/
|
|
76
163
|
async function alignDataplaneCredentialsAfterDeploy(options, controllerUrl) {
|
|
77
164
|
const quiet = options.platformInstall === true || options.quiet === true;
|
|
78
|
-
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);
|
|
79
171
|
if (aligned) {
|
|
80
172
|
if (!quiet) {
|
|
81
173
|
logger.log(formatSuccessLine('Dataplane pipeline credentials verified.'));
|
|
@@ -83,12 +175,16 @@ async function alignDataplaneCredentialsAfterDeploy(options, controllerUrl) {
|
|
|
83
175
|
return;
|
|
84
176
|
}
|
|
85
177
|
|
|
178
|
+
if (bootstrapPath) {
|
|
179
|
+
throw new Error(BOOTSTRAP_MISALIGN_ERROR);
|
|
180
|
+
}
|
|
181
|
+
|
|
86
182
|
if (!quiet) {
|
|
87
183
|
logger.log(chalk.blue('Aligning dataplane pipeline credentials with controller...'));
|
|
88
184
|
}
|
|
89
|
-
await rotateSecret(DATAPLANE_APP_KEY, { ...options, quiet });
|
|
185
|
+
await rotateSecret(DATAPLANE_APP_KEY, { ...options, quiet, exitOnFailure: false });
|
|
90
186
|
|
|
91
|
-
const stillMisaligned = !(await
|
|
187
|
+
const stillMisaligned = !(await credentialsAcceptTokenWithRetry(controllerUrl, maxAttempts, delayMs));
|
|
92
188
|
if (stillMisaligned) {
|
|
93
189
|
throw new Error(MISALIGN_ERROR);
|
|
94
190
|
}
|
|
@@ -100,5 +196,7 @@ async function alignDataplaneCredentialsAfterDeploy(options, controllerUrl) {
|
|
|
100
196
|
module.exports = {
|
|
101
197
|
alignDataplaneCredentialsAfterDeploy,
|
|
102
198
|
probeDataplaneClientToken,
|
|
103
|
-
credentialsAcceptToken
|
|
199
|
+
credentialsAcceptToken,
|
|
200
|
+
loadDataplanePipelineCredentialsFromSecretsFile,
|
|
201
|
+
resolveControllerUrlsForCredentialProbe
|
|
104
202
|
};
|
|
@@ -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 };
|
|
@@ -14,6 +14,30 @@ const { readAppEnvironmentScopedFlagForAppPath } = require('../utils/app-scoped-
|
|
|
14
14
|
const { expandDeclarativeUrlsInEnvContent } = require('../utils/url-declarative-resolve');
|
|
15
15
|
const { rewriteInactiveDeclarativeVdirPublicContent } = require('../utils/url-declarative-vdir-inactive-env');
|
|
16
16
|
const { refreshUrlsLocalRegistryFromBuilder } = require('../utils/urls-local-registry');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {() => Promise<*>} getter
|
|
20
|
+
* @param {*} [fallback]
|
|
21
|
+
* @returns {Promise<*>}
|
|
22
|
+
*/
|
|
23
|
+
async function tryConfigValue(getter, fallback = null) {
|
|
24
|
+
try {
|
|
25
|
+
return await getter();
|
|
26
|
+
} catch {
|
|
27
|
+
return fallback;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {() => Promise<*>} getter
|
|
33
|
+
* @returns {Promise<string|null>}
|
|
34
|
+
*/
|
|
35
|
+
async function tryConfigTrimmedString(getter) {
|
|
36
|
+
const value = await tryConfigValue(getter, null);
|
|
37
|
+
const trimmed = value !== null && value !== undefined ? String(value).trim() : '';
|
|
38
|
+
return trimmed || null;
|
|
39
|
+
}
|
|
40
|
+
|
|
17
41
|
/**
|
|
18
42
|
* Config inputs for declarative url:// expansion.
|
|
19
43
|
* @param {string} appPath
|
|
@@ -21,30 +45,16 @@ const { refreshUrlsLocalRegistryFromBuilder } = require('../utils/urls-local-reg
|
|
|
21
45
|
*/
|
|
22
46
|
async function loadDeclarativeUrlExpandInputs(appPath) {
|
|
23
47
|
const userCfg = await config.getConfig();
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
} catch {
|
|
31
|
-
remoteServer = null;
|
|
32
|
-
}
|
|
33
|
-
let developerIdRaw = null;
|
|
34
|
-
try {
|
|
35
|
-
developerIdRaw = await config.getDeveloperId();
|
|
36
|
-
} catch {
|
|
37
|
-
developerIdRaw = null;
|
|
38
|
-
}
|
|
39
|
-
let infraTlsEnabled = false;
|
|
40
|
-
try {
|
|
41
|
-
infraTlsEnabled = await config.getTlsEnabled();
|
|
42
|
-
} catch {
|
|
43
|
-
infraTlsEnabled = false;
|
|
44
|
-
}
|
|
48
|
+
const [remoteServer, developerRootDomain, developerIdRaw, infraTlsEnabled] = await Promise.all([
|
|
49
|
+
tryConfigTrimmedString(() => config.getRemoteServer()),
|
|
50
|
+
tryConfigTrimmedString(() => config.getDeveloperRootDomain()),
|
|
51
|
+
tryConfigValue(() => config.getDeveloperId(), null),
|
|
52
|
+
tryConfigValue(() => config.getTlsEnabled(), false)
|
|
53
|
+
]);
|
|
45
54
|
return {
|
|
46
55
|
userCfg,
|
|
47
56
|
remoteServer,
|
|
57
|
+
developerRootDomain,
|
|
48
58
|
developerIdRaw,
|
|
49
59
|
infraTlsEnabled,
|
|
50
60
|
appScopedFlag: readAppEnvironmentScopedFlagForAppPath(appPath)
|
|
@@ -60,7 +70,7 @@ async function loadDeclarativeUrlExpandInputs(appPath) {
|
|
|
60
70
|
* @returns {Object}
|
|
61
71
|
*/
|
|
62
72
|
function buildDeclarativeUrlExpandContextFromInputs(appName, variablesPath, environment, inputs) {
|
|
63
|
-
const { userCfg, remoteServer, developerIdRaw, infraTlsEnabled, appScopedFlag } = inputs;
|
|
73
|
+
const { userCfg, remoteServer, developerRootDomain, developerIdRaw, infraTlsEnabled, appScopedFlag } = inputs;
|
|
64
74
|
let projectRoot = null;
|
|
65
75
|
try {
|
|
66
76
|
const r = pathsUtil.getProjectRoot();
|
|
@@ -77,6 +87,7 @@ function buildDeclarativeUrlExpandContextFromInputs(appName, variablesPath, envi
|
|
|
77
87
|
useEnvironmentScopedResources: Boolean(userCfg.useEnvironmentScopedResources),
|
|
78
88
|
appEnvironmentScopedResources: appScopedFlag,
|
|
79
89
|
remoteServer,
|
|
90
|
+
developerRootDomain,
|
|
80
91
|
developerIdRaw,
|
|
81
92
|
infraTlsEnabled,
|
|
82
93
|
userCfg,
|
|
@@ -136,12 +147,13 @@ async function expandDeclarativeUrlsIfPresent(resolved, appName, appPath, variab
|
|
|
136
147
|
if (envOnly || !variablesPath) {
|
|
137
148
|
return resolved;
|
|
138
149
|
}
|
|
139
|
-
const { userCfg, remoteServer, developerIdRaw, infraTlsEnabled, appScopedFlag } =
|
|
150
|
+
const { userCfg, remoteServer, developerRootDomain, developerIdRaw, infraTlsEnabled, appScopedFlag } =
|
|
140
151
|
await loadDeclarativeUrlExpandInputs(appPath);
|
|
141
152
|
resolved = rewriteInactiveDeclarativeVdirPublicContent(resolved, variablesPath, userCfg);
|
|
142
153
|
const ctx = buildDeclarativeUrlExpandContextFromInputs(appName, variablesPath, environment, {
|
|
143
154
|
userCfg,
|
|
144
155
|
remoteServer,
|
|
156
|
+
developerRootDomain,
|
|
145
157
|
developerIdRaw,
|
|
146
158
|
infraTlsEnabled,
|
|
147
159
|
appScopedFlag
|