@aifabrix/builder 2.57.0 → 2.59.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/lib/agent-kit/codex-app-server.js +44 -0
- package/lib/agent-kit/init.js +4 -17
- package/lib/agent-kit/session-report.js +15 -10
- package/lib/agent-kit/setup-auth.js +131 -0
- package/lib/agent-kit/setup-context.js +118 -0
- package/lib/agent-kit/setup-github.js +70 -0
- package/lib/agent-kit/setup-managed.js +79 -0
- package/lib/agent-kit/setup-process.js +42 -0
- package/lib/agent-kit/setup-session.js +53 -0
- package/lib/agent-kit/setup-state.js +52 -0
- package/lib/agent-kit/setup-workspace.js +113 -0
- package/lib/agent-kit/setup.js +84 -0
- package/lib/agent-kit/start.js +4 -3
- package/lib/agent-kit/tmux-sessions.js +20 -3
- package/lib/api/dev.api.js +1 -1
- package/lib/api/miso-health.api.js +26 -0
- package/lib/api/types/dev.types.js +3 -3
- package/lib/app/deploy.js +0 -102
- package/lib/app/push.js +98 -41
- package/lib/build/standard-docker-build.js +13 -11
- package/lib/cli/index.js +4 -0
- package/lib/cli/setup-app.js +16 -5
- package/lib/cli/setup-dev-path-commands.js +53 -2
- package/lib/cli/setup-dev.js +7 -4
- package/lib/cli/setup-environment.js +17 -4
- package/lib/cli/setup-help.js +86 -0
- package/lib/cli/setup-onboarding.js +37 -0
- package/lib/cli/setup-utility-repair.js +7 -5
- package/lib/commands/agent-kit-help.js +21 -7
- package/lib/commands/agent-kit-sessions.js +126 -0
- package/lib/commands/agent-kit-setup.js +44 -0
- package/lib/commands/agent-kit.js +5 -79
- package/lib/commands/dev-init.js +21 -23
- package/lib/commands/dev-refresh.js +2 -2
- package/lib/commands/dev-registry.js +155 -0
- package/lib/commands/dev-show-display.js +10 -2
- package/lib/commands/governance-verify-external.js +12 -5
- package/lib/commands/repair-datasource.js +9 -9
- package/lib/commands/repair.js +45 -8
- package/lib/commands/setup-image-refresh.js +26 -0
- package/lib/commands/upload-qualification-client.js +56 -11
- package/lib/commands/upload-qualification-poll.js +51 -22
- package/lib/commands/wizard-dataplane.js +16 -52
- package/lib/core/config-attach-extensions.js +4 -1
- package/lib/datasource/datasource-validate-summary.js +8 -5
- package/lib/datasource/sync-defaults.js +53 -0
- package/lib/deployment/installation/azure-infra-stage.js +50 -9
- package/lib/deployment/installation/azure-preflight.js +63 -7
- package/lib/deployment/installation/azure-readiness.js +59 -7
- package/lib/deployment/installation/environment-auth.js +60 -20
- package/lib/deployment/installation/environment-stage.js +9 -5
- package/lib/deployment/installation/infra-catalog.js +57 -5
- package/lib/deployment/installation/infra-manifest.js +22 -2
- package/lib/deployment/installation/infra-projection.js +14 -3
- package/lib/deployment/installation/registry-auth-mode.js +121 -0
- package/lib/deployment/installation/stage-input.js +21 -6
- package/lib/deployment/marketplace-environment.js +26 -1
- package/lib/generator/builders.js +8 -9
- package/lib/generator/index.js +23 -12
- package/lib/integration-definition/apply.js +2 -2
- package/lib/internal/fs-real-sync.js +4 -1
- package/lib/programmatic/bearer-auth.js +3 -62
- package/lib/programmatic/builder-api-startup.js +51 -0
- package/lib/programmatic/builder-miso-runtime.js +167 -0
- package/lib/programmatic/openapi-descriptions.js +3 -3
- package/lib/programmatic/openapi-schema-descriptions-core.js +3 -3
- package/lib/programmatic/openapi-spec.js +2 -1
- package/lib/programmatic/route-handler-map.js +3 -2
- package/lib/schema/environment-deploy-request.schema.json +1 -1
- package/lib/schema/external-datasource.schema.json +20 -5
- package/lib/schema/infra-parameter.schema.json +1 -1
- package/lib/schema/infra.parameter.yaml +2 -1
- package/lib/schema/infrastructure-schema.json +200 -3
- package/lib/schema/installation-environment.schema.json +1 -1
- package/lib/schema/wizard-config.schema.json +1 -1
- package/lib/utils/app-register-config.js +2 -4
- package/lib/utils/build-resolve-image.js +2 -21
- package/lib/utils/cli-utils.js +8 -1
- package/lib/utils/config-paths.js +24 -29
- package/lib/utils/config-registry-preference.js +113 -0
- package/lib/utils/dev-init-ssh-merge.js +5 -4
- package/lib/utils/dev-user-groups.js +2 -1
- package/lib/utils/docker-build.js +53 -19
- package/lib/utils/docker-daemon-tls-ca.js +2 -2
- package/lib/utils/env-map.js +23 -6
- package/lib/utils/error-formatters/validation-errors.js +25 -5
- package/lib/utils/help-builder.js +4 -3
- package/lib/utils/image-tags.js +88 -0
- package/lib/utils/promote-dev-docker-image.js +6 -0
- package/lib/utils/registry-credentials.js +236 -0
- package/lib/utils/resolve-docker-image-ref.js +78 -9
- package/lib/utils/secrets-helpers.js +9 -1
- package/lib/utils/token-manager.js +29 -36
- package/lib/utils/variable-transformer.js +2 -4
- package/package.json +4 -2
- package/templates/agent-kit/instructions/AGENTKIT.md +39 -2
- package/templates/agent-kit/instructions/root.AGENTS.md +40 -1
- package/templates/agent-kit/skills/shared/hosts.md +23 -5
- package/templates/agent-kit/workspace/README.md +14 -1
- package/templates/applications/builder-api/application.yaml +1 -1
- package/templates/applications/dataplane/application.yaml +1 -1
- package/templates/applications/dataplane/env.template +1 -1
- package/templates/applications/miso-controller/application.yaml +1 -1
- package/templates/applications/miso-controller/env.template +1 -1
- package/templates/external-system/external-datasource.yaml.hbs +7 -6
- package/templates/marketplace/main.json +440 -82
|
@@ -336,6 +336,8 @@ function logConfigurationSection(p) {
|
|
|
336
336
|
logRow('Environment', p.environment);
|
|
337
337
|
logRow('Controller', p.controller);
|
|
338
338
|
logRow('Format', p.formatVal);
|
|
339
|
+
logRow('Registry', p.registry);
|
|
340
|
+
if (p.registry) logRow('Registry login', p.registryCredentials);
|
|
339
341
|
logRow('Platform mode', p.setupPlatformMode);
|
|
340
342
|
logRow('Traefik', p.traefikEnabled ? 'on' : 'off');
|
|
341
343
|
logRow('Scoped resources', p.scopedResourcesLabel);
|
|
@@ -389,14 +391,16 @@ async function loadDevShowData(devIdStr) {
|
|
|
389
391
|
const ports = devConfig.getDevPorts(Number.isNaN(devIdNum) ? 0 : devIdNum);
|
|
390
392
|
const [
|
|
391
393
|
environment, tlsEnabled, controller, formatRaw, useScoped, secretsRaw, setupPlatformMode, traefikEnabled,
|
|
392
|
-
remoteServer, dockerEndpoint, dockerTlsSkipVerify, mutagenFolder, syncSshUser, syncSshHost
|
|
394
|
+
remoteServer, dockerEndpoint, dockerTlsSkipVerify, mutagenFolder, syncSshUser, syncSshHost, registry
|
|
393
395
|
] = await Promise.all([
|
|
394
396
|
config.getCurrentEnvironment(), config.getTlsEnabled(), config.getControllerUrl(),
|
|
395
397
|
config.getFormat(), config.getUseEnvironmentScopedResources(), config.getAifabrixSecretsPath(),
|
|
396
398
|
config.getSetupPlatformMode(), config.getTraefikEnabled(),
|
|
397
399
|
config.getRemoteServer(), config.getDockerEndpoint(), config.getDockerTlsSkipVerify(),
|
|
398
|
-
config.getUserMutagenFolder(), config.getSyncSshUser(), config.getSyncSshHost()
|
|
400
|
+
config.getUserMutagenFolder(), config.getSyncSshUser(), config.getSyncSshHost(),
|
|
401
|
+
config.getRegistry ? config.getRegistry() : null
|
|
399
402
|
]);
|
|
403
|
+
const { registryCredentialLabel } = require('./dev-registry');
|
|
400
404
|
const urlsLocalPath = getUrlsLocalYamlPath();
|
|
401
405
|
const scopedResourcesLabel = useScoped ? 'on' : 'off (default)';
|
|
402
406
|
const workResolved = paths.getAifabrixWork();
|
|
@@ -412,6 +416,8 @@ async function loadDevShowData(devIdStr) {
|
|
|
412
416
|
tlsEnabled,
|
|
413
417
|
controller,
|
|
414
418
|
formatVal: formatRaw ?? null,
|
|
419
|
+
registry: registry || null,
|
|
420
|
+
registryCredentials: registryCredentialLabel(registry || null),
|
|
415
421
|
scopedResourcesLabel,
|
|
416
422
|
setupPlatformMode,
|
|
417
423
|
traefikEnabled,
|
|
@@ -451,6 +457,8 @@ function renderDevShow(data) {
|
|
|
451
457
|
tlsEnabled: data.tlsEnabled,
|
|
452
458
|
controller: data.controller,
|
|
453
459
|
formatVal: data.formatVal,
|
|
460
|
+
registry: data.registry,
|
|
461
|
+
registryCredentials: data.registryCredentials,
|
|
454
462
|
setupPlatformMode: data.setupPlatformMode,
|
|
455
463
|
traefikEnabled: data.traefikEnabled,
|
|
456
464
|
scopedResourcesLabel: data.scopedResourcesLabel,
|
|
@@ -10,7 +10,7 @@ const {
|
|
|
10
10
|
aggregateCoverage,
|
|
11
11
|
buildPackOverrideResult
|
|
12
12
|
} = require('./verify-governance-result-builders');
|
|
13
|
-
const { VERDICT
|
|
13
|
+
const { VERDICT } = require('../lifecycle/product-model');
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* @async
|
|
@@ -43,14 +43,21 @@ async function runGovernanceVerifyForExternalSystem(systemKey, options = {}) {
|
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
const
|
|
46
|
+
const allNotApplicable = datasourceRows.every(
|
|
47
|
+
r => r.verdict === VERDICT.NOT_APPLICABLE
|
|
48
|
+
);
|
|
49
|
+
const allClosed = datasourceRows.every(
|
|
50
|
+
r => r.verdict === VERDICT.VERIFIED || r.verdict === VERDICT.NOT_APPLICABLE
|
|
51
|
+
);
|
|
47
52
|
const coverage = aggregateCoverage(datasourceRows);
|
|
48
53
|
return {
|
|
49
54
|
systemKey: resolvedKey,
|
|
50
55
|
command: 'governance verify',
|
|
51
|
-
verdict:
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
verdict: allNotApplicable
|
|
57
|
+
? VERDICT.NOT_APPLICABLE
|
|
58
|
+
: (allClosed ? VERDICT.VERIFIED : VERDICT.FAILED),
|
|
59
|
+
policyCoveragePercent: coverage.policy ?? (allClosed ? 100 : 0),
|
|
60
|
+
dimensionCoveragePercent: coverage.dimension ?? (allClosed ? 100 : 0),
|
|
54
61
|
enforcementScenarios: sumEnforcementScenarios(datasourceRows),
|
|
55
62
|
datasourceRows
|
|
56
63
|
};
|
|
@@ -16,11 +16,12 @@ const {
|
|
|
16
16
|
repairNoneLlmToServiceContract,
|
|
17
17
|
stripDeprecatedServiceCategory
|
|
18
18
|
} = require('./repair-datasource-service-contract');
|
|
19
|
+
const {
|
|
20
|
+
DEFAULT_DATASOURCE_SYNC,
|
|
21
|
+
createDefaultDatasourceSync
|
|
22
|
+
} = require('../datasource/sync-defaults');
|
|
19
23
|
|
|
20
|
-
const DEFAULT_SYNC =
|
|
21
|
-
mode: 'pull',
|
|
22
|
-
batchSize: 500
|
|
23
|
-
};
|
|
24
|
+
const DEFAULT_SYNC = DEFAULT_DATASOURCE_SYNC;
|
|
24
25
|
|
|
25
26
|
const MINIMAL_METADATA_SCHEMA = {
|
|
26
27
|
type: 'object',
|
|
@@ -348,7 +349,7 @@ function repairExposeFromAttributes(parsed, changes) {
|
|
|
348
349
|
}
|
|
349
350
|
|
|
350
351
|
/**
|
|
351
|
-
* Adds default sync section
|
|
352
|
+
* Adds a default sync section only when absent. Not applied for entityType 'none'.
|
|
352
353
|
*
|
|
353
354
|
* @param {Object} parsed - Parsed datasource (mutated)
|
|
354
355
|
* @param {string[]} changes - Change log
|
|
@@ -356,10 +357,9 @@ function repairExposeFromAttributes(parsed, changes) {
|
|
|
356
357
|
*/
|
|
357
358
|
function repairSyncSection(parsed, changes) {
|
|
358
359
|
if (isNoneEntityType(parsed?.entityType)) return false;
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
changes.push('Added default sync section (mode: pull, batchSize: 500)');
|
|
360
|
+
if (Object.prototype.hasOwnProperty.call(parsed, 'sync')) return false;
|
|
361
|
+
parsed.sync = createDefaultDatasourceSync();
|
|
362
|
+
changes.push('Added default sync section (enabled: true, mode: pull, schedule: @monthly UTC, batchSize: 500)');
|
|
363
363
|
return true;
|
|
364
364
|
}
|
|
365
365
|
|
package/lib/commands/repair.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Repair external integration
|
|
2
|
+
* Repair application documentation and external integration configuration drift.
|
|
3
3
|
*
|
|
4
4
|
* Aligns application.yaml with actual system/datasource files, fixes system key mismatch,
|
|
5
5
|
* creates missing externalIntegration block, extracts rbac.yaml from system when needed,
|
|
6
6
|
* and regenerates the deployment manifest.
|
|
7
7
|
*
|
|
8
|
-
* @fileoverview Repair external integration drift
|
|
8
|
+
* @fileoverview Repair application README or external integration drift
|
|
9
9
|
* @author AI Fabrix Team
|
|
10
10
|
* @version 2.0.0
|
|
11
11
|
*/
|
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
'use strict';
|
|
14
14
|
|
|
15
15
|
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const { generateReadmeMd } = require('../app/readme');
|
|
18
|
+
const { backupIntegrationFile } = require('../utils/integration-file-backup');
|
|
19
|
+
const { trackRepairWrite } = require('./repair-changed-files');
|
|
16
20
|
const { detectAppType } = require('../utils/paths');
|
|
17
21
|
const { resolveApplicationConfigPath } = require('../utils/app-config-resolver');
|
|
18
22
|
const { loadConfigFile, isYamlPath } = require('../utils/config-format');
|
|
@@ -43,7 +47,7 @@ function loadConfigAndDiscover(appPath, configPath) {
|
|
|
43
47
|
* Validates repair inputs and resolves app path and config path.
|
|
44
48
|
* @param {string} appName - Application name
|
|
45
49
|
* @param {Object} options - Command options (auth)
|
|
46
|
-
* @returns {Promise<{ appPath: string, configPath: string, dryRun: boolean, authOption: string|undefined }>}
|
|
50
|
+
* @returns {Promise<{ appPath: string, configPath: string, dryRun: boolean, authOption: string|undefined, isExternal: boolean }>}
|
|
47
51
|
*/
|
|
48
52
|
async function validateAndResolveRepairPaths(appName, options) {
|
|
49
53
|
if (!appName || typeof appName !== 'string') throw new Error('App name is required');
|
|
@@ -52,10 +56,42 @@ async function validateAndResolveRepairPaths(appName, options) {
|
|
|
52
56
|
throw new Error('Option --auth must be a string');
|
|
53
57
|
}
|
|
54
58
|
const { appPath, isExternal } = await detectAppType(appName, options);
|
|
55
|
-
if (!isExternal)
|
|
59
|
+
if (!isExternal) validateApplicationRepairOptions(appName, options);
|
|
56
60
|
const configPath = resolveApplicationConfigPath(appPath);
|
|
57
61
|
if (!fs.existsSync(configPath)) throw new Error(`Application config not found: ${configPath}`);
|
|
58
|
-
return { appPath, configPath, dryRun, authOption };
|
|
62
|
+
return { appPath, configPath, dryRun, authOption, isExternal };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Validate the documentation-only repair supported for normal applications. */
|
|
66
|
+
function validateApplicationRepairOptions(appName, options) {
|
|
67
|
+
const integrationOptions = ['all', 'api', 'auth', 'rbac', 'expose', 'sync', 'test',
|
|
68
|
+
'splitDatasources', 'rbacPreset', 'authWebhook', 'authSync'];
|
|
69
|
+
if (!options.doc || integrationOptions.some(key => options[key] !== undefined && options[key] !== false && options[key] !== null)) {
|
|
70
|
+
throw new Error(`App '${appName}' is not an external integration. Normal applications support repair --doc only (with --dry-run or --no-backup).`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Regenerate a normal application's README using its current local configuration. */
|
|
75
|
+
function repairApplicationReadme(appName, appPath, configPath, backupCtx) {
|
|
76
|
+
const variables = loadConfigFile(configPath);
|
|
77
|
+
const content = generateReadmeMd(appName, { ...variables, ...variables.app });
|
|
78
|
+
const readmePath = path.join(appPath, 'README.md');
|
|
79
|
+
trackRepairWrite(readmePath, backupCtx);
|
|
80
|
+
if (!backupCtx.dryRun) {
|
|
81
|
+
backupIntegrationFile(readmePath, backupCtx);
|
|
82
|
+
fs.writeFileSync(readmePath, content, { mode: 0o644, encoding: 'utf8' });
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
updated: true,
|
|
86
|
+
applicationReadme: true,
|
|
87
|
+
actionsPerformed: true,
|
|
88
|
+
readmeRegenerated: true,
|
|
89
|
+
manifestRegenerated: false,
|
|
90
|
+
changes: ['Regenerated README.md from application configuration'],
|
|
91
|
+
changedFiles: backupCtx.dryRun ? backupCtx.wouldChangeFiles : backupCtx.changedFiles,
|
|
92
|
+
backupPaths: backupCtx.backupPaths,
|
|
93
|
+
warnings: []
|
|
94
|
+
};
|
|
59
95
|
}
|
|
60
96
|
|
|
61
97
|
/**
|
|
@@ -79,17 +115,18 @@ function createRepairBackupContext(options, dryRun) {
|
|
|
79
115
|
}
|
|
80
116
|
|
|
81
117
|
/**
|
|
82
|
-
* Repairs external integration config
|
|
118
|
+
* Repairs external integration config or regenerates a normal application's README.
|
|
83
119
|
* @async
|
|
84
120
|
* @param {string} appName - Application/integration name
|
|
85
121
|
* @param {Object} [options] - Options
|
|
86
122
|
* @param {boolean} [options.dryRun] - If true, only report changes; do not write
|
|
87
|
-
* @param {boolean} [options.doc] -
|
|
123
|
+
* @param {boolean} [options.doc] - Regenerate README.md from application config or integration deployment manifest
|
|
88
124
|
* @returns {Promise<Object>}
|
|
89
125
|
*/
|
|
90
126
|
async function repairExternalIntegration(appName, options = {}) {
|
|
91
|
-
const { appPath, configPath, dryRun, authOption } = await validateAndResolveRepairPaths(appName, options);
|
|
127
|
+
const { appPath, configPath, dryRun, authOption, isExternal } = await validateAndResolveRepairPaths(appName, options);
|
|
92
128
|
const { backupPaths, backupCtx } = createRepairBackupContext(options, dryRun);
|
|
129
|
+
if (!isExternal) return repairApplicationReadme(appName, appPath, configPath, backupCtx);
|
|
93
130
|
const loaded = loadConfigAndDiscover(appPath, configPath);
|
|
94
131
|
return runRepairExternalIntegration({
|
|
95
132
|
appName,
|
|
@@ -21,9 +21,30 @@ const infraHelpers = require('../infrastructure/helpers');
|
|
|
21
21
|
const upCommon = require('./up-common');
|
|
22
22
|
const { sectionTitle, formatDatasourceListRow } = require('../utils/cli-test-layout-chalk');
|
|
23
23
|
const { startSpinner, stopSpinnerSuccess } = require('./setup-spinners');
|
|
24
|
+
const { ensureLoginForImageRefs } = require('../utils/registry-credentials');
|
|
24
25
|
|
|
25
26
|
const PLATFORM_APPS = ['keycloak', 'miso-controller', 'dataplane'];
|
|
26
27
|
|
|
28
|
+
/**
|
|
29
|
+
* `image:` values of a compose file (plain scan; variables are left unresolved and skipped).
|
|
30
|
+
* @param {string} composePath - Compose file path
|
|
31
|
+
* @returns {string[]}
|
|
32
|
+
*/
|
|
33
|
+
function readComposeImageRefs(composePath) {
|
|
34
|
+
let text = '';
|
|
35
|
+
try {
|
|
36
|
+
text = String(fs.readFileSync(composePath, 'utf8') || '');
|
|
37
|
+
} catch {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
const refs = [];
|
|
41
|
+
for (const line of text.split(/\r?\n/)) {
|
|
42
|
+
const match = line.match(/^\s*image:\s*["']?([^"'\s#]+)["']?/);
|
|
43
|
+
if (match && !match[1].includes('$')) refs.push(match[1]);
|
|
44
|
+
}
|
|
45
|
+
return refs;
|
|
46
|
+
}
|
|
47
|
+
|
|
27
48
|
/**
|
|
28
49
|
* Pull infra-compose images (postgres, redis, optional pgadmin/redis-commander/traefik).
|
|
29
50
|
*
|
|
@@ -42,6 +63,8 @@ async function pullInfraImages(opts = {}) {
|
|
|
42
63
|
}
|
|
43
64
|
return;
|
|
44
65
|
}
|
|
66
|
+
// Authenticate once per private registry host before compose pulls (stored pairs only).
|
|
67
|
+
await ensureLoginForImageRefs(readComposeImageRefs(composePath));
|
|
45
68
|
const project = infraHelpers.getInfraProjectName(devId);
|
|
46
69
|
const composeCmd = await dockerUtils.getComposeCommand();
|
|
47
70
|
if (!quiet) {
|
|
@@ -91,6 +114,8 @@ async function pullPlatformAppImage(appName, opts = {}) {
|
|
|
91
114
|
}
|
|
92
115
|
return false;
|
|
93
116
|
}
|
|
117
|
+
// A partial stored credential pair fails closed here instead of degrading to a warning.
|
|
118
|
+
await ensureLoginForImageRefs([imageRef]);
|
|
94
119
|
try {
|
|
95
120
|
if (!quiet) {
|
|
96
121
|
const spin = startSpinner(`Pulling ${appName} image...`);
|
|
@@ -142,5 +167,6 @@ module.exports = {
|
|
|
142
167
|
PLATFORM_APPS,
|
|
143
168
|
pullInfraImages,
|
|
144
169
|
pullPlatformImages,
|
|
170
|
+
readComposeImageRefs,
|
|
145
171
|
runSetupImageRefresh
|
|
146
172
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Resumable Dataplane qualification upload: accept, --wait/--no-wait, poll, resume.
|
|
3
|
-
* Mirrors dev-host-ops.js's job id + wait/no-wait contract
|
|
3
|
+
* Mirrors dev-host-ops.js's job id + wait/no-wait contract. Never invents
|
|
4
4
|
* qualification truth — accept/poll surface exactly what Dataplane returns.
|
|
5
5
|
* @author AI Fabrix Team
|
|
6
6
|
* @version 1.0.0
|
|
@@ -12,13 +12,38 @@ const logger = require('../utils/logger');
|
|
|
12
12
|
const { formatSuccessLine, formatWarningLine } = require('../utils/cli-test-layout-chalk');
|
|
13
13
|
const { formatApiError } = require('../utils/api-error-handler');
|
|
14
14
|
const { uploadApplicationViaPipelineWithAuthRetry } = require('../utils/pipeline-upload-auth-retry');
|
|
15
|
-
const { pollQualificationJobUntilTerminal
|
|
15
|
+
const { pollQualificationJobUntilTerminal, requireQualificationSuccess,
|
|
16
|
+
isTransientQualificationPollError } = require('./upload-qualification-poll');
|
|
16
17
|
const { createQualificationPollHandlers } = require('../utils/qualification-poll-ui');
|
|
17
18
|
|
|
19
|
+
function isAmbiguousAcceptance(error) {
|
|
20
|
+
const status = error?.response?.status || error?.statusCode || error?.status;
|
|
21
|
+
const code = error?.cause?.code || error?.code;
|
|
22
|
+
return isTransientQualificationPollError(status) || (!status && (
|
|
23
|
+
error instanceof TypeError || ['AbortError', 'TimeoutError'].includes(error?.name) ||
|
|
24
|
+
['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'EAI_AGAIN', 'ENOTFOUND',
|
|
25
|
+
'ECONNABORTED', 'UND_ERR_CONNECT_TIMEOUT'].includes(code)
|
|
26
|
+
));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function submitQualificationUpload(dataplaneUrl, authConfig, payload, systemKey, options) {
|
|
30
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
31
|
+
try {
|
|
32
|
+
const res = await uploadApplicationViaPipelineWithAuthRetry(
|
|
33
|
+
dataplaneUrl, authConfig, payload, systemKey, options
|
|
34
|
+
);
|
|
35
|
+
if (res?.success !== false || !isTransientQualificationPollError(res.status) || attempt === 1) {
|
|
36
|
+
return res;
|
|
37
|
+
}
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (!isAmbiguousAcceptance(error) || attempt === 1) throw error;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
18
44
|
/**
|
|
19
|
-
* Ctrl-C
|
|
20
|
-
* the
|
|
21
|
-
* This only makes that safe fact visible, printing the resume command before exiting 130.
|
|
45
|
+
* Ctrl-C detaches from status following without asserting an unobserved job outcome.
|
|
46
|
+
* Prints the resume command before exiting 130.
|
|
22
47
|
* @param {string} jobId
|
|
23
48
|
* @param {string} systemKey
|
|
24
49
|
* @returns {() => void} Call to remove the listeners once polling finishes normally.
|
|
@@ -27,7 +52,7 @@ function registerUploadInterruptHint(jobId, systemKey) {
|
|
|
27
52
|
const onSignal = () => {
|
|
28
53
|
logger.log(
|
|
29
54
|
formatWarningLine(
|
|
30
|
-
`
|
|
55
|
+
`Stopped following upload job ${jobId}; its current outcome is unknown. Resume with: aifabrix upload ${systemKey} --resume ${jobId}`
|
|
31
56
|
)
|
|
32
57
|
);
|
|
33
58
|
process.exit(130);
|
|
@@ -52,12 +77,24 @@ function registerUploadInterruptHint(jobId, systemKey) {
|
|
|
52
77
|
* @returns {Promise<{jobId: string, state: string, resumed: boolean, candidateFingerprint: string}>}
|
|
53
78
|
*/
|
|
54
79
|
async function acceptQualificationUpload(dataplaneUrl, authConfig, payload, systemKey, options = {}) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
80
|
+
let res;
|
|
81
|
+
try {
|
|
82
|
+
res = await submitQualificationUpload(dataplaneUrl, authConfig, payload, systemKey, {
|
|
83
|
+
...options,
|
|
84
|
+
async: true
|
|
85
|
+
});
|
|
86
|
+
} catch (error) {
|
|
87
|
+
if (!isAmbiguousAcceptance(error)) throw error;
|
|
88
|
+
const unknown = new Error('Upload acceptance outcome is unknown. Rerun the same upload command to recover the job.');
|
|
89
|
+
unknown.cause = error;
|
|
90
|
+
unknown.statusCode = error?.response?.status || error?.statusCode || error?.status;
|
|
91
|
+
throw unknown;
|
|
92
|
+
}
|
|
59
93
|
if (res?.success === false) {
|
|
60
|
-
const
|
|
94
|
+
const ambiguous = isTransientQualificationPollError(res.status);
|
|
95
|
+
const err = new Error(ambiguous
|
|
96
|
+
? 'Upload acceptance outcome is unknown. Rerun the same upload command to recover the job.'
|
|
97
|
+
: formatApiError(res, dataplaneUrl));
|
|
61
98
|
const status = Number(res.status || res.statusCode);
|
|
62
99
|
err.statusCode = status >= 400 && status < 600 ? status : 400;
|
|
63
100
|
throw err;
|
|
@@ -107,6 +144,14 @@ async function resumeQualificationUpload(dataplaneUrl, authConfig, jobId, system
|
|
|
107
144
|
*/
|
|
108
145
|
async function runQualificationUpload(dataplaneUrl, authConfig, payload, systemKey, options = {}) {
|
|
109
146
|
const accepted = await acceptQualificationUpload(dataplaneUrl, authConfig, payload, systemKey, options);
|
|
147
|
+
if (accepted.state === 'completed' || accepted.state === 'failed') {
|
|
148
|
+
requireQualificationSuccess(accepted);
|
|
149
|
+
return {
|
|
150
|
+
...accepted,
|
|
151
|
+
candidateFingerprint: accepted.result?.candidateFingerprint,
|
|
152
|
+
terminal: accepted
|
|
153
|
+
};
|
|
154
|
+
}
|
|
110
155
|
const wait = options.wait !== false;
|
|
111
156
|
if (!wait) {
|
|
112
157
|
if (options.json) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview Poll a Dataplane qualification
|
|
2
|
+
* @fileoverview Poll a Dataplane qualification job while retaining resumable identity.
|
|
3
3
|
* Never invents success and never marks the candidate failed just because the CLI stopped polling —
|
|
4
4
|
* timing out returns a resumable jobId instead.
|
|
5
5
|
* @author AI Fabrix Team
|
|
@@ -13,7 +13,7 @@ const { formatApiError } = require('../utils/api-error-handler');
|
|
|
13
13
|
|
|
14
14
|
const POLL_INTERVAL_MS = 3000;
|
|
15
15
|
const POLL_TIMEOUT_MS = 30 * 60 * 1000;
|
|
16
|
-
const TRANSIENT_HTTP_STATUSES = new Set([502, 503, 504]);
|
|
16
|
+
const TRANSIENT_HTTP_STATUSES = new Set([429, 502, 503, 504]);
|
|
17
17
|
const MAX_TRANSIENT_POLL_ERRORS = 20;
|
|
18
18
|
|
|
19
19
|
const GOVERNANCE_SCENARIOS_MISSING = 'GOVERNANCE_SCENARIOS_MISSING';
|
|
@@ -46,6 +46,17 @@ function formatQualificationJobFailure(job) {
|
|
|
46
46
|
};
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/** Throw only for an authoritative stored failure, retaining the original job. */
|
|
50
|
+
function requireQualificationSuccess(job) {
|
|
51
|
+
if (job.state !== 'failed') return;
|
|
52
|
+
const failure = formatQualificationJobFailure(job);
|
|
53
|
+
const err = new Error(failure.message);
|
|
54
|
+
err.jobId = job.jobId;
|
|
55
|
+
err.job = job;
|
|
56
|
+
err.skipDoctor = failure.userFacing;
|
|
57
|
+
throw err;
|
|
58
|
+
}
|
|
59
|
+
|
|
49
60
|
/**
|
|
50
61
|
* @param {number|string} status
|
|
51
62
|
* @returns {boolean}
|
|
@@ -54,6 +65,35 @@ function isTransientQualificationPollError(status) {
|
|
|
54
65
|
return TRANSIENT_HTTP_STATUSES.has(Number(status));
|
|
55
66
|
}
|
|
56
67
|
|
|
68
|
+
/** Preserve the accepted identity without asserting a business outcome. */
|
|
69
|
+
function unavailableJob(jobId, message) {
|
|
70
|
+
const error = new Error(
|
|
71
|
+
`${message} Job ${jobId}: status unavailable. Resume with: aifabrix upload <systemKey> --resume ${jobId}`
|
|
72
|
+
);
|
|
73
|
+
error.jobId = jobId;
|
|
74
|
+
error.resumable = true;
|
|
75
|
+
return error;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Normalize transport exceptions so returned HTTP errors and throws share retries. */
|
|
79
|
+
async function readQualificationJob(dataplaneUrl, authConfig, jobId) {
|
|
80
|
+
try {
|
|
81
|
+
return await getPublishJobStatus(dataplaneUrl, authConfig, jobId);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
const status = error?.response?.status || error?.statusCode || error?.status;
|
|
84
|
+
const code = error?.cause?.code || error?.code;
|
|
85
|
+
const network = ['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'EAI_AGAIN',
|
|
86
|
+
'ENOTFOUND', 'ECONNABORTED', 'UND_ERR_CONNECT_TIMEOUT'].includes(code) ||
|
|
87
|
+
error instanceof TypeError || ['AbortError', 'TimeoutError'].includes(error?.name);
|
|
88
|
+
if (isTransientQualificationPollError(status) || (!status && network)) {
|
|
89
|
+
return { success: false, status: status || 503 };
|
|
90
|
+
}
|
|
91
|
+
const failure = unavailableJob(jobId, 'Unable to check upload progress.');
|
|
92
|
+
failure.statusCode = status;
|
|
93
|
+
throw failure;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
57
97
|
/**
|
|
58
98
|
* @param {string} dataplaneUrl
|
|
59
99
|
* @param {Object} authConfig
|
|
@@ -64,19 +104,19 @@ function isTransientQualificationPollError(status) {
|
|
|
64
104
|
async function fetchQualificationJobWithRetry(dataplaneUrl, authConfig, jobId, pollUi = {}) {
|
|
65
105
|
const started = Date.now();
|
|
66
106
|
for (let attempt = 0; attempt <= MAX_TRANSIENT_POLL_ERRORS; attempt += 1) {
|
|
67
|
-
const res = await
|
|
107
|
+
const res = await readQualificationJob(dataplaneUrl, authConfig, jobId);
|
|
68
108
|
if (res?.success !== false) {
|
|
69
109
|
return res.data;
|
|
70
110
|
}
|
|
71
111
|
if (!isTransientQualificationPollError(res.status)) {
|
|
72
|
-
const err =
|
|
112
|
+
const err = unavailableJob(jobId, formatApiError(res, dataplaneUrl));
|
|
73
113
|
const status = Number(res.status);
|
|
74
114
|
err.statusCode = status >= 400 && status < 600 ? status : 400;
|
|
75
115
|
throw err;
|
|
76
116
|
}
|
|
77
117
|
if (attempt >= MAX_TRANSIENT_POLL_ERRORS) {
|
|
78
|
-
throw
|
|
79
|
-
`The upload service returned HTTP ${res.status} repeatedly
|
|
118
|
+
throw unavailableJob(
|
|
119
|
+
jobId, `The upload service returned HTTP ${res.status} repeatedly.`
|
|
80
120
|
);
|
|
81
121
|
}
|
|
82
122
|
if (typeof pollUi.onTransientError === 'function') {
|
|
@@ -84,13 +124,13 @@ async function fetchQualificationJobWithRetry(dataplaneUrl, authConfig, jobId, p
|
|
|
84
124
|
}
|
|
85
125
|
await sleep(POLL_INTERVAL_MS);
|
|
86
126
|
}
|
|
87
|
-
throw
|
|
127
|
+
throw unavailableJob(jobId, 'Status retries exhausted.');
|
|
88
128
|
}
|
|
89
129
|
|
|
90
130
|
/**
|
|
91
131
|
* Poll a Dataplane qualification job to terminal state (completed|failed). The timeout only
|
|
92
132
|
* bounds how long *this CLI process* waits — Dataplane keeps running the job regardless; timing
|
|
93
|
-
* out returns a resumable jobId, never a fabricated failure
|
|
133
|
+
* out returns a resumable jobId, never a fabricated failure.
|
|
94
134
|
* @param {string} dataplaneUrl
|
|
95
135
|
* @param {Object} authConfig
|
|
96
136
|
* @param {string} jobId
|
|
@@ -112,25 +152,13 @@ async function pollQualificationJobUntilTerminal(
|
|
|
112
152
|
if (typeof pollUi.onPollProgress === 'function') {
|
|
113
153
|
pollUi.onPollProgress(job);
|
|
114
154
|
}
|
|
115
|
-
|
|
116
|
-
const failure = formatQualificationJobFailure(job);
|
|
117
|
-
const err = new Error(failure.message);
|
|
118
|
-
err.jobId = jobId;
|
|
119
|
-
err.job = job;
|
|
120
|
-
err.skipDoctor = failure.userFacing;
|
|
121
|
-
throw err;
|
|
122
|
-
}
|
|
155
|
+
requireQualificationSuccess(job);
|
|
123
156
|
if (job.state === 'completed') {
|
|
124
157
|
return job;
|
|
125
158
|
}
|
|
126
159
|
await sleep(POLL_INTERVAL_MS);
|
|
127
160
|
}
|
|
128
|
-
|
|
129
|
-
`Timed out waiting for upload job ${jobId}. It is still running — resume with: aifabrix upload <systemKey> --resume ${jobId}`
|
|
130
|
-
);
|
|
131
|
-
timeoutErr.jobId = jobId;
|
|
132
|
-
timeoutErr.resumable = true;
|
|
133
|
-
throw timeoutErr;
|
|
161
|
+
throw unavailableJob(jobId, 'Timed out waiting for upload progress.');
|
|
134
162
|
}
|
|
135
163
|
|
|
136
164
|
module.exports = {
|
|
@@ -139,5 +167,6 @@ module.exports = {
|
|
|
139
167
|
isTransientQualificationPollError,
|
|
140
168
|
fetchQualificationJobWithRetry,
|
|
141
169
|
formatQualificationJobFailure,
|
|
170
|
+
requireQualificationSuccess,
|
|
142
171
|
pollQualificationJobUntilTerminal
|
|
143
172
|
};
|
|
@@ -4,43 +4,11 @@
|
|
|
4
4
|
* @version 2.0.0
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
const chalk = require('chalk');
|
|
8
7
|
const logger = require('../utils/logger');
|
|
9
8
|
const { formatProgress, formatSuccessLine } = require('../utils/cli-test-layout-chalk');
|
|
10
9
|
const { getDataplaneUrl } = require('../datasource/deploy');
|
|
11
|
-
const { listEnvironmentApplications } = require('../api/environments.api');
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
* Find dataplane service application key from environment applications list
|
|
15
|
-
* @async
|
|
16
|
-
* @function findDataplaneServiceAppKey
|
|
17
|
-
* @param {string} controllerUrl - Controller URL
|
|
18
|
-
* @param {string} environment - Environment key
|
|
19
|
-
* @param {Object} authConfig - Authentication configuration
|
|
20
|
-
* @returns {Promise<string|null>} Dataplane service appKey or null if not found
|
|
21
|
-
*/
|
|
22
|
-
// eslint-disable-next-line complexity
|
|
23
|
-
async function findDataplaneServiceAppKey(controllerUrl, environment, authConfig) {
|
|
24
|
-
try {
|
|
25
|
-
const response = await listEnvironmentApplications(controllerUrl, environment, authConfig, { pageSize: 100 });
|
|
26
|
-
if (!response.success || !response.data) return null;
|
|
27
|
-
const applications = response.data.data || response.data || [];
|
|
28
|
-
for (const app of applications) {
|
|
29
|
-
const appKey = app.key || app.id;
|
|
30
|
-
if (!appKey) continue;
|
|
31
|
-
const keyLower = appKey.toLowerCase();
|
|
32
|
-
const appType = app.configuration?.type || app.type;
|
|
33
|
-
const nameLower = (app.displayName || app.name || '').toLowerCase();
|
|
34
|
-
if (keyLower === 'dataplane' || keyLower.includes('dataplane') || (appType === 'service' && nameLower.includes('dataplane'))) {
|
|
35
|
-
return appKey;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
return null;
|
|
39
|
-
} catch (error) {
|
|
40
|
-
logger.log(chalk.gray(` Could not list applications: ${error.message}`));
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
11
|
+
const DATAPLANE_APPLICATION_KEY = 'dataplane';
|
|
44
12
|
|
|
45
13
|
/**
|
|
46
14
|
* Check if error is a "Not Found" error
|
|
@@ -65,7 +33,7 @@ function createDataplaneNotFoundError() {
|
|
|
65
33
|
}
|
|
66
34
|
|
|
67
35
|
/**
|
|
68
|
-
*
|
|
36
|
+
* Get dataplane URL from the canonical controller application key.
|
|
69
37
|
* @async
|
|
70
38
|
* @param {string} controllerUrl - Controller URL
|
|
71
39
|
* @param {string} environment - Environment key
|
|
@@ -73,18 +41,23 @@ function createDataplaneNotFoundError() {
|
|
|
73
41
|
* @returns {Promise<string>} Dataplane URL
|
|
74
42
|
* @throws {Error} If dataplane URL cannot be retrieved
|
|
75
43
|
*/
|
|
76
|
-
async function
|
|
44
|
+
async function getExactDataplaneUrl(controllerUrl, environment, authConfig, silent) {
|
|
77
45
|
try {
|
|
78
|
-
const
|
|
46
|
+
const dataplaneUrl = await getDataplaneUrl(
|
|
47
|
+
controllerUrl,
|
|
48
|
+
DATAPLANE_APPLICATION_KEY,
|
|
49
|
+
environment,
|
|
50
|
+
authConfig
|
|
51
|
+
);
|
|
79
52
|
if (!silent) {
|
|
80
|
-
logger.log(formatSuccessLine(`Dataplane URL: ${
|
|
53
|
+
logger.log(formatSuccessLine(`Dataplane URL: ${dataplaneUrl}`));
|
|
81
54
|
}
|
|
82
|
-
return
|
|
83
|
-
} catch (
|
|
84
|
-
if (isNotFoundError(
|
|
55
|
+
return dataplaneUrl;
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (isNotFoundError(error)) {
|
|
85
58
|
throw createDataplaneNotFoundError();
|
|
86
59
|
}
|
|
87
|
-
throw
|
|
60
|
+
throw error;
|
|
88
61
|
}
|
|
89
62
|
}
|
|
90
63
|
|
|
@@ -105,15 +78,7 @@ async function discoverDataplaneUrl(controllerUrl, environment, authConfig, opts
|
|
|
105
78
|
logger.log(formatProgress('Getting dataplane URL from controller...'));
|
|
106
79
|
}
|
|
107
80
|
try {
|
|
108
|
-
|
|
109
|
-
if (dataplaneAppKey) {
|
|
110
|
-
const dataplaneUrl = await getDataplaneUrl(controllerUrl, dataplaneAppKey, environment, authConfig);
|
|
111
|
-
if (!silent) {
|
|
112
|
-
logger.log(formatSuccessLine(`Dataplane URL: ${dataplaneUrl}`));
|
|
113
|
-
}
|
|
114
|
-
return dataplaneUrl;
|
|
115
|
-
}
|
|
116
|
-
return await tryFallbackDataplaneUrl(controllerUrl, environment, authConfig, silent);
|
|
81
|
+
return await getExactDataplaneUrl(controllerUrl, environment, authConfig, silent);
|
|
117
82
|
} catch (error) {
|
|
118
83
|
if (error.message.includes('Could not discover dataplane URL')) {
|
|
119
84
|
throw error;
|
|
@@ -126,6 +91,5 @@ async function discoverDataplaneUrl(controllerUrl, environment, authConfig, opts
|
|
|
126
91
|
}
|
|
127
92
|
|
|
128
93
|
module.exports = {
|
|
129
|
-
discoverDataplaneUrl
|
|
130
|
-
findDataplaneServiceAppKey
|
|
94
|
+
discoverDataplaneUrl
|
|
131
95
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Attach token, path, format,
|
|
2
|
+
* Attach token, path, format, scoped-resources, and registry helpers to config exports.
|
|
3
3
|
*
|
|
4
4
|
* @fileoverview Keeps lib/core/config.js under max-lines
|
|
5
5
|
* @author AI Fabrix Team
|
|
@@ -42,6 +42,9 @@ function attachConfigExtensions(exportsObj, deps) {
|
|
|
42
42
|
const { createScopedResourcesPreferenceFunctions } = require('../utils/config-scoped-resources-preference');
|
|
43
43
|
Object.assign(exportsObj, createScopedResourcesPreferenceFunctions(getConfig, saveConfig));
|
|
44
44
|
|
|
45
|
+
const { createRegistryPreferenceFunctions } = require('../utils/config-registry-preference');
|
|
46
|
+
Object.assign(exportsObj, createRegistryPreferenceFunctions(getConfig, saveConfig));
|
|
47
|
+
|
|
45
48
|
const { createSecretsPathFunctions } = require('../utils/config-secrets-path');
|
|
46
49
|
Object.assign(exportsObj, createSecretsPathFunctions(getConfig, saveConfig));
|
|
47
50
|
}
|