@aifabrix/builder 2.44.0 → 2.44.2
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/rules/cli-layout.mdc +75 -0
- package/.cursor/rules/project-rules.mdc +8 -0
- package/.npmrc.token +1 -0
- package/.nyc_output/55e9d034-ddab-4579-a706-e02a91d75c91.json +1 -0
- package/.nyc_output/processinfo/55e9d034-ddab-4579-a706-e02a91d75c91.json +1 -0
- package/.nyc_output/processinfo/index.json +1 -0
- package/jest.projects.js +15 -2
- package/lib/api/certificates.api.js +62 -0
- package/lib/api/index.js +11 -2
- package/lib/api/types/certificates.types.js +48 -0
- package/lib/api/validation-run.api.js +16 -4
- package/lib/api/validation-runner.js +13 -3
- package/lib/app/certification-show-enrich.js +129 -0
- package/lib/app/certification-verify-rows.js +60 -0
- package/lib/app/show-display.js +43 -0
- package/lib/app/show.js +92 -8
- package/lib/certification/cli-cert-sync-skip.js +21 -0
- package/lib/certification/merge-certification-from-artifact.js +185 -0
- package/lib/certification/post-unified-cert-sync.js +33 -0
- package/lib/certification/sync-after-external-command.js +52 -0
- package/lib/certification/sync-system-certification.js +197 -0
- package/lib/cli/setup-app.js +4 -0
- package/lib/cli/setup-app.test-commands.js +24 -8
- package/lib/cli/setup-external-system.js +22 -1
- package/lib/cli/setup-secrets.js +34 -13
- package/lib/cli/setup-utility.js +18 -2
- package/lib/commands/app.js +10 -1
- package/lib/commands/datasource-unified-test-cli.js +50 -117
- package/lib/commands/datasource-unified-test-cli.options.js +44 -2
- package/lib/commands/datasource-unified-test-e2e-cli-helpers.js +106 -0
- package/lib/commands/datasource-validation-cli.js +15 -1
- package/lib/commands/datasource.js +25 -2
- package/lib/commands/upload.js +17 -6
- package/lib/core/secrets.js +47 -13
- package/lib/datasource/log-viewer.js +135 -20
- package/lib/datasource/test-e2e.js +35 -17
- package/lib/datasource/unified-validation-run-body.js +3 -0
- package/lib/datasource/unified-validation-run.js +2 -1
- package/lib/external-system/deploy.js +53 -18
- package/lib/infrastructure/compose.js +12 -3
- package/lib/infrastructure/helpers-docker-check.js +67 -0
- package/lib/infrastructure/helpers.js +47 -58
- package/lib/infrastructure/index.js +3 -1
- package/lib/infrastructure/services.js +4 -56
- package/lib/schema/external-system.schema.json +25 -3
- package/lib/schema/type/document-storage.json +15 -2
- package/lib/utils/api.js +28 -3
- package/lib/utils/app-service-env-from-builder.js +47 -6
- package/lib/utils/config-paths.js +4 -0
- package/lib/utils/configuration-env-resolver.js +11 -8
- package/lib/utils/credential-secrets-env.js +5 -5
- package/lib/utils/datasource-test-run-certificate-tty.js +82 -0
- package/lib/utils/datasource-test-run-display.js +19 -2
- package/lib/utils/datasource-test-run-exit.js +25 -0
- package/lib/utils/external-system-display.js +8 -0
- package/lib/utils/external-system-system-test-tty-overview.js +120 -0
- package/lib/utils/external-system-system-test-tty.js +417 -0
- package/lib/utils/paths.js +14 -0
- package/lib/utils/url-declarative-resolve-load-doc.js +50 -20
- package/lib/utils/urls-local-registry.js +36 -12
- package/lib/utils/validation-run-poll.js +28 -5
- package/lib/utils/validation-run-post-retry.js +20 -8
- package/lib/utils/validation-run-request.js +18 -0
- package/lib/validation/validate-external-cert-sync.js +23 -0
- package/lib/validation/validate.js +4 -1
- package/package.json +4 -3
- package/scripts/install-local.js +4 -1
- package/scripts/pnpm-global-remove.js +48 -0
- package/templates/applications/dataplane/env.template +4 -0
- package/templates/infra/compose.yaml.hbs +15 -14
- package/templates/infra/servers.json.hbs +3 -1
|
@@ -27,6 +27,8 @@ const { parseControllerDeploymentOutcome } = require('../utils/controller-deploy
|
|
|
27
27
|
const { generateControllerManifest } = require('../generator/external-controller-manifest');
|
|
28
28
|
const { validateExternalSystemComplete } = require('../validation/validate');
|
|
29
29
|
const { displayValidationResults } = require('../validation/validate-display');
|
|
30
|
+
const { maybeSyncSystemCertificationFromDataplane } = require('../certification/sync-system-certification');
|
|
31
|
+
const { cliOptsSkipCertSync } = require('../certification/cli-cert-sync-skip');
|
|
30
32
|
|
|
31
33
|
/**
|
|
32
34
|
* Lists datasources for a system and loads system record for docs URLs.
|
|
@@ -97,6 +99,54 @@ async function fetchDataplaneDeployReadiness(controllerUrl, environment, authCon
|
|
|
97
99
|
/**
|
|
98
100
|
* @param {Object} deploymentOutcome - from parseControllerDeploymentOutcome
|
|
99
101
|
*/
|
|
102
|
+
/**
|
|
103
|
+
* Optional dataplane validation/run after external deploy (--probe).
|
|
104
|
+
* @async
|
|
105
|
+
* @param {{ dataplaneUrl: string|null, error: Error|null }} ctx
|
|
106
|
+
* @param {string} systemKey
|
|
107
|
+
* @param {Object} authConfig
|
|
108
|
+
* @param {Object} options
|
|
109
|
+
* @returns {Promise<Object|null>} Unwrapped probe payload or null
|
|
110
|
+
*/
|
|
111
|
+
async function runOptionalExternalDeployProbe(ctx, systemKey, authConfig, options) {
|
|
112
|
+
if (!options.probe || !ctx.dataplaneUrl || ctx.error) return null;
|
|
113
|
+
logger.log(chalk.blue('\nRunning runtime checks (--probe)...'));
|
|
114
|
+
try {
|
|
115
|
+
const pr = await testSystemViaPipeline(ctx.dataplaneUrl, systemKey, authConfig, {}, {
|
|
116
|
+
timeout: options.probeTimeout || 120000
|
|
117
|
+
});
|
|
118
|
+
if (pr.success === false) {
|
|
119
|
+
logger.log(chalk.yellow(`⚠ Probe request failed: ${pr.formattedError || pr.error || 'unknown error'}`));
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
return unwrapApiData(pr);
|
|
123
|
+
} catch (e) {
|
|
124
|
+
logger.log(chalk.yellow(`⚠ Probe failed: ${e.message}`));
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* @param {Object} deploymentOutcome
|
|
131
|
+
* @param {{ dataplaneUrl: string|null, error: Error|null }} ctx
|
|
132
|
+
* @param {Object} manifest
|
|
133
|
+
* @param {Object} options
|
|
134
|
+
* @param {Object} authConfig
|
|
135
|
+
* @returns {Promise<void>}
|
|
136
|
+
*/
|
|
137
|
+
async function maybeSyncCertificationAfterExternalDeploy(deploymentOutcome, ctx, manifest, options, authConfig) {
|
|
138
|
+
if (!deploymentOutcome.ok || !ctx.dataplaneUrl || ctx.error) return;
|
|
139
|
+
const dsKeys = (manifest.dataSources || []).map((ds) => ds && ds.key).filter(Boolean);
|
|
140
|
+
await maybeSyncSystemCertificationFromDataplane({
|
|
141
|
+
label: 'deploy',
|
|
142
|
+
noCertSync: cliOptsSkipCertSync(options),
|
|
143
|
+
systemKey: manifest.key,
|
|
144
|
+
dataplaneUrl: ctx.dataplaneUrl,
|
|
145
|
+
authConfig,
|
|
146
|
+
datasourceKeys: dsKeys
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
100
150
|
function logImmediateControllerDeploymentOutcome(deploymentOutcome) {
|
|
101
151
|
if (deploymentOutcome.ok) {
|
|
102
152
|
logger.log(formatSuccessParagraph('Controller deployment OK'));
|
|
@@ -148,24 +198,7 @@ async function executeDeployAndDisplay(manifest, controllerUrl, environment, aut
|
|
|
148
198
|
manifest.key
|
|
149
199
|
);
|
|
150
200
|
|
|
151
|
-
|
|
152
|
-
if (options.probe && ctx.dataplaneUrl && !ctx.error) {
|
|
153
|
-
logger.log(chalk.blue('\nRunning runtime checks (--probe)...'));
|
|
154
|
-
try {
|
|
155
|
-
const pr = await testSystemViaPipeline(ctx.dataplaneUrl, manifest.key, authConfig, {}, {
|
|
156
|
-
timeout: options.probeTimeout || 120000
|
|
157
|
-
});
|
|
158
|
-
if (pr.success === false) {
|
|
159
|
-
logger.log(
|
|
160
|
-
chalk.yellow(`⚠ Probe request failed: ${pr.formattedError || pr.error || 'unknown error'}`)
|
|
161
|
-
);
|
|
162
|
-
} else {
|
|
163
|
-
probeData = unwrapApiData(pr);
|
|
164
|
-
}
|
|
165
|
-
} catch (e) {
|
|
166
|
-
logger.log(chalk.yellow(`⚠ Probe failed: ${e.message}`));
|
|
167
|
-
}
|
|
168
|
-
}
|
|
201
|
+
const probeData = await runOptionalExternalDeployProbe(ctx, manifest.key, authConfig, options);
|
|
169
202
|
|
|
170
203
|
logDeployReadinessSummary({
|
|
171
204
|
environment,
|
|
@@ -179,6 +212,8 @@ async function executeDeployAndDisplay(manifest, controllerUrl, environment, aut
|
|
|
179
212
|
probeData
|
|
180
213
|
});
|
|
181
214
|
|
|
215
|
+
await maybeSyncCertificationAfterExternalDeploy(deploymentOutcome, ctx, manifest, options, authConfig);
|
|
216
|
+
|
|
182
217
|
return result;
|
|
183
218
|
}
|
|
184
219
|
|
|
@@ -109,13 +109,23 @@ function generateComposeFile(templatePath, devId, idNum, ports, infraDir, option
|
|
|
109
109
|
const template = handlebars.compile(templateContent);
|
|
110
110
|
const networkName = idNum === 0 ? 'infra-aifabrix-network' : `infra-dev${devId}-aifabrix-network`;
|
|
111
111
|
const serversJsonPath = path.join(infraDir, 'servers.json');
|
|
112
|
+
const serversJsonBind = toDockerBindMountSource(serversJsonPath);
|
|
112
113
|
const pgpassPath = path.join(infraDir, 'pgpass');
|
|
113
114
|
const traefikConfig = typeof options.traefik === 'object'
|
|
114
115
|
? options.traefik
|
|
115
116
|
: buildTraefikConfig(!!options.traefik);
|
|
116
|
-
const
|
|
117
|
+
const pgadminConfigRaw = options.pgadmin && typeof options.pgadmin.enabled === 'boolean'
|
|
117
118
|
? options.pgadmin
|
|
118
119
|
: { enabled: true };
|
|
120
|
+
const pgadminEnabled = !!pgadminConfigRaw.enabled;
|
|
121
|
+
const pgpassBootstrapPath = path.join(infraDir, '.pgpass.bootstrap');
|
|
122
|
+
const pgadminConfig = {
|
|
123
|
+
...pgadminConfigRaw,
|
|
124
|
+
enabled: pgadminEnabled,
|
|
125
|
+
...(pgadminEnabled
|
|
126
|
+
? { pgpassBootstrapBind: toDockerBindMountSource(pgpassBootstrapPath) }
|
|
127
|
+
: {})
|
|
128
|
+
};
|
|
119
129
|
const redisCommanderConfig = options.redisCommander && typeof options.redisCommander.enabled === 'boolean'
|
|
120
130
|
? options.redisCommander
|
|
121
131
|
: { enabled: true };
|
|
@@ -132,7 +142,6 @@ function generateComposeFile(templatePath, devId, idNum, ports, infraDir, option
|
|
|
132
142
|
}
|
|
133
143
|
: traefikConfig;
|
|
134
144
|
const initScriptsBind = toDockerBindMountSource(path.join(infraDir, 'init-scripts'));
|
|
135
|
-
const infraDirBind = toDockerBindMountSource(infraDir);
|
|
136
145
|
const composeContent = template({
|
|
137
146
|
devId: devId,
|
|
138
147
|
postgresPort: ports.postgres,
|
|
@@ -143,10 +152,10 @@ function generateComposeFile(templatePath, devId, idNum, ports, infraDir, option
|
|
|
143
152
|
traefikHttpsPort: ports.traefikHttps,
|
|
144
153
|
networkName: networkName,
|
|
145
154
|
serversJsonPath: serversJsonPath,
|
|
155
|
+
serversJsonBind: serversJsonBind,
|
|
146
156
|
pgpassPath: pgpassPath,
|
|
147
157
|
infraDir: infraDir,
|
|
148
158
|
initScriptsBind: initScriptsBind,
|
|
149
|
-
infraDirBind: infraDirBind,
|
|
150
159
|
traefik: traefikForCompose,
|
|
151
160
|
pgadmin: pgadminConfig,
|
|
152
161
|
redisCommander: redisCommanderConfig
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Docker / Compose availability check and user-facing failure text for infra helpers.
|
|
3
|
+
* @author AI Fabrix Team
|
|
4
|
+
* @version 2.0.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const dockerUtils = require('../utils/docker');
|
|
10
|
+
const { ensureDevCertsIfNeededForRemoteDocker } = require('../utils/ensure-dev-certs-for-remote-docker');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* User-facing error when Docker/Compose checks fail (tailored by underlying message).
|
|
14
|
+
* @param {string} detail - Error message from ensureDockerAndCompose / Docker CLI
|
|
15
|
+
* @returns {string}
|
|
16
|
+
*/
|
|
17
|
+
function formatDockerInfrastructureFailure(detail) {
|
|
18
|
+
const cause = (detail || '').trim() || 'unknown error';
|
|
19
|
+
|
|
20
|
+
if (/Docker Compose is not available/i.test(cause)) {
|
|
21
|
+
return (
|
|
22
|
+
'Cannot use Docker for infrastructure: Docker Compose check failed (see Cause below).\n\n' +
|
|
23
|
+
`Cause: ${cause}\n\n` +
|
|
24
|
+
'If Cause mentions TLS, certificate, or handshake, fix client TLS for docker-endpoint (cert.pem, key.pem, ca.pem under ~/.aifabrix/certs/<developer-id>/) or docker-tls-skip-verify when appropriate. ' +
|
|
25
|
+
'If Cause suggests a missing plugin, install Docker Compose v2 for your user (docker CLI + plugin; no unix socket needed when using tcp:// docker-endpoint). ' +
|
|
26
|
+
'Or set AIFABRIX_COMPOSE_CMD. Run `aifabrix doctor` for diagnostics.'
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (/AIFABRIX_COMPOSE_CMD/i.test(cause) && /is set but failed/i.test(cause)) {
|
|
31
|
+
return (
|
|
32
|
+
'Cannot use Docker for infrastructure: AIFABRIX_COMPOSE_CMD failed.\n\n' +
|
|
33
|
+
`Cause: ${cause}\n\n` +
|
|
34
|
+
'Unset or fix AIFABRIX_COMPOSE_CMD, or install a working Compose. Run `aifabrix doctor` for diagnostics.'
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
'Cannot use Docker for infrastructure (Docker CLI missing, Compose missing, or remote Docker misconfigured).\n\n' +
|
|
40
|
+
`Cause: ${cause}\n\n` +
|
|
41
|
+
'Install Docker Engine and Compose on this machine (or set AIFABRIX_COMPOSE_CMD). ' +
|
|
42
|
+
'If you use docker-endpoint in dev config: install cert.pem, key.pem, and ca.pem for full TLS verify; use `aifabrix dev pin` / ' +
|
|
43
|
+
'`dev init --pin` as needed; or enable TLS skip-verify (config or AIFABRIX_DOCKER_TLS_SKIP_VERIFY) when appropriate. ' +
|
|
44
|
+
'Run `aifabrix doctor` for diagnostics.'
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Check Docker availability (local daemon or remote via docker-endpoint + TLS).
|
|
50
|
+
* @async
|
|
51
|
+
* @returns {Promise<void>}
|
|
52
|
+
* @throws {Error} If Docker/Compose cannot be used (includes underlying cause)
|
|
53
|
+
*/
|
|
54
|
+
async function checkDockerAvailability() {
|
|
55
|
+
await ensureDevCertsIfNeededForRemoteDocker();
|
|
56
|
+
try {
|
|
57
|
+
await dockerUtils.ensureDockerAndCompose();
|
|
58
|
+
} catch (error) {
|
|
59
|
+
const detail = (error && error.message) || String(error);
|
|
60
|
+
throw new Error(formatDockerInfrastructureFailure(detail));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = {
|
|
65
|
+
formatDockerInfrastructureFailure,
|
|
66
|
+
checkDockerAvailability
|
|
67
|
+
};
|
|
@@ -17,7 +17,6 @@ const chalk = require('chalk');
|
|
|
17
17
|
const handlebars = require('handlebars');
|
|
18
18
|
const adminSecrets = require('../core/admin-secrets');
|
|
19
19
|
const logger = require('../utils/logger');
|
|
20
|
-
const dockerUtils = require('../utils/docker');
|
|
21
20
|
const paths = require('../utils/paths');
|
|
22
21
|
const secretsEnsure = require('../core/secrets-ensure');
|
|
23
22
|
const {
|
|
@@ -25,7 +24,7 @@ const {
|
|
|
25
24
|
getInfraParameterCatalog,
|
|
26
25
|
readRelaxedCatalogDefaults
|
|
27
26
|
} = require('../parameters/infra-parameter-catalog');
|
|
28
|
-
const {
|
|
27
|
+
const { checkDockerAvailability } = require('./helpers-docker-check');
|
|
29
28
|
|
|
30
29
|
/**
|
|
31
30
|
* Lazy-load core/secrets at call time. A top-level require creates a circular dependency:
|
|
@@ -59,58 +58,6 @@ function getInfraProjectName(devId) {
|
|
|
59
58
|
return idNum === 0 ? 'infra' : `infra-dev${devId}`;
|
|
60
59
|
}
|
|
61
60
|
|
|
62
|
-
/**
|
|
63
|
-
* User-facing error when Docker/Compose checks fail (tailored by underlying message).
|
|
64
|
-
* @param {string} detail - Error message from ensureDockerAndCompose / Docker CLI
|
|
65
|
-
* @returns {string}
|
|
66
|
-
*/
|
|
67
|
-
function formatDockerInfrastructureFailure(detail) {
|
|
68
|
-
const cause = (detail || '').trim() || 'unknown error';
|
|
69
|
-
|
|
70
|
-
if (/Docker Compose is not available/i.test(cause)) {
|
|
71
|
-
return (
|
|
72
|
-
'Cannot use Docker for infrastructure: Docker Compose check failed (see Cause below).\n\n' +
|
|
73
|
-
`Cause: ${cause}\n\n` +
|
|
74
|
-
'If Cause mentions TLS, certificate, or handshake, fix client TLS for docker-endpoint (cert.pem, key.pem, ca.pem under ~/.aifabrix/certs/<developer-id>/) or docker-tls-skip-verify when appropriate. ' +
|
|
75
|
-
'If Cause suggests a missing plugin, install Docker Compose v2 for your user (docker CLI + plugin; no unix socket needed when using tcp:// docker-endpoint). ' +
|
|
76
|
-
'Or set AIFABRIX_COMPOSE_CMD. Run `aifabrix doctor` for diagnostics.'
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (/AIFABRIX_COMPOSE_CMD/i.test(cause) && /is set but failed/i.test(cause)) {
|
|
81
|
-
return (
|
|
82
|
-
'Cannot use Docker for infrastructure: AIFABRIX_COMPOSE_CMD failed.\n\n' +
|
|
83
|
-
`Cause: ${cause}\n\n` +
|
|
84
|
-
'Unset or fix AIFABRIX_COMPOSE_CMD, or install a working Compose. Run `aifabrix doctor` for diagnostics.'
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return (
|
|
89
|
-
'Cannot use Docker for infrastructure (Docker CLI missing, Compose missing, or remote Docker misconfigured).\n\n' +
|
|
90
|
-
`Cause: ${cause}\n\n` +
|
|
91
|
-
'Install Docker Engine and Compose on this machine (or set AIFABRIX_COMPOSE_CMD). ' +
|
|
92
|
-
'If you use docker-endpoint in dev config: install cert.pem, key.pem, and ca.pem for full TLS verify; use `aifabrix dev pin` / ' +
|
|
93
|
-
'`dev init --pin` as needed; or enable TLS skip-verify (config or AIFABRIX_DOCKER_TLS_SKIP_VERIFY) when appropriate. ' +
|
|
94
|
-
'Run `aifabrix doctor` for diagnostics.'
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Check Docker availability (local daemon or remote via docker-endpoint + TLS).
|
|
100
|
-
* @async
|
|
101
|
-
* @returns {Promise<void>}
|
|
102
|
-
* @throws {Error} If Docker/Compose cannot be used (includes underlying cause)
|
|
103
|
-
*/
|
|
104
|
-
async function checkDockerAvailability() {
|
|
105
|
-
await ensureDevCertsIfNeededForRemoteDocker();
|
|
106
|
-
try {
|
|
107
|
-
await dockerUtils.ensureDockerAndCompose();
|
|
108
|
-
} catch (error) {
|
|
109
|
-
const detail = (error && error.message) || String(error);
|
|
110
|
-
throw new Error(formatDockerInfrastructureFailure(detail));
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
61
|
/**
|
|
115
62
|
* Fallback for admin password/email when validated catalog load failed but YAML is still readable.
|
|
116
63
|
* @returns {Record<string, string>}
|
|
@@ -289,12 +236,37 @@ async function ensureAdminSecrets(options = {}) {
|
|
|
289
236
|
return adminSecretsPath;
|
|
290
237
|
}
|
|
291
238
|
|
|
239
|
+
/** Host-side pgpass for pgAdmin bind mount (must exist before container starts so servers.json import succeeds). */
|
|
240
|
+
const PGPASS_BOOTSTRAP_BASENAME = '.pgpass.bootstrap';
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Writes pgpass next to servers.json for Docker bind mount into /pgadmin4 (not under /var/lib/pgadmin volume).
|
|
244
|
+
* Prefer chown to pgAdmin UID so mode 600 is readable in the container; fall back to 644 if not root.
|
|
245
|
+
*
|
|
246
|
+
* @param {string} infraDir - Infrastructure directory path
|
|
247
|
+
* @param {string} postgresPassword - PostgreSQL password for the pgpass line
|
|
248
|
+
*/
|
|
249
|
+
function writePgpassBootstrap(infraDir, postgresPassword) {
|
|
250
|
+
const line = `postgres:5432:postgres:pgadmin:${postgresPassword}\n`;
|
|
251
|
+
const dest = path.join(infraDir, PGPASS_BOOTSTRAP_BASENAME);
|
|
252
|
+
fs.writeFileSync(dest, line, { mode: 0o600 });
|
|
253
|
+
try {
|
|
254
|
+
fs.chownSync(dest, 5050, 5050);
|
|
255
|
+
} catch {
|
|
256
|
+
try {
|
|
257
|
+
fs.chmodSync(dest, 0o644);
|
|
258
|
+
} catch {
|
|
259
|
+
// Ignore — container may still read depending on daemon / user namespace
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
292
264
|
/**
|
|
293
|
-
* Generates pgAdmin4 servers.json only. pgpass
|
|
294
|
-
*
|
|
265
|
+
* Generates pgAdmin4 servers.json only. pgpass for the container is supplied via bind-mounted
|
|
266
|
+
* `.pgpass.bootstrap` (written by writePgpassBootstrap); not embedded in servers.json.
|
|
295
267
|
*
|
|
296
268
|
* @param {string} infraDir - Infrastructure directory path
|
|
297
|
-
* @param {string} postgresPassword -
|
|
269
|
+
* @param {string} postgresPassword - Used only for consistency / future template fields (password is not written into servers.json)
|
|
298
270
|
*/
|
|
299
271
|
function generatePgAdminConfig(infraDir, postgresPassword) {
|
|
300
272
|
const serversJsonTemplatePath = path.join(__dirname, '..', '..', 'templates', 'infra', 'servers.json.hbs');
|
|
@@ -401,9 +373,10 @@ psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "miso" -c "GRANT AL
|
|
|
401
373
|
* @async
|
|
402
374
|
* @param {string} devId - Developer ID
|
|
403
375
|
* @param {string} adminSecretsPath - Path to admin secrets file
|
|
376
|
+
* @param {{ pgpassBootstrap?: boolean }} [prepOptions] - When pgpassBootstrap is false, skip/remove host pgpass bootstrap file (pgAdmin disabled)
|
|
404
377
|
* @returns {Promise<Object>} Object with infraDir and postgresPassword
|
|
405
378
|
*/
|
|
406
|
-
async function prepareInfraDirectory(devId, adminSecretsPath) {
|
|
379
|
+
async function prepareInfraDirectory(devId, adminSecretsPath, prepOptions = {}) {
|
|
407
380
|
const aifabrixDir = paths.getAifabrixSystemDir();
|
|
408
381
|
const infraDirName = getInfraDirName(devId);
|
|
409
382
|
const infraDir = path.join(aifabrixDir, infraDirName);
|
|
@@ -427,6 +400,20 @@ async function prepareInfraDirectory(devId, adminSecretsPath) {
|
|
|
427
400
|
'';
|
|
428
401
|
generatePgAdminConfig(infraDir, postgresPassword);
|
|
429
402
|
|
|
403
|
+
const pgpassBootstrap = prepOptions.pgpassBootstrap !== false;
|
|
404
|
+
const bootstrapPath = path.join(infraDir, PGPASS_BOOTSTRAP_BASENAME);
|
|
405
|
+
if (pgpassBootstrap) {
|
|
406
|
+
writePgpassBootstrap(infraDir, postgresPassword);
|
|
407
|
+
} else {
|
|
408
|
+
try {
|
|
409
|
+
if (fs.existsSync(bootstrapPath)) {
|
|
410
|
+
fs.unlinkSync(bootstrapPath);
|
|
411
|
+
}
|
|
412
|
+
} catch {
|
|
413
|
+
// Ignore
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
430
417
|
return { infraDir, postgresPassword };
|
|
431
418
|
}
|
|
432
419
|
|
|
@@ -483,6 +470,8 @@ module.exports = {
|
|
|
483
470
|
checkDockerAvailability,
|
|
484
471
|
ensureAdminSecrets,
|
|
485
472
|
generatePgAdminConfig,
|
|
473
|
+
writePgpassBootstrap,
|
|
474
|
+
PGPASS_BOOTSTRAP_BASENAME,
|
|
486
475
|
prepareInfraDirectory,
|
|
487
476
|
resolveInfraStatePaths,
|
|
488
477
|
ensureMisoInitScript,
|
|
@@ -107,7 +107,9 @@ async function prepareInfrastructureEnvironment(developerId, options = {}) {
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
// Prepare infrastructure directory
|
|
110
|
-
const { infraDir } = await prepareInfraDirectory(devId, adminSecretsPath
|
|
110
|
+
const { infraDir } = await prepareInfraDirectory(devId, adminSecretsPath, {
|
|
111
|
+
pgpassBootstrap: options.pgadmin !== false
|
|
112
|
+
});
|
|
111
113
|
await ensureMisoInitScript(infraDir);
|
|
112
114
|
|
|
113
115
|
return { devId, idNum, ports, templatePath, infraDir, adminSecretsPath, trustForwardedHeaders };
|
|
@@ -50,30 +50,6 @@ async function startDockerServices(composePath, projectName, adminSecretsPath, i
|
|
|
50
50
|
logger.log('Infrastructure services started successfully');
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
/**
|
|
54
|
-
* Copy pgAdmin4 configuration files into container
|
|
55
|
-
* @async
|
|
56
|
-
* @param {string} pgadminContainerName - pgAdmin container name
|
|
57
|
-
* @param {string} serversJsonPath - Path to servers.json file
|
|
58
|
-
* @param {string} pgpassPath - Path to pgpass file
|
|
59
|
-
*/
|
|
60
|
-
async function copyPgAdminConfig(pgadminContainerName, serversJsonPath, pgpassPath) {
|
|
61
|
-
const { execWithDockerEnv } = require('../utils/docker-exec');
|
|
62
|
-
try {
|
|
63
|
-
await new Promise(resolve => setTimeout(resolve, 2000)); // Wait for container to be ready
|
|
64
|
-
if (fs.existsSync(serversJsonPath)) {
|
|
65
|
-
await execWithDockerEnv(`docker cp "${serversJsonPath}" ${pgadminContainerName}:/pgadmin4/servers.json`);
|
|
66
|
-
}
|
|
67
|
-
if (fs.existsSync(pgpassPath)) {
|
|
68
|
-
await execWithDockerEnv(`docker cp "${pgpassPath}" ${pgadminContainerName}:/pgpass`);
|
|
69
|
-
await execWithDockerEnv(`docker exec ${pgadminContainerName} chmod 600 /pgpass`);
|
|
70
|
-
}
|
|
71
|
-
} catch (error) {
|
|
72
|
-
// Ignore copy errors - files might already be there or container not ready
|
|
73
|
-
logger.log('Note: Could not copy pgAdmin4 config files (this is OK if container was just restarted)');
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
53
|
/**
|
|
78
54
|
* Prepare run env file from decrypted admin secrets.
|
|
79
55
|
* @async
|
|
@@ -89,34 +65,12 @@ async function prepareRunEnv(infraDir) {
|
|
|
89
65
|
}
|
|
90
66
|
|
|
91
67
|
/**
|
|
92
|
-
*
|
|
93
|
-
* @async
|
|
94
|
-
* @param {string} infraDir - Infrastructure directory
|
|
95
|
-
* @param {Object} adminObj - Decrypted admin secrets object
|
|
96
|
-
* @param {string} devId - Developer ID
|
|
97
|
-
* @param {number} idNum - Developer ID number
|
|
98
|
-
* @returns {Promise<string>} Path to pgpass run file
|
|
99
|
-
*/
|
|
100
|
-
async function writePgpassAndCopyPgAdminConfig(infraDir, adminObj, devId, idNum) {
|
|
101
|
-
const pgpassRunPath = path.join(infraDir, '.pgpass.run');
|
|
102
|
-
const pgadminContainerName = idNum === 0 ? 'aifabrix-pgadmin' : `aifabrix-dev${devId}-pgadmin`;
|
|
103
|
-
const serversJsonPath = path.join(infraDir, 'servers.json');
|
|
104
|
-
const postgresPassword = adminObj.POSTGRES_PASSWORD || '';
|
|
105
|
-
const pgpassContent = `postgres:5432:postgres:pgadmin:${postgresPassword}\n`;
|
|
106
|
-
fs.writeFileSync(pgpassRunPath, pgpassContent, { mode: 0o600 });
|
|
107
|
-
await copyPgAdminConfig(pgadminContainerName, serversJsonPath, pgpassRunPath);
|
|
108
|
-
return pgpassRunPath;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Remove temporary run files (env and pgpass) if they exist.
|
|
68
|
+
* Remove temporary run files (env) if they exist.
|
|
113
69
|
* @param {string} runEnvPath - Path to .env.run
|
|
114
|
-
* @param {string} [pgpassRunPath] - Path to .pgpass.run
|
|
115
70
|
*/
|
|
116
|
-
function cleanupRunFiles(runEnvPath
|
|
71
|
+
function cleanupRunFiles(runEnvPath) {
|
|
117
72
|
try {
|
|
118
73
|
if (fs.existsSync(runEnvPath)) fs.unlinkSync(runEnvPath);
|
|
119
|
-
if (pgpassRunPath && fs.existsSync(pgpassRunPath)) fs.unlinkSync(pgpassRunPath);
|
|
120
74
|
} catch {
|
|
121
75
|
// Ignore unlink errors
|
|
122
76
|
}
|
|
@@ -136,11 +90,9 @@ function cleanupRunFiles(runEnvPath, pgpassRunPath) {
|
|
|
136
90
|
*/
|
|
137
91
|
async function startDockerServicesAndConfigure(composePath, devId, idNum, infraDir, opts = {}) {
|
|
138
92
|
let runEnvPath;
|
|
139
|
-
let pgpassRunPath;
|
|
140
|
-
let adminObj;
|
|
141
93
|
const { pgadmin = true, redisCommander = true, traefik = false } = opts;
|
|
142
94
|
try {
|
|
143
|
-
({
|
|
95
|
+
({ runEnvPath } = await prepareRunEnv(infraDir));
|
|
144
96
|
} catch (err) {
|
|
145
97
|
throw new Error(`Failed to prepare infra env: ${err.message}`);
|
|
146
98
|
}
|
|
@@ -148,13 +100,10 @@ async function startDockerServicesAndConfigure(composePath, devId, idNum, infraD
|
|
|
148
100
|
try {
|
|
149
101
|
const projectName = getInfraProjectName(devId);
|
|
150
102
|
await startDockerServices(composePath, projectName, runEnvPath, infraDir);
|
|
151
|
-
if (pgadmin) {
|
|
152
|
-
pgpassRunPath = await writePgpassAndCopyPgAdminConfig(infraDir, adminObj, devId, idNum);
|
|
153
|
-
}
|
|
154
103
|
await waitForServices(devId, { pgadmin, redisCommander, traefik });
|
|
155
104
|
logger.log('All services are healthy and ready');
|
|
156
105
|
} finally {
|
|
157
|
-
cleanupRunFiles(runEnvPath
|
|
106
|
+
cleanupRunFiles(runEnvPath);
|
|
158
107
|
}
|
|
159
108
|
}
|
|
160
109
|
|
|
@@ -236,7 +185,6 @@ async function checkInfraHealth(devId = null, options = {}) {
|
|
|
236
185
|
module.exports = {
|
|
237
186
|
execAsyncWithCwd,
|
|
238
187
|
startDockerServices,
|
|
239
|
-
copyPgAdminConfig,
|
|
240
188
|
startDockerServicesAndConfigure,
|
|
241
189
|
waitForServices,
|
|
242
190
|
checkInfraHealth
|
|
@@ -679,12 +679,15 @@
|
|
|
679
679
|
"publicKey":{
|
|
680
680
|
"type":"string",
|
|
681
681
|
"minLength":1,
|
|
682
|
-
"description":"Public
|
|
682
|
+
"description":"Public verification material (SPKI PEM for RS256; dev placeholder for HS256). Private keys must never appear in config."
|
|
683
683
|
},
|
|
684
684
|
"algorithm":{
|
|
685
685
|
"type":"string",
|
|
686
|
-
"
|
|
687
|
-
|
|
686
|
+
"enum":[
|
|
687
|
+
"RS256",
|
|
688
|
+
"HS256"
|
|
689
|
+
],
|
|
690
|
+
"description":"Signing algorithm for certificate verification (RS256 in production; HS256 only for local dev when the dataplane uses the dev HMAC signer)."
|
|
688
691
|
},
|
|
689
692
|
"issuer":{
|
|
690
693
|
"type":"string",
|
|
@@ -695,6 +698,25 @@
|
|
|
695
698
|
"type":"string",
|
|
696
699
|
"minLength":1,
|
|
697
700
|
"description":"Certification version identifier; must align with external system versioning."
|
|
701
|
+
},
|
|
702
|
+
"status":{
|
|
703
|
+
"type":"string",
|
|
704
|
+
"enum":[
|
|
705
|
+
"passed",
|
|
706
|
+
"not_passed",
|
|
707
|
+
"pending"
|
|
708
|
+
],
|
|
709
|
+
"description":"Outcome of certification evaluation for this block (aligns with DatasourceTestRun certificate.status)."
|
|
710
|
+
},
|
|
711
|
+
"level":{
|
|
712
|
+
"type":"string",
|
|
713
|
+
"enum":[
|
|
714
|
+
"BRONZE",
|
|
715
|
+
"SILVER",
|
|
716
|
+
"GOLD",
|
|
717
|
+
"PLATINUM"
|
|
718
|
+
],
|
|
719
|
+
"description":"Achieved certification tier (aligns with integration certificate certificationLevel)."
|
|
698
720
|
}
|
|
699
721
|
},
|
|
700
722
|
"additionalProperties":false
|
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
"key": "document-storage-schema",
|
|
8
8
|
"name": "Document Storage Configuration Schema",
|
|
9
9
|
"description": "JSON schema for validating document storage configurations",
|
|
10
|
-
"version": "1.
|
|
10
|
+
"version": "1.3.0",
|
|
11
11
|
"type": "schema",
|
|
12
12
|
"category": "document-storage",
|
|
13
13
|
"author": "AI Fabrix Team",
|
|
14
14
|
"createdAt": "2026-01-02T00:00:00Z",
|
|
15
|
-
"updatedAt": "2026-
|
|
15
|
+
"updatedAt": "2026-04-22T00:00:00Z",
|
|
16
16
|
"compatibility": {
|
|
17
17
|
"minVersion": "1.0.0",
|
|
18
18
|
"maxVersion": "2.0.0",
|
|
@@ -63,6 +63,14 @@
|
|
|
63
63
|
"Added optional securityLevel classification field (public/internal/restricted/confidential)"
|
|
64
64
|
],
|
|
65
65
|
"breaking": false
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"version": "1.3.0",
|
|
69
|
+
"date": "2026-04-22T00:00:00Z",
|
|
70
|
+
"changes": [
|
|
71
|
+
"Added optional parameterLookupCoalesceNestedItemScope (boolean, default true) for manifest-controlled binary parameter lookup enrichment"
|
|
72
|
+
],
|
|
73
|
+
"breaking": false
|
|
66
74
|
}
|
|
67
75
|
]
|
|
68
76
|
},
|
|
@@ -120,6 +128,11 @@
|
|
|
120
128
|
"default": false,
|
|
121
129
|
"description": "If true, removes fetch.query when applying binary retrieval path override."
|
|
122
130
|
},
|
|
131
|
+
"parameterLookupCoalesceNestedItemScope": {
|
|
132
|
+
"type": "boolean",
|
|
133
|
+
"default": true,
|
|
134
|
+
"description": "When true, binary parameterMapping and HTTP path templates use a lookup view that merges metadata and coalesces storage-scope ids from a nested item parentReference when the row's parentReference omits them. Set false for strict manifest-only paths."
|
|
135
|
+
},
|
|
123
136
|
"processing": {
|
|
124
137
|
"type": "object",
|
|
125
138
|
"properties": {
|
package/lib/utils/api.js
CHANGED
|
@@ -16,6 +16,26 @@ const auditLogger = require('../core/audit-logger');
|
|
|
16
16
|
/** Default timeout for HTTP requests (ms). Prevents hanging when the controller is unreachable. 30s allows Azure Web App cold start to complete. */
|
|
17
17
|
const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
|
|
18
18
|
|
|
19
|
+
/** Cap for optional per-request ``timeoutMs`` (validation run E2E can block on one POST). */
|
|
20
|
+
const MAX_SINGLE_REQUEST_TIMEOUT_MS = 45 * 60 * 1000;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Resolve per-request AbortSignal timeout from fetch options.
|
|
24
|
+
* @param {Object} options - Same object passed to fetch (may include timeoutMs / requestTimeoutMs)
|
|
25
|
+
* @returns {number}
|
|
26
|
+
*/
|
|
27
|
+
function resolveSingleRequestTimeoutMs(options) {
|
|
28
|
+
const raw = options?.requestTimeoutMs ?? options?.timeoutMs;
|
|
29
|
+
if (raw === undefined || raw === null || raw === '') {
|
|
30
|
+
return DEFAULT_REQUEST_TIMEOUT_MS;
|
|
31
|
+
}
|
|
32
|
+
const n = typeof raw === 'number' ? raw : parseInt(String(raw), 10);
|
|
33
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
34
|
+
return DEFAULT_REQUEST_TIMEOUT_MS;
|
|
35
|
+
}
|
|
36
|
+
return Math.min(n, MAX_SINGLE_REQUEST_TIMEOUT_MS);
|
|
37
|
+
}
|
|
38
|
+
|
|
19
39
|
/**
|
|
20
40
|
* Logs API request performance metrics and errors to audit log
|
|
21
41
|
* @param {Object} params - Performance logging parameters
|
|
@@ -276,9 +296,11 @@ async function handleNetworkError(error, url, options, duration) {
|
|
|
276
296
|
|
|
277
297
|
/**
|
|
278
298
|
* Make an API call with proper error handling
|
|
279
|
-
* Uses a 30s timeout
|
|
299
|
+
* Uses a 30s timeout by default. Pass ``options.timeoutMs`` or ``options.requestTimeoutMs`` for
|
|
300
|
+
* longer single requests (e.g. dataplane validation run E2E POST); capped at 45 minutes.
|
|
280
301
|
* @param {string} url - API endpoint URL
|
|
281
302
|
* @param {Object} options - Fetch options (signal, method, headers, body, etc.)
|
|
303
|
+
* @param {number} [options.timeoutMs] - Optional per-request timeout (ms) when ``signal`` omitted
|
|
282
304
|
* @returns {Promise<Object>} Response object with success flag
|
|
283
305
|
*/
|
|
284
306
|
async function makeApiCall(url, options = {}) {
|
|
@@ -292,9 +314,12 @@ async function makeApiCall(url, options = {}) {
|
|
|
292
314
|
|
|
293
315
|
const startTime = Date.now();
|
|
294
316
|
const fetchOptions = { ...options };
|
|
317
|
+
const singleRequestTimeoutMs = resolveSingleRequestTimeoutMs(fetchOptions);
|
|
295
318
|
if (!fetchOptions.signal) {
|
|
296
|
-
fetchOptions.signal = AbortSignal.timeout(
|
|
319
|
+
fetchOptions.signal = AbortSignal.timeout(singleRequestTimeoutMs);
|
|
297
320
|
}
|
|
321
|
+
delete fetchOptions.timeoutMs;
|
|
322
|
+
delete fetchOptions.requestTimeoutMs;
|
|
298
323
|
|
|
299
324
|
try {
|
|
300
325
|
const response = await fetch(url, fetchOptions);
|
|
@@ -309,7 +334,7 @@ async function makeApiCall(url, options = {}) {
|
|
|
309
334
|
const duration = Date.now() - startTime;
|
|
310
335
|
const error = err?.name === 'AbortError'
|
|
311
336
|
? new Error(
|
|
312
|
-
`Request timed out after ${
|
|
337
|
+
`Request timed out after ${Math.round(singleRequestTimeoutMs / 1000)} seconds. The controller may be unreachable. Check the URL and network.`
|
|
313
338
|
)
|
|
314
339
|
: err;
|
|
315
340
|
return await handleNetworkError(error, url, options, duration);
|