@aifabrix/builder 2.55.2 → 2.57.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/session-report.js +76 -0
- package/lib/agent-kit/start.js +124 -57
- package/lib/agent-kit/tmux-sessions.js +163 -0
- package/lib/api/environments.api.js +38 -0
- package/lib/api/marketplace-bootstrap.api.js +31 -0
- package/lib/api/pipeline.api.js +32 -3
- package/lib/app/deploy.js +6 -5
- package/lib/cli/setup-app.js +7 -6
- package/lib/cli/setup-environment.js +76 -19
- package/lib/cli/setup-external-system-help.js +15 -3
- package/lib/cli/setup-external-system.js +21 -3
- package/lib/commands/agent-kit-help.js +32 -15
- package/lib/commands/agent-kit.js +90 -8
- package/lib/commands/repair-datasource-auto-rbac.js +4 -9
- package/lib/commands/upload-openapi-sync-followup.js +59 -0
- package/lib/commands/upload-qualification-client.js +131 -0
- package/lib/commands/upload-qualification-followups.js +104 -0
- package/lib/commands/upload-qualification-poll.js +143 -0
- package/lib/commands/upload.js +37 -156
- package/lib/deployment/deploy-poll-ui.js +14 -10
- package/lib/deployment/deployer-status.js +4 -4
- package/lib/deployment/installation/azure-infra-stage.js +160 -0
- package/lib/deployment/installation/azure-preflight.js +102 -0
- package/lib/deployment/installation/azure-readiness.js +71 -0
- package/lib/deployment/installation/docker-register.js +112 -0
- package/lib/deployment/installation/environment-auth.js +76 -0
- package/lib/deployment/installation/environment-manifest.js +165 -0
- package/lib/deployment/installation/environment-stage.js +191 -0
- package/lib/deployment/installation/index.js +39 -0
- package/lib/deployment/installation/infra-catalog.js +165 -0
- package/lib/deployment/installation/infra-manifest.js +161 -0
- package/lib/deployment/installation/infra-projection.js +86 -0
- package/lib/deployment/installation/public-ip.js +65 -0
- package/lib/deployment/installation/stage-input.js +169 -0
- package/lib/deployment/installation/stage-output.js +53 -0
- package/lib/deployment/installation/workspace-env.js +76 -0
- package/lib/deployment/installation/workspace-files.js +178 -0
- package/lib/deployment/installation/workspace-schema.js +40 -0
- package/lib/deployment/installation/workspace-secrets.js +66 -0
- package/lib/deployment/installation/workspace.js +128 -0
- package/lib/deployment/marketplace-environment.js +305 -0
- package/lib/external-system/deploy.js +46 -51
- package/lib/generator/builders.js +4 -0
- package/lib/generator/helpers.js +55 -99
- package/lib/generator/index.js +0 -4
- package/lib/generator/split-variables.js +11 -5
- package/lib/internal/fs-real-sync.js +29 -0
- package/lib/programmatic/builder-api-request.js +8 -4
- package/lib/programmatic/capability-registry.js +2 -0
- package/lib/programmatic/get-deploy-status.js +45 -0
- package/lib/programmatic/get-upload-status.js +45 -0
- package/lib/programmatic/http-accepted-response.js +31 -0
- package/lib/programmatic/openapi-descriptions.js +14 -4
- package/lib/programmatic/request-timeout.js +21 -15
- package/lib/programmatic/route-handler-map.js +6 -0
- package/lib/programmatic/run-deploy.js +7 -3
- package/lib/programmatic/run-upload.js +11 -7
- package/lib/role-assistant/package-validate.js +4 -0
- package/lib/role-assistant/test-runner-clarification-resubmit.js +149 -0
- package/lib/role-assistant/test-runner.js +14 -17
- package/lib/schema/application-schema.json +20 -2
- package/lib/schema/infra-parameter.schema.json +7 -0
- package/lib/schema/infra.parameter.yaml +32 -43
- package/lib/schema/infrastructure-schema.json +1480 -399
- package/lib/schema/installation-environment.schema.json +601 -0
- package/lib/utils/cli-utils.js +3 -2
- package/lib/utils/qualification-poll-ui.js +136 -0
- package/lib/utils/variable-transformer.js +3 -0
- package/lib/validation/required-variables.js +41 -0
- package/lib/validation/validator.js +8 -5
- package/package.json +7 -2
- package/templates/README.md +4 -0
- package/templates/agent-kit/instructions/AGENTKIT.md +6 -6
- package/templates/agent-kit/skills/shared/hosts.md +10 -10
- package/templates/applications/builder-api/env.template +4 -0
- package/templates/applications/dataplane/application.yaml +1 -1
- package/templates/applications/dataplane/env.template +1 -1
- package/templates/applications/keycloak/Dockerfile +12 -5
- package/templates/applications/keycloak/application.yaml +2 -2
- package/templates/applications/keycloak/cimd-lenient-parse/AbstractClientIdMetadataDocumentExecutor.java +3 -3
- package/templates/applications/keycloak/cimd-lenient-parse/SOURCE.md +6 -6
- package/templates/applications/keycloak/env.template +13 -1
- package/templates/applications/miso-controller/application.yaml +3 -2
- package/templates/applications/miso-controller/env.template +28 -22
- package/templates/marketplace/createUiDefinition.json +77 -0
- package/templates/marketplace/install.parameters.example.json +9 -0
- package/templates/marketplace/main.json +4087 -0
- package/lib/generator/deploy-manifest-azure-kv.js +0 -81
package/lib/generator/helpers.js
CHANGED
|
@@ -11,7 +11,24 @@
|
|
|
11
11
|
const fs = require('fs');
|
|
12
12
|
const path = require('path');
|
|
13
13
|
const { loadConfigFile } = require('../utils/config-format');
|
|
14
|
-
const {
|
|
14
|
+
const { getInfraParameterCatalog } = require('../parameters/infra-parameter-catalog');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Baseline `required` for a keyvault-located key, from infra.parameter.yaml's
|
|
18
|
+
* `requiredOnSystemOnboarding` (matched by exact key or keyPattern). Returns false when the
|
|
19
|
+
* catalog can't be loaded or has no matching entry, so generation never hard-fails on this.
|
|
20
|
+
* @param {string} vaultKey - Key with any `kv://` prefix already stripped
|
|
21
|
+
* @returns {boolean}
|
|
22
|
+
*/
|
|
23
|
+
function baselineRequiredForVaultKey(vaultKey) {
|
|
24
|
+
try {
|
|
25
|
+
const catalog = getInfraParameterCatalog();
|
|
26
|
+
const entry = catalog.findEntryForKey(vaultKey);
|
|
27
|
+
return entry?.requiredOnSystemOnboarding === true;
|
|
28
|
+
} catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
15
32
|
|
|
16
33
|
/**
|
|
17
34
|
* Loads application config file (application.yaml, application.json, or legacy path) via converter.
|
|
@@ -149,34 +166,41 @@ function validatePortalInput(portalInput, variableName) {
|
|
|
149
166
|
}
|
|
150
167
|
|
|
151
168
|
/**
|
|
152
|
-
* Parses environment variables from env.template and merges
|
|
169
|
+
* Parses environment variables from env.template and merges explicit application config
|
|
153
170
|
* @param {string} envTemplate - Content of env.template file
|
|
154
171
|
* @param {Object|null} [variablesConfig=null] - Optional configuration from application.yaml
|
|
155
172
|
* @returns {Array<Object>} Configuration array with merged portalInput
|
|
156
173
|
* @throws {Error} If portalInput structure is invalid
|
|
157
174
|
*/
|
|
158
175
|
/**
|
|
159
|
-
* Creates a map of
|
|
160
|
-
*
|
|
176
|
+
* Creates a map of configuration metadata from application.yaml.
|
|
177
|
+
* `required` is declarative: env names and kv:// values never imply it.
|
|
178
|
+
* @function createApplicationConfigMap
|
|
161
179
|
* @param {Object|null} variablesConfig - Configuration from application.yaml
|
|
162
|
-
* @returns {Map} Map of variable names to { portalInput
|
|
180
|
+
* @returns {Map} Map of variable names to { portalInput?, category?, required? }
|
|
163
181
|
*/
|
|
164
|
-
function
|
|
165
|
-
const
|
|
182
|
+
function createApplicationConfigMap(variablesConfig) {
|
|
183
|
+
const applicationConfigMap = new Map();
|
|
166
184
|
if (variablesConfig && variablesConfig.configuration && Array.isArray(variablesConfig.configuration)) {
|
|
167
185
|
for (const configItem of variablesConfig.configuration) {
|
|
168
|
-
if (configItem.name
|
|
169
|
-
|
|
186
|
+
if (!configItem.name) continue;
|
|
187
|
+
const overlay = {};
|
|
188
|
+
if (configItem.portalInput) {
|
|
170
189
|
validatePortalInput(configItem.portalInput, configItem.name);
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
190
|
+
overlay.portalInput = configItem.portalInput;
|
|
191
|
+
}
|
|
192
|
+
if (typeof configItem.category === 'string' && configItem.category.trim().length > 0) {
|
|
193
|
+
overlay.category = configItem.category.trim();
|
|
194
|
+
}
|
|
195
|
+
if (typeof configItem.required === 'boolean') {
|
|
196
|
+
overlay.required = configItem.required;
|
|
197
|
+
}
|
|
198
|
+
if (Object.keys(overlay).length > 0) {
|
|
199
|
+
applicationConfigMap.set(configItem.name, overlay);
|
|
176
200
|
}
|
|
177
201
|
}
|
|
178
202
|
}
|
|
179
|
-
return
|
|
203
|
+
return applicationConfigMap;
|
|
180
204
|
}
|
|
181
205
|
|
|
182
206
|
/**
|
|
@@ -210,68 +234,13 @@ function parseEnvironmentVariableLine(line) {
|
|
|
210
234
|
}
|
|
211
235
|
|
|
212
236
|
/**
|
|
213
|
-
* Determines location
|
|
237
|
+
* Determines storage location for a variable.
|
|
214
238
|
* @function determineVariableLocation
|
|
215
239
|
* @param {string} value - Variable value
|
|
216
|
-
* @
|
|
217
|
-
* @returns {Object} Object with location and required properties
|
|
240
|
+
* @returns {string} Deployment configuration location
|
|
218
241
|
*/
|
|
219
|
-
function determineVariableLocation(value
|
|
220
|
-
|
|
221
|
-
let required = false;
|
|
222
|
-
|
|
223
|
-
if (value.startsWith('kv://')) {
|
|
224
|
-
location = 'keyvault';
|
|
225
|
-
required = true;
|
|
226
|
-
} else if (value.startsWith('url://')) {
|
|
227
|
-
location = 'keyvault';
|
|
228
|
-
required = true;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// Check if it's a sensitive variable
|
|
232
|
-
const sensitiveKeys = ['password', 'secret', 'key', 'token', 'auth'];
|
|
233
|
-
if (sensitiveKeys.some(sensitive => key.toLowerCase().includes(sensitive))) {
|
|
234
|
-
required = true;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
if (location === 'keyvault' && value.startsWith('kv://')) {
|
|
238
|
-
try {
|
|
239
|
-
const { getInfraParameterCatalog } = require('../parameters/infra-parameter-catalog');
|
|
240
|
-
const entry = getInfraParameterCatalog().findEntryForKey(value.slice('kv://'.length));
|
|
241
|
-
if (entry?.scope === 'infra' && entry.generator?.type === 'emptyAllowed') {
|
|
242
|
-
required = false;
|
|
243
|
-
}
|
|
244
|
-
} catch {
|
|
245
|
-
// Keep the conservative required=true default when the catalog is unavailable.
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
return { location, required };
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* Maps env.template url:// value(s) to Key Vault secret name(s) for deploy JSON.
|
|
254
|
-
* Comma-separated lists are supported (e.g. CORS ALLOWED_ORIGINS): each segment
|
|
255
|
-
* starting with url:// is mapped; other segments are left unchanged (e.g. http://localhost:*).
|
|
256
|
-
*
|
|
257
|
-
* @param {string} appKey - application.yaml app.key
|
|
258
|
-
* @param {string} value - Full value after KEY= (must start with url:// for caller)
|
|
259
|
-
* @returns {string} Comma-joined secret names / literals
|
|
260
|
-
*/
|
|
261
|
-
function mapUrlTemplateValueToKeyVaultNames(appKey, value) {
|
|
262
|
-
const segments = value
|
|
263
|
-
.split(',')
|
|
264
|
-
.map((s) => s.trim())
|
|
265
|
-
.filter((s) => s.length > 0);
|
|
266
|
-
return segments
|
|
267
|
-
.map((seg) => {
|
|
268
|
-
if (seg.startsWith('url://')) {
|
|
269
|
-
const token = seg.slice('url://'.length).trim();
|
|
270
|
-
return urlTokenToKeyVaultSecretName(appKey, token);
|
|
271
|
-
}
|
|
272
|
-
return seg;
|
|
273
|
-
})
|
|
274
|
-
.join(',');
|
|
242
|
+
function determineVariableLocation(value) {
|
|
243
|
+
return value.startsWith('kv://') ? 'keyvault' : 'variable';
|
|
275
244
|
}
|
|
276
245
|
|
|
277
246
|
/**
|
|
@@ -280,26 +249,22 @@ function mapUrlTemplateValueToKeyVaultNames(appKey, value) {
|
|
|
280
249
|
* @param {string} key - Variable key
|
|
281
250
|
* @param {string} value - Variable value
|
|
282
251
|
* @param {string} location - Variable location
|
|
283
|
-
* @param {
|
|
284
|
-
* @param {Map} portalInputMap - Map of portalInput configurations
|
|
285
|
-
* @param {string|null} appKey - application.yaml app.key (required when value uses url://)
|
|
252
|
+
* @param {Map} applicationConfigMap - Explicit application.yaml configuration
|
|
286
253
|
* @returns {Object} Configuration item
|
|
287
254
|
*/
|
|
288
|
-
function createConfigItem(key, value, location,
|
|
255
|
+
function createConfigItem(key, value, location, applicationConfigMap) {
|
|
289
256
|
let storedValue = value;
|
|
290
257
|
if (location === 'keyvault') {
|
|
291
258
|
if (value.startsWith('kv://')) {
|
|
292
259
|
storedValue = value.replace('kv://', '');
|
|
293
|
-
} else if (value.startsWith('url://')) {
|
|
294
|
-
if (!appKey) {
|
|
295
|
-
throw new Error(
|
|
296
|
-
`Cannot resolve ${key}=${value}: application app.key is required to map url:// to a Key Vault secret name`
|
|
297
|
-
);
|
|
298
|
-
}
|
|
299
|
-
storedValue = mapUrlTemplateValueToKeyVaultNames(appKey, value);
|
|
300
260
|
}
|
|
301
261
|
}
|
|
302
262
|
|
|
263
|
+
const overlay = applicationConfigMap.get(key);
|
|
264
|
+
const required =
|
|
265
|
+
typeof overlay?.required === 'boolean'
|
|
266
|
+
? overlay.required
|
|
267
|
+
: location === 'keyvault' && baselineRequiredForVaultKey(storedValue);
|
|
303
268
|
const configItem = {
|
|
304
269
|
name: key,
|
|
305
270
|
value: storedValue,
|
|
@@ -307,10 +272,8 @@ function createConfigItem(key, value, location, required, portalInputMap, appKey
|
|
|
307
272
|
required
|
|
308
273
|
};
|
|
309
274
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
const overlay = portalInputMap.get(key);
|
|
313
|
-
configItem.portalInput = overlay.portalInput;
|
|
275
|
+
if (overlay) {
|
|
276
|
+
if (overlay.portalInput) configItem.portalInput = overlay.portalInput;
|
|
314
277
|
if (typeof overlay.category === 'string' && overlay.category.length > 0) {
|
|
315
278
|
configItem.category = overlay.category;
|
|
316
279
|
}
|
|
@@ -322,26 +285,19 @@ function createConfigItem(key, value, location, required, portalInputMap, appKey
|
|
|
322
285
|
function parseEnvironmentVariables(envTemplate, variablesConfig = null) {
|
|
323
286
|
const configuration = [];
|
|
324
287
|
const lines = envTemplate.split('\n');
|
|
325
|
-
const
|
|
326
|
-
const appKey =
|
|
327
|
-
variablesConfig && variablesConfig.app && variablesConfig.app.key
|
|
328
|
-
? String(variablesConfig.app.key).trim()
|
|
329
|
-
: null;
|
|
330
|
-
|
|
288
|
+
const applicationConfigMap = createApplicationConfigMap(variablesConfig);
|
|
331
289
|
for (const line of lines) {
|
|
332
290
|
const parsed = parseEnvironmentVariableLine(line);
|
|
333
291
|
if (!parsed) {
|
|
334
292
|
continue;
|
|
335
293
|
}
|
|
336
294
|
|
|
337
|
-
const
|
|
295
|
+
const location = determineVariableLocation(parsed.value);
|
|
338
296
|
const configItem = createConfigItem(
|
|
339
297
|
parsed.key,
|
|
340
298
|
parsed.value,
|
|
341
299
|
location,
|
|
342
|
-
|
|
343
|
-
portalInputMap,
|
|
344
|
-
appKey
|
|
300
|
+
applicationConfigMap
|
|
345
301
|
);
|
|
346
302
|
configuration.push(configItem);
|
|
347
303
|
}
|
package/lib/generator/index.js
CHANGED
|
@@ -22,7 +22,6 @@ const { generateControllerManifest } = require('./external-controller-manifest')
|
|
|
22
22
|
const { resolveVersionForApp } = require('../utils/image-version');
|
|
23
23
|
const { getContainerPort } = require('../utils/port-resolver');
|
|
24
24
|
const { buildEnvVarMap } = require('../utils/env-map');
|
|
25
|
-
const { rewriteFrontDoorHostForAzureDeploy } = require('./deploy-manifest-azure-kv');
|
|
26
25
|
|
|
27
26
|
/**
|
|
28
27
|
* Generates deployment JSON from application configuration files
|
|
@@ -192,8 +191,6 @@ function buildAndValidateDeployment(appName, variables, envTemplate, rbac, optio
|
|
|
192
191
|
substituteEnvVarsInDeployment(deployment, envVarMap);
|
|
193
192
|
}
|
|
194
193
|
|
|
195
|
-
rewriteFrontDoorHostForAzureDeploy(deployment);
|
|
196
|
-
|
|
197
194
|
// Ensure no other ${...} placeholders remain in manifest
|
|
198
195
|
_validator.validateNoUnresolvedVariablesInDeployment(deployment);
|
|
199
196
|
|
|
@@ -239,7 +236,6 @@ async function buildDeploymentManifestInMemory(appName, options = {}) {
|
|
|
239
236
|
}
|
|
240
237
|
const envVarMap = await buildEnvVarMap('docker', null, null, { appPort: effectivePort });
|
|
241
238
|
substituteEnvVarsInDeployment(deployment, envVarMap);
|
|
242
|
-
rewriteFrontDoorHostForAzureDeploy(deployment);
|
|
243
239
|
_validator.validateNoUnresolvedVariablesInDeployment(deployment);
|
|
244
240
|
return { deployment, appPath };
|
|
245
241
|
}
|
|
@@ -57,7 +57,7 @@ function extractOptionalSections(deployment) {
|
|
|
57
57
|
const names = [
|
|
58
58
|
'healthCheck', 'authentication', 'build', 'repository', 'deployment',
|
|
59
59
|
'startupCommand', 'runtimeVersion', 'scaling', 'frontDoorRouting',
|
|
60
|
-
'environmentScopedResources'
|
|
60
|
+
'environmentScopedResources', 'required'
|
|
61
61
|
];
|
|
62
62
|
for (const sectionName of names) {
|
|
63
63
|
extractOptionalSection(deployment, sectionName, optional);
|
|
@@ -66,19 +66,25 @@ function extractOptionalSections(deployment) {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
|
-
*
|
|
69
|
+
* Explicit application configuration metadata for application.yaml.
|
|
70
|
+
* required=false remains the default and is omitted.
|
|
70
71
|
* @param {Array} configuration - Configuration array from deployment JSON
|
|
71
|
-
* @returns {Array<{ name: string, category?: string, portalInput
|
|
72
|
+
* @returns {Array<{ name: string, category?: string, required?: boolean, portalInput?: Object }>}
|
|
72
73
|
*/
|
|
73
74
|
function extractPortalInputConfiguration(configuration) {
|
|
74
75
|
if (!Array.isArray(configuration) || configuration.length === 0) return [];
|
|
75
76
|
return configuration
|
|
76
|
-
.filter(item => item &&
|
|
77
|
+
.filter(item => item && typeof item.name === 'string' && (
|
|
78
|
+
item.portalInput || item.required === true ||
|
|
79
|
+
(typeof item.category === 'string' && item.category.trim().length > 0)
|
|
80
|
+
))
|
|
77
81
|
.map(item => {
|
|
78
|
-
const entry = { name: item.name
|
|
82
|
+
const entry = { name: item.name };
|
|
83
|
+
if (item.portalInput) entry.portalInput = item.portalInput;
|
|
79
84
|
if (typeof item.category === 'string' && item.category.trim().length > 0) {
|
|
80
85
|
entry.category = item.category.trim();
|
|
81
86
|
}
|
|
87
|
+
if (item.required === true) entry.required = true;
|
|
82
88
|
return entry;
|
|
83
89
|
});
|
|
84
90
|
}
|
|
@@ -137,8 +137,37 @@ function appendFileSync(p, data, opts) {
|
|
|
137
137
|
return delegate('appendFileSync', opts !== undefined ? [p, data, opts] : [p, data]);
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
/**
|
|
141
|
+
* @param {string} p
|
|
142
|
+
* @param {number|string} mode
|
|
143
|
+
* @returns {void}
|
|
144
|
+
*/
|
|
145
|
+
function chmodSync(p, mode) {
|
|
146
|
+
return delegate('chmodSync', [p, mode]);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* @param {string} p
|
|
151
|
+
* @returns {import('node:fs').Stats}
|
|
152
|
+
*/
|
|
153
|
+
function lstatSync(p) {
|
|
154
|
+
return delegate('lstatSync', [p]);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* @param {string} from
|
|
159
|
+
* @param {string} to
|
|
160
|
+
* @returns {void}
|
|
161
|
+
*/
|
|
162
|
+
function renameSync(from, to) {
|
|
163
|
+
return delegate('renameSync', [from, to]);
|
|
164
|
+
}
|
|
165
|
+
|
|
140
166
|
module.exports = {
|
|
141
167
|
existsSync,
|
|
168
|
+
chmodSync,
|
|
169
|
+
lstatSync,
|
|
170
|
+
renameSync,
|
|
142
171
|
readFileSync,
|
|
143
172
|
readFileSyncDirect,
|
|
144
173
|
writeFileSync,
|
|
@@ -14,6 +14,7 @@ const { withWorkspaceLock } = require('./workspace-lock');
|
|
|
14
14
|
const { getIdempotentResult, setIdempotentResult } = require('./idempotency-cache');
|
|
15
15
|
const { withRequestTimeout } = require('./request-timeout');
|
|
16
16
|
const { isHttpRawResult } = require('./http-raw-response');
|
|
17
|
+
const { isHttpAcceptedResult } = require('./http-accepted-response');
|
|
17
18
|
const { publicOriginFromRequest } = require('../channels/vendor-dataplane-url');
|
|
18
19
|
|
|
19
20
|
/**
|
|
@@ -103,10 +104,11 @@ async function runHandler(auth, capability, params, body, query) {
|
|
|
103
104
|
* @param {Object} options.body
|
|
104
105
|
* @param {string} options.requestId
|
|
105
106
|
* @param {Object} options.result
|
|
107
|
+
* @param {number} [options.httpStatus=200] - 202 for accepted-async operations (upload/deploy); idempotency replay preserves it.
|
|
106
108
|
* @returns {{ httpStatus: number, body: Object, requestId: string, userId: string }}
|
|
107
109
|
*/
|
|
108
110
|
function buildSuccessResponse(options) {
|
|
109
|
-
const { auth, idempotencyKey, capability, routeKey, params, body, requestId, result } = options;
|
|
111
|
+
const { auth, idempotencyKey, capability, routeKey, params, body, requestId, result, httpStatus = 200 } = options;
|
|
110
112
|
const responseBody = { success: true, data: result, correlationId: requestId };
|
|
111
113
|
if (idempotencyKey && isMutating(capability)) {
|
|
112
114
|
setIdempotentResult(
|
|
@@ -116,10 +118,10 @@ function buildSuccessResponse(options) {
|
|
|
116
118
|
route: routeKey,
|
|
117
119
|
systemKey: resolveIdempotencySystemKey(params, body)
|
|
118
120
|
},
|
|
119
|
-
{ httpStatus
|
|
121
|
+
{ httpStatus, body: responseBody, correlationId: requestId }
|
|
120
122
|
);
|
|
121
123
|
}
|
|
122
|
-
return { httpStatus
|
|
124
|
+
return { httpStatus, body: responseBody, requestId, userId: auth.userId };
|
|
123
125
|
}
|
|
124
126
|
|
|
125
127
|
/**
|
|
@@ -163,6 +165,7 @@ async function executeCapabilityRequest(options) {
|
|
|
163
165
|
if (isHttpRawResult(result)) {
|
|
164
166
|
return { httpStatus: 200, raw: result, requestId, userId: auth.userId };
|
|
165
167
|
}
|
|
168
|
+
const accepted = isHttpAcceptedResult(result);
|
|
166
169
|
return buildSuccessResponse({
|
|
167
170
|
auth,
|
|
168
171
|
idempotencyKey: req.headers['idempotency-key'],
|
|
@@ -171,7 +174,8 @@ async function executeCapabilityRequest(options) {
|
|
|
171
174
|
params,
|
|
172
175
|
body,
|
|
173
176
|
requestId,
|
|
174
|
-
result
|
|
177
|
+
result: accepted ? result.data : result,
|
|
178
|
+
httpStatus: accepted ? 202 : 200
|
|
175
179
|
});
|
|
176
180
|
}
|
|
177
181
|
|
|
@@ -48,7 +48,9 @@ const CAPABILITIES = Object.freeze([
|
|
|
48
48
|
{ method: 'POST', route: '/systems/:systemKey/certify/:pillar', handlerId: 'runCertifyPillar', operationType: 'certify', mutating: true },
|
|
49
49
|
{ method: 'GET', route: '/systems/:systemKey/lifecycle', handlerId: 'getLifecycleReport', operationType: 'certify.read', mutating: false },
|
|
50
50
|
{ method: 'POST', route: '/systems/:systemKey/upload', handlerId: 'runUpload', operationType: 'apply.dev', mutating: true },
|
|
51
|
+
{ method: 'GET', route: '/systems/:systemKey/upload/:jobId/status', handlerId: 'getUploadStatus', operationType: 'apply.dev.read', mutating: false },
|
|
51
52
|
{ method: 'POST', route: '/systems/:systemKey/deploy', handlerId: 'runDeploySystem', operationType: 'apply.prod', mutating: true },
|
|
53
|
+
{ method: 'GET', route: '/systems/:systemKey/deploy/:deploymentId/status', handlerId: 'getDeployStatus', operationType: 'apply.prod.read', mutating: false },
|
|
52
54
|
{ method: 'POST', route: '/systems/:systemKey/download', handlerId: 'runDownload', operationType: 'workspace.write', mutating: true },
|
|
53
55
|
{
|
|
54
56
|
method: 'POST',
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builder-api status read for an async external system deploy (plan 191.0 task 3).
|
|
3
|
+
* Thin proxy over Miso's own pipeline deployment status — Builder never invents deployment state.
|
|
4
|
+
*
|
|
5
|
+
* @fileoverview GET /systems/:systemKey/deploy/:deploymentId/status
|
|
6
|
+
* @author AI Fabrix Team
|
|
7
|
+
* @version 1.0.0
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const { runWithWorkspaceContext } = require('./workspace-context');
|
|
13
|
+
const { buildBearerAuthConfig } = require('./auth-config');
|
|
14
|
+
const { getPipelineDeployment } = require('../api/pipeline.api');
|
|
15
|
+
const { formatApiError } = require('../utils/api-error-handler');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {Object} res
|
|
19
|
+
* @param {string} controllerUrl
|
|
20
|
+
* @returns {Error & { statusCode: number }}
|
|
21
|
+
*/
|
|
22
|
+
function toDeployStatusError(res, controllerUrl) {
|
|
23
|
+
const err = new Error(formatApiError(res, controllerUrl));
|
|
24
|
+
const status = Number(res.status || res.statusCode);
|
|
25
|
+
err.statusCode = status >= 400 && status < 600 ? status : 502;
|
|
26
|
+
return err;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {import('./workspace-context').WorkspaceContext} ctx
|
|
31
|
+
* @param {string} deploymentId
|
|
32
|
+
* @returns {Promise<Object>} Miso deployment status (id, status, progress, updatedAt, qualification, ...)
|
|
33
|
+
*/
|
|
34
|
+
async function getDeployStatus(ctx, deploymentId) {
|
|
35
|
+
return runWithWorkspaceContext(ctx, async() => {
|
|
36
|
+
const authConfig = buildBearerAuthConfig(ctx.bearerToken);
|
|
37
|
+
const res = await getPipelineDeployment(ctx.controllerUrl, ctx.environment, deploymentId, authConfig);
|
|
38
|
+
if (res?.success === false) {
|
|
39
|
+
throw toDeployStatusError(res, ctx.controllerUrl);
|
|
40
|
+
}
|
|
41
|
+
return res.data ?? res;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = { getDeployStatus };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builder-api status read for an async qualification upload job (plan 191.0 task 3).
|
|
3
|
+
* Thin proxy over Dataplane's own publish-job status — Builder never invents job state.
|
|
4
|
+
*
|
|
5
|
+
* @fileoverview GET /systems/:systemKey/upload/:jobId/status
|
|
6
|
+
* @author AI Fabrix Team
|
|
7
|
+
* @version 1.0.0
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const { runWithWorkspaceContext } = require('./workspace-context');
|
|
13
|
+
const { buildProgrammaticAuthContext } = require('./auth-config');
|
|
14
|
+
const { getPublishJobStatus } = require('../api/pipeline.api');
|
|
15
|
+
const { formatApiError } = require('../utils/api-error-handler');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {Object} res
|
|
19
|
+
* @param {string} dataplaneUrl
|
|
20
|
+
* @returns {Error & { statusCode: number }}
|
|
21
|
+
*/
|
|
22
|
+
function toJobStatusError(res, dataplaneUrl) {
|
|
23
|
+
const err = new Error(formatApiError(res, dataplaneUrl));
|
|
24
|
+
const status = Number(res.status || res.statusCode);
|
|
25
|
+
err.statusCode = status >= 400 && status < 600 ? status : 502;
|
|
26
|
+
return err;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {import('./workspace-context').WorkspaceContext} ctx
|
|
31
|
+
* @param {string} jobId
|
|
32
|
+
* @returns {Promise<Object>} Dataplane PublishJobStatusResponse (jobId, state, steps, error, result)
|
|
33
|
+
*/
|
|
34
|
+
async function getUploadStatus(ctx, jobId) {
|
|
35
|
+
return runWithWorkspaceContext(ctx, async() => {
|
|
36
|
+
const { authConfig, dataplaneUrl } = buildProgrammaticAuthContext(ctx);
|
|
37
|
+
const res = await getPublishJobStatus(dataplaneUrl, authConfig, jobId);
|
|
38
|
+
if (res?.success === false) {
|
|
39
|
+
throw toJobStatusError(res, dataplaneUrl);
|
|
40
|
+
}
|
|
41
|
+
return res.data;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = { getUploadStatus };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Marker for builder-api handlers that accepted a durable async operation (202), instead of
|
|
3
|
+
* completing synchronously (200). Mirrors http-raw-response.js's escape-hatch pattern so
|
|
4
|
+
* executeCapabilityRequest can special-case the response status without every other handler
|
|
5
|
+
* needing to know about it (plan 191.0 task 3: replace 5-minute sync upload/deploy web requests
|
|
6
|
+
* with accepted operation responses).
|
|
7
|
+
*
|
|
8
|
+
* @fileoverview 202-accepted result marker for upload/deploy web handlers
|
|
9
|
+
* @author AI Fabrix Team
|
|
10
|
+
* @version 1.0.0
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {Object} data - Operation identity/state to return in the response body (e.g. {jobId, candidateFingerprint} or {deploymentId, status}).
|
|
17
|
+
* @returns {{ __httpAccepted: true, data: Object }}
|
|
18
|
+
*/
|
|
19
|
+
function createHttpAcceptedResult(data) {
|
|
20
|
+
return { __httpAccepted: true, data };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {unknown} result
|
|
25
|
+
* @returns {boolean}
|
|
26
|
+
*/
|
|
27
|
+
function isHttpAcceptedResult(result) {
|
|
28
|
+
return Boolean(result && typeof result === 'object' && result.__httpAccepted === true);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = { createHttpAcceptedResult, isHttpAcceptedResult };
|
|
@@ -129,15 +129,25 @@ const OPERATION_DESCRIPTIONS = Object.freeze({
|
|
|
129
129
|
tags: ['certify']
|
|
130
130
|
},
|
|
131
131
|
runUpload: {
|
|
132
|
-
summary: '
|
|
132
|
+
summary: 'Submit integration for durable Dataplane qualification (async)',
|
|
133
133
|
description:
|
|
134
|
-
'
|
|
134
|
+
'Validates, syncs deploy JSON, pushes credentials, then durably submits the candidate for Dataplane qualification (plan 191.0). Returns 202 immediately with {jobId, candidateFingerprint} — never blocks on the qualification ladder. Poll GET .../upload/{jobId}/status for terminal state. Requires external-system:publish in dev. Mutating; use Idempotency-Key.',
|
|
135
|
+
tags: ['apply']
|
|
136
|
+
},
|
|
137
|
+
getUploadStatus: {
|
|
138
|
+
summary: 'Read qualification job status',
|
|
139
|
+
description: 'Poll the durable Dataplane qualification job accepted by runUpload. Read-only proxy; terminal states are completed/failed.',
|
|
135
140
|
tags: ['apply']
|
|
136
141
|
},
|
|
137
142
|
runDeploySystem: {
|
|
138
|
-
summary: 'Deploy integration via controller (promote)',
|
|
143
|
+
summary: 'Deploy integration via controller (promote, async)',
|
|
139
144
|
description:
|
|
140
|
-
'Controller deploy for external systems (CLI aifabrix deploy parity). Optional body.gate
|
|
145
|
+
'Controller deploy for external systems (CLI aifabrix deploy parity). Optional body.gate is a no-op notice — qualification gates now run once during upload, not at deploy time. Returns 202 immediately with {deploymentId} — never blocks on the outer Miso deployment (fast when the candidate is already qualified, slower on the legacy ladder otherwise). Poll GET .../deploy/{deploymentId}/status for terminal state. Mutating; use Idempotency-Key.',
|
|
146
|
+
tags: ['apply']
|
|
147
|
+
},
|
|
148
|
+
getDeployStatus: {
|
|
149
|
+
summary: 'Read deployment status',
|
|
150
|
+
description: 'Poll the durable Miso deployment accepted by runDeploySystem. Read-only proxy; includes heartbeat (updatedAt) and, for qualified Connected Systems, a nested qualification/promotion summary.',
|
|
141
151
|
tags: ['apply']
|
|
142
152
|
},
|
|
143
153
|
runDownload: {
|
|
@@ -8,27 +8,33 @@
|
|
|
8
8
|
|
|
9
9
|
'use strict';
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Per-handler {envVar, defaultMs}. Plan 191.0 task 3: runUpload/runDeploySystem are accept-only
|
|
13
|
+
* (202) — local validation + manifest build + one accept HTTP call, never the qualification
|
|
14
|
+
* ladder or outer deployment — so their budget shrunk from 300000 (the old
|
|
15
|
+
* blocking-on-the-full-ladder value). Their status-read siblings are short, single-proxy reads.
|
|
16
|
+
* @type {Readonly<Record<string, { envVar: string, defaultMs: number }>>}
|
|
17
|
+
*/
|
|
18
|
+
const HANDLER_TIMEOUT_CONFIG = Object.freeze({
|
|
19
|
+
runUpload: { envVar: 'MAX_UPLOAD_TIMEOUT_MS', defaultMs: 45000 },
|
|
20
|
+
getUploadStatus: { envVar: 'MAX_UPLOAD_STATUS_TIMEOUT_MS', defaultMs: 15000 },
|
|
21
|
+
runDeploySystem: { envVar: 'MAX_DEPLOY_TIMEOUT_MS', defaultMs: 45000 },
|
|
22
|
+
getDeployStatus: { envVar: 'MAX_DEPLOY_STATUS_TIMEOUT_MS', defaultMs: 15000 },
|
|
23
|
+
runDownload: { envVar: 'MAX_DOWNLOAD_TIMEOUT_MS', defaultMs: 300000 },
|
|
24
|
+
runTest: { envVar: 'MAX_TEST_E2E_TIMEOUT_MS', defaultMs: 120000 },
|
|
25
|
+
runCertifyPillar: { envVar: 'MAX_CERTIFY_TIMEOUT_MS', defaultMs: 120000 }
|
|
26
|
+
});
|
|
27
|
+
|
|
11
28
|
/**
|
|
12
29
|
* @param {string} handlerId
|
|
13
30
|
* @returns {number}
|
|
14
31
|
*/
|
|
15
32
|
function getTimeoutMsForHandler(handlerId) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if (handlerId === 'runDeploySystem') {
|
|
20
|
-
return Number(process.env.MAX_DEPLOY_TIMEOUT_MS || 300000);
|
|
21
|
-
}
|
|
22
|
-
if (handlerId === 'runDownload') {
|
|
23
|
-
return Number(process.env.MAX_DOWNLOAD_TIMEOUT_MS || 300000);
|
|
24
|
-
}
|
|
25
|
-
if (handlerId === 'runTest') {
|
|
26
|
-
return Number(process.env.MAX_TEST_E2E_TIMEOUT_MS || 120000);
|
|
27
|
-
}
|
|
28
|
-
if (handlerId === 'runCertifyPillar') {
|
|
29
|
-
return Number(process.env.MAX_CERTIFY_TIMEOUT_MS || 120000);
|
|
33
|
+
const config = HANDLER_TIMEOUT_CONFIG[handlerId];
|
|
34
|
+
if (!config) {
|
|
35
|
+
return Number(process.env.MAX_REQUEST_TIMEOUT_MS || 120000);
|
|
30
36
|
}
|
|
31
|
-
return Number(process.env.
|
|
37
|
+
return Number(process.env[config.envVar] || config.defaultMs);
|
|
32
38
|
}
|
|
33
39
|
|
|
34
40
|
/**
|
|
@@ -19,6 +19,8 @@ const { runRepair } = require('./run-repair');
|
|
|
19
19
|
const { runValidate } = require('./run-validate');
|
|
20
20
|
const { runPreviewUpload, runUpload } = require('./run-upload');
|
|
21
21
|
const { runDeploySystem } = require('./run-deploy');
|
|
22
|
+
const { getUploadStatus } = require('./get-upload-status');
|
|
23
|
+
const { getDeployStatus } = require('./get-deploy-status');
|
|
22
24
|
const { runDownload } = require('./run-download');
|
|
23
25
|
const { runProtectionUpload, runProtectionCreate } = require('./run-protection');
|
|
24
26
|
const { runTest } = require('./run-test');
|
|
@@ -157,6 +159,8 @@ const HANDLER_MAP = {
|
|
|
157
159
|
probe: body.probe === true,
|
|
158
160
|
minimal: true
|
|
159
161
|
}),
|
|
162
|
+
getUploadStatus: (params, auth) =>
|
|
163
|
+
getUploadStatus(buildContext(auth, params.systemKey), params.jobId),
|
|
160
164
|
runDeploySystem: (params, auth, body) =>
|
|
161
165
|
runDeploySystem(buildContext(auth, params.systemKey), {
|
|
162
166
|
gate: body.gate,
|
|
@@ -165,6 +169,8 @@ const HANDLER_MAP = {
|
|
|
165
169
|
sync: body.sync === true,
|
|
166
170
|
verbose: body.verbose === true
|
|
167
171
|
}),
|
|
172
|
+
getDeployStatus: (params, auth) =>
|
|
173
|
+
getDeployStatus(buildContext(auth, params.systemKey), params.deploymentId),
|
|
168
174
|
runDownload: (params, auth, body = {}) =>
|
|
169
175
|
runDownload(buildContext(auth, params.systemKey), {
|
|
170
176
|
force: body.force === true,
|
|
@@ -12,6 +12,7 @@ const { deployExternalSystem } = require('../external-system/deploy');
|
|
|
12
12
|
const { runWithWorkspaceContext } = require('./workspace-context');
|
|
13
13
|
const { mergeProgrammaticOptions } = require('./command-options');
|
|
14
14
|
const { touchWorkspaceMeta } = require('./workspace-meta');
|
|
15
|
+
const { createHttpAcceptedResult } = require('./http-accepted-response');
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* @param {unknown} gate
|
|
@@ -39,9 +40,11 @@ function attachDeployStatus(err) {
|
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
/**
|
|
43
|
+
* Builder-api web callers never block on the outer Miso deployment (plan 191.0 task 3) — accept
|
|
44
|
+
* only, return 202 with the deployment id immediately; poll via getDeployStatus.
|
|
42
45
|
* @param {import('./workspace-context').WorkspaceContext} ctx
|
|
43
46
|
* @param {Object} [options]
|
|
44
|
-
* @param {string} [options.gate] - Same as CLI --gate (operations,trust,governance,full)
|
|
47
|
+
* @param {string} [options.gate] - Same as CLI --gate (operations,trust,governance,full); now a no-op notice, kept for compatibility.
|
|
45
48
|
* @param {string} [options.subjectEmail]
|
|
46
49
|
* @param {boolean} [options.probe]
|
|
47
50
|
* @param {boolean} [options.sync]
|
|
@@ -55,13 +58,14 @@ async function runDeploySystem(ctx, options = {}) {
|
|
|
55
58
|
subjectEmail: options.subjectEmail,
|
|
56
59
|
probe: options.probe === true,
|
|
57
60
|
sync: options.sync === true,
|
|
58
|
-
verbose: options.verbose === true
|
|
61
|
+
verbose: options.verbose === true,
|
|
62
|
+
poll: false
|
|
59
63
|
});
|
|
60
64
|
return runWithWorkspaceContext(ctx, async() => {
|
|
61
65
|
try {
|
|
62
66
|
const result = await deployExternalSystem(ctx.systemKey, merged);
|
|
63
67
|
touchWorkspaceMeta(ctx, { lastOperation: 'deploy' });
|
|
64
|
-
return result && typeof result === 'object' ? result : { deployed: true };
|
|
68
|
+
return createHttpAcceptedResult(result && typeof result === 'object' ? result : { deployed: true });
|
|
65
69
|
} catch (err) {
|
|
66
70
|
throw attachDeployStatus(err);
|
|
67
71
|
}
|