@aifabrix/builder 2.48.0 → 2.49.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.temp/jest-protection-convert/run-IFN2nv/.protection/hubspot-companies.json +1 -0
- package/.temp/jest-protection-convert/run-IvmKLO/.protection/hubspot-companies.yaml +17 -0
- package/.temp/jest-protection-convert/run-SdvblF/.protection/hubspot-companies.json +1 -0
- package/.temp/jest-protection-convert/run-b51TsW/.protection/hubspot-companies.json +1 -0
- package/.temp/jest-protection-convert/run-fiLmb1/.protection/hubspot-companies.json +1 -0
- package/.temp/jest-protection-convert/run-qYGHMU/.protection/hubspot-companies.yaml +17 -0
- package/.temp/jest-protection-fixtures/hubspot-Mq5re8/hubspot-companies.yaml +17 -0
- package/.temp/jest-protection-fixtures/hubspot-f3u4YL/hubspot-companies.yaml +17 -0
- package/.temp/jest-protection-fixtures/hubspot-iQtd1O/hubspot-companies.yaml +17 -0
- package/.temp/jest-protection-fixtures/hubspot-mdmz5M/hubspot-companies.yaml +17 -0
- package/.temp/jest-protection-fixtures/hubspot-qbKYxQ/hubspot-companies.yaml +17 -0
- package/.temp/jest-protection-fixtures/hubspot-yzlJy6/hubspot-companies.yaml +17 -0
- package/.temp/validation/summary.txt +5 -0
- package/jest.isolated-projects.js +3 -0
- package/jest.projects.js +12 -0
- package/lib/api/applications.api.js +23 -0
- package/lib/api/dev.api.js +1 -1
- package/lib/api/index.js +5 -2
- package/lib/api/types/dev.types.js +3 -2
- package/lib/app/deploy-config.js +6 -4
- package/lib/app/register.js +1 -1
- package/lib/app/run-container-start.js +0 -9
- package/lib/app/run-helpers.js +8 -4
- package/lib/app/run-reload-sync.js +16 -18
- package/lib/app/run-stop-containers.js +48 -5
- package/lib/app/run.js +6 -3
- package/lib/app/show.js +2 -2
- package/lib/cli/infra-guided-auth.js +42 -6
- package/lib/cli/infra-guided-footers.js +164 -0
- package/lib/cli/infra-guided-spinners.js +57 -0
- package/lib/cli/infra-guided.js +41 -179
- package/lib/cli/setup-app.js +1 -1
- package/lib/cli/setup-dev.js +3 -2
- package/lib/cli/setup-infra.js +3 -2
- package/lib/cli/setup-platform.js +3 -0
- package/lib/commands/login-device.js +6 -2
- package/lib/commands/login.js +4 -1
- package/lib/commands/setup-modes.js +19 -3
- package/lib/commands/setup-platform-auth.js +25 -0
- package/lib/commands/setup-prompts-admin.js +9 -0
- package/lib/commands/setup.js +60 -8
- package/lib/commands/up-dataplane-bootstrap.js +149 -0
- package/lib/commands/up-dataplane-credentials.js +19 -4
- package/lib/commands/up-dataplane.js +74 -11
- package/lib/core/secrets-env-declarative-expand.js +35 -23
- package/lib/core/setup-platform-mode.js +43 -0
- package/lib/deployment/deployer-status.js +26 -3
- package/lib/deployment/deployer.js +7 -14
- package/lib/external-system/download-helpers.js +10 -1
- package/lib/infrastructure/compose.js +40 -12
- package/lib/infrastructure/services.js +15 -3
- package/lib/infrastructure/traefik-tls-dynamic.js +88 -0
- package/lib/utils/app-bootstrap-api.js +53 -0
- package/lib/utils/app-run-containers.js +14 -3
- package/lib/utils/applications-config-defaults.js +19 -3
- package/lib/utils/compose-generate-docker-compose.js +16 -5
- package/lib/utils/compose-generator.js +34 -15
- package/lib/utils/compose-traefik-ingress-base.js +13 -14
- package/lib/utils/config-paths.js +6 -1
- package/lib/utils/dev-user-groups.js +1 -0
- package/lib/utils/docker-daemon-tls-ca.js +94 -57
- package/lib/utils/docker-reload-mount.js +12 -1
- package/lib/utils/external-env-template.js +53 -18
- package/lib/utils/health-check-poll-ui.js +84 -0
- package/lib/utils/health-check-url.js +54 -34
- package/lib/utils/health-check.js +43 -16
- package/lib/utils/infra-optional-service-flags.js +31 -2
- package/lib/utils/mutagen.js +7 -4
- package/lib/utils/platform-controller-url.js +23 -1
- package/lib/utils/refresh-remote-settings.js +67 -0
- package/lib/utils/remote-host-for-front-door.js +43 -0
- package/lib/utils/run-cli-flags.js +37 -1
- package/lib/utils/url-declarative-public-base.js +11 -4
- package/lib/utils/url-declarative-resolve-build-urls.js +11 -2
- package/lib/utils/url-declarative-resolve-expand-token.js +12 -1
- package/lib/utils/url-declarative-resolve-load-doc.js +30 -11
- package/lib/utils/url-declarative-resolve.js +2 -0
- package/lib/utils/urls-local-registry-scan.js +95 -10
- package/lib/utils/urls-local-registry.js +24 -1
- package/lib/utils/worker-sidecars.js +2 -2
- package/lib/validation/external-system-auth-rules.js +45 -7
- package/package.json +2 -1
- package/scripts/test-dataplane-bootstrap.js +331 -0
- package/templates/applications/dataplane/env.template +22 -5
- package/templates/applications/dataplane/rbac.yaml +11 -0
- package/templates/applications/miso-controller/application.yaml +1 -1
- package/templates/applications/miso-controller/env.template +3 -0
- package/templates/infra/compose.yaml.hbs +13 -9
- package/templates/python/docker-compose.hbs +3 -0
- package/templates/typescript/docker-compose.hbs +3 -0
|
@@ -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
|
/**
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Traefik v3 dynamic TLS configuration (file provider).
|
|
3
|
+
* Static CLI flags for certificate stores were removed in Traefik v3.
|
|
4
|
+
*
|
|
5
|
+
* @fileoverview Write traefik-tls.yaml for infra compose
|
|
6
|
+
* @author AI Fabrix Team
|
|
7
|
+
* @version 1.0.0
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
const TRAEFIK_TLS_DYNAMIC_BASENAME = 'traefik-tls.yaml';
|
|
16
|
+
const TRAEFIK_TLS_CONTAINER_PATH = '/etc/traefik/dynamic/tls.yaml';
|
|
17
|
+
const TRAEFIK_CERT_CONTAINER_PATH = '/etc/traefik/certs/tls.crt';
|
|
18
|
+
const TRAEFIK_KEY_CONTAINER_PATH = '/etc/traefik/certs/tls.key';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {string} storeName
|
|
22
|
+
* @returns {string}
|
|
23
|
+
*/
|
|
24
|
+
function buildTraefikTlsDynamicYaml(storeName) {
|
|
25
|
+
const store = String(storeName || 'default').trim() || 'default';
|
|
26
|
+
const certLine = ` certFile: ${TRAEFIK_CERT_CONTAINER_PATH}`;
|
|
27
|
+
const keyLine = ` keyFile: ${TRAEFIK_KEY_CONTAINER_PATH}`;
|
|
28
|
+
const blocks = [
|
|
29
|
+
'tls:',
|
|
30
|
+
' stores:',
|
|
31
|
+
' default:',
|
|
32
|
+
' defaultCertificate:',
|
|
33
|
+
certLine,
|
|
34
|
+
keyLine
|
|
35
|
+
];
|
|
36
|
+
if (store !== 'default') {
|
|
37
|
+
blocks.push(` ${store}:`);
|
|
38
|
+
blocks.push(' defaultCertificate:');
|
|
39
|
+
blocks.push(certLine);
|
|
40
|
+
blocks.push(keyLine);
|
|
41
|
+
}
|
|
42
|
+
return `${blocks.join('\n')}\n`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param {Object} traefikConfig
|
|
47
|
+
* @returns {boolean}
|
|
48
|
+
*/
|
|
49
|
+
function traefikConfigHasTlsFiles(traefikConfig) {
|
|
50
|
+
return Boolean(
|
|
51
|
+
traefikConfig &&
|
|
52
|
+
traefikConfig.certStore &&
|
|
53
|
+
traefikConfig.certFile &&
|
|
54
|
+
traefikConfig.keyFile
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Write dynamic TLS config for Traefik file provider when cert store + files are set.
|
|
60
|
+
*
|
|
61
|
+
* @param {string} infraDir
|
|
62
|
+
* @param {Object} traefikConfig
|
|
63
|
+
* @returns {{ hostPath: string, containerPath: string, certContainerPath: string, keyContainerPath: string }|null}
|
|
64
|
+
*/
|
|
65
|
+
function writeTraefikTlsDynamicConfig(infraDir, traefikConfig) {
|
|
66
|
+
if (!traefikConfigHasTlsFiles(traefikConfig)) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
const hostPath = path.join(infraDir, TRAEFIK_TLS_DYNAMIC_BASENAME);
|
|
70
|
+
const content = buildTraefikTlsDynamicYaml(traefikConfig.certStore);
|
|
71
|
+
fs.writeFileSync(hostPath, content, { mode: 0o600 });
|
|
72
|
+
return {
|
|
73
|
+
hostPath,
|
|
74
|
+
containerPath: TRAEFIK_TLS_CONTAINER_PATH,
|
|
75
|
+
certContainerPath: TRAEFIK_CERT_CONTAINER_PATH,
|
|
76
|
+
keyContainerPath: TRAEFIK_KEY_CONTAINER_PATH
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = {
|
|
81
|
+
TRAEFIK_TLS_DYNAMIC_BASENAME,
|
|
82
|
+
TRAEFIK_TLS_CONTAINER_PATH,
|
|
83
|
+
TRAEFIK_CERT_CONTAINER_PATH,
|
|
84
|
+
TRAEFIK_KEY_CONTAINER_PATH,
|
|
85
|
+
buildTraefikTlsDynamicYaml,
|
|
86
|
+
traefikConfigHasTlsFiles,
|
|
87
|
+
writeTraefikTlsDynamicConfig
|
|
88
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const { formatBlockingError } = require('./cli-test-layout-chalk');
|
|
2
|
+
/**
|
|
3
|
+
* Bootstrap API utilities for install-time application provisioning.
|
|
4
|
+
*
|
|
5
|
+
* @fileoverview Bootstrap API call helpers
|
|
6
|
+
* @author AI Fabrix Team
|
|
7
|
+
* @version 1.0.0
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const chalk = require('chalk');
|
|
11
|
+
const logger = require('./logger');
|
|
12
|
+
const { bootstrapApplication } = require('../api/applications.api');
|
|
13
|
+
const { formatApiError } = require('./api-error-handler');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Call bootstrap provisioning API.
|
|
17
|
+
* @async
|
|
18
|
+
* @param {string} apiUrl - Controller URL
|
|
19
|
+
* @param {string} encryptionKey - Shared ENCRYPTION_KEY (never log)
|
|
20
|
+
* @param {string} environment - Environment key
|
|
21
|
+
* @param {Object} registrationData - Registration payload
|
|
22
|
+
* @returns {Promise<Object>} Application data (credentials only when newly created)
|
|
23
|
+
*/
|
|
24
|
+
async function callBootstrapApi(apiUrl, encryptionKey, environment, registrationData) {
|
|
25
|
+
try {
|
|
26
|
+
const response = await bootstrapApplication(apiUrl, environment, encryptionKey, registrationData);
|
|
27
|
+
|
|
28
|
+
if (!response.success) {
|
|
29
|
+
const formattedError = response.formattedError || formatApiError(response, apiUrl);
|
|
30
|
+
logger.error(formattedError);
|
|
31
|
+
logger.error(chalk.gray(`\nController URL: ${apiUrl}`));
|
|
32
|
+
throw new Error(formattedError);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const apiResponse = response.data;
|
|
36
|
+
if (apiResponse && apiResponse.data) {
|
|
37
|
+
return apiResponse.data;
|
|
38
|
+
}
|
|
39
|
+
if (apiResponse && (apiResponse.application || apiResponse.status)) {
|
|
40
|
+
return apiResponse;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
logger.error(formatBlockingError('Invalid bootstrap response: missing application data'));
|
|
44
|
+
throw new Error('Invalid bootstrap response');
|
|
45
|
+
} catch (error) {
|
|
46
|
+
logger.error(formatBlockingError('Bootstrap API call failed'));
|
|
47
|
+
logger.error(chalk.gray(`Controller URL: ${apiUrl}`));
|
|
48
|
+
logger.error(chalk.gray(`Error: ${error.message}`));
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = { callBootstrapApi };
|
|
@@ -136,10 +136,19 @@ async function checkContainerRunning(appName, developerId, debug = false, scopeO
|
|
|
136
136
|
* @param {number|string} developerId - Developer ID (0 = default infra, > 0 = developer-specific; string allowed)
|
|
137
137
|
* @param {boolean} [debug=false] - Enable debug logging
|
|
138
138
|
*/
|
|
139
|
-
async function stopAndRemoveContainer(
|
|
139
|
+
async function stopAndRemoveContainer(
|
|
140
|
+
appName,
|
|
141
|
+
developerId,
|
|
142
|
+
debug = false,
|
|
143
|
+
scopeOpts = null,
|
|
144
|
+
opts = {}
|
|
145
|
+
) {
|
|
146
|
+
const suppressLog = opts && opts.suppressLog === true;
|
|
140
147
|
try {
|
|
141
148
|
const containerName = getContainerName(appName, developerId, scopeOpts);
|
|
142
|
-
|
|
149
|
+
if (!suppressLog) {
|
|
150
|
+
logger.log(formatProgress(`Stopping container ${containerName}…`));
|
|
151
|
+
}
|
|
143
152
|
const stopCmd = `docker stop ${containerName}`;
|
|
144
153
|
if (debug) {
|
|
145
154
|
logger.log(chalk.gray(`[DEBUG] Executing: ${stopCmd}`));
|
|
@@ -150,7 +159,9 @@ async function stopAndRemoveContainer(appName, developerId, debug = false, scope
|
|
|
150
159
|
logger.log(chalk.gray(`[DEBUG] Executing: ${rmCmd}`));
|
|
151
160
|
}
|
|
152
161
|
await execWithDockerEnv(rmCmd);
|
|
153
|
-
|
|
162
|
+
if (!suppressLog) {
|
|
163
|
+
logger.log(formatSuccessLine(`Container ${containerName} stopped and removed`));
|
|
164
|
+
}
|
|
154
165
|
} catch (error) {
|
|
155
166
|
if (debug) {
|
|
156
167
|
logger.log(chalk.gray(`[DEBUG] Stop/remove container error: ${error.message}`));
|
|
@@ -120,8 +120,8 @@ function coerceTriStateBool(v) {
|
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
/**
|
|
123
|
-
*
|
|
124
|
-
*
|
|
123
|
+
* Stored per-app `proxy` preference from config (may be stale when `traefik: false`).
|
|
124
|
+
* Use {@link getEffectiveApplicationsRunProxyHint} for runtime / display.
|
|
125
125
|
*
|
|
126
126
|
* @param {Object|null|undefined} userConfig - Parsed config.yaml root
|
|
127
127
|
* @param {string} appKey - Application key
|
|
@@ -183,6 +183,21 @@ async function persistApplicationRunProxyFlag(appKey, proxyEnabled) {
|
|
|
183
183
|
await config.saveConfig(cfg);
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
/**
|
|
187
|
+
* Effective proxy hint for URL expansion and CLI display.
|
|
188
|
+
* Always **false** when infra Traefik is off — stored `applications.<app>.proxy` is ignored.
|
|
189
|
+
*
|
|
190
|
+
* @param {Object|null|undefined} userConfig
|
|
191
|
+
* @param {string} appKey
|
|
192
|
+
* @returns {boolean}
|
|
193
|
+
*/
|
|
194
|
+
function getEffectiveApplicationsRunProxyHint(userConfig, appKey) {
|
|
195
|
+
if (!userConfig || userConfig.traefik !== true) {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
return getApplicationsRunProxyHint(userConfig, appKey);
|
|
199
|
+
}
|
|
200
|
+
|
|
186
201
|
/**
|
|
187
202
|
* Whether declarative `url://*` expansion should treat Traefik as on (infra Traefik + per-app `proxy: true`).
|
|
188
203
|
*
|
|
@@ -191,11 +206,12 @@ async function persistApplicationRunProxyFlag(appKey, proxyEnabled) {
|
|
|
191
206
|
* @returns {boolean}
|
|
192
207
|
*/
|
|
193
208
|
function isDeclarativeTraefikUrlsEnabled(userConfig, appKey) {
|
|
194
|
-
return
|
|
209
|
+
return getEffectiveApplicationsRunProxyHint(userConfig, appKey);
|
|
195
210
|
}
|
|
196
211
|
|
|
197
212
|
module.exports = {
|
|
198
213
|
getApplicationsRunProxyHint,
|
|
214
|
+
getEffectiveApplicationsRunProxyHint,
|
|
199
215
|
isApplicationsReloadDefaultOn,
|
|
200
216
|
isDeclarativeTraefikUrlsEnabled,
|
|
201
217
|
isPreferLocalEnvOutputPath,
|
|
@@ -144,6 +144,8 @@ function buildComposeTemplateContext(ctx) {
|
|
|
144
144
|
dbInitEnvFile: ctx.dbInitEnvFileAbsolutePath,
|
|
145
145
|
databasePasswords: ctx.databasePasswords,
|
|
146
146
|
devId: ctx.idNum,
|
|
147
|
+
idNum: ctx.idNum,
|
|
148
|
+
traefikScoped: ctx.idNum > 0,
|
|
147
149
|
networkName: ctx.networkName,
|
|
148
150
|
containerName: ctx.containerName,
|
|
149
151
|
misoEnvironment: resolveMisoEnvironment(ctx.options),
|
|
@@ -201,7 +203,12 @@ async function readDatabasePasswordsIfNeeded(readDatabasePasswords, requiresData
|
|
|
201
203
|
* @returns {Promise<object>}
|
|
202
204
|
*/
|
|
203
205
|
async function loadComposeHead(deps, appName, appConfig, options) {
|
|
204
|
-
const {
|
|
206
|
+
const {
|
|
207
|
+
loadDockerComposeTemplate,
|
|
208
|
+
resolveComposeImageOverrideString,
|
|
209
|
+
getDeveloperIdAndNumeric,
|
|
210
|
+
getFrontDoorHostContextForCompose
|
|
211
|
+
} = deps;
|
|
205
212
|
const language = appConfig.build?.language || appConfig.language || 'typescript';
|
|
206
213
|
const template = loadDockerComposeTemplate(language);
|
|
207
214
|
const port = options.port || appConfig.port || 3000;
|
|
@@ -209,10 +216,13 @@ async function loadComposeHead(deps, appName, appConfig, options) {
|
|
|
209
216
|
const { devId, idNum } = await getDeveloperIdAndNumeric();
|
|
210
217
|
const { scoped } = resolveScopedComposeOpts(options);
|
|
211
218
|
let remoteServer = null;
|
|
212
|
-
|
|
213
|
-
|
|
219
|
+
let developerRootDomain = null;
|
|
220
|
+
if (typeof getFrontDoorHostContextForCompose === 'function') {
|
|
221
|
+
const frontDoorCtx = await getFrontDoorHostContextForCompose();
|
|
222
|
+
remoteServer = frontDoorCtx.remoteServer;
|
|
223
|
+
developerRootDomain = frontDoorCtx.developerRootDomain;
|
|
214
224
|
}
|
|
215
|
-
return { template, port, imageOverride, devId, idNum, scoped, remoteServer };
|
|
225
|
+
return { template, port, imageOverride, devId, idNum, scoped, remoteServer, developerRootDomain };
|
|
216
226
|
}
|
|
217
227
|
|
|
218
228
|
/**
|
|
@@ -224,12 +234,13 @@ async function loadComposeHead(deps, appName, appConfig, options) {
|
|
|
224
234
|
*/
|
|
225
235
|
function buildComposeLayouts(deps, appName, appConfig, head, options) {
|
|
226
236
|
const { buildNetworkAndContainerNames, buildServiceConfig, buildVolumesConfig, buildNetworksConfig } = deps;
|
|
227
|
-
const { port, imageOverride, devId, idNum, scoped, remoteServer } = head;
|
|
237
|
+
const { port, imageOverride, devId, idNum, scoped, remoteServer, developerRootDomain } = head;
|
|
228
238
|
const { networkName, containerName } = buildNetworkAndContainerNames(appName, devId, idNum, scoped);
|
|
229
239
|
const serviceConfig = buildServiceConfig(appName, appConfig, port, devId, {
|
|
230
240
|
imageOverride,
|
|
231
241
|
scopeOpts: scoped,
|
|
232
242
|
remoteServer,
|
|
243
|
+
developerRootDomain,
|
|
233
244
|
omitAppTraefikLabels: options && options.omitAppTraefikLabels === true
|
|
234
245
|
});
|
|
235
246
|
const volumesConfig = buildVolumesConfig(appName);
|