@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
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Promote a developer-scoped local Docker image to the manifest base repository (tag only).
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Used by `aifabrix build --base --from-dev` and dev-promote workflows
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 1.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const config = require('../core/config');
|
|
12
|
+
const { loadAndValidateConfig } = require('./build-helpers');
|
|
13
|
+
const { resolveBuildDockerTagNames } = require('./build-resolve-image');
|
|
14
|
+
const { execWithDockerEnv } = require('./docker-exec');
|
|
15
|
+
const logger = require('./logger');
|
|
16
|
+
const { formatSuccessLine } = require('./cli-test-layout-chalk');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {string} value
|
|
20
|
+
* @returns {string}
|
|
21
|
+
*/
|
|
22
|
+
function shellQuote(value) {
|
|
23
|
+
return `'${String(value).replace(/'/g, '\'\\\'\'')}'`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {string} imageRef - repository:tag
|
|
28
|
+
* @returns {Promise<boolean>}
|
|
29
|
+
*/
|
|
30
|
+
async function dockerImageRefExists(imageRef) {
|
|
31
|
+
try {
|
|
32
|
+
await execWithDockerEnv(`docker image inspect ${shellQuote(imageRef)}`);
|
|
33
|
+
return true;
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {string} imageRef
|
|
41
|
+
* @returns {Promise<string|null>}
|
|
42
|
+
*/
|
|
43
|
+
async function readDockerImageId(imageRef) {
|
|
44
|
+
try {
|
|
45
|
+
const { stdout } = await execWithDockerEnv(
|
|
46
|
+
`docker image inspect --format '{{.Id}}' ${shellQuote(imageRef)}`
|
|
47
|
+
);
|
|
48
|
+
const id = stdout.trim();
|
|
49
|
+
return id || null;
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @param {string} sourceRef
|
|
57
|
+
* @param {string} targetRef
|
|
58
|
+
* @returns {Promise<void>}
|
|
59
|
+
*/
|
|
60
|
+
async function dockerTagImageRef(sourceRef, targetRef) {
|
|
61
|
+
await execWithDockerEnv(`docker tag ${shellQuote(sourceRef)} ${shellQuote(targetRef)}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {string} appName
|
|
66
|
+
* @param {Object} variables
|
|
67
|
+
* @param {string|number} developerId
|
|
68
|
+
* @param {Object} options - `tag` (base target), `fromDevTag` (dev source; default `latest`)
|
|
69
|
+
* @returns {Promise<{ devRef: string, baseRef: string, devTag: string, baseTag: string }>}
|
|
70
|
+
*/
|
|
71
|
+
async function resolveDevAndBaseImageRefs(appName, variables, developerId, options) {
|
|
72
|
+
const baseTag = options.tag || 'latest';
|
|
73
|
+
const devTag = options.fromDevTag || 'latest';
|
|
74
|
+
const { buildImageName: baseName } = await resolveBuildDockerTagNames(appName, variables, developerId, {
|
|
75
|
+
...options,
|
|
76
|
+
base: true
|
|
77
|
+
});
|
|
78
|
+
const { buildImageName: devName } = await resolveBuildDockerTagNames(appName, variables, developerId, {
|
|
79
|
+
...options,
|
|
80
|
+
base: false
|
|
81
|
+
});
|
|
82
|
+
return {
|
|
83
|
+
devRef: `${devName}:${devTag}`,
|
|
84
|
+
baseRef: `${baseName}:${baseTag}`,
|
|
85
|
+
devTag,
|
|
86
|
+
baseTag
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Find a local developer-scoped image to promote (dev-build usually tags `latest`).
|
|
92
|
+
* @param {string} appName
|
|
93
|
+
* @param {Object} appConfig
|
|
94
|
+
* @param {string|number} developerId
|
|
95
|
+
* @param {Object} options
|
|
96
|
+
* @returns {Promise<{ devRef: string, baseRef: string }|null>}
|
|
97
|
+
*/
|
|
98
|
+
async function resolvePromoteSourceAndTargetRefs(appName, appConfig, developerId, options) {
|
|
99
|
+
const baseTag = options.tag || 'latest';
|
|
100
|
+
const devTagCandidates = ['latest'];
|
|
101
|
+
if (baseTag !== 'latest' && !devTagCandidates.includes(baseTag)) {
|
|
102
|
+
devTagCandidates.push(baseTag);
|
|
103
|
+
}
|
|
104
|
+
for (const fromDevTag of devTagCandidates) {
|
|
105
|
+
const refs = await resolveDevAndBaseImageRefs(appName, appConfig, developerId, {
|
|
106
|
+
...options,
|
|
107
|
+
fromDevTag
|
|
108
|
+
});
|
|
109
|
+
if (await dockerImageRefExists(refs.devRef)) {
|
|
110
|
+
return { devRef: refs.devRef, baseRef: refs.baseRef };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Tag developer-scoped image as manifest base when present locally.
|
|
118
|
+
* @param {string} appName
|
|
119
|
+
* @param {Object} options - build options (`base`, `tag`, `registry`, `fromDev`)
|
|
120
|
+
* @returns {Promise<string|null>} base image ref when promoted/skipped; null when dev image missing
|
|
121
|
+
*/
|
|
122
|
+
async function tryPromoteDevImageToBase(appName, options = {}) {
|
|
123
|
+
if (options.base !== true) {
|
|
124
|
+
throw new Error('Promote-from-dev requires --base.');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const developerId = await config.getDeveloperId();
|
|
128
|
+
const idNum = typeof developerId === 'string' ? parseInt(developerId, 10) : Number(developerId);
|
|
129
|
+
if (!Number.isFinite(idNum) || idNum <= 0) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const { config: appConfig } = await loadAndValidateConfig(appName);
|
|
134
|
+
const resolved = await resolvePromoteSourceAndTargetRefs(appName, appConfig, developerId, options);
|
|
135
|
+
if (!resolved) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
const { devRef, baseRef } = resolved;
|
|
139
|
+
|
|
140
|
+
const devId = await readDockerImageId(devRef);
|
|
141
|
+
if (await dockerImageRefExists(baseRef)) {
|
|
142
|
+
const baseId = await readDockerImageId(baseRef);
|
|
143
|
+
if (devId && baseId && devId === baseId) {
|
|
144
|
+
logger.log(formatSuccessLine(`Base image already matches dev build: ${baseRef}`));
|
|
145
|
+
return baseRef;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
await dockerTagImageRef(devRef, baseRef);
|
|
150
|
+
logger.log(formatSuccessLine(`Promoted dev image to base (tag only): ${baseRef}`));
|
|
151
|
+
return baseRef;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* @param {Object} options
|
|
156
|
+
* @returns {boolean}
|
|
157
|
+
*/
|
|
158
|
+
function shouldAttemptDevPromote(options) {
|
|
159
|
+
return options.fromDev === true && options.base === true;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
module.exports = {
|
|
163
|
+
tryPromoteDevImageToBase,
|
|
164
|
+
shouldAttemptDevPromote,
|
|
165
|
+
resolveDevAndBaseImageRefs,
|
|
166
|
+
resolvePromoteSourceAndTargetRefs,
|
|
167
|
+
dockerImageRefExists,
|
|
168
|
+
readDockerImageId,
|
|
169
|
+
dockerTagImageRef
|
|
170
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Refresh ~/.aifabrix/config.yaml from GET /api/dev/settings when remote dev auth is available.
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Merge builder-server settings (developer-root-domain, sync paths, etc.)
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 1.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const chalk = require('chalk');
|
|
12
|
+
const config = require('../core/config');
|
|
13
|
+
const devApi = require('../api/dev.api');
|
|
14
|
+
const { getRemoteDevAuth } = require('./remote-dev-auth');
|
|
15
|
+
const { getCertDir, readClientKeyPem } = require('./dev-cert-helper');
|
|
16
|
+
const { getConfigDirForPaths } = require('./paths');
|
|
17
|
+
const logger = require('./logger');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {Object} RefreshRemoteSettingsResult
|
|
21
|
+
* @property {boolean} refreshed
|
|
22
|
+
* @property {string} [reason]
|
|
23
|
+
* @property {Object} [settings]
|
|
24
|
+
* @property {Error} [error]
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Fetch GET /api/dev/settings and merge into config when remote-server + client cert exist.
|
|
29
|
+
*
|
|
30
|
+
* @async
|
|
31
|
+
* @param {{ quiet?: boolean }} [opts]
|
|
32
|
+
* @returns {Promise<RefreshRemoteSettingsResult>}
|
|
33
|
+
*/
|
|
34
|
+
async function refreshRemoteSettingsFromBuilderIfPossible(opts = {}) {
|
|
35
|
+
const quiet = opts.quiet !== false;
|
|
36
|
+
const auth = await getRemoteDevAuth();
|
|
37
|
+
if (!auth) {
|
|
38
|
+
return { refreshed: false, reason: 'no-remote-auth' };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const devId = await config.getDeveloperId();
|
|
42
|
+
const certDir = getCertDir(getConfigDirForPaths(), devId);
|
|
43
|
+
const clientKeyPem = readClientKeyPem(certDir);
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const settings = await devApi.getSettings(
|
|
47
|
+
auth.serverUrl,
|
|
48
|
+
auth.clientCertPem,
|
|
49
|
+
clientKeyPem || undefined,
|
|
50
|
+
auth.serverCaPem || undefined
|
|
51
|
+
);
|
|
52
|
+
await config.mergeRemoteSettings(settings);
|
|
53
|
+
if (!quiet) {
|
|
54
|
+
logger.log(chalk.gray('Synced developer settings from Builder Server'));
|
|
55
|
+
}
|
|
56
|
+
return { refreshed: true, settings };
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (!quiet) {
|
|
59
|
+
logger.warn(
|
|
60
|
+
chalk.yellow(`Could not refresh settings from Builder Server: ${error.message}`)
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
return { refreshed: false, reason: 'fetch-failed', error };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = { refreshRemoteSettingsFromBuilderIfPossible };
|
|
@@ -6,10 +6,12 @@
|
|
|
6
6
|
* @version 2.0.0
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
const fs = require('fs');
|
|
9
10
|
const path = require('path');
|
|
10
11
|
const config = require('../core/config');
|
|
11
|
-
const { getCertDir } = require('./dev-cert-helper');
|
|
12
|
+
const { getCertDir, readServerCaPem } = require('./dev-cert-helper');
|
|
12
13
|
const { getConfigDirForPaths } = require('./paths');
|
|
14
|
+
const { resolveRemoteDockerDaemonVerify } = require('./docker-daemon-tls-ca');
|
|
13
15
|
|
|
14
16
|
function devTlsCertPaths(certDir) {
|
|
15
17
|
return {
|
|
@@ -43,10 +45,13 @@ function missingCaError(trimmed, certDir) {
|
|
|
43
45
|
* When docker-endpoint is set, we do not fall back to the local daemon without that endpoint
|
|
44
46
|
* (avoids accidentally using Docker Desktop while the dev profile targets a remote engine).
|
|
45
47
|
*
|
|
46
|
-
* When **TLS skip-verify** is enabled (config or
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
48
|
+
* When **TLS skip-verify** is enabled (config or `AIFABRIX_DOCKER_TLS_SKIP_VERIFY`), Docker uses
|
|
49
|
+
* DOCKER_TLS_VERIFY=0 (client cert/key still sent when present). Skip-verify applies even when ca.pem
|
|
50
|
+
* exists — ca.pem may hold the internal issue-cert CA while the Docker daemon uses a public/wildcard cert.
|
|
51
|
+
*
|
|
52
|
+
* When skip-verify is off and ca.pem exists, the CLI verifies the daemon only if ca.pem includes the
|
|
53
|
+
* Docker daemon trust chain (merged at dev init/refresh or lazily on first Docker use). Otherwise it
|
|
54
|
+
* falls back to DOCKER_TLS_VERIFY=0 with client mTLS (public CA daemon on a trusted builder host).
|
|
50
55
|
*
|
|
51
56
|
* Without skip-verify, cert.pem, key.pem, and ca.pem are required in the dev cert directory.
|
|
52
57
|
*
|
|
@@ -61,7 +66,6 @@ async function getRemoteDockerEnv() {
|
|
|
61
66
|
const trimmed = endpoint.trim();
|
|
62
67
|
const certDir = getCertDir(getConfigDirForPaths(), await config.getDeveloperId());
|
|
63
68
|
const { certPath, keyPath, caPath } = devTlsCertPaths(certDir);
|
|
64
|
-
const fs = require('fs');
|
|
65
69
|
const skipVerify = await config.getDockerTlsSkipVerify();
|
|
66
70
|
const hasClient = fs.existsSync(certPath) && fs.existsSync(keyPath);
|
|
67
71
|
const hasCa = fs.existsSync(caPath);
|
|
@@ -71,7 +75,18 @@ async function getRemoteDockerEnv() {
|
|
|
71
75
|
return { DOCKER_HOST: trimmed, DOCKER_TLS_VERIFY: '0' };
|
|
72
76
|
}
|
|
73
77
|
if (!hasCa && !skipVerify) throw missingCaError(trimmed, certDir);
|
|
74
|
-
|
|
78
|
+
|
|
79
|
+
let verifyDaemon = false;
|
|
80
|
+
if (!skipVerify && hasCa) {
|
|
81
|
+
verifyDaemon = await resolveRemoteDockerDaemonVerify({
|
|
82
|
+
certDir,
|
|
83
|
+
existingCaPem: readServerCaPem(certDir),
|
|
84
|
+
dockerEndpoint: trimmed,
|
|
85
|
+
remoteServerUrl: await config.getRemoteServer(),
|
|
86
|
+
skipVerify: false
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
75
90
|
return {
|
|
76
91
|
DOCKER_HOST: trimmed,
|
|
77
92
|
DOCKER_TLS_VERIFY: verifyDaemon ? '1' : '0',
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve hostname for `${REMOTE_HOST}` in frontDoorRouting.host templates.
|
|
3
|
+
* Prefers `developer-root-domain` from builder-server settings over `remote-server` hostname.
|
|
4
|
+
*
|
|
5
|
+
* @fileoverview App public hostname root (separate from builder API host)
|
|
6
|
+
* @author AI Fabrix Team
|
|
7
|
+
* @version 1.0.0
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const { hostnameFromUrl } = require('./config-paths');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Hostname for `${REMOTE_HOST}` expansion (Traefik labels, url:// public, setup health).
|
|
16
|
+
*
|
|
17
|
+
* @param {Object} [input]
|
|
18
|
+
* @param {string|null|undefined} input.remoteServer - Builder API base URL
|
|
19
|
+
* @param {string|null|undefined} input.developerRootDomain - App hostname root from GET /api/dev/settings
|
|
20
|
+
* @returns {string}
|
|
21
|
+
*/
|
|
22
|
+
function resolveRemoteHostForFrontDoor(input = {}) {
|
|
23
|
+
const root = String(input.developerRootDomain || '').trim();
|
|
24
|
+
if (root) {
|
|
25
|
+
return root.replace(/^\.+|\.+$/g, '');
|
|
26
|
+
}
|
|
27
|
+
return hostnameFromUrl(input.remoteServer) || '';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Whether front-door host expansion can resolve `${REMOTE_HOST}` (root domain or remote-server).
|
|
32
|
+
*
|
|
33
|
+
* @param {Object} [input]
|
|
34
|
+
* @returns {boolean}
|
|
35
|
+
*/
|
|
36
|
+
function hasFrontDoorRemoteHostContext(input = {}) {
|
|
37
|
+
return Boolean(resolveRemoteHostForFrontDoor(input));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = {
|
|
41
|
+
resolveRemoteHostForFrontDoor,
|
|
42
|
+
hasFrontDoorRemoteHostContext
|
|
43
|
+
};
|
|
@@ -24,6 +24,42 @@ function isRunCliNoProxy(options) {
|
|
|
24
24
|
return options.noProxy === true;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Whether `aifabrix run` should persist `applications.<app>.proxy: true`.
|
|
29
|
+
* Proxy / front-door URL hints require infra Traefik (`traefik: true` in config).
|
|
30
|
+
* Direct-port (`setupPlatformMode: single`) always persists false even though Commander defaults `--proxy` on.
|
|
31
|
+
*
|
|
32
|
+
* @param {Object} [options] - Commander action options
|
|
33
|
+
* @param {Object|null|undefined} [userCfg] - Parsed `~/.aifabrix/config.yaml`
|
|
34
|
+
* @returns {boolean}
|
|
35
|
+
*/
|
|
36
|
+
function resolveRunProxyPersistEnabled(options, userCfg) {
|
|
37
|
+
if (isRunCliNoProxy(options)) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
return Boolean(userCfg && userCfg.traefik === true);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Effective no-proxy for env/url expansion during run.
|
|
45
|
+
*
|
|
46
|
+
* @param {Object} [options]
|
|
47
|
+
* @param {Object|null|undefined} [userCfg]
|
|
48
|
+
* @param {boolean} savedProxyHint - `applications.<app>.proxy` from config
|
|
49
|
+
* @returns {boolean}
|
|
50
|
+
*/
|
|
51
|
+
function resolveRunEffectiveNoProxy(options, userCfg, savedProxyHint) {
|
|
52
|
+
if (isRunCliNoProxy(options)) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
if (!userCfg || userCfg.traefik !== true) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
return !savedProxyHint;
|
|
59
|
+
}
|
|
60
|
+
|
|
27
61
|
module.exports = {
|
|
28
|
-
isRunCliNoProxy
|
|
62
|
+
isRunCliNoProxy,
|
|
63
|
+
resolveRunProxyPersistEnabled,
|
|
64
|
+
resolveRunEffectiveNoProxy
|
|
29
65
|
};
|
|
@@ -19,7 +19,7 @@ const { updateContainerPortInEnvFile } = require('./env-ports');
|
|
|
19
19
|
const { buildEnvVarMap } = require('./env-map');
|
|
20
20
|
const { getLocalPortFromPath } = require('./port-resolver');
|
|
21
21
|
const { readYamlAtPath, applyCanonicalSecretsOverride } = require('./secrets-canonical');
|
|
22
|
-
const { collectUniqueKvPathStrings } = require('./secrets-kv-refs');
|
|
22
|
+
const { collectUniqueKvPathStrings, isCommentOrEmptyLine, activeEnvLineForKvScan, envLineWithoutInlineComment } = require('./secrets-kv-refs');
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* Interpolate ${VAR} occurrences with values from envVars map
|
|
@@ -34,16 +34,6 @@ function interpolateEnvVars(content, envVars) {
|
|
|
34
34
|
});
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
/**
|
|
38
|
-
* Returns true if the line is a comment or empty (should be skipped for kv:// resolution)
|
|
39
|
-
* @param {string} line - Single line
|
|
40
|
-
* @returns {boolean}
|
|
41
|
-
*/
|
|
42
|
-
function isCommentOrEmptyLine(line) {
|
|
43
|
-
const t = line.trim();
|
|
44
|
-
return t === '' || t.startsWith('#');
|
|
45
|
-
}
|
|
46
|
-
|
|
47
37
|
/** Regex for kv:// path (allows slashes, e.g. kv://hubspot/clientId) */
|
|
48
38
|
const KV_REF_PATTERN = /kv:\/\/([a-zA-Z0-9_\-/]+)/g;
|
|
49
39
|
|
|
@@ -162,10 +152,11 @@ function collectMissingSecrets(content, secrets, scopedKv = null) {
|
|
|
162
152
|
const seen = new Set();
|
|
163
153
|
const missing = [];
|
|
164
154
|
for (const line of content.split('\n')) {
|
|
165
|
-
|
|
155
|
+
const scanLine = activeEnvLineForKvScan(line);
|
|
156
|
+
if (!scanLine) continue;
|
|
166
157
|
let match;
|
|
167
158
|
KV_REF_PATTERN.lastIndex = 0;
|
|
168
|
-
while ((match = KV_REF_PATTERN.exec(
|
|
159
|
+
while ((match = KV_REF_PATTERN.exec(scanLine)) !== null) {
|
|
169
160
|
const pathStr = match[1];
|
|
170
161
|
if (seen.has(pathStr)) continue;
|
|
171
162
|
seen.add(pathStr);
|
|
@@ -222,7 +213,10 @@ function replaceKvInContent(content, secrets, envVars, scopedKv = null) {
|
|
|
222
213
|
const lines = content.split('\n');
|
|
223
214
|
const result = lines.map(line => {
|
|
224
215
|
if (isCommentOrEmptyLine(line)) return line;
|
|
225
|
-
|
|
216
|
+
const scanLine = envLineWithoutInlineComment(line);
|
|
217
|
+
if (!scanLine.trim()) return line;
|
|
218
|
+
const suffix = line.slice(scanLine.length);
|
|
219
|
+
const replaced = scanLine.replace(KV_REF_PATTERN, (match, pathStr) => {
|
|
226
220
|
let value = resolveKvRefValue(secretsMap, pathStr, scopedKv?.envKey, effective);
|
|
227
221
|
if ((value === undefined || value === null) && isKvKeyAllowedEmptyWhenAbsent(pathStr)) {
|
|
228
222
|
value = '';
|
|
@@ -234,6 +228,7 @@ function replaceKvInContent(content, secrets, envVars, scopedKv = null) {
|
|
|
234
228
|
}
|
|
235
229
|
return value !== null && value !== undefined ? String(value) : match;
|
|
236
230
|
});
|
|
231
|
+
return replaced + suffix;
|
|
237
232
|
});
|
|
238
233
|
return result.join('\n');
|
|
239
234
|
}
|
|
@@ -7,11 +7,51 @@
|
|
|
7
7
|
|
|
8
8
|
const KV_REF_PATTERN = /kv:\/\/([a-zA-Z0-9_\-/]+)/g;
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* True when the line is empty or a full-line comment (# after trim).
|
|
12
|
+
*
|
|
13
|
+
* @param {string} line
|
|
14
|
+
* @returns {boolean}
|
|
15
|
+
*/
|
|
10
16
|
function isCommentOrEmptyLine(line) {
|
|
11
|
-
const t = line.trim();
|
|
17
|
+
const t = String(line).trim();
|
|
12
18
|
return t === '' || t.startsWith('#');
|
|
13
19
|
}
|
|
14
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Active env line for kv:// scanning: drop inline `#` comments (not inside quotes).
|
|
23
|
+
*
|
|
24
|
+
* @param {string} line
|
|
25
|
+
* @returns {string}
|
|
26
|
+
*/
|
|
27
|
+
function envLineWithoutInlineComment(line) {
|
|
28
|
+
if (isCommentOrEmptyLine(line)) {
|
|
29
|
+
return '';
|
|
30
|
+
}
|
|
31
|
+
let inSingle = false;
|
|
32
|
+
let inDouble = false;
|
|
33
|
+
const raw = String(line);
|
|
34
|
+
for (let i = 0; i < raw.length; i++) {
|
|
35
|
+
const c = raw[i];
|
|
36
|
+
if (c === '\'' && !inDouble) {
|
|
37
|
+
inSingle = !inSingle;
|
|
38
|
+
} else if (c === '"' && !inSingle) {
|
|
39
|
+
inDouble = !inDouble;
|
|
40
|
+
} else if (c === '#' && !inSingle && !inDouble) {
|
|
41
|
+
return raw.slice(0, i);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return raw;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {string} line
|
|
49
|
+
* @returns {string}
|
|
50
|
+
*/
|
|
51
|
+
function activeEnvLineForKvScan(line) {
|
|
52
|
+
return envLineWithoutInlineComment(line).trim();
|
|
53
|
+
}
|
|
54
|
+
|
|
15
55
|
/**
|
|
16
56
|
* @param {string} content
|
|
17
57
|
* @returns {string[]}
|
|
@@ -23,10 +63,11 @@ function collectUniqueKvPathStrings(content) {
|
|
|
23
63
|
return out;
|
|
24
64
|
}
|
|
25
65
|
for (const line of content.split('\n')) {
|
|
26
|
-
|
|
66
|
+
const scanLine = activeEnvLineForKvScan(line);
|
|
67
|
+
if (!scanLine) continue;
|
|
27
68
|
let match;
|
|
28
69
|
KV_REF_PATTERN.lastIndex = 0;
|
|
29
|
-
while ((match = KV_REF_PATTERN.exec(
|
|
70
|
+
while ((match = KV_REF_PATTERN.exec(scanLine)) !== null) {
|
|
30
71
|
const pathStr = match[1];
|
|
31
72
|
if (!seen.has(pathStr)) {
|
|
32
73
|
seen.add(pathStr);
|
|
@@ -38,5 +79,8 @@ function collectUniqueKvPathStrings(content) {
|
|
|
38
79
|
}
|
|
39
80
|
|
|
40
81
|
module.exports = {
|
|
82
|
+
isCommentOrEmptyLine,
|
|
83
|
+
envLineWithoutInlineComment,
|
|
84
|
+
activeEnvLineForKvScan,
|
|
41
85
|
collectUniqueKvPathStrings
|
|
42
86
|
};
|
|
@@ -9,15 +9,7 @@
|
|
|
9
9
|
'use strict';
|
|
10
10
|
|
|
11
11
|
const { formatMissingSecretsFileInfo } = require('./secrets-helpers');
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* @param {string} line
|
|
15
|
-
* @returns {boolean}
|
|
16
|
-
*/
|
|
17
|
-
function isCommentOrEmptyLine(line) {
|
|
18
|
-
const t = String(line).trim();
|
|
19
|
-
return t === '' || t.startsWith('#');
|
|
20
|
-
}
|
|
12
|
+
const { isCommentOrEmptyLine } = require('./secrets-kv-refs');
|
|
21
13
|
|
|
22
14
|
/**
|
|
23
15
|
* @param {string} kvRef - e.g. kv://hubspot-demo/clientSecret
|
|
@@ -183,12 +183,22 @@ async function resolveCredentialAppForDeployment(appName) {
|
|
|
183
183
|
|
|
184
184
|
/**
|
|
185
185
|
* @param {string} appName
|
|
186
|
+
* @param {{ secretsFileOnly?: boolean }} [options]
|
|
186
187
|
* @returns {Promise<{ clientId: string, clientSecret: string }|null>}
|
|
187
188
|
*/
|
|
188
|
-
async function loadClientCredentials(appName) {
|
|
189
|
+
async function loadClientCredentials(appName, options = {}) {
|
|
189
190
|
if (!appName || typeof appName !== 'string') {
|
|
190
191
|
throw new Error('App name is required and must be a string');
|
|
191
192
|
}
|
|
193
|
+
if (options.secretsFileOnly === true) {
|
|
194
|
+
for (const key of credentialAppKeysToTry(appName)) {
|
|
195
|
+
const fromFile = await readClientCredentialsFromSecretsFileDecrypted(key);
|
|
196
|
+
if (fromFile) {
|
|
197
|
+
return fromFile;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
192
202
|
const resolved = await resolveCredentialAppForDeployment(appName);
|
|
193
203
|
return resolved.credentials;
|
|
194
204
|
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
'use strict';
|
|
10
10
|
|
|
11
11
|
const { expandFrontDoorHostPlaceholders } = require('./compose-generator');
|
|
12
|
+
const { hasFrontDoorRemoteHostContext } = require('./remote-host-for-front-door');
|
|
12
13
|
const { publishedHostPort, localHostPort } = require('./declarative-url-ports');
|
|
13
14
|
const {
|
|
14
15
|
normalizeSetupPlatformMode,
|
|
@@ -23,11 +24,12 @@ const { isLocalhostPublicSchemeHostname } = require('./localhost-public-host');
|
|
|
23
24
|
* @param {Object} opts
|
|
24
25
|
* @param {string|number|null|undefined} opts.developerIdRaw
|
|
25
26
|
* @param {string|null|undefined} opts.remoteServer
|
|
27
|
+
* @param {string|null|undefined} [opts.developerRootDomain]
|
|
26
28
|
* @returns {string}
|
|
27
29
|
*/
|
|
28
30
|
function expandFrontDoorHostTemplateForUrls(template, opts) {
|
|
29
|
-
const { developerIdRaw, remoteServer } = opts || {};
|
|
30
|
-
return expandFrontDoorHostPlaceholders(template, developerIdRaw, remoteServer);
|
|
31
|
+
const { developerIdRaw, remoteServer, developerRootDomain } = opts || {};
|
|
32
|
+
return expandFrontDoorHostPlaceholders(template, developerIdRaw, remoteServer, developerRootDomain);
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
function hostPortForProfile(profile, listenPort, developerIdNum) {
|
|
@@ -174,6 +176,7 @@ function buildTraefikPublicBaseIfApplicable(opts) {
|
|
|
174
176
|
hostTemplate,
|
|
175
177
|
developerIdRaw,
|
|
176
178
|
remoteServer,
|
|
179
|
+
developerRootDomain,
|
|
177
180
|
profile,
|
|
178
181
|
developerIdNum
|
|
179
182
|
} = opts;
|
|
@@ -181,7 +184,10 @@ function buildTraefikPublicBaseIfApplicable(opts) {
|
|
|
181
184
|
if (!traefik || !pathActive || !hostTemplate || !String(hostTemplate).trim()) {
|
|
182
185
|
return null;
|
|
183
186
|
}
|
|
184
|
-
if (
|
|
187
|
+
if (
|
|
188
|
+
templateUsesRemoteHostPlaceholder(hostTemplate) &&
|
|
189
|
+
!hasFrontDoorRemoteHostContext({ remoteServer, developerRootDomain })
|
|
190
|
+
) {
|
|
185
191
|
// Local full-platform (no remote domain): use the single localhost front door port (3000 + devId×100).
|
|
186
192
|
return localhostFrontDoorBaseForMissingRemote({
|
|
187
193
|
profile,
|
|
@@ -190,7 +196,8 @@ function buildTraefikPublicBaseIfApplicable(opts) {
|
|
|
190
196
|
}
|
|
191
197
|
const expanded = expandFrontDoorHostTemplateForUrls(hostTemplate, {
|
|
192
198
|
developerIdRaw,
|
|
193
|
-
remoteServer
|
|
199
|
+
remoteServer,
|
|
200
|
+
developerRootDomain
|
|
194
201
|
});
|
|
195
202
|
if (!expanded) {
|
|
196
203
|
return null;
|