@aifabrix/builder 2.32.3 → 2.33.1
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 +8 -0
- package/README.md +36 -8
- package/bin/aifabrix.js +6 -8
- package/integration/hubspot/README.md +12 -11
- package/integration/hubspot/companies.json +2048 -0
- package/integration/hubspot/create-hubspot.js +665 -0
- package/integration/hubspot/{hubspot-deploy-company.json → hubspot-datasource-company.json} +1 -1
- package/integration/hubspot/{hubspot-deploy-contact.json → hubspot-datasource-contact.json} +1 -1
- package/integration/hubspot/{hubspot-deploy-deal.json → hubspot-datasource-deal.json} +1 -1
- package/integration/hubspot/hubspot-deploy.json +832 -81
- package/integration/hubspot/hubspot-system.json +99 -0
- package/integration/hubspot/test-artifacts/wizard-hubspot-credential-real.yaml +20 -0
- package/integration/hubspot/test-artifacts/wizard-hubspot-env-vars.yaml +9 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-add-datasource.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-app-name.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-credential-create.yaml +7 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-credential-select.yaml +7 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-known-platform.yaml +4 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-missing-app.yaml +4 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-missing-source.yaml +2 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-mode.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-openapi-file.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-openapi-url.yaml +4 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-source.yaml +4 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-array-test.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-key-test.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-path-test.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-test.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-rbac-test.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-rbac-yaml-test.yaml +5 -0
- package/integration/hubspot/test-dataplane-down-helpers.js +246 -0
- package/integration/hubspot/test-dataplane-down-tests.js +419 -0
- package/integration/hubspot/test-dataplane-down.js +157 -0
- package/integration/hubspot/test.js +1517 -0
- package/integration/hubspot/variables.yaml +4 -4
- package/integration/hubspot/wizard-hubspot-e2e.yaml +16 -0
- package/integration/hubspot/wizard-hubspot-platform.yaml +8 -0
- package/lib/api/applications.api.js +1 -0
- package/lib/api/index.js +6 -2
- package/lib/api/types/wizard.types.js +176 -38
- package/lib/api/wizard.api.js +161 -23
- package/lib/app/deploy.js +116 -54
- package/lib/app/display.js +6 -5
- package/lib/app/dockerfile.js +2 -1
- package/lib/app/list.js +17 -10
- package/lib/app/readme.js +41 -112
- package/lib/app/register.js +44 -9
- package/lib/app/rotate-secret.js +48 -31
- package/lib/cli.js +219 -70
- package/lib/commands/app.js +4 -9
- package/lib/commands/auth-config.js +125 -0
- package/lib/commands/auth-status.js +7 -8
- package/lib/commands/datasource.js +3 -6
- package/lib/commands/login-credentials.js +4 -4
- package/lib/commands/login-device.js +26 -17
- package/lib/commands/login.js +12 -10
- package/lib/commands/wizard-config-normalizer.js +92 -0
- package/lib/commands/wizard-core.js +515 -0
- package/lib/commands/wizard-dataplane.js +122 -0
- package/lib/commands/wizard-headless.js +115 -0
- package/lib/commands/wizard.js +110 -332
- package/lib/core/config.js +46 -0
- package/lib/core/secrets.js +3 -22
- package/lib/core/templates-env.js +1 -1
- package/lib/datasource/deploy.js +59 -23
- package/lib/datasource/list.js +108 -19
- package/lib/deployment/deployer.js +25 -0
- package/lib/deployment/environment.js +10 -13
- package/lib/external-system/delete.js +151 -0
- package/lib/external-system/deploy.js +53 -378
- package/lib/external-system/download-helpers.js +45 -65
- package/lib/external-system/download.js +33 -13
- package/lib/external-system/generator.js +11 -7
- package/lib/external-system/test-auth.js +4 -3
- package/lib/generator/builders.js +3 -1
- package/lib/generator/external-controller-manifest.js +157 -0
- package/lib/generator/external-schema-utils.js +236 -0
- package/lib/generator/external.js +55 -3
- package/lib/generator/index.js +22 -10
- package/lib/generator/wizard-prompts.js +33 -10
- package/lib/generator/wizard.js +69 -86
- package/lib/infrastructure/compose.js +100 -0
- package/lib/infrastructure/helpers.js +139 -0
- package/lib/infrastructure/index.js +52 -311
- package/lib/infrastructure/services.js +168 -0
- package/lib/schema/application-schema.json +23 -4
- package/lib/schema/external-datasource.schema.json +2 -2
- package/lib/schema/wizard-config.schema.json +234 -0
- package/lib/utils/api.js +102 -52
- package/lib/utils/app-existence.js +42 -0
- package/lib/utils/app-register-config.js +7 -2
- package/lib/utils/auth-config-validator.js +92 -0
- package/lib/utils/command-header.js +43 -0
- package/lib/utils/compose-generator.js +113 -70
- package/lib/utils/controller-url.js +65 -17
- package/lib/utils/dataplane-health.js +115 -0
- package/lib/utils/dataplane-resolver.js +29 -0
- package/lib/utils/dev-config.js +6 -2
- package/lib/utils/env-copy.js +2 -1
- package/lib/utils/env-ports.js +2 -1
- package/lib/utils/env-template.js +1 -1
- package/lib/utils/error-formatter.js +49 -0
- package/lib/utils/error-formatters/network-errors.js +13 -3
- package/lib/utils/external-readme.js +125 -0
- package/lib/utils/help-builder.js +190 -0
- package/lib/utils/infra-status.js +13 -3
- package/lib/utils/paths.js +17 -2
- package/lib/utils/port-resolver.js +111 -0
- package/lib/utils/secrets-helpers.js +3 -15
- package/lib/utils/secrets-utils.js +2 -2
- package/lib/utils/token-manager.js +9 -4
- package/lib/utils/variable-transformer.js +7 -2
- package/lib/validation/external-manifest-validator.js +202 -0
- package/lib/validation/validate-display.js +406 -0
- package/lib/validation/validate.js +159 -123
- package/lib/validation/validator.js +36 -3
- package/lib/validation/wizard-config-validator.js +267 -0
- package/package.json +4 -2
- package/templates/applications/README.md.hbs +18 -16
- package/templates/applications/miso-controller/env.template +1 -1
- package/templates/applications/miso-controller/rbac.yaml +7 -7
- package/templates/external-system/README.md.hbs +99 -0
- package/templates/github/ci.yaml.hbs +44 -1
- package/templates/github/release.yaml.hbs +44 -0
- package/templates/infra/compose.yaml.hbs +35 -0
- package/templates/python/docker-compose.hbs +26 -0
- package/templates/typescript/docker-compose.hbs +26 -0
package/lib/core/secrets.js
CHANGED
|
@@ -44,6 +44,7 @@ const {
|
|
|
44
44
|
} = require('../utils/secrets-utils');
|
|
45
45
|
const { decryptSecret, isEncrypted } = require('../utils/secrets-encryption');
|
|
46
46
|
const pathsUtil = require('../utils/paths');
|
|
47
|
+
const { getContainerPortFromPath } = require('../utils/port-resolver');
|
|
47
48
|
|
|
48
49
|
/**
|
|
49
50
|
* Generates a canonical secret name from an environment variable key.
|
|
@@ -253,26 +254,6 @@ function applyDockerEnvOverride(dockerEnv) {
|
|
|
253
254
|
return dockerEnv;
|
|
254
255
|
}
|
|
255
256
|
|
|
256
|
-
/**
|
|
257
|
-
* Gets container port from variables.yaml
|
|
258
|
-
* @function getContainerPortFromVariables
|
|
259
|
-
* @param {string} variablesPath - Path to variables.yaml
|
|
260
|
-
* @returns {number|null} Container port or null
|
|
261
|
-
*/
|
|
262
|
-
function getContainerPortFromVariables(variablesPath) {
|
|
263
|
-
if (!variablesPath || !fs.existsSync(variablesPath)) {
|
|
264
|
-
return null;
|
|
265
|
-
}
|
|
266
|
-
try {
|
|
267
|
-
const variablesContent = fs.readFileSync(variablesPath, 'utf8');
|
|
268
|
-
const variables = yaml.load(variablesContent);
|
|
269
|
-
// Use containerPort if specified, otherwise use base port (no developer-id offset)
|
|
270
|
-
return variables?.build?.containerPort || variables?.port || null;
|
|
271
|
-
} catch {
|
|
272
|
-
return null;
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
257
|
/**
|
|
277
258
|
* Gets container port from docker environment config
|
|
278
259
|
* @function getContainerPortFromDockerEnv
|
|
@@ -305,7 +286,7 @@ async function updatePortForDocker(resolved, variablesPath) {
|
|
|
305
286
|
dockerEnv = applyDockerEnvOverride(dockerEnv);
|
|
306
287
|
|
|
307
288
|
// Step 3: Get PORT value for container (should be container port, NOT host port)
|
|
308
|
-
let containerPort =
|
|
289
|
+
let containerPort = getContainerPortFromPath(variablesPath);
|
|
309
290
|
if (containerPort === null) {
|
|
310
291
|
containerPort = getContainerPortFromDockerEnv(dockerEnv);
|
|
311
292
|
}
|
|
@@ -454,7 +435,7 @@ async function generateAdminSecretsEnv(secretsPath) {
|
|
|
454
435
|
|
|
455
436
|
const adminSecrets = `# Infrastructure Admin Credentials
|
|
456
437
|
POSTGRES_PASSWORD=${postgresPassword}
|
|
457
|
-
PGADMIN_DEFAULT_EMAIL=admin@aifabrix.
|
|
438
|
+
PGADMIN_DEFAULT_EMAIL=admin@aifabrix.dev
|
|
458
439
|
PGADMIN_DEFAULT_PASSWORD=${postgresPassword}
|
|
459
440
|
REDIS_HOST=local:redis:6379:0:
|
|
460
441
|
REDIS_COMMANDER_USER=admin
|
|
@@ -104,7 +104,7 @@ function buildMonitoringEnv(config) {
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
return {
|
|
107
|
-
'MISO_CONTROLLER_URL': config.controllerUrl || 'https://controller.aifabrix.
|
|
107
|
+
'MISO_CONTROLLER_URL': config.controllerUrl || 'https://controller.aifabrix.dev',
|
|
108
108
|
'MISO_ENVIRONMENT': 'dev',
|
|
109
109
|
'MISO_CLIENTID': 'kv://miso-controller-client-idKeyVault',
|
|
110
110
|
'MISO_CLIENTSECRET': 'kv://miso-controller-client-secretKeyVault',
|
package/lib/datasource/deploy.js
CHANGED
|
@@ -48,9 +48,10 @@ async function getDataplaneUrl(controllerUrl, appKey, environment, authConfig) {
|
|
|
48
48
|
application.configuration?.dataplaneUrl;
|
|
49
49
|
|
|
50
50
|
if (!dataplaneUrl) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
const appType = application.configuration?.type || application.type;
|
|
52
|
+
if (appType === 'external') {
|
|
53
|
+
throw new Error('Dataplane URL not found for external system. Provide --dataplane <url>.');
|
|
54
|
+
}
|
|
54
55
|
throw new Error('Dataplane URL not found in application configuration');
|
|
55
56
|
}
|
|
56
57
|
|
|
@@ -64,19 +65,13 @@ async function getDataplaneUrl(controllerUrl, appKey, environment, authConfig) {
|
|
|
64
65
|
* @param {Object} options - Options
|
|
65
66
|
* @throws {Error} If validation fails
|
|
66
67
|
*/
|
|
67
|
-
function validateDeploymentInputs(appKey, filePath
|
|
68
|
+
function validateDeploymentInputs(appKey, filePath) {
|
|
68
69
|
if (!appKey || typeof appKey !== 'string') {
|
|
69
70
|
throw new Error('Application key is required');
|
|
70
71
|
}
|
|
71
72
|
if (!filePath || typeof filePath !== 'string') {
|
|
72
73
|
throw new Error('File path is required');
|
|
73
74
|
}
|
|
74
|
-
if (!options.controller) {
|
|
75
|
-
throw new Error('Controller URL is required (--controller)');
|
|
76
|
-
}
|
|
77
|
-
if (!options.environment) {
|
|
78
|
-
throw new Error('Environment is required (-e, --environment)');
|
|
79
|
-
}
|
|
80
75
|
}
|
|
81
76
|
|
|
82
77
|
/**
|
|
@@ -112,18 +107,38 @@ async function validateAndLoadDatasourceFile(filePath) {
|
|
|
112
107
|
* @param {string} controllerUrl - Controller URL
|
|
113
108
|
* @param {string} environment - Environment key
|
|
114
109
|
* @param {string} appKey - Application key
|
|
110
|
+
* @param {Object} [options] - Options
|
|
111
|
+
* @param {string} [options.dataplane] - Dataplane URL override
|
|
115
112
|
* @returns {Promise<Object>} Object with authConfig and dataplaneUrl
|
|
116
113
|
*/
|
|
117
114
|
async function setupDeploymentAuth(controllerUrl, environment, appKey) {
|
|
115
|
+
const { resolveDataplaneUrl } = require('../utils/dataplane-resolver');
|
|
118
116
|
logger.log(chalk.blue('🔐 Getting authentication...'));
|
|
119
117
|
const authConfig = await getDeploymentAuth(controllerUrl, environment, appKey);
|
|
120
118
|
logger.log(chalk.green('✓ Authentication successful'));
|
|
121
119
|
|
|
122
|
-
logger.log(chalk.blue('🌐
|
|
123
|
-
|
|
124
|
-
|
|
120
|
+
logger.log(chalk.blue('🌐 Resolving dataplane URL...'));
|
|
121
|
+
let dataplaneUrl;
|
|
122
|
+
try {
|
|
123
|
+
dataplaneUrl = await resolveDataplaneUrl(controllerUrl, environment, authConfig);
|
|
124
|
+
logger.log(chalk.green(`✓ Dataplane URL: ${dataplaneUrl}`));
|
|
125
|
+
} catch (error) {
|
|
126
|
+
logger.error(chalk.red('❌ Failed to resolve dataplane URL:'), error.message);
|
|
127
|
+
logger.error(chalk.gray('\nThe dataplane URL is automatically discovered from the controller.'));
|
|
128
|
+
logger.error(chalk.gray('If discovery fails, ensure you are logged in and the controller is accessible:'));
|
|
129
|
+
logger.error(chalk.gray(' aifabrix login'));
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Validate dataplane URL
|
|
134
|
+
if (!dataplaneUrl || !dataplaneUrl.trim()) {
|
|
135
|
+
logger.error(chalk.red('❌ Dataplane URL is empty.'));
|
|
136
|
+
logger.error(chalk.gray('The dataplane URL could not be discovered from the controller.'));
|
|
137
|
+
logger.error(chalk.gray('Ensure the dataplane service is registered in the controller.'));
|
|
138
|
+
throw new Error('Dataplane URL is empty');
|
|
139
|
+
}
|
|
125
140
|
|
|
126
|
-
return { authConfig, dataplaneUrl };
|
|
141
|
+
return { authConfig, dataplaneUrl: dataplaneUrl.trim() };
|
|
127
142
|
}
|
|
128
143
|
|
|
129
144
|
/**
|
|
@@ -145,6 +160,17 @@ async function publishDatasourceToDataplane(dataplaneUrl, systemKey, authConfig,
|
|
|
145
160
|
const formattedError = publishResponse.formattedError || formatApiError(publishResponse);
|
|
146
161
|
logger.error(chalk.red('❌ Publish failed:'));
|
|
147
162
|
logger.error(formattedError);
|
|
163
|
+
|
|
164
|
+
// Show dataplane URL and endpoint information
|
|
165
|
+
if (publishResponse.errorData && publishResponse.errorData.endpointUrl) {
|
|
166
|
+
logger.error(chalk.gray(`\nEndpoint URL: ${publishResponse.errorData.endpointUrl}`));
|
|
167
|
+
} else if (dataplaneUrl) {
|
|
168
|
+
logger.error(chalk.gray(`\nDataplane URL: ${dataplaneUrl}`));
|
|
169
|
+
logger.error(chalk.gray(`System Key: ${systemKey}`));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
logger.error(chalk.gray('\nFull response for debugging:'));
|
|
173
|
+
logger.error(chalk.gray(JSON.stringify(publishResponse, null, 2)));
|
|
148
174
|
throw new Error(`Dataplane publish failed: ${formattedError}`);
|
|
149
175
|
}
|
|
150
176
|
|
|
@@ -165,20 +191,30 @@ function displayDeploymentResults(datasourceConfig, systemKey, environment) {
|
|
|
165
191
|
}
|
|
166
192
|
|
|
167
193
|
/**
|
|
168
|
-
* Deploys datasource to dataplane
|
|
194
|
+
* Deploys datasource to dataplane.
|
|
195
|
+
* Controller and environment come from config.yaml (set via aifabrix login or aifabrix auth config).
|
|
169
196
|
*
|
|
170
197
|
* @async
|
|
171
198
|
* @function deployDatasource
|
|
172
199
|
* @param {string} appKey - Application key
|
|
173
200
|
* @param {string} filePath - Path to datasource JSON file
|
|
174
|
-
* @param {Object}
|
|
175
|
-
* @param {string} options.controller - Controller URL
|
|
176
|
-
* @param {string} options.environment - Environment key
|
|
201
|
+
* @param {Object} [_options] - Deployment options (reserved)
|
|
177
202
|
* @returns {Promise<Object>} Deployment result
|
|
178
203
|
* @throws {Error} If deployment fails
|
|
179
204
|
*/
|
|
180
|
-
async function deployDatasource(appKey, filePath,
|
|
181
|
-
|
|
205
|
+
async function deployDatasource(appKey, filePath, _options) {
|
|
206
|
+
const { resolveControllerUrl } = require('../utils/controller-url');
|
|
207
|
+
const { resolveEnvironment } = require('../core/config');
|
|
208
|
+
const { displayCommandHeader } = require('../utils/command-header');
|
|
209
|
+
|
|
210
|
+
validateDeploymentInputs(appKey, filePath);
|
|
211
|
+
|
|
212
|
+
// Resolve controller and environment from config
|
|
213
|
+
const controllerUrl = await resolveControllerUrl();
|
|
214
|
+
const environment = await resolveEnvironment();
|
|
215
|
+
|
|
216
|
+
// Display command header
|
|
217
|
+
displayCommandHeader(controllerUrl, environment);
|
|
182
218
|
|
|
183
219
|
logger.log(chalk.blue('📋 Deploying datasource...\n'));
|
|
184
220
|
|
|
@@ -192,19 +228,19 @@ async function deployDatasource(appKey, filePath, options) {
|
|
|
192
228
|
}
|
|
193
229
|
|
|
194
230
|
// Setup authentication and get dataplane URL
|
|
195
|
-
const { authConfig, dataplaneUrl } = await setupDeploymentAuth(
|
|
231
|
+
const { authConfig, dataplaneUrl } = await setupDeploymentAuth(controllerUrl, environment, appKey);
|
|
196
232
|
|
|
197
233
|
// Publish to dataplane
|
|
198
234
|
await publishDatasourceToDataplane(dataplaneUrl, systemKey, authConfig, datasourceConfig);
|
|
199
235
|
|
|
200
236
|
// Display results
|
|
201
|
-
displayDeploymentResults(datasourceConfig, systemKey,
|
|
237
|
+
displayDeploymentResults(datasourceConfig, systemKey, environment);
|
|
202
238
|
|
|
203
239
|
return {
|
|
204
240
|
success: true,
|
|
205
241
|
datasourceKey: datasourceConfig.key,
|
|
206
242
|
systemKey: systemKey,
|
|
207
|
-
environment:
|
|
243
|
+
environment: environment,
|
|
208
244
|
dataplaneUrl: dataplaneUrl
|
|
209
245
|
};
|
|
210
246
|
}
|
package/lib/datasource/list.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Datasource List Command
|
|
3
3
|
*
|
|
4
|
-
* Lists datasources from an environment via
|
|
4
|
+
* Lists datasources from an environment via dataplane API.
|
|
5
|
+
* Gets dataplane URL from controller, then lists datasources from dataplane.
|
|
5
6
|
*
|
|
6
7
|
* @fileoverview Datasource listing for AI Fabrix Builder
|
|
7
8
|
* @author AI Fabrix Team
|
|
@@ -9,9 +10,10 @@
|
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
const chalk = require('chalk');
|
|
12
|
-
const { getConfig } = require('../core/config');
|
|
13
|
+
const { getConfig, resolveEnvironment } = require('../core/config');
|
|
13
14
|
const { getOrRefreshDeviceToken } = require('../utils/token-manager');
|
|
14
|
-
const {
|
|
15
|
+
const { listDatasources: listDatasourcesFromDataplane } = require('../api/datasources-core.api');
|
|
16
|
+
const { resolveDataplaneUrl } = require('../utils/dataplane-resolver');
|
|
15
17
|
const { formatApiError } = require('../utils/api-error-handler');
|
|
16
18
|
const logger = require('../utils/logger');
|
|
17
19
|
|
|
@@ -103,14 +105,19 @@ function extractDatasources(response) {
|
|
|
103
105
|
* @function displayDatasources
|
|
104
106
|
* @param {Array} datasources - Array of datasource objects
|
|
105
107
|
* @param {string} environment - Environment key
|
|
108
|
+
* @param {string} dataplaneUrl - Dataplane URL for header display
|
|
106
109
|
*/
|
|
107
|
-
function displayDatasources(datasources, environment) {
|
|
110
|
+
function displayDatasources(datasources, environment, dataplaneUrl) {
|
|
111
|
+
const environmentName = environment || 'dev';
|
|
112
|
+
const header = `Datasources in ${environmentName} environment${dataplaneUrl ? ` (${dataplaneUrl})` : ''}`;
|
|
113
|
+
|
|
108
114
|
if (datasources.length === 0) {
|
|
109
|
-
logger.log(chalk.
|
|
115
|
+
logger.log(chalk.bold(`\n📋 ${header}:\n`));
|
|
116
|
+
logger.log(chalk.gray(' No datasources found in this environment.\n'));
|
|
110
117
|
return;
|
|
111
118
|
}
|
|
112
119
|
|
|
113
|
-
logger.log(chalk.
|
|
120
|
+
logger.log(chalk.bold(`\n📋 ${header}:\n`));
|
|
114
121
|
logger.log(chalk.gray('Key'.padEnd(30) + 'Display Name'.padEnd(30) + 'System Key'.padEnd(20) + 'Version'.padEnd(15) + 'Status'));
|
|
115
122
|
logger.log(chalk.gray('-'.repeat(120)));
|
|
116
123
|
|
|
@@ -130,8 +137,7 @@ function displayDatasources(datasources, environment) {
|
|
|
130
137
|
*
|
|
131
138
|
* @async
|
|
132
139
|
* @function listDatasources
|
|
133
|
-
* @param {Object}
|
|
134
|
-
* @param {string} options.environment - Environment ID or key
|
|
140
|
+
* @param {Object} _options - Command options (unused, kept for compatibility)
|
|
135
141
|
* @throws {Error} If listing fails
|
|
136
142
|
*/
|
|
137
143
|
/**
|
|
@@ -170,7 +176,7 @@ async function getDeviceTokenFromConfig(config) {
|
|
|
170
176
|
* @param {string|null} controllerUrl - Controller URL
|
|
171
177
|
*/
|
|
172
178
|
function validateDatasourceListingAuth(token, controllerUrl) {
|
|
173
|
-
if (!token || !controllerUrl) {
|
|
179
|
+
if (!token || !controllerUrl || (typeof controllerUrl === 'string' && !controllerUrl.trim())) {
|
|
174
180
|
logger.error(chalk.red('❌ Not logged in. Run: aifabrix login'));
|
|
175
181
|
logger.error(chalk.gray(' Use device code flow: aifabrix login --method device --controller <url>'));
|
|
176
182
|
process.exit(1);
|
|
@@ -182,37 +188,120 @@ function validateDatasourceListingAuth(token, controllerUrl) {
|
|
|
182
188
|
* @function handleDatasourceApiError
|
|
183
189
|
* @param {Object} response - API response
|
|
184
190
|
*/
|
|
185
|
-
function handleDatasourceApiError(response) {
|
|
191
|
+
function handleDatasourceApiError(response, dataplaneUrl = null) {
|
|
186
192
|
const formattedError = response.formattedError || formatApiError(response);
|
|
187
193
|
logger.error(formattedError);
|
|
194
|
+
|
|
195
|
+
// Show endpoint URL from error data if available (more specific than dataplane URL)
|
|
196
|
+
if (response.errorData && response.errorData.endpointUrl) {
|
|
197
|
+
logger.error(chalk.gray(`\nEndpoint URL: ${response.errorData.endpointUrl}`));
|
|
198
|
+
} else if (response.errorData && response.errorData.controllerUrl) {
|
|
199
|
+
logger.error(chalk.gray(`\nDataplane URL: ${response.errorData.controllerUrl}`));
|
|
200
|
+
} else if (dataplaneUrl) {
|
|
201
|
+
logger.error(chalk.gray(`\nDataplane URL: ${dataplaneUrl}`));
|
|
202
|
+
}
|
|
203
|
+
|
|
188
204
|
logger.error(chalk.gray('\nFull response for debugging:'));
|
|
189
205
|
logger.error(chalk.gray(JSON.stringify(response, null, 2)));
|
|
190
206
|
process.exit(1);
|
|
191
207
|
}
|
|
192
208
|
|
|
193
|
-
|
|
209
|
+
/**
|
|
210
|
+
* Validates and trims controller URL
|
|
211
|
+
* @function validateControllerUrl
|
|
212
|
+
* @param {string} controllerUrl - Controller URL to validate
|
|
213
|
+
* @returns {string} Trimmed controller URL
|
|
214
|
+
*/
|
|
215
|
+
function validateControllerUrl(controllerUrl) {
|
|
216
|
+
const trimmed = controllerUrl.trim();
|
|
217
|
+
if (!trimmed) {
|
|
218
|
+
logger.error(chalk.red('❌ Controller URL is empty.'));
|
|
219
|
+
logger.error(chalk.gray(` Controller URL from config: ${JSON.stringify(controllerUrl)}`));
|
|
220
|
+
logger.error(chalk.gray(' Run: aifabrix login --method device --controller <url>'));
|
|
221
|
+
process.exit(1);
|
|
222
|
+
}
|
|
223
|
+
return trimmed;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Resolves and validates dataplane URL from controller
|
|
228
|
+
* @async
|
|
229
|
+
* @function resolveAndValidateDataplaneUrl
|
|
230
|
+
* @param {string} controllerUrl - Controller URL
|
|
231
|
+
* @param {string} environment - Environment key
|
|
232
|
+
* @param {Object} authConfig - Authentication configuration
|
|
233
|
+
* @returns {Promise<string>} Validated dataplane URL
|
|
234
|
+
*/
|
|
235
|
+
async function resolveAndValidateDataplaneUrl(controllerUrl, environment, authConfig) {
|
|
236
|
+
let dataplaneUrl;
|
|
237
|
+
try {
|
|
238
|
+
// discoverDataplaneUrl already logs progress and success messages
|
|
239
|
+
dataplaneUrl = await resolveDataplaneUrl(controllerUrl, environment, authConfig);
|
|
240
|
+
} catch (error) {
|
|
241
|
+
logger.error(chalk.red('❌ Failed to resolve dataplane URL:'), error.message);
|
|
242
|
+
logger.error(chalk.gray('\nThe dataplane URL is automatically discovered from the controller.'));
|
|
243
|
+
logger.error(chalk.gray('If discovery fails, ensure you are logged in and the controller is accessible:'));
|
|
244
|
+
logger.error(chalk.gray(' aifabrix login'));
|
|
245
|
+
process.exit(1);
|
|
246
|
+
// eslint-disable-next-line no-unreachable
|
|
247
|
+
throw error; // Never reached in production, but needed for tests when process.exit is mocked
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (!dataplaneUrl || typeof dataplaneUrl !== 'string' || !dataplaneUrl.trim()) {
|
|
251
|
+
logger.error(chalk.red('❌ Dataplane URL is empty.'));
|
|
252
|
+
logger.error(chalk.gray('The dataplane URL could not be discovered from the controller.'));
|
|
253
|
+
logger.error(chalk.gray('Ensure the dataplane service is registered in the controller.'));
|
|
254
|
+
process.exit(1);
|
|
255
|
+
// eslint-disable-next-line no-unreachable
|
|
256
|
+
throw new Error('Dataplane URL is empty'); // Never reached in production, but needed for tests
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return dataplaneUrl.trim();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Sets up authentication configuration for dataplane API calls
|
|
264
|
+
* @function setupAuthConfig
|
|
265
|
+
* @param {string} token - Authentication token
|
|
266
|
+
* @param {string} controllerUrl - Controller URL
|
|
267
|
+
* @returns {Object} Authentication configuration
|
|
268
|
+
*/
|
|
269
|
+
function setupAuthConfig(token, controllerUrl) {
|
|
270
|
+
return {
|
|
271
|
+
type: 'bearer',
|
|
272
|
+
token: token,
|
|
273
|
+
controller: controllerUrl
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function listDatasources(_options) {
|
|
194
278
|
const config = await getConfig();
|
|
195
279
|
|
|
280
|
+
// Resolve environment from config.yaml (no flags)
|
|
281
|
+
const environment = await resolveEnvironment();
|
|
282
|
+
|
|
196
283
|
// Try to get device token
|
|
197
284
|
const authInfo = await getDeviceTokenFromConfig(config);
|
|
198
285
|
validateDatasourceListingAuth(authInfo?.token, authInfo?.controllerUrl);
|
|
199
286
|
|
|
200
|
-
// Call controller API using centralized API client
|
|
201
|
-
// Note: validateDatasourceListingAuth will exit if auth is missing, so this check is defensive
|
|
202
287
|
if (!authInfo || !authInfo.token || !authInfo.controllerUrl) {
|
|
203
|
-
validateDatasourceListingAuth(null, null);
|
|
204
|
-
return;
|
|
288
|
+
validateDatasourceListingAuth(null, null);
|
|
289
|
+
return;
|
|
205
290
|
}
|
|
206
|
-
|
|
207
|
-
const
|
|
291
|
+
|
|
292
|
+
const controllerUrl = validateControllerUrl(authInfo.controllerUrl);
|
|
293
|
+
const authConfig = setupAuthConfig(authInfo.token, controllerUrl);
|
|
294
|
+
const dataplaneUrl = await resolveAndValidateDataplaneUrl(controllerUrl, environment, authConfig);
|
|
295
|
+
|
|
296
|
+
const response = await listDatasourcesFromDataplane(dataplaneUrl, authConfig);
|
|
208
297
|
|
|
209
298
|
if (!response.success || !response.data) {
|
|
210
|
-
handleDatasourceApiError(response);
|
|
299
|
+
handleDatasourceApiError(response, dataplaneUrl);
|
|
211
300
|
return; // Ensure we don't continue after exit
|
|
212
301
|
}
|
|
213
302
|
|
|
214
303
|
const datasources = extractDatasources(response);
|
|
215
|
-
displayDatasources(datasources,
|
|
304
|
+
displayDatasources(datasources, environment, dataplaneUrl);
|
|
216
305
|
}
|
|
217
306
|
|
|
218
307
|
module.exports = {
|
|
@@ -52,6 +52,27 @@ async function buildValidationData(manifest, validatedEnvKey, authConfig, option
|
|
|
52
52
|
return { validationData, pipelineAuthConfig };
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Handle authentication errors during validation
|
|
57
|
+
* @param {Error} error - Error object
|
|
58
|
+
* @param {string} appKey - Application key
|
|
59
|
+
* @throws {Error} Enhanced authentication error
|
|
60
|
+
*/
|
|
61
|
+
function handleValidationAuthError(error, appKey) {
|
|
62
|
+
if (error.status === 401 || (error.response && error.response.status === 401)) {
|
|
63
|
+
const authError = new Error(
|
|
64
|
+
`Authentication failed: Invalid or expired credentials for application '${appKey}'.\n` +
|
|
65
|
+
'The provided Client ID and Client Secret are incorrect or have been revoked.\n\n' +
|
|
66
|
+
'💡 If the application already exists, rotate the secret:\n' +
|
|
67
|
+
` aifabrix app rotate-secret ${appKey}\n\n` +
|
|
68
|
+
'💡 Otherwise, ensure credentials are correct in ~/.aifabrix/secrets.local.yaml or use --client-id and --client-secret flags.'
|
|
69
|
+
);
|
|
70
|
+
authError.status = 401;
|
|
71
|
+
authError.originalError = error;
|
|
72
|
+
throw authError;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
55
76
|
/**
|
|
56
77
|
* Validates deployment configuration via validate endpoint
|
|
57
78
|
* This is the first step in the deployment process
|
|
@@ -79,6 +100,10 @@ async function validateDeployment(url, envKey, manifest, authConfig, options = {
|
|
|
79
100
|
return handleValidationResponse(response);
|
|
80
101
|
} catch (error) {
|
|
81
102
|
lastError = error;
|
|
103
|
+
|
|
104
|
+
// Handle authentication errors (401) - credentials are invalid, not missing
|
|
105
|
+
handleValidationAuthError(error, manifest.key);
|
|
106
|
+
|
|
82
107
|
const shouldRetry = attempt < maxRetries && error.status && error.status >= 500;
|
|
83
108
|
if (shouldRetry) {
|
|
84
109
|
const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000);
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
const chalk = require('chalk');
|
|
13
13
|
const logger = require('../utils/logger');
|
|
14
14
|
const config = require('../core/config');
|
|
15
|
+
const { resolveControllerUrl } = require('../utils/controller-url');
|
|
15
16
|
const { validateControllerUrl, validateEnvironmentKey } = require('../utils/deployment-validation');
|
|
16
17
|
const { getOrRefreshDeviceToken } = require('../utils/token-manager');
|
|
17
18
|
const { getEnvironmentStatus } = require('../api/environments.api');
|
|
@@ -266,27 +267,19 @@ function displayDeploymentResults(result) {
|
|
|
266
267
|
* @throws {Error} If deployment fails
|
|
267
268
|
*
|
|
268
269
|
* @example
|
|
269
|
-
* await deployEnvironment('dev', { controller: 'https://controller.aifabrix.
|
|
270
|
+
* await deployEnvironment('dev', { controller: 'https://controller.aifabrix.dev' });
|
|
270
271
|
*/
|
|
271
272
|
/**
|
|
272
|
-
* Validates deployment input parameters
|
|
273
|
+
* Validates deployment input parameters (environment key only).
|
|
274
|
+
* Controller URL is resolved from config.yaml via resolveControllerUrl().
|
|
273
275
|
* @function validateDeploymentInput
|
|
274
276
|
* @param {string} envKey - Environment key
|
|
275
|
-
* @param {Object} options - Deployment options
|
|
276
|
-
* @returns {string} Controller URL
|
|
277
277
|
* @throws {Error} If validation fails
|
|
278
278
|
*/
|
|
279
|
-
function validateDeploymentInput(envKey
|
|
279
|
+
function validateDeploymentInput(envKey) {
|
|
280
280
|
if (!envKey || typeof envKey !== 'string' || envKey.trim().length === 0) {
|
|
281
281
|
throw new Error('Environment key is required');
|
|
282
282
|
}
|
|
283
|
-
|
|
284
|
-
const controllerUrl = options.controller || options['controller-url'];
|
|
285
|
-
if (!controllerUrl) {
|
|
286
|
-
throw new Error('Controller URL is required. Use --controller flag');
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
return controllerUrl;
|
|
290
283
|
}
|
|
291
284
|
|
|
292
285
|
/**
|
|
@@ -365,7 +358,11 @@ async function pollDeploymentStatusIfEnabled(result, validatedControllerUrl, env
|
|
|
365
358
|
|
|
366
359
|
async function deployEnvironment(envKey, options = {}) {
|
|
367
360
|
try {
|
|
368
|
-
|
|
361
|
+
validateDeploymentInput(envKey);
|
|
362
|
+
const controllerUrl = await resolveControllerUrl();
|
|
363
|
+
if (!controllerUrl) {
|
|
364
|
+
throw new Error('Controller URL is required. Run "aifabrix login" to set the controller URL in config.yaml');
|
|
365
|
+
}
|
|
369
366
|
const authConfig = await prepareEnvironmentDeployment(envKey, controllerUrl, options);
|
|
370
367
|
|
|
371
368
|
const validatedControllerUrl = validateControllerUrl(authConfig.controller);
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External System Delete Module
|
|
3
|
+
*
|
|
4
|
+
* Deletes external systems from dataplane and confirms before removal.
|
|
5
|
+
*
|
|
6
|
+
* @fileoverview External system delete functionality for AI Fabrix Builder
|
|
7
|
+
* @author AI Fabrix Team
|
|
8
|
+
* @version 2.0.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const chalk = require('chalk');
|
|
14
|
+
const inquirer = require('inquirer');
|
|
15
|
+
const logger = require('../utils/logger');
|
|
16
|
+
const { getDeploymentAuth } = require('../utils/token-manager');
|
|
17
|
+
const { resolveControllerUrl } = require('../utils/controller-url');
|
|
18
|
+
const { getExternalSystemConfig, deleteExternalSystem } = require('../api/external-systems.api');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Validates system key format
|
|
22
|
+
* @param {string} systemKey - System key to validate
|
|
23
|
+
*/
|
|
24
|
+
function validateSystemKey(systemKey) {
|
|
25
|
+
if (!systemKey || typeof systemKey !== 'string') {
|
|
26
|
+
throw new Error('System key is required and must be a string');
|
|
27
|
+
}
|
|
28
|
+
if (!/^[a-z0-9-_]+$/.test(systemKey)) {
|
|
29
|
+
throw new Error('System key must contain only lowercase letters, numbers, hyphens, and underscores');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Gets dataplane URL and authentication configuration
|
|
35
|
+
* @async
|
|
36
|
+
* @param {string} systemKey - System key
|
|
37
|
+
* @param {Object} options - Command options
|
|
38
|
+
* @returns {Promise<Object>} Auth and dataplane details
|
|
39
|
+
*/
|
|
40
|
+
async function getAuthAndDataplane(systemKey, _options) {
|
|
41
|
+
const { resolveEnvironment } = require('../core/config');
|
|
42
|
+
const environment = await resolveEnvironment();
|
|
43
|
+
const controllerUrl = await resolveControllerUrl();
|
|
44
|
+
const authConfig = await getDeploymentAuth(controllerUrl, environment, systemKey);
|
|
45
|
+
|
|
46
|
+
if (!authConfig.token && !authConfig.clientId) {
|
|
47
|
+
throw new Error('Authentication required. Run "aifabrix login" or "aifabrix app register" first.');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const { resolveDataplaneUrl } = require('../utils/dataplane-resolver');
|
|
51
|
+
logger.log(chalk.blue('🌐 Resolving dataplane URL...'));
|
|
52
|
+
const dataplaneUrl = await resolveDataplaneUrl(controllerUrl, environment, authConfig);
|
|
53
|
+
logger.log(chalk.green(`✓ Dataplane URL: ${dataplaneUrl}`));
|
|
54
|
+
|
|
55
|
+
return { authConfig, dataplaneUrl, environment, controllerUrl };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Fetches external system configuration for warning display
|
|
60
|
+
* @async
|
|
61
|
+
* @param {string} dataplaneUrl - Dataplane URL
|
|
62
|
+
* @param {string} systemKey - System key
|
|
63
|
+
* @param {Object} authConfig - Authentication configuration
|
|
64
|
+
* @returns {Promise<Object>} System config response data
|
|
65
|
+
*/
|
|
66
|
+
async function fetchExternalSystemConfig(dataplaneUrl, systemKey, authConfig) {
|
|
67
|
+
const response = await getExternalSystemConfig(dataplaneUrl, systemKey, authConfig);
|
|
68
|
+
if (!response || response.success === false) {
|
|
69
|
+
throw new Error(response?.error || response?.formattedError || `External system '${systemKey}' not found`);
|
|
70
|
+
}
|
|
71
|
+
return response.data?.data || response.data || {};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Formats datasources for warning output
|
|
76
|
+
* @param {Array} dataSources - Datasource objects
|
|
77
|
+
* @returns {string[]} Datasource labels
|
|
78
|
+
*/
|
|
79
|
+
function formatDatasourceList(dataSources) {
|
|
80
|
+
if (!Array.isArray(dataSources)) {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
return dataSources
|
|
84
|
+
.map(ds => ds.key || ds.displayName || 'unknown-datasource')
|
|
85
|
+
.filter(Boolean);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Prompts for delete confirmation if needed
|
|
90
|
+
* @async
|
|
91
|
+
* @param {string} systemKey - System key
|
|
92
|
+
* @param {string[]} datasources - Datasource keys
|
|
93
|
+
* @param {Object} options - Command options
|
|
94
|
+
* @returns {Promise<boolean>} True if confirmed
|
|
95
|
+
*/
|
|
96
|
+
async function confirmDeletion(systemKey, datasources, options) {
|
|
97
|
+
if (options.yes || options.force) {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
logger.log(chalk.yellow(`\n⚠️ Warning: Deleting external system '${systemKey}' will also delete all associated datasources:`));
|
|
102
|
+
if (datasources.length > 0) {
|
|
103
|
+
datasources.forEach(ds => logger.log(chalk.yellow(` - ${ds}`)));
|
|
104
|
+
} else {
|
|
105
|
+
logger.log(chalk.yellow(' - (no datasources found)'));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const answer = await inquirer.prompt([{
|
|
109
|
+
type: 'input',
|
|
110
|
+
name: 'confirm',
|
|
111
|
+
message: `Are you sure you want to delete external system '${systemKey}'? (yes/no):`,
|
|
112
|
+
default: 'no'
|
|
113
|
+
}]);
|
|
114
|
+
|
|
115
|
+
return String(answer.confirm).trim().toLowerCase() === 'yes';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Deletes an external system from dataplane
|
|
120
|
+
* @async
|
|
121
|
+
* @function deleteExternalSystemCommand
|
|
122
|
+
* @param {string} systemKey - System key
|
|
123
|
+
* @param {Object} options - Command options
|
|
124
|
+
* @returns {Promise<void>}
|
|
125
|
+
* @throws {Error} If deletion fails or is cancelled
|
|
126
|
+
*/
|
|
127
|
+
async function deleteExternalSystemCommand(systemKey, options = {}) {
|
|
128
|
+
validateSystemKey(systemKey);
|
|
129
|
+
|
|
130
|
+
const { authConfig, dataplaneUrl } = await getAuthAndDataplane(systemKey, options);
|
|
131
|
+
const configData = await fetchExternalSystemConfig(dataplaneUrl, systemKey, authConfig);
|
|
132
|
+
const dataSources = formatDatasourceList(configData.dataSources || []);
|
|
133
|
+
|
|
134
|
+
const confirmed = await confirmDeletion(systemKey, dataSources, options);
|
|
135
|
+
if (!confirmed) {
|
|
136
|
+
logger.log(chalk.yellow('Deletion cancelled.'));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const response = await deleteExternalSystem(dataplaneUrl, systemKey, authConfig);
|
|
141
|
+
if (!response || response.success === false) {
|
|
142
|
+
throw new Error(response?.error || response?.formattedError || `Failed to delete external system '${systemKey}'`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
logger.log(chalk.green(`✓ External system '${systemKey}' deleted successfully`));
|
|
146
|
+
logger.log(chalk.green('✓ All associated datasources have been removed'));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
module.exports = {
|
|
150
|
+
deleteExternalSystem: deleteExternalSystemCommand
|
|
151
|
+
};
|