@aifabrix/builder 2.36.2 → 2.37.5
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/project-rules.mdc +19 -0
- package/README.md +68 -104
- package/integration/hubspot/test.js +1 -1
- package/lib/api/wizard.api.js +24 -1
- package/lib/app/deploy.js +43 -7
- package/lib/app/display.js +1 -1
- package/lib/app/list.js +3 -1
- package/lib/app/run-helpers.js +1 -1
- package/lib/build/index.js +3 -4
- package/lib/cli/index.js +45 -0
- package/lib/cli/setup-app.js +230 -0
- package/lib/cli/setup-auth.js +88 -0
- package/lib/cli/setup-dev.js +101 -0
- package/lib/cli/setup-environment.js +53 -0
- package/lib/cli/setup-external-system.js +87 -0
- package/lib/cli/setup-infra.js +219 -0
- package/lib/cli/setup-secrets.js +48 -0
- package/lib/cli/setup-utility.js +202 -0
- package/lib/cli.js +7 -961
- package/lib/commands/up-common.js +31 -1
- package/lib/commands/up-miso.js +6 -2
- package/lib/commands/wizard-core.js +32 -7
- package/lib/core/config.js +10 -0
- package/lib/core/ensure-encryption-key.js +56 -0
- package/lib/deployment/deployer-status.js +101 -0
- package/lib/deployment/deployer.js +62 -110
- package/lib/deployment/environment.js +133 -34
- package/lib/external-system/deploy.js +5 -1
- package/lib/external-system/test-auth.js +14 -7
- package/lib/generator/wizard.js +37 -41
- package/lib/infrastructure/helpers.js +1 -1
- package/lib/schema/environment-deploy-request.schema.json +64 -0
- package/lib/utils/help-builder.js +5 -2
- package/lib/utils/paths.js +22 -4
- package/lib/utils/secrets-generator.js +23 -8
- package/lib/utils/secrets-helpers.js +46 -21
- package/package.json +1 -1
- package/scripts/install-local.js +11 -2
- package/templates/applications/README.md.hbs +3 -3
- package/templates/applications/dataplane/variables.yaml +0 -2
- package/templates/applications/miso-controller/variables.yaml +0 -2
- package/templates/external-system/deploy.js.hbs +69 -0
- package/templates/infra/environment-dev.json +10 -0
|
@@ -30,6 +30,36 @@ async function ensureTemplateAtPath(appName, targetAppPath) {
|
|
|
30
30
|
return true;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Patches variables.yaml to set build.envOutputPath to null for deploy-only (no local code).
|
|
35
|
+
* Use when running up-miso/up-platform so we do not copy .env to repo paths or show that message.
|
|
36
|
+
* Patches both primary builder path and cwd/builder if different.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} appName - Application name (e.g. miso-controller, dataplane)
|
|
39
|
+
*/
|
|
40
|
+
function patchEnvOutputPathForDeployOnly(appName) {
|
|
41
|
+
if (!appName || typeof appName !== 'string') return;
|
|
42
|
+
const pathsToPatch = [pathsUtil.getBuilderPath(appName)];
|
|
43
|
+
const cwdBuilderPath = path.join(process.cwd(), 'builder', appName);
|
|
44
|
+
if (path.resolve(cwdBuilderPath) !== path.resolve(pathsToPatch[0])) {
|
|
45
|
+
pathsToPatch.push(cwdBuilderPath);
|
|
46
|
+
}
|
|
47
|
+
const envOutputPathLine = /^(\s*envOutputPath:)\s*.*$/m;
|
|
48
|
+
const replacement = '$1 null # deploy only, no copy';
|
|
49
|
+
for (const appPath of pathsToPatch) {
|
|
50
|
+
const variablesPath = path.join(appPath, 'variables.yaml');
|
|
51
|
+
if (!fs.existsSync(variablesPath)) continue;
|
|
52
|
+
try {
|
|
53
|
+
let content = fs.readFileSync(variablesPath, 'utf8');
|
|
54
|
+
if (!envOutputPathLine.test(content)) continue;
|
|
55
|
+
content = content.replace(envOutputPathLine, replacement);
|
|
56
|
+
fs.writeFileSync(variablesPath, content, 'utf8');
|
|
57
|
+
} catch (err) {
|
|
58
|
+
logger.warn(chalk.yellow(`Could not patch envOutputPath in ${variablesPath}: ${err.message}`));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
33
63
|
/**
|
|
34
64
|
* Ensures builder app directory exists from template if variables.yaml is missing.
|
|
35
65
|
* If builder/<appName>/variables.yaml does not exist, copies from templates/applications/<appName>.
|
|
@@ -69,4 +99,4 @@ async function ensureAppFromTemplate(appName) {
|
|
|
69
99
|
return primaryCopied;
|
|
70
100
|
}
|
|
71
101
|
|
|
72
|
-
module.exports = { ensureAppFromTemplate };
|
|
102
|
+
module.exports = { ensureAppFromTemplate, patchEnvOutputPathForDeployOnly };
|
package/lib/commands/up-miso.js
CHANGED
|
@@ -19,7 +19,7 @@ const secrets = require('../core/secrets');
|
|
|
19
19
|
const infra = require('../infrastructure');
|
|
20
20
|
const app = require('../app');
|
|
21
21
|
const { saveLocalSecret } = require('../utils/local-secrets');
|
|
22
|
-
const { ensureAppFromTemplate } = require('./up-common');
|
|
22
|
+
const { ensureAppFromTemplate, patchEnvOutputPathForDeployOnly } = require('./up-common');
|
|
23
23
|
|
|
24
24
|
/** Keycloak base port (from templates/applications/keycloak/variables.yaml) */
|
|
25
25
|
const KEYCLOAK_BASE_PORT = 8082;
|
|
@@ -126,12 +126,16 @@ async function handleUpMiso(options = {}) {
|
|
|
126
126
|
const health = await infra.checkInfraHealth(undefined, { strict: true });
|
|
127
127
|
const allHealthy = Object.values(health).every(status => status === 'healthy');
|
|
128
128
|
if (!allHealthy) {
|
|
129
|
-
throw new Error('Infrastructure is not up. Run \'aifabrix up\' first.');
|
|
129
|
+
throw new Error('Infrastructure is not up. Run \'aifabrix up-infra\' first.');
|
|
130
130
|
}
|
|
131
131
|
logger.log(chalk.green('✓ Infrastructure is up'));
|
|
132
132
|
await ensureAppFromTemplate('keycloak');
|
|
133
133
|
await ensureAppFromTemplate('miso-controller');
|
|
134
134
|
await ensureAppFromTemplate('dataplane');
|
|
135
|
+
// Deploy-only: do not copy .env to repo paths; patch variables so envOutputPath is null
|
|
136
|
+
patchEnvOutputPathForDeployOnly('keycloak');
|
|
137
|
+
patchEnvOutputPathForDeployOnly('miso-controller');
|
|
138
|
+
patchEnvOutputPathForDeployOnly('dataplane');
|
|
135
139
|
const developerId = await config.getDeveloperId();
|
|
136
140
|
const devIdNum = typeof developerId === 'string' ? parseInt(developerId, 10) : developerId;
|
|
137
141
|
await setMisoSecretsAndResolve(devIdNum);
|
|
@@ -18,7 +18,8 @@ const {
|
|
|
18
18
|
detectType,
|
|
19
19
|
generateConfig,
|
|
20
20
|
validateWizardConfig,
|
|
21
|
-
getDeploymentDocs
|
|
21
|
+
getDeploymentDocs,
|
|
22
|
+
postDeploymentDocs
|
|
22
23
|
} = require('../api/wizard.api');
|
|
23
24
|
const { generateWizardFiles } = require('../generator/wizard');
|
|
24
25
|
const {
|
|
@@ -353,23 +354,47 @@ async function handleFileSaving(appName, systemConfig, datasourceConfigs, system
|
|
|
353
354
|
logger.log(chalk.blue('\n\uD83D\uDCCB Step 7: Save Files'));
|
|
354
355
|
const spinner = ora('Saving files...').start();
|
|
355
356
|
try {
|
|
356
|
-
|
|
357
|
-
if (systemKey && dataplaneUrl && authConfig) {
|
|
357
|
+
const generatedFiles = await generateWizardFiles(appName, systemConfig, datasourceConfigs, systemKey, { aiGeneratedReadme: null });
|
|
358
|
+
if (systemKey && dataplaneUrl && authConfig && generatedFiles.appPath) {
|
|
358
359
|
try {
|
|
359
|
-
const
|
|
360
|
-
|
|
360
|
+
const appPath = generatedFiles.appPath;
|
|
361
|
+
const deployKey = appName;
|
|
362
|
+
const variablesPath = path.join(appPath, 'variables.yaml');
|
|
363
|
+
const deployPath = path.join(appPath, `${deployKey}-deploy.json`);
|
|
364
|
+
let variablesYaml = null;
|
|
365
|
+
let deployJson = null;
|
|
366
|
+
try {
|
|
367
|
+
variablesYaml = await fs.readFile(variablesPath, 'utf8');
|
|
368
|
+
} catch {
|
|
369
|
+
// optional
|
|
370
|
+
}
|
|
371
|
+
try {
|
|
372
|
+
const deployContent = await fs.readFile(deployPath, 'utf8');
|
|
373
|
+
deployJson = JSON.parse(deployContent);
|
|
374
|
+
} catch {
|
|
375
|
+
// optional
|
|
376
|
+
}
|
|
377
|
+
const body = (variablesYaml !== null && variablesYaml !== undefined) || (deployJson !== null && deployJson !== undefined) ? { variablesYaml: variablesYaml || null, deployJson: deployJson || null } : null;
|
|
378
|
+
const docsResponse = body
|
|
379
|
+
? await postDeploymentDocs(dataplaneUrl, authConfig, systemKey, body)
|
|
380
|
+
: await getDeploymentDocs(dataplaneUrl, authConfig, systemKey);
|
|
381
|
+
const content = docsResponse?.data?.content ?? docsResponse?.content;
|
|
382
|
+
if (content && typeof content === 'string') {
|
|
383
|
+
const readmePath = path.join(appPath, 'README.md');
|
|
384
|
+
await fs.writeFile(readmePath, content, 'utf8');
|
|
385
|
+
logger.log(chalk.gray(' Updated README.md from deployment-docs API (variables.yaml + deploy JSON).'));
|
|
386
|
+
}
|
|
361
387
|
} catch (e) {
|
|
362
388
|
logger.log(chalk.gray(` Could not fetch AI-generated README: ${e.message}`));
|
|
363
389
|
}
|
|
364
390
|
}
|
|
365
|
-
const generatedFiles = await generateWizardFiles(appName, systemConfig, datasourceConfigs, systemKey, { aiGeneratedReadme });
|
|
366
391
|
spinner.stop();
|
|
367
392
|
logger.log(chalk.green('\n\u2713 Wizard completed successfully!'));
|
|
368
393
|
logger.log(chalk.green(`\nFiles created in: ${generatedFiles.appPath}`));
|
|
369
394
|
logger.log(chalk.blue('\nNext steps:'));
|
|
370
395
|
logger.log(chalk.gray(` 1. Review the generated files in integration/${appName}/`));
|
|
371
396
|
logger.log(chalk.gray(' 2. Update env.template with your authentication details'));
|
|
372
|
-
logger.log(chalk.gray(` 3. Deploy using:
|
|
397
|
+
logger.log(chalk.gray(` 3. Deploy using: node deploy.js or aifabrix deploy ${appName}`));
|
|
373
398
|
return generatedFiles;
|
|
374
399
|
} catch (error) {
|
|
375
400
|
spinner.stop();
|
package/lib/core/config.js
CHANGED
|
@@ -396,6 +396,15 @@ async function setSecretsEncryptionKey(key) {
|
|
|
396
396
|
await saveConfig(config);
|
|
397
397
|
}
|
|
398
398
|
|
|
399
|
+
/**
|
|
400
|
+
* Ensure secrets encryption key exists (empty install). Delegates to ensure-encryption-key module.
|
|
401
|
+
* @returns {Promise<void>}
|
|
402
|
+
*/
|
|
403
|
+
async function ensureSecretsEncryptionKey() {
|
|
404
|
+
const { ensureSecretsEncryptionKey: run } = require('./ensure-encryption-key');
|
|
405
|
+
await run({ getSecretsEncryptionKey, setSecretsEncryptionKey, getSecretsPath });
|
|
406
|
+
}
|
|
407
|
+
|
|
399
408
|
async function getSecretsPath() {
|
|
400
409
|
const config = await getConfig();
|
|
401
410
|
return config['aifabrix-secrets'] || config['secrets-path'] || null;
|
|
@@ -427,6 +436,7 @@ const exportsObj = {
|
|
|
427
436
|
decryptTokenValue,
|
|
428
437
|
getSecretsEncryptionKey,
|
|
429
438
|
setSecretsEncryptionKey,
|
|
439
|
+
ensureSecretsEncryptionKey,
|
|
430
440
|
getSecretsPath,
|
|
431
441
|
setSecretsPath,
|
|
432
442
|
normalizeControllerUrl,
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ensure secrets encryption key exists on empty install.
|
|
3
|
+
* If missing from config and from user/project secrets, generates and saves one. Never logs the key.
|
|
4
|
+
*
|
|
5
|
+
* @fileoverview Encryption key bootstrap for empty installation
|
|
6
|
+
* @author AI Fabrix Team
|
|
7
|
+
* @version 2.0.0
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const yaml = require('js-yaml');
|
|
13
|
+
const crypto = require('crypto');
|
|
14
|
+
const pathsUtil = require('../utils/paths');
|
|
15
|
+
const { saveLocalSecret } = require('../utils/local-secrets');
|
|
16
|
+
|
|
17
|
+
const ENCRYPTION_KEY = 'secrets-encryptionKeyVault';
|
|
18
|
+
|
|
19
|
+
function readKeyFromFile(filePath) {
|
|
20
|
+
try {
|
|
21
|
+
if (!fs.existsSync(filePath)) return null;
|
|
22
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
23
|
+
const data = yaml.load(content);
|
|
24
|
+
if (data && typeof data[ENCRYPTION_KEY] === 'string') return data[ENCRYPTION_KEY];
|
|
25
|
+
} catch {
|
|
26
|
+
// Ignore
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Ensure secrets encryption key exists. If config already has it, do nothing.
|
|
33
|
+
* If key exists in user or project secrets file, set config. Otherwise generate, write to user secrets, set config.
|
|
34
|
+
* @param {Object} config - Config module (getSecretsEncryptionKey, setSecretsEncryptionKey, getSecretsPath)
|
|
35
|
+
* @returns {Promise<void>}
|
|
36
|
+
*/
|
|
37
|
+
async function ensureSecretsEncryptionKey(config) {
|
|
38
|
+
const existing = await config.getSecretsEncryptionKey();
|
|
39
|
+
if (existing) return;
|
|
40
|
+
|
|
41
|
+
const userSecretsPath = path.join(pathsUtil.getAifabrixHome(), 'secrets.local.yaml');
|
|
42
|
+
const projectSecretsPath = await config.getSecretsPath();
|
|
43
|
+
|
|
44
|
+
let key = readKeyFromFile(userSecretsPath);
|
|
45
|
+
if (!key && projectSecretsPath) key = readKeyFromFile(path.resolve(projectSecretsPath));
|
|
46
|
+
if (key) {
|
|
47
|
+
await config.setSecretsEncryptionKey(key);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const newKey = crypto.randomBytes(32).toString('hex');
|
|
52
|
+
await saveLocalSecret(ENCRYPTION_KEY, newKey);
|
|
53
|
+
await config.setSecretsEncryptionKey(newKey);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = { ensureSecretsEncryptionKey };
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deployment status and polling helpers for deployer.
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Deployment status checks and polling utilities
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 2.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const chalk = require('chalk');
|
|
10
|
+
const logger = require('../utils/logger');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Checks if deployment status is terminal
|
|
14
|
+
* @param {string} status - Deployment status
|
|
15
|
+
* @returns {boolean} True if status is terminal
|
|
16
|
+
*/
|
|
17
|
+
function isTerminalStatus(status) {
|
|
18
|
+
return status === 'completed' || status === 'failed' || status === 'cancelled';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Convert authConfig to pipeline auth config format
|
|
23
|
+
* @param {Object} authConfig - Authentication configuration
|
|
24
|
+
* @returns {Object} Pipeline auth config
|
|
25
|
+
*/
|
|
26
|
+
function convertToPipelineAuthConfig(authConfig) {
|
|
27
|
+
return authConfig.type === 'bearer'
|
|
28
|
+
? { type: 'bearer', token: authConfig.token }
|
|
29
|
+
: { type: 'client-credentials', clientId: authConfig.clientId, clientSecret: authConfig.clientSecret };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Handles error response from deployment status check
|
|
34
|
+
* @param {Object} response - API response
|
|
35
|
+
* @param {string} deploymentId - Deployment ID
|
|
36
|
+
* @throws {Error} Appropriate error message
|
|
37
|
+
*/
|
|
38
|
+
function handleDeploymentStatusError(response, deploymentId) {
|
|
39
|
+
if (response.status === 404) {
|
|
40
|
+
throw new Error(`Deployment ${deploymentId || response.deploymentId || 'unknown'} not found`);
|
|
41
|
+
}
|
|
42
|
+
throw new Error(`Status check failed: ${response.formattedError || response.error || 'Unknown error'}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Extracts deployment data from response
|
|
47
|
+
* @param {Object} response - API response
|
|
48
|
+
* @returns {Object} Deployment data
|
|
49
|
+
*/
|
|
50
|
+
function extractDeploymentData(response) {
|
|
51
|
+
const responseData = response.data;
|
|
52
|
+
return responseData.data || responseData;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Logs deployment progress
|
|
57
|
+
* @param {Object} deploymentData - Deployment data
|
|
58
|
+
* @param {number} attempt - Current attempt
|
|
59
|
+
* @param {number} maxAttempts - Maximum attempts
|
|
60
|
+
*/
|
|
61
|
+
function logDeploymentProgress(deploymentData, attempt, maxAttempts) {
|
|
62
|
+
const status = deploymentData.status;
|
|
63
|
+
const progress = deploymentData.progress || 0;
|
|
64
|
+
logger.log(chalk.blue(` Status: ${status} (${progress}%) (attempt ${attempt + 1}/${maxAttempts})`));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Process deployment status response
|
|
69
|
+
* @param {Object} response - API response
|
|
70
|
+
* @param {number} attempt - Current attempt number
|
|
71
|
+
* @param {number} maxAttempts - Maximum attempts
|
|
72
|
+
* @param {number} interval - Polling interval
|
|
73
|
+
* @param {string} deploymentId - Deployment ID for error messages
|
|
74
|
+
* @returns {Promise<Object|null>} Deployment data if terminal, null if needs to continue polling
|
|
75
|
+
*/
|
|
76
|
+
async function processDeploymentStatusResponse(response, attempt, maxAttempts, interval, deploymentId) {
|
|
77
|
+
if (!response.success || !response.data) {
|
|
78
|
+
handleDeploymentStatusError(response, deploymentId);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const deploymentData = extractDeploymentData(response);
|
|
82
|
+
if (isTerminalStatus(deploymentData.status)) {
|
|
83
|
+
return deploymentData;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
logDeploymentProgress(deploymentData, attempt, maxAttempts);
|
|
87
|
+
if (attempt < maxAttempts - 1) {
|
|
88
|
+
await new Promise(resolve => setTimeout(resolve, interval));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = {
|
|
95
|
+
isTerminalStatus,
|
|
96
|
+
convertToPipelineAuthConfig,
|
|
97
|
+
handleDeploymentStatusError,
|
|
98
|
+
extractDeploymentData,
|
|
99
|
+
logDeploymentProgress,
|
|
100
|
+
processDeploymentStatusResponse
|
|
101
|
+
};
|
|
@@ -16,6 +16,23 @@ const { validateControllerUrl, validateEnvironmentKey } = require('../utils/depl
|
|
|
16
16
|
const { handleDeploymentError, handleDeploymentErrors } = require('../utils/deployment-errors');
|
|
17
17
|
const { validatePipeline, deployPipeline, getPipelineDeployment } = require('../api/pipeline.api');
|
|
18
18
|
const { handleValidationResponse } = require('../utils/deployment-validation-helpers');
|
|
19
|
+
const {
|
|
20
|
+
convertToPipelineAuthConfig,
|
|
21
|
+
processDeploymentStatusResponse
|
|
22
|
+
} = require('./deployer-status');
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* For external systems, send full manifest (application + inline system + full dataSources).
|
|
26
|
+
* No transform - controller receives complete application, external system, and data sources.
|
|
27
|
+
* @param {Object} manifest - Full manifest (type 'external', system, dataSources as full objects)
|
|
28
|
+
* @returns {Object} Manifest to send to pipeline (external sent as-is)
|
|
29
|
+
*/
|
|
30
|
+
function transformExternalManifestForPipeline(manifest) {
|
|
31
|
+
if (!manifest) {
|
|
32
|
+
return manifest;
|
|
33
|
+
}
|
|
34
|
+
return manifest;
|
|
35
|
+
}
|
|
19
36
|
|
|
20
37
|
/**
|
|
21
38
|
* Build validation data for deployment
|
|
@@ -28,27 +45,42 @@ const { handleValidationResponse } = require('../utils/deployment-validation-hel
|
|
|
28
45
|
*/
|
|
29
46
|
async function buildValidationData(manifest, validatedEnvKey, authConfig, options) {
|
|
30
47
|
const tokenManager = require('../utils/token-manager');
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
48
|
+
let clientId;
|
|
49
|
+
let clientSecret;
|
|
50
|
+
let pipelineAuthConfig;
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const credentials = await tokenManager.extractClientCredentials(
|
|
54
|
+
authConfig,
|
|
55
|
+
manifest.key,
|
|
56
|
+
validatedEnvKey,
|
|
57
|
+
options
|
|
58
|
+
);
|
|
59
|
+
clientId = credentials.clientId;
|
|
60
|
+
clientSecret = credentials.clientSecret;
|
|
61
|
+
pipelineAuthConfig = {
|
|
62
|
+
type: 'client-credentials',
|
|
63
|
+
clientId,
|
|
64
|
+
clientSecret
|
|
65
|
+
};
|
|
66
|
+
} catch (credError) {
|
|
67
|
+
if (authConfig.type === 'bearer' && authConfig.token) {
|
|
68
|
+
pipelineAuthConfig = { type: 'bearer', token: authConfig.token };
|
|
69
|
+
clientId = manifest.key;
|
|
70
|
+
clientSecret = '';
|
|
71
|
+
} else {
|
|
72
|
+
throw credError;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
37
75
|
|
|
38
76
|
const repositoryUrl = options.repositoryUrl || `https://github.com/aifabrix/${manifest.key}`;
|
|
39
77
|
const validationData = {
|
|
40
|
-
clientId: clientId,
|
|
41
|
-
clientSecret: clientSecret,
|
|
78
|
+
clientId: clientId || '',
|
|
79
|
+
clientSecret: clientSecret || '',
|
|
42
80
|
repositoryUrl: repositoryUrl,
|
|
43
81
|
applicationConfig: manifest
|
|
44
82
|
};
|
|
45
83
|
|
|
46
|
-
const pipelineAuthConfig = {
|
|
47
|
-
type: 'client-credentials',
|
|
48
|
-
clientId: clientId,
|
|
49
|
-
clientSecret: clientSecret
|
|
50
|
-
};
|
|
51
|
-
|
|
52
84
|
return { validationData, pipelineAuthConfig };
|
|
53
85
|
}
|
|
54
86
|
|
|
@@ -130,7 +162,7 @@ function validateDeploymentCredentials(authConfig) {
|
|
|
130
162
|
}
|
|
131
163
|
|
|
132
164
|
/**
|
|
133
|
-
* Build deployment data and auth config
|
|
165
|
+
* Build deployment data and auth config (supports bearer-only when no client credentials)
|
|
134
166
|
* @param {string} validateToken - Validation token
|
|
135
167
|
* @param {Object} authConfig - Authentication configuration
|
|
136
168
|
* @param {Object} options - Deployment options
|
|
@@ -143,11 +175,13 @@ function buildDeploymentData(validateToken, authConfig, options) {
|
|
|
143
175
|
imageTag: imageTag
|
|
144
176
|
};
|
|
145
177
|
|
|
146
|
-
const pipelineAuthConfig =
|
|
147
|
-
type: '
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
178
|
+
const pipelineAuthConfig = authConfig.type === 'bearer' && authConfig.token && !authConfig.clientId
|
|
179
|
+
? { type: 'bearer', token: authConfig.token }
|
|
180
|
+
: {
|
|
181
|
+
type: 'client-credentials',
|
|
182
|
+
clientId: authConfig.clientId,
|
|
183
|
+
clientSecret: authConfig.clientSecret
|
|
184
|
+
};
|
|
151
185
|
|
|
152
186
|
return { deployData, pipelineAuthConfig };
|
|
153
187
|
}
|
|
@@ -212,10 +246,12 @@ async function sendDeploymentRequest(url, envKey, validateToken, authConfig, opt
|
|
|
212
246
|
const validatedEnvKey = validateEnvironmentKey(envKey);
|
|
213
247
|
const maxRetries = options.maxRetries || 3;
|
|
214
248
|
|
|
215
|
-
|
|
216
|
-
|
|
249
|
+
const useBearerOnly = authConfig.type === 'bearer' && authConfig.token && !authConfig.clientId;
|
|
250
|
+
if (!useBearerOnly) {
|
|
251
|
+
validateDeploymentCredentials(authConfig);
|
|
252
|
+
}
|
|
217
253
|
|
|
218
|
-
// Build deployment data
|
|
254
|
+
// Build deployment data (supports bearer-only when no client credentials)
|
|
219
255
|
const { deployData, pipelineAuthConfig } = buildDeploymentData(validateToken, authConfig, options);
|
|
220
256
|
|
|
221
257
|
// Wrap API call with retry logic
|
|
@@ -237,92 +273,6 @@ async function sendDeploymentRequest(url, envKey, validateToken, authConfig, opt
|
|
|
237
273
|
throwDeploymentError(lastError, maxRetries);
|
|
238
274
|
}
|
|
239
275
|
|
|
240
|
-
/**
|
|
241
|
-
* Checks if deployment status is terminal
|
|
242
|
-
* @function isTerminalStatus
|
|
243
|
-
* @param {string} status - Deployment status
|
|
244
|
-
* @returns {boolean} True if status is terminal
|
|
245
|
-
*/
|
|
246
|
-
function isTerminalStatus(status) {
|
|
247
|
-
return status === 'completed' || status === 'failed' || status === 'cancelled';
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* Convert authConfig to pipeline auth config format
|
|
252
|
-
* @param {Object} authConfig - Authentication configuration
|
|
253
|
-
* @returns {Object} Pipeline auth config
|
|
254
|
-
*/
|
|
255
|
-
function convertToPipelineAuthConfig(authConfig) {
|
|
256
|
-
return authConfig.type === 'bearer'
|
|
257
|
-
? { type: 'bearer', token: authConfig.token }
|
|
258
|
-
: { type: 'client-credentials', clientId: authConfig.clientId, clientSecret: authConfig.clientSecret };
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* Process deployment status response
|
|
263
|
-
* @param {Object} response - API response
|
|
264
|
-
* @param {number} attempt - Current attempt number
|
|
265
|
-
* @param {number} maxAttempts - Maximum attempts
|
|
266
|
-
* @param {number} interval - Polling interval
|
|
267
|
-
* @param {string} deploymentId - Deployment ID for error messages
|
|
268
|
-
* @returns {Object|null} Deployment data if terminal, null if needs to continue polling
|
|
269
|
-
*/
|
|
270
|
-
/**
|
|
271
|
-
* Handles error response from deployment status check
|
|
272
|
-
* @function handleDeploymentStatusError
|
|
273
|
-
* @param {Object} response - API response
|
|
274
|
-
* @param {string} deploymentId - Deployment ID
|
|
275
|
-
* @throws {Error} Appropriate error message
|
|
276
|
-
*/
|
|
277
|
-
function handleDeploymentStatusError(response, deploymentId) {
|
|
278
|
-
if (response.status === 404) {
|
|
279
|
-
throw new Error(`Deployment ${deploymentId || response.deploymentId || 'unknown'} not found`);
|
|
280
|
-
}
|
|
281
|
-
throw new Error(`Status check failed: ${response.formattedError || response.error || 'Unknown error'}`);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/**
|
|
285
|
-
* Extracts deployment data from response
|
|
286
|
-
* @function extractDeploymentData
|
|
287
|
-
* @param {Object} response - API response
|
|
288
|
-
* @returns {Object} Deployment data
|
|
289
|
-
*/
|
|
290
|
-
function extractDeploymentData(response) {
|
|
291
|
-
const responseData = response.data;
|
|
292
|
-
return responseData.data || responseData;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* Logs deployment progress
|
|
297
|
-
* @function logDeploymentProgress
|
|
298
|
-
* @param {Object} deploymentData - Deployment data
|
|
299
|
-
* @param {number} attempt - Current attempt
|
|
300
|
-
* @param {number} maxAttempts - Maximum attempts
|
|
301
|
-
*/
|
|
302
|
-
function logDeploymentProgress(deploymentData, attempt, maxAttempts) {
|
|
303
|
-
const status = deploymentData.status;
|
|
304
|
-
const progress = deploymentData.progress || 0;
|
|
305
|
-
logger.log(chalk.blue(` Status: ${status} (${progress}%) (attempt ${attempt + 1}/${maxAttempts})`));
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
async function processDeploymentStatusResponse(response, attempt, maxAttempts, interval, deploymentId) {
|
|
309
|
-
if (!response.success || !response.data) {
|
|
310
|
-
handleDeploymentStatusError(response, deploymentId);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
const deploymentData = extractDeploymentData(response);
|
|
314
|
-
if (isTerminalStatus(deploymentData.status)) {
|
|
315
|
-
return deploymentData;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
logDeploymentProgress(deploymentData, attempt, maxAttempts);
|
|
319
|
-
if (attempt < maxAttempts - 1) {
|
|
320
|
-
await new Promise(resolve => setTimeout(resolve, interval));
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
return null;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
276
|
/**
|
|
327
277
|
* Polls deployment status from controller
|
|
328
278
|
* Uses pipeline endpoint for CI/CD monitoring with minimal deployment info
|
|
@@ -470,9 +420,11 @@ async function deployToController(manifest, controllerUrl, envKey, authConfig, o
|
|
|
470
420
|
// Log deployment attempt for audit
|
|
471
421
|
await auditLogger.logDeploymentAttempt(manifest.key, url, options);
|
|
472
422
|
|
|
423
|
+
const pipelineManifest = transformExternalManifestForPipeline(manifest);
|
|
424
|
+
|
|
473
425
|
try {
|
|
474
426
|
// Send deployment request
|
|
475
|
-
const result = await sendDeployment(url, validatedEnvKey,
|
|
427
|
+
const result = await sendDeployment(url, validatedEnvKey, pipelineManifest, authConfig, options);
|
|
476
428
|
|
|
477
429
|
// Poll for deployment status if enabled
|
|
478
430
|
return await pollDeployment(result, url, validatedEnvKey, authConfig, options);
|