@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
|
@@ -35,8 +35,23 @@ 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';
|
|
44
|
+
/** Automation / af-server: wait for edge routing before dataplane (nginx + Traefik). */
|
|
45
|
+
const CONTROLLER_HEALTH_RETRY_ATTEMPTS = 30;
|
|
46
|
+
const CONTROLLER_HEALTH_RETRY_DELAY_MS = 2000;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {number} ms
|
|
50
|
+
* @returns {Promise<void>}
|
|
51
|
+
*/
|
|
52
|
+
function sleep(ms) {
|
|
53
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
54
|
+
}
|
|
40
55
|
|
|
41
56
|
/**
|
|
42
57
|
* Check if controller is reachable (health endpoint).
|
|
@@ -72,19 +87,46 @@ function promptForControllerUrl(currentUrl) {
|
|
|
72
87
|
}
|
|
73
88
|
|
|
74
89
|
/**
|
|
75
|
-
*
|
|
76
|
-
* @
|
|
77
|
-
* @
|
|
90
|
+
* Poll controller /health until success or attempts exhausted.
|
|
91
|
+
* @param {string} baseUrl - Controller base URL (no trailing slash)
|
|
92
|
+
* @param {{ attempts?: number, delayMs?: number }} [opts]
|
|
93
|
+
* @returns {Promise<boolean>}
|
|
78
94
|
*/
|
|
79
|
-
async function
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
let
|
|
84
|
-
|
|
85
|
-
|
|
95
|
+
async function waitForControllerHealthy(baseUrl, opts = {}) {
|
|
96
|
+
const attempts = opts.attempts ?? CONTROLLER_HEALTH_RETRY_ATTEMPTS;
|
|
97
|
+
const delayMs = opts.delayMs ?? CONTROLLER_HEALTH_RETRY_DELAY_MS;
|
|
98
|
+
const normalized = baseUrl.replace(/\/+$/, '');
|
|
99
|
+
for (let i = 0; i < attempts; i += 1) {
|
|
100
|
+
if (await isControllerHealthy(normalized)) {
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
if (i < attempts - 1) {
|
|
104
|
+
await sleep(delayMs);
|
|
105
|
+
}
|
|
86
106
|
}
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @param {string} controllerUrl
|
|
112
|
+
* @returns {string}
|
|
113
|
+
*/
|
|
114
|
+
function buildControllerUnavailableMessage(controllerUrl) {
|
|
115
|
+
return (
|
|
116
|
+
`Controller at ${controllerUrl} is not responding after ${CONTROLLER_HEALTH_RETRY_ATTEMPTS} health checks ` +
|
|
117
|
+
`(~${Math.round((CONTROLLER_HEALTH_RETRY_ATTEMPTS * CONTROLLER_HEALTH_RETRY_DELAY_MS) / 1000)}s). ` +
|
|
118
|
+
'Miso may be up on localhost before nginx/Traefik routes are ready. ' +
|
|
119
|
+
'On the builder host run: sudo systemctl start aifabrix-sync-dev-traefik-nginx.service ' +
|
|
120
|
+
'then retry: aifabrix up-dataplane --force'
|
|
121
|
+
);
|
|
122
|
+
}
|
|
87
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Prompt for a new controller URL and verify it responds to health checks.
|
|
126
|
+
* @param {string} controllerUrl - URL that failed the initial health check
|
|
127
|
+
* @returns {Promise<string>} Verified controller URL
|
|
128
|
+
*/
|
|
129
|
+
async function promptForHealthyControllerUrl(controllerUrl) {
|
|
88
130
|
logger.log(chalk.yellow(`\nController at ${controllerUrl} is not responding (health check failed).\n`));
|
|
89
131
|
const newUrl = await promptForControllerUrl(controllerUrl);
|
|
90
132
|
if (!newUrl) {
|
|
@@ -99,7 +141,7 @@ async function resolveControllerUrlWithHealthCheck() {
|
|
|
99
141
|
|
|
100
142
|
await setControllerUrl(newUrl);
|
|
101
143
|
const normalizedNew = newUrl.trim().replace(/\/+$/, '');
|
|
102
|
-
healthy = await isControllerHealthy(normalizedNew);
|
|
144
|
+
const healthy = await isControllerHealthy(normalizedNew);
|
|
103
145
|
if (!healthy) {
|
|
104
146
|
throw new Error(`Controller at ${normalizedNew} is not responding. Ensure the controller is running and reachable, then run "aifabrix up-dataplane" again.`);
|
|
105
147
|
}
|
|
@@ -108,6 +150,28 @@ async function resolveControllerUrlWithHealthCheck() {
|
|
|
108
150
|
return normalizedNew;
|
|
109
151
|
}
|
|
110
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Resolve controller URL and ensure it is healthy; if not, prompt once for new URL (TTY only).
|
|
155
|
+
* @returns {Promise<string>} Controller URL to use
|
|
156
|
+
* @throws {Error} If controller unavailable and user aborts or new URL still unhealthy
|
|
157
|
+
*/
|
|
158
|
+
async function resolveControllerUrlWithHealthCheck() {
|
|
159
|
+
let controllerUrl = await resolveControllerUrl();
|
|
160
|
+
controllerUrl = controllerUrl.replace(/\/+$/, '');
|
|
161
|
+
|
|
162
|
+
const healthy = await waitForControllerHealthy(controllerUrl);
|
|
163
|
+
if (healthy) {
|
|
164
|
+
return controllerUrl;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const isInteractive = Boolean(process.stdin && process.stdin.isTTY);
|
|
168
|
+
if (!isInteractive) {
|
|
169
|
+
throw new Error(buildControllerUnavailableMessage(controllerUrl));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return promptForHealthyControllerUrl(controllerUrl);
|
|
173
|
+
}
|
|
174
|
+
|
|
111
175
|
async function resolveUpDataplaneAuthContext(options, environmentKey) {
|
|
112
176
|
if (options.skipInfraCheck !== true) {
|
|
113
177
|
await assertDevInfraUp();
|
|
@@ -120,7 +184,9 @@ async function resolveUpDataplaneAuthContext(options, environmentKey) {
|
|
|
120
184
|
if (options.allowNonDev !== true) {
|
|
121
185
|
assertDevEnvironmentForDataplane(cfg);
|
|
122
186
|
}
|
|
123
|
-
|
|
187
|
+
if (options.platformInstall !== true) {
|
|
188
|
+
logger.log(formatSuccessLine(`Logged in (environment: ${environmentKey})`));
|
|
189
|
+
}
|
|
124
190
|
return { controllerUrl, authConfig };
|
|
125
191
|
}
|
|
126
192
|
|
|
@@ -223,6 +289,57 @@ async function computeDataplaneRunOverrides(options) {
|
|
|
223
289
|
return { runEnvKey, portOverride };
|
|
224
290
|
}
|
|
225
291
|
|
|
292
|
+
/**
|
|
293
|
+
* Resolve controller URL and auth for bootstrap vs device-login paths.
|
|
294
|
+
* @async
|
|
295
|
+
* @param {Object} options
|
|
296
|
+
* @param {string} environmentKey
|
|
297
|
+
* @returns {Promise<{ controllerUrl: string, authConfig: Object|null, useBootstrap: boolean }>}
|
|
298
|
+
*/
|
|
299
|
+
async function resolveUpDataplaneControllerContext(options, environmentKey) {
|
|
300
|
+
const useBootstrap = await canUseBootstrapProvision(options);
|
|
301
|
+
if (useBootstrap) {
|
|
302
|
+
if (options.skipInfraCheck !== true) {
|
|
303
|
+
await assertDevInfraUp();
|
|
304
|
+
}
|
|
305
|
+
const controllerUrl = await resolveControllerUrlWithHealthCheck();
|
|
306
|
+
const cfg = await config.getConfig();
|
|
307
|
+
if (options.allowNonDev !== true) {
|
|
308
|
+
assertDevEnvironmentForDataplane(cfg);
|
|
309
|
+
}
|
|
310
|
+
if (options.platformInstall !== true) {
|
|
311
|
+
logger.log(formatSuccessLine('Using bootstrap provision (no device login)'));
|
|
312
|
+
}
|
|
313
|
+
return { controllerUrl, authConfig: null, useBootstrap: true };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const authContext = await resolveUpDataplaneAuthContext(options, environmentKey);
|
|
317
|
+
return {
|
|
318
|
+
controllerUrl: authContext.controllerUrl,
|
|
319
|
+
authConfig: authContext.authConfig,
|
|
320
|
+
useBootstrap: false
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Register/bootstrap, deploy manifest, align credentials.
|
|
326
|
+
* @async
|
|
327
|
+
*/
|
|
328
|
+
async function provisionAndDeployDataplane(options, environmentKey, controllerUrl, useBootstrap, authConfig) {
|
|
329
|
+
if (useBootstrap) {
|
|
330
|
+
await bootstrapDataplaneApplication(options, controllerUrl, environmentKey, buildDataplaneImageRef);
|
|
331
|
+
} else {
|
|
332
|
+
await registerOrRotateDataplane(options, controllerUrl, environmentKey, authConfig);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
await deployDataplaneToController({
|
|
336
|
+
...options,
|
|
337
|
+
envKey: environmentKey,
|
|
338
|
+
...(useBootstrap ? { deploymentAuth: 'client-credentials' } : {})
|
|
339
|
+
});
|
|
340
|
+
logger.log('');
|
|
341
|
+
}
|
|
342
|
+
|
|
226
343
|
/**
|
|
227
344
|
* Handle up-dataplane command: ensure logged in, environment dev, ensure dataplane,
|
|
228
345
|
* register or rotate (if already registered), deploy (send manifest to controller),
|
|
@@ -239,43 +356,57 @@ async function computeDataplaneRunOverrides(options) {
|
|
|
239
356
|
* @throws {Error} If infra not up, controller unavailable, not logged in, environment not dev, or any step fails
|
|
240
357
|
*/
|
|
241
358
|
async function handleUpDataplane(options = {}) {
|
|
242
|
-
|
|
359
|
+
if (options.platformInstall !== true) {
|
|
360
|
+
logger.log(chalk.blue('Starting up-dataplane (register/rotate, deploy, then run dataplane locally)...\n'));
|
|
361
|
+
}
|
|
243
362
|
|
|
244
363
|
const environmentKey =
|
|
245
364
|
options && options.envKey
|
|
246
365
|
? String(options.envKey).trim().toLowerCase()
|
|
247
366
|
: await resolveEnvironment();
|
|
248
|
-
|
|
367
|
+
|
|
368
|
+
const { controllerUrl, authConfig, useBootstrap } = await resolveUpDataplaneControllerContext(
|
|
369
|
+
options,
|
|
370
|
+
environmentKey
|
|
371
|
+
);
|
|
249
372
|
|
|
250
373
|
await ensureDataplaneAppsFromTemplate();
|
|
251
|
-
// If envOutputPath target folder does not exist, set envOutputPath to null
|
|
252
374
|
validateEnvOutputPathFolderOrNull('dataplane');
|
|
253
|
-
|
|
254
375
|
refreshUrlsLocalRegistryForCurrentProject();
|
|
255
376
|
|
|
256
377
|
if (options.platformInstall !== true) {
|
|
257
378
|
emitDataplaneManifestLineFromBuilder();
|
|
258
379
|
}
|
|
259
380
|
|
|
260
|
-
await
|
|
261
|
-
|
|
262
|
-
await deployDataplaneToController({ ...options, envKey: environmentKey });
|
|
263
|
-
logger.log('');
|
|
381
|
+
await provisionAndDeployDataplane(options, environmentKey, controllerUrl, useBootstrap, authConfig);
|
|
264
382
|
|
|
265
|
-
await alignDataplaneCredentialsAfterDeploy(
|
|
383
|
+
await alignDataplaneCredentialsAfterDeploy(
|
|
384
|
+
{ ...options, useBootstrap },
|
|
385
|
+
controllerUrl
|
|
386
|
+
);
|
|
266
387
|
|
|
267
388
|
const { runEnvKey, portOverride } = await computeDataplaneRunOverrides(options);
|
|
268
389
|
|
|
269
|
-
|
|
390
|
+
const runAppOpts = {
|
|
270
391
|
skipEnvOutputPath: true,
|
|
271
392
|
env: runEnvKey,
|
|
272
393
|
port: portOverride,
|
|
273
394
|
registry: options.registry || undefined,
|
|
274
395
|
base: options.base !== false,
|
|
275
396
|
skipManifestMetadataLine: true
|
|
276
|
-
}
|
|
397
|
+
};
|
|
398
|
+
if (options.platformInstall === true) {
|
|
399
|
+
if (typeof options.onPollText === 'function') {
|
|
400
|
+
runAppOpts.onPollText = options.onPollText;
|
|
401
|
+
} else {
|
|
402
|
+
runAppOpts.silentPoll = true;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
await app.runApp('dataplane', runAppOpts);
|
|
277
406
|
|
|
278
|
-
|
|
407
|
+
if (options.platformInstall !== true) {
|
|
408
|
+
logger.log(formatSuccessParagraph('up-dataplane complete. Dataplane is registered, deployed in dev, and running locally.'));
|
|
409
|
+
}
|
|
279
410
|
}
|
|
280
411
|
|
|
281
412
|
module.exports = { handleUpDataplane, buildDataplaneImageRef };
|
package/lib/commands/up-miso.js
CHANGED
|
@@ -57,11 +57,15 @@ async function runMisoApps(options) {
|
|
|
57
57
|
base: useBaseImage,
|
|
58
58
|
skipManifestMetadataLine: true
|
|
59
59
|
};
|
|
60
|
-
const
|
|
60
|
+
const platformPollOpts =
|
|
61
|
+
options.platformInstall === true
|
|
62
|
+
? { silentPoll: true }
|
|
63
|
+
: {};
|
|
64
|
+
const keycloakRunOpts = { ...common, ...platformPollOpts };
|
|
61
65
|
if (imageMap.keycloak) {
|
|
62
66
|
keycloakRunOpts.image = imageMap.keycloak;
|
|
63
67
|
}
|
|
64
|
-
const misoRunOpts = { ...common };
|
|
68
|
+
const misoRunOpts = { ...common, ...platformPollOpts };
|
|
65
69
|
if (imageMap['miso-controller']) {
|
|
66
70
|
misoRunOpts.image = imageMap['miso-controller'];
|
|
67
71
|
}
|
|
@@ -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
|
|
@@ -61,6 +61,46 @@ function flagsForSetupPlatformMode(mode) {
|
|
|
61
61
|
};
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/** Platform app keys (setup does not modify `applications.<key>.proxy`; use `aifabrix run --proxy` / `--no-proxy`). */
|
|
65
|
+
const PLATFORM_APPLICATION_KEYS = Object.freeze(['keycloak', 'miso-controller', 'dataplane']);
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Apply setup platform mode to config.yaml infra topology (`traefik`, `tlsEnabled`, env scope).
|
|
69
|
+
* Does **not** change per-app `applications.<key>.proxy` — that is owned by `aifabrix run`.
|
|
70
|
+
*
|
|
71
|
+
* @param {Object} cfg - Mutable config root
|
|
72
|
+
* @param {unknown} mode
|
|
73
|
+
* @returns {Object} Same cfg reference
|
|
74
|
+
*/
|
|
75
|
+
function applyPlatformTopologyFlagsToConfig(cfg, mode) {
|
|
76
|
+
const normalized = normalizeSetupPlatformMode(mode);
|
|
77
|
+
const derived = flagsForSetupPlatformMode(normalized);
|
|
78
|
+
cfg.setupPlatformMode = normalized;
|
|
79
|
+
cfg.traefik = derived.traefik;
|
|
80
|
+
cfg.tlsEnabled = derived.tlsEnabled;
|
|
81
|
+
cfg.useEnvironmentScopedResources = derived.useEnvironmentScopedResources;
|
|
82
|
+
return cfg;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* True when persisted traefik/tls/env-scope flags disagree with setupPlatformMode.
|
|
87
|
+
*
|
|
88
|
+
* @param {Object} cfg
|
|
89
|
+
* @returns {boolean}
|
|
90
|
+
*/
|
|
91
|
+
function platformTopologyNeedsReconcile(cfg) {
|
|
92
|
+
if (!cfg || typeof cfg !== 'object' || !cfg.setupPlatformMode) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
const expected = flagsForSetupPlatformMode(cfg.setupPlatformMode || SETUP_PLATFORM_MODE.SINGLE);
|
|
96
|
+
if (Boolean(cfg.traefik) !== Boolean(expected.traefik)) return true;
|
|
97
|
+
if (Boolean(cfg.tlsEnabled) !== Boolean(expected.tlsEnabled)) return true;
|
|
98
|
+
if (Boolean(cfg.useEnvironmentScopedResources) !== Boolean(expected.useEnvironmentScopedResources)) {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
64
104
|
/**
|
|
65
105
|
* Interactive setup menu choices (full platform hidden until multi-env path ships).
|
|
66
106
|
* Traefik front door requires `remote-server` (from `aifabrix dev init`); without it only direct ports is offered.
|
|
@@ -90,8 +130,11 @@ function getSetupPlatformModePromptChoices(opts = {}) {
|
|
|
90
130
|
module.exports = {
|
|
91
131
|
SETUP_PLATFORM_MODE,
|
|
92
132
|
SETUP_PLATFORM_MODE_CLI_VALUES,
|
|
133
|
+
PLATFORM_APPLICATION_KEYS,
|
|
93
134
|
normalizeSetupPlatformMode,
|
|
94
135
|
flagsForSetupPlatformMode,
|
|
136
|
+
applyPlatformTopologyFlagsToConfig,
|
|
137
|
+
platformTopologyNeedsReconcile,
|
|
95
138
|
getSetupPlatformModePromptChoices
|
|
96
139
|
};
|
|
97
140
|
|
|
@@ -18,15 +18,37 @@ function isTerminalStatus(status) {
|
|
|
18
18
|
return status === 'completed' || status === 'failed' || status === 'cancelled';
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* True when pipeline calls should authenticate with a token header only (Bearer or x-client-token).
|
|
23
|
+
* @param {Object} authConfig
|
|
24
|
+
* @returns {boolean}
|
|
25
|
+
*/
|
|
26
|
+
function isTokenOnlyPipelineAuth(authConfig) {
|
|
27
|
+
if (!authConfig || !authConfig.token) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
if (authConfig.clientId || authConfig.clientSecret) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
return authConfig.type === 'bearer' || authConfig.type === 'client-token';
|
|
34
|
+
}
|
|
35
|
+
|
|
21
36
|
/**
|
|
22
37
|
* Convert authConfig to pipeline auth config format
|
|
23
38
|
* @param {Object} authConfig - Authentication configuration
|
|
24
39
|
* @returns {Object} Pipeline auth config
|
|
25
40
|
*/
|
|
26
41
|
function convertToPipelineAuthConfig(authConfig) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
42
|
+
if (isTokenOnlyPipelineAuth(authConfig)) {
|
|
43
|
+
return authConfig.type === 'client-token'
|
|
44
|
+
? { type: 'client-token', token: authConfig.token }
|
|
45
|
+
: { type: 'bearer', token: authConfig.token };
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
type: 'client-credentials',
|
|
49
|
+
clientId: authConfig.clientId,
|
|
50
|
+
clientSecret: authConfig.clientSecret
|
|
51
|
+
};
|
|
30
52
|
}
|
|
31
53
|
|
|
32
54
|
/**
|
|
@@ -118,6 +140,7 @@ async function processDeploymentStatusResponse(
|
|
|
118
140
|
|
|
119
141
|
module.exports = {
|
|
120
142
|
isTerminalStatus,
|
|
143
|
+
isTokenOnlyPipelineAuth,
|
|
121
144
|
convertToPipelineAuthConfig,
|
|
122
145
|
handleDeploymentStatusError,
|
|
123
146
|
extractDeploymentData,
|
|
@@ -20,7 +20,8 @@ const { getAuthUser } = require('../api/auth.api');
|
|
|
20
20
|
const { handleValidationResponse } = require('../utils/deployment-validation-helpers');
|
|
21
21
|
const {
|
|
22
22
|
convertToPipelineAuthConfig,
|
|
23
|
-
processDeploymentStatusResponse
|
|
23
|
+
processDeploymentStatusResponse,
|
|
24
|
+
isTokenOnlyPipelineAuth
|
|
24
25
|
} = require('./deployer-status');
|
|
25
26
|
const { resolvePollIntervalFromController } = require('./poll-interval');
|
|
26
27
|
const { createDeployPollHandlers } = require('./deploy-poll-ui');
|
|
@@ -54,8 +55,8 @@ async function buildValidationData(manifest, validatedEnvKey, authConfig, option
|
|
|
54
55
|
manifest?.repository?.repositoryUrl ||
|
|
55
56
|
`https://github.com/aifabrix/${manifest.key}`;
|
|
56
57
|
|
|
57
|
-
if (authConfig
|
|
58
|
-
const pipelineAuthConfig =
|
|
58
|
+
if (isTokenOnlyPipelineAuth(authConfig)) {
|
|
59
|
+
const pipelineAuthConfig = convertToPipelineAuthConfig(authConfig);
|
|
59
60
|
const validationData = {
|
|
60
61
|
clientId: manifest.key,
|
|
61
62
|
repositoryUrl,
|
|
@@ -181,15 +182,7 @@ function buildDeploymentData(validateToken, authConfig, options) {
|
|
|
181
182
|
imageTag: imageTag
|
|
182
183
|
};
|
|
183
184
|
|
|
184
|
-
|
|
185
|
-
? { type: 'bearer', token: authConfig.token }
|
|
186
|
-
: {
|
|
187
|
-
type: 'client-credentials',
|
|
188
|
-
clientId: authConfig.clientId,
|
|
189
|
-
clientSecret: authConfig.clientSecret
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
return { deployData, pipelineAuthConfig };
|
|
185
|
+
return { deployData, pipelineAuthConfig: convertToPipelineAuthConfig(authConfig) };
|
|
193
186
|
}
|
|
194
187
|
|
|
195
188
|
/**
|
|
@@ -254,8 +247,8 @@ async function sendDeploymentRequest(url, envKey, validateToken, authConfig, opt
|
|
|
254
247
|
const validatedEnvKey = validateEnvironmentKey(envKey);
|
|
255
248
|
const maxRetries = options.maxRetries || 3;
|
|
256
249
|
|
|
257
|
-
const
|
|
258
|
-
if (!
|
|
250
|
+
const useTokenOnlyAuth = isTokenOnlyPipelineAuth(authConfig);
|
|
251
|
+
if (!useTokenOnlyAuth) {
|
|
259
252
|
validateDeploymentCredentials(authConfig);
|
|
260
253
|
}
|
|
261
254
|
|
|
@@ -16,6 +16,7 @@ const {
|
|
|
16
16
|
} = require('../utils/cli-test-layout-chalk');
|
|
17
17
|
const logger = require('../utils/logger');
|
|
18
18
|
const { generateExternalReadmeContent } = require('../utils/external-readme');
|
|
19
|
+
const { getAuthCredentialTemplate } = require('../utils/authentication-shape');
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* Validates system type from downloaded application
|
|
@@ -159,7 +160,15 @@ function generateReadme(systemKey, application, dataSources) {
|
|
|
159
160
|
};
|
|
160
161
|
});
|
|
161
162
|
|
|
162
|
-
const
|
|
163
|
+
const auth = application.authentication;
|
|
164
|
+
let authType = auth?.authType;
|
|
165
|
+
if (auth?.authType === 'credential') {
|
|
166
|
+
authType = getAuthCredentialTemplate(auth)?.method || authType;
|
|
167
|
+
} else if (auth?.authType === 'apiProvider') {
|
|
168
|
+
authType = auth.apiProvider?.apiProviderKey || 'apiProvider';
|
|
169
|
+
} else {
|
|
170
|
+
authType = auth?.type || auth?.method || authType;
|
|
171
|
+
}
|
|
163
172
|
return generateExternalReadmeContent({
|
|
164
173
|
appName: systemKey,
|
|
165
174
|
systemKey,
|
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
const fs = require('fs');
|
|
12
12
|
const handlebars = require('handlebars');
|
|
13
13
|
const path = require('path');
|
|
14
|
+
const {
|
|
15
|
+
writeTraefikTlsDynamicConfig
|
|
16
|
+
} = require('./traefik-tls-dynamic');
|
|
14
17
|
|
|
15
18
|
/**
|
|
16
19
|
* Normalize a host path for Docker Compose bind mounts. Docker Desktop sends
|
|
@@ -91,6 +94,40 @@ function validateTraefikConfig(traefikConfig) {
|
|
|
91
94
|
return { valid: errors.length === 0, errors };
|
|
92
95
|
}
|
|
93
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Normalize Traefik config for compose template (bind mounts + dynamic TLS file).
|
|
99
|
+
*
|
|
100
|
+
* @param {string} infraDir
|
|
101
|
+
* @param {Object|boolean} traefikConfig
|
|
102
|
+
* @returns {Object|boolean}
|
|
103
|
+
*/
|
|
104
|
+
function buildTraefikComposeContext(infraDir, traefikConfig) {
|
|
105
|
+
if (!traefikConfig || typeof traefikConfig !== 'object') {
|
|
106
|
+
return traefikConfig;
|
|
107
|
+
}
|
|
108
|
+
const traefikForCompose = {
|
|
109
|
+
...traefikConfig,
|
|
110
|
+
trustForwardedHeaders: !!traefikConfig.trustForwardedHeaders,
|
|
111
|
+
...(traefikConfig.certFile
|
|
112
|
+
? { certFile: toDockerBindMountSource(traefikConfig.certFile) }
|
|
113
|
+
: {}),
|
|
114
|
+
...(traefikConfig.keyFile
|
|
115
|
+
? { keyFile: toDockerBindMountSource(traefikConfig.keyFile) }
|
|
116
|
+
: {})
|
|
117
|
+
};
|
|
118
|
+
const tlsDynamic = writeTraefikTlsDynamicConfig(infraDir, traefikConfig);
|
|
119
|
+
if (!tlsDynamic) {
|
|
120
|
+
return traefikForCompose;
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
...traefikForCompose,
|
|
124
|
+
tlsDynamicBind: toDockerBindMountSource(tlsDynamic.hostPath),
|
|
125
|
+
tlsDynamicContainerPath: tlsDynamic.containerPath,
|
|
126
|
+
certContainerPath: tlsDynamic.certContainerPath,
|
|
127
|
+
keyContainerPath: tlsDynamic.keyContainerPath
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
94
131
|
/**
|
|
95
132
|
* Generate docker-compose file from template
|
|
96
133
|
* @param {string} templatePath - Path to compose template
|
|
@@ -129,21 +166,12 @@ function generateComposeFile(templatePath, devId, idNum, ports, infraDir, option
|
|
|
129
166
|
const redisCommanderConfig = options.redisCommander && typeof options.redisCommander.enabled === 'boolean'
|
|
130
167
|
? options.redisCommander
|
|
131
168
|
: { enabled: true };
|
|
132
|
-
const traefikForCompose =
|
|
133
|
-
? {
|
|
134
|
-
...traefikConfig,
|
|
135
|
-
trustForwardedHeaders: !!traefikConfig.trustForwardedHeaders,
|
|
136
|
-
...(traefikConfig.certFile
|
|
137
|
-
? { certFile: toDockerBindMountSource(traefikConfig.certFile) }
|
|
138
|
-
: {}),
|
|
139
|
-
...(traefikConfig.keyFile
|
|
140
|
-
? { keyFile: toDockerBindMountSource(traefikConfig.keyFile) }
|
|
141
|
-
: {})
|
|
142
|
-
}
|
|
143
|
-
: traefikConfig;
|
|
169
|
+
const traefikForCompose = buildTraefikComposeContext(infraDir, traefikConfig);
|
|
144
170
|
const initScriptsBind = toDockerBindMountSource(path.join(infraDir, 'init-scripts'));
|
|
145
171
|
const composeContent = template({
|
|
146
172
|
devId: devId,
|
|
173
|
+
idNum,
|
|
174
|
+
traefikScoped: idNum > 0,
|
|
147
175
|
postgresPort: ports.postgres,
|
|
148
176
|
redisPort: ports.redis,
|
|
149
177
|
pgadminPort: ports.pgadmin,
|
|
@@ -117,10 +117,12 @@ async function waitForServices(devId = null, opts = {}) {
|
|
|
117
117
|
const maxAttempts = 30;
|
|
118
118
|
const delay = 2000; // 2 seconds
|
|
119
119
|
const { pgadmin = true, redisCommander = true, traefik = false } = opts;
|
|
120
|
+
let lastHealth = {};
|
|
120
121
|
|
|
121
122
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
122
123
|
const health = await checkInfraHealth(devId, { pgadmin, redisCommander, traefik });
|
|
123
|
-
|
|
124
|
+
lastHealth = health;
|
|
125
|
+
const allHealthy = Object.values(health).every((status) => status === 'healthy');
|
|
124
126
|
|
|
125
127
|
if (allHealthy) {
|
|
126
128
|
return;
|
|
@@ -128,11 +130,21 @@ async function waitForServices(devId = null, opts = {}) {
|
|
|
128
130
|
|
|
129
131
|
if (attempt < maxAttempts) {
|
|
130
132
|
logger.log(`Waiting for services to be healthy... (${attempt}/${maxAttempts})`);
|
|
131
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
133
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
132
134
|
}
|
|
133
135
|
}
|
|
134
136
|
|
|
135
|
-
|
|
137
|
+
const timeoutSec = (maxAttempts * delay) / 1000;
|
|
138
|
+
const unhealthy = Object.entries(lastHealth)
|
|
139
|
+
.filter(([, status]) => status !== 'healthy')
|
|
140
|
+
.map(([name, status]) => `${name}=${status}`)
|
|
141
|
+
.join(', ');
|
|
142
|
+
const detail = unhealthy || 'unknown';
|
|
143
|
+
throw new Error(
|
|
144
|
+
`Infrastructure services did not become healthy within ${timeoutSec}s. ` +
|
|
145
|
+
`Unhealthy: ${detail}. ` +
|
|
146
|
+
'Check container logs (e.g. `docker ps`, `aifabrix doctor`, or `aifabrix logs` for platform apps).'
|
|
147
|
+
);
|
|
136
148
|
}
|
|
137
149
|
|
|
138
150
|
/**
|