@aifabrix/builder 2.42.1 → 2.43.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/README.md +1 -1
- package/bin/aifabrix.js +1 -1
- package/integration/hubspot-test/README.md +126 -0
- package/integration/{hubspot → hubspot-test}/application.json +6 -6
- package/integration/{hubspot → hubspot-test}/create-hubspot.js +5 -5
- package/integration/hubspot-test/env.template +4 -0
- package/integration/{hubspot/hubspot-datasource-company.json → hubspot-test/hubspot-test-datasource-company.json} +3 -2
- package/integration/{hubspot/hubspot-datasource-contact.json → hubspot-test/hubspot-test-datasource-contact.json} +3 -2
- package/integration/{hubspot/hubspot-datasource-deal.json → hubspot-test/hubspot-test-datasource-deal.json} +3 -2
- package/integration/{hubspot/hubspot-datasource-users.json → hubspot-test/hubspot-test-datasource-users.json} +3 -2
- package/integration/{hubspot/hubspot-deploy.json → hubspot-test/hubspot-test-deploy.json} +198 -21
- package/integration/{hubspot/hubspot-system.json → hubspot-test/hubspot-test-system.json} +8 -7
- package/integration/hubspot-test/rbac.json +166 -0
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-hubspot-credential-real.yaml +3 -3
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-hubspot-env-vars.yaml +2 -2
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-add-datasource.yaml +1 -1
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-credential-create.yaml +1 -1
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-credential-select.yaml +1 -1
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-known-platform.yaml +1 -1
- package/integration/hubspot-test/test-artifacts/wizard-invalid-missing-source.yaml +2 -0
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-mode.yaml +1 -1
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-openapi-file.yaml +1 -1
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-openapi-url.yaml +1 -1
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-source.yaml +1 -1
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-valid-for-dimension-array-test.yaml +1 -1
- package/integration/hubspot-test/test-artifacts/wizard-valid-for-dimension-key-test.yaml +5 -0
- package/integration/hubspot-test/test-artifacts/wizard-valid-for-dimension-path-test.yaml +5 -0
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-valid-for-dimension-test.yaml +1 -1
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-valid-for-rbac-test.yaml +1 -1
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-valid-for-rbac-yaml-test.yaml +1 -1
- package/integration/{hubspot → hubspot-test}/test.js +102 -59
- package/integration/{hubspot → hubspot-test}/wizard-hubspot-e2e.yaml +2 -2
- package/integration/{hubspot → hubspot-test}/wizard-hubspot-platform.yaml +1 -1
- package/lib/api/external-test.api.js +1 -1
- package/lib/api/service-users.api.js +111 -2
- package/lib/api/types/service-users.types.js +41 -0
- package/lib/app/register.js +3 -1
- package/lib/app/rotate-secret.js +3 -0
- package/lib/cli/setup-app.js +2 -2
- package/lib/cli/setup-auth.js +19 -11
- package/lib/cli/setup-dev.js +62 -32
- package/lib/cli/setup-environment.js +6 -21
- package/lib/cli/setup-infra.js +13 -0
- package/lib/cli/setup-secrets.js +45 -6
- package/lib/cli/setup-service-user.js +146 -20
- package/lib/cli/setup-utility.js +12 -0
- package/lib/commands/auth-config.js +4 -8
- package/lib/commands/datasource.js +46 -1
- package/lib/commands/dev-init.js +1 -1
- package/lib/commands/repair-env-template.js +14 -8
- package/lib/commands/repair-rbac.js +25 -19
- package/lib/commands/repair.js +96 -30
- package/lib/commands/secrets-remove.js +1 -1
- package/lib/commands/secrets-validate.js +17 -4
- package/lib/commands/service-user.js +231 -2
- package/lib/commands/up-common.js +25 -0
- package/lib/commands/up-dataplane.js +2 -2
- package/lib/core/admin-secrets.js +2 -0
- package/lib/core/config.js +7 -5
- package/lib/core/ensure-encryption-key.js +1 -3
- package/lib/core/secrets.js +32 -9
- package/lib/core/templates.js +1 -1
- package/lib/datasource/abac-validator.js +157 -0
- package/lib/datasource/field-reference-validator.js +74 -36
- package/lib/datasource/log-viewer.js +221 -0
- package/lib/datasource/resolve-app.js +109 -0
- package/lib/datasource/test-e2e.js +11 -20
- package/lib/datasource/test-integration.js +42 -22
- package/lib/datasource/validate.js +5 -2
- package/lib/external-system/generator.js +12 -8
- package/lib/external-system/test-system-level.js +1 -1
- package/lib/generator/external-controller-manifest.js +3 -3
- package/lib/generator/external.js +7 -7
- package/lib/generator/helpers.js +13 -9
- package/lib/generator/index.js +4 -4
- package/lib/generator/split.js +45 -10
- package/lib/generator/wizard.js +9 -6
- package/lib/infrastructure/helpers.js +50 -35
- package/lib/infrastructure/index.js +39 -23
- package/lib/schema/env-config.yaml +19 -2
- package/lib/schema/external-datasource.schema.json +11 -1
- package/lib/utils/app-config-resolver.js +23 -1
- package/lib/utils/config-paths.js +48 -4
- package/lib/utils/credential-secrets-env.js +16 -1
- package/lib/utils/env-map.js +7 -3
- package/lib/utils/error-formatter.js +37 -0
- package/lib/utils/external-env-template.js +180 -0
- package/lib/utils/external-system-display.js +43 -0
- package/lib/utils/external-system-validators.js +2 -2
- package/lib/utils/help-builder.js +3 -5
- package/lib/utils/local-secrets.js +26 -3
- package/lib/utils/paths.js +2 -1
- package/lib/utils/secrets-generator.js +2 -2
- package/lib/utils/secrets-utils.js +4 -0
- package/lib/utils/secure-file-permissions.js +91 -0
- package/lib/utils/token-manager.js +36 -3
- package/lib/utils/yaml-preserve.js +59 -1
- package/lib/validation/env-template-auth.js +50 -2
- package/lib/validation/external-manifest-validator.js +8 -0
- package/lib/validation/validate.js +8 -0
- package/lib/validation/validator.js +10 -13
- package/package.json +5 -1
- package/templates/applications/dataplane/env.template +5 -1
- package/templates/applications/miso-controller/application.yaml +1 -1
- package/templates/applications/miso-controller/env.template +13 -2
- package/templates/external-system/env.template.hbs +22 -0
- package/integration/hubspot/README.md +0 -102
- package/integration/hubspot/env.template +0 -4
- package/integration/hubspot/test-artifacts/wizard-invalid-missing-source.yaml +0 -2
- package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-key-test.yaml +0 -5
- package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-path-test.yaml +0 -5
- /package/integration/{hubspot → hubspot-test}/companies.json +0 -0
- /package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-app-name.yaml +0 -0
- /package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-missing-app.yaml +0 -0
- /package/integration/{hubspot → hubspot-test}/test-dataplane-down-helpers.js +0 -0
- /package/integration/{hubspot → hubspot-test}/test-dataplane-down-tests.js +0 -0
- /package/integration/{hubspot → hubspot-test}/test-dataplane-down.js +0 -0
|
@@ -17,6 +17,7 @@ const { compareDatasources } = require('../datasource/diff');
|
|
|
17
17
|
const { deployDatasource } = require('../datasource/deploy');
|
|
18
18
|
const { runDatasourceTestIntegration } = require('../datasource/test-integration');
|
|
19
19
|
const { runDatasourceTestE2E } = require('../datasource/test-e2e');
|
|
20
|
+
const { runLogViewer } = require('../datasource/log-viewer');
|
|
20
21
|
const { displayIntegrationTestResults, displayE2EResults } = require('../utils/external-system-display');
|
|
21
22
|
|
|
22
23
|
function setupDatasourceValidateCommand(datasource) {
|
|
@@ -81,9 +82,10 @@ function setupDatasourceUploadCommand(datasource) {
|
|
|
81
82
|
function setupDatasourceTestIntegrationCommand(datasource) {
|
|
82
83
|
datasource.command('test-integration <datasourceKey>')
|
|
83
84
|
.description('Run integration (config) test for one datasource via dataplane pipeline')
|
|
84
|
-
.option('-a, --app <appKey>', 'App key (
|
|
85
|
+
.option('-a, --app <appKey>', 'App key (optional: resolve from cwd or datasource key if single match)')
|
|
85
86
|
.option('-p, --payload <file>', 'Path to custom test payload file')
|
|
86
87
|
.option('-e, --env <env>', 'Environment: dev, tst, or pro')
|
|
88
|
+
.option('-v, --verbose', 'Show detailed step and validation output')
|
|
87
89
|
.option('--debug', 'Include debug output and write log to integration/<appKey>/logs/')
|
|
88
90
|
.option('--timeout <ms>', 'Request timeout in milliseconds', '30000')
|
|
89
91
|
.action(async(datasourceKey, options) => {
|
|
@@ -144,18 +146,61 @@ function setupDatasourceTestE2ECommand(datasource) {
|
|
|
144
146
|
});
|
|
145
147
|
}
|
|
146
148
|
|
|
149
|
+
function setupDatasourceLogE2ECommand(datasource) {
|
|
150
|
+
datasource.command('log-e2e <datasourceKey>')
|
|
151
|
+
.description('Display latest or specified E2E test log in readable format')
|
|
152
|
+
.option('-a, --app <appKey>', 'App key (optional: resolve from cwd or datasource key if single match)')
|
|
153
|
+
.option('-f, --file <path>', 'Path to log file (default: latest in app logs folder)')
|
|
154
|
+
.action(async(datasourceKey, options) => {
|
|
155
|
+
try {
|
|
156
|
+
await runLogViewer(datasourceKey, {
|
|
157
|
+
app: options.app,
|
|
158
|
+
file: options.file,
|
|
159
|
+
logType: 'test-e2e'
|
|
160
|
+
});
|
|
161
|
+
} catch (error) {
|
|
162
|
+
logger.error(chalk.red('❌ log-e2e failed:'), error.message);
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function setupDatasourceLogIntegrationCommand(datasource) {
|
|
169
|
+
datasource.command('log-integration <datasourceKey>')
|
|
170
|
+
.description('Display latest or specified integration test log in readable format')
|
|
171
|
+
.option('-a, --app <appKey>', 'App key (optional: resolve from cwd or datasource key if single match)')
|
|
172
|
+
.option('-f, --file <path>', 'Path to log file (default: latest in app logs folder)')
|
|
173
|
+
.action(async(datasourceKey, options) => {
|
|
174
|
+
try {
|
|
175
|
+
await runLogViewer(datasourceKey, {
|
|
176
|
+
app: options.app,
|
|
177
|
+
file: options.file,
|
|
178
|
+
logType: 'test-integration'
|
|
179
|
+
});
|
|
180
|
+
} catch (error) {
|
|
181
|
+
logger.error(chalk.red('❌ log-integration failed:'), error.message);
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
147
187
|
/**
|
|
148
188
|
* Setup datasource management commands
|
|
149
189
|
* @param {Command} program - Commander program instance
|
|
150
190
|
*/
|
|
151
191
|
function setupDatasourceCommands(program) {
|
|
152
192
|
const datasource = program.command('datasource').description('Manage external data sources');
|
|
193
|
+
if (typeof datasource.alias === 'function') {
|
|
194
|
+
datasource.alias('ds');
|
|
195
|
+
}
|
|
153
196
|
setupDatasourceValidateCommand(datasource);
|
|
154
197
|
setupDatasourceListCommand(datasource);
|
|
155
198
|
setupDatasourceDiffCommand(datasource);
|
|
156
199
|
setupDatasourceUploadCommand(datasource);
|
|
157
200
|
setupDatasourceTestIntegrationCommand(datasource);
|
|
158
201
|
setupDatasourceTestE2ECommand(datasource);
|
|
202
|
+
setupDatasourceLogE2ECommand(datasource);
|
|
203
|
+
setupDatasourceLogIntegrationCommand(datasource);
|
|
159
204
|
}
|
|
160
205
|
|
|
161
206
|
module.exports = { setupDatasourceCommands };
|
package/lib/commands/dev-init.js
CHANGED
|
@@ -341,7 +341,7 @@ async function runDevRefresh(options = {}) {
|
|
|
341
341
|
logger.log(chalk.blue('\n🔄 Fetching settings from Builder Server...\n'));
|
|
342
342
|
const settings = await devApi.getSettings(auth.serverUrl, clientCertPem, clientKeyPem || undefined);
|
|
343
343
|
await config.mergeRemoteSettings(settings);
|
|
344
|
-
logger.log(chalk.green('✓ Config updated from server. Run "aifabrix dev
|
|
344
|
+
logger.log(chalk.green('✓ Config updated from server. Run "aifabrix dev show" to verify.\n'));
|
|
345
345
|
}
|
|
346
346
|
|
|
347
347
|
module.exports = { runDevInit, runDevRefresh };
|
|
@@ -11,6 +11,7 @@ const path = require('path');
|
|
|
11
11
|
const fs = require('fs');
|
|
12
12
|
const { systemKeyToKvPrefix, kvEnvKeyToPath, securityKeyToVar } = require('../utils/credential-secrets-env');
|
|
13
13
|
const { extractEnvTemplate } = require('../generator/split');
|
|
14
|
+
const { generateExternalEnvTemplateContent } = require('../utils/external-env-template');
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Normalizes a keyvault config entry to canonical KV_* name and path-style value.
|
|
@@ -126,20 +127,25 @@ function buildExpectedByKey(effective) {
|
|
|
126
127
|
}
|
|
127
128
|
|
|
128
129
|
/**
|
|
129
|
-
* Creates env.template when missing
|
|
130
|
+
* Creates env.template when missing using the Handlebars template (Authentication + Configuration sections).
|
|
130
131
|
* @param {string} envPath - Path to env.template
|
|
131
|
-
* @param {Map<string, string>} expectedByKey - Expected key->line map
|
|
132
|
+
* @param {Map<string, string>} expectedByKey - Expected key->line map (fallback when no systemParsed)
|
|
132
133
|
* @param {boolean} dryRun - If true, do not write
|
|
133
134
|
* @param {string[]} changes - Array to append to
|
|
135
|
+
* @param {Object} [systemParsed] - Parsed system config for template context (preferred)
|
|
134
136
|
* @returns {boolean}
|
|
135
137
|
*/
|
|
136
|
-
function createEnvTemplateIfMissing(envPath, expectedByKey, dryRun, changes) {
|
|
138
|
+
function createEnvTemplateIfMissing(envPath, expectedByKey, dryRun, changes, systemParsed) {
|
|
137
139
|
if (fs.existsSync(envPath)) return false;
|
|
138
|
-
|
|
139
|
-
const content =
|
|
140
|
-
|
|
140
|
+
if (expectedByKey.size === 0) return false;
|
|
141
|
+
const content = systemParsed
|
|
142
|
+
? generateExternalEnvTemplateContent(systemParsed)
|
|
143
|
+
: Array.from(expectedByKey.values()).join('\n');
|
|
144
|
+
if (!content || !content.trim()) return false;
|
|
145
|
+
const hasKeyValueLine = /^[A-Z_][A-Z0-9_]*=/m.test(content);
|
|
146
|
+
if (!hasKeyValueLine) return false;
|
|
141
147
|
if (!dryRun) {
|
|
142
|
-
fs.writeFileSync(envPath, content + '\n', { mode: 0o644, encoding: 'utf8' });
|
|
148
|
+
fs.writeFileSync(envPath, content + (content.endsWith('\n') ? '' : '\n'), { mode: 0o644, encoding: 'utf8' });
|
|
143
149
|
}
|
|
144
150
|
changes.push('Created env.template from system configuration');
|
|
145
151
|
return true;
|
|
@@ -236,7 +242,7 @@ function repairEnvTemplate(appPath, systemParsed, systemKey, dryRun, changes) {
|
|
|
236
242
|
const expectedByKey = buildExpectedByKey(effective);
|
|
237
243
|
const envPath = path.join(appPath, 'env.template');
|
|
238
244
|
|
|
239
|
-
if (createEnvTemplateIfMissing(envPath, expectedByKey, dryRun, changes)) {
|
|
245
|
+
if (createEnvTemplateIfMissing(envPath, expectedByKey, dryRun, changes, systemParsed)) {
|
|
240
246
|
return true;
|
|
241
247
|
}
|
|
242
248
|
if (!fs.existsSync(envPath)) {
|
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
|
|
11
11
|
const path = require('path');
|
|
12
12
|
const fs = require('fs');
|
|
13
|
-
const yaml = require('js-yaml');
|
|
14
13
|
const chalk = require('chalk');
|
|
15
14
|
const logger = require('../utils/logger');
|
|
16
|
-
const { loadConfigFile } = require('../utils/config-format');
|
|
15
|
+
const { loadConfigFile, writeConfigFile } = require('../utils/config-format');
|
|
16
|
+
const { resolveRbacPath } = require('../utils/app-config-resolver');
|
|
17
17
|
|
|
18
18
|
const DEFAULT_CAPABILITIES = ['list', 'get', 'create', 'update', 'delete'];
|
|
19
19
|
|
|
@@ -55,26 +55,30 @@ function collectPermissionNames(appPath, datasourceFiles) {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
|
-
* Loads existing rbac or creates empty structure.
|
|
58
|
+
* Loads existing RBAC file (rbac.yaml, rbac.yml, or rbac.json) or creates empty structure.
|
|
59
|
+
* Uses extractRbacFromSystem when no file exists. New file path respects format (rbac.json when format is 'json').
|
|
60
|
+
*
|
|
59
61
|
* @param {string} appPath - Application path
|
|
60
62
|
* @param {Object} [systemParsed] - Parsed system for extractRbacFromSystem
|
|
61
63
|
* @param {Function} extractRbacFromSystem - (system) => rbac or null
|
|
62
|
-
* @
|
|
64
|
+
* @param {string} [format] - 'json' or 'yaml'; used only when creating a new file (default 'yaml')
|
|
65
|
+
* @returns {{ rbac: Object, rbacPath: string }} rbacPath is resolved path or path.join(appPath, 'rbac.{json|yaml}') for new file
|
|
63
66
|
*/
|
|
64
|
-
function loadOrCreateRbac(appPath, systemParsed, extractRbacFromSystem) {
|
|
65
|
-
const
|
|
66
|
-
const rbacYmlPath = path.join(appPath, 'rbac.yml');
|
|
67
|
+
function loadOrCreateRbac(appPath, systemParsed, extractRbacFromSystem, format) {
|
|
68
|
+
const resolvedPath = resolveRbacPath(appPath);
|
|
67
69
|
let rbac;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
let rbacPath;
|
|
71
|
+
if (resolvedPath) {
|
|
72
|
+
rbac = loadConfigFile(resolvedPath);
|
|
73
|
+
rbacPath = resolvedPath;
|
|
72
74
|
} else {
|
|
73
75
|
rbac = extractRbacFromSystem(systemParsed) || { roles: [], permissions: [] };
|
|
74
76
|
if (!Array.isArray(rbac.roles)) rbac.roles = [];
|
|
75
77
|
if (!Array.isArray(rbac.permissions)) rbac.permissions = [];
|
|
78
|
+
const ext = (format === 'json') ? 'rbac.json' : 'rbac.yaml';
|
|
79
|
+
rbacPath = path.join(appPath, ext);
|
|
76
80
|
}
|
|
77
|
-
return { rbac, rbacPath
|
|
81
|
+
return { rbac, rbacPath };
|
|
78
82
|
}
|
|
79
83
|
|
|
80
84
|
/**
|
|
@@ -123,31 +127,33 @@ function ensureDefaultRoles(rbac, systemKey, displayName, changes) {
|
|
|
123
127
|
if (p.name && listGetPerms.includes(p.name) && !p.roles.includes(readerValue)) p.roles.push(readerValue);
|
|
124
128
|
if (!p.roles.includes(adminValue)) p.roles.push(adminValue);
|
|
125
129
|
}
|
|
126
|
-
changes.push('Added default Admin and Reader roles to rbac
|
|
130
|
+
changes.push('Added default Admin and Reader roles to rbac file');
|
|
127
131
|
return true;
|
|
128
132
|
}
|
|
129
133
|
|
|
130
134
|
/**
|
|
131
135
|
* Merges RBAC from datasources: ensures permission per resourceType:capability, adds Admin/Reader roles if none.
|
|
136
|
+
* When creating a new RBAC file, uses rbac.json if format is 'json', otherwise rbac.yaml.
|
|
137
|
+
*
|
|
132
138
|
* @param {string} appPath - Application path
|
|
133
139
|
* @param {Object} systemParsed - Parsed system (key, displayName)
|
|
134
140
|
* @param {string[]} datasourceFiles - Datasource file names
|
|
135
141
|
* @param {Function} extractRbacFromSystem - (system) => rbac or null
|
|
136
|
-
* @param {boolean}
|
|
137
|
-
* @param {string[]} changes - Array to append change descriptions to
|
|
142
|
+
* @param {{ format?: string, dryRun: boolean, changes: string[] }} options - format ('json'|'yaml'), dryRun, changes array
|
|
138
143
|
* @returns {boolean} True if rbac was updated (or would be in dry-run)
|
|
139
144
|
*/
|
|
140
|
-
function mergeRbacFromDatasources(appPath, systemParsed, datasourceFiles, extractRbacFromSystem,
|
|
145
|
+
function mergeRbacFromDatasources(appPath, systemParsed, datasourceFiles, extractRbacFromSystem, options) {
|
|
146
|
+
const { format = 'yaml', dryRun, changes } = options;
|
|
147
|
+
const rbacFormat = format === 'json' ? 'json' : 'yaml';
|
|
141
148
|
const permissionNames = collectPermissionNames(appPath, datasourceFiles);
|
|
142
149
|
if (permissionNames.size === 0) return false;
|
|
143
150
|
const systemKey = systemParsed?.key || 'system';
|
|
144
151
|
const displayName = systemParsed?.displayName || systemKey;
|
|
145
|
-
const { rbac, rbacPath
|
|
152
|
+
const { rbac, rbacPath } = loadOrCreateRbac(appPath, systemParsed, extractRbacFromSystem, rbacFormat);
|
|
146
153
|
let updated = addMissingPermissions(rbac, permissionNames, changes);
|
|
147
154
|
updated = ensureDefaultRoles(rbac, systemKey, displayName, changes) || updated;
|
|
148
155
|
if (updated && !dryRun) {
|
|
149
|
-
|
|
150
|
-
fs.writeFileSync(outPath, yaml.dump(rbac, { indent: 2, lineWidth: -1 }), { mode: 0o644, encoding: 'utf8' });
|
|
156
|
+
writeConfigFile(rbacPath, rbac);
|
|
151
157
|
}
|
|
152
158
|
return updated;
|
|
153
159
|
}
|
package/lib/commands/repair.js
CHANGED
|
@@ -16,14 +16,14 @@
|
|
|
16
16
|
const path = require('path');
|
|
17
17
|
const fs = require('fs');
|
|
18
18
|
const chalk = require('chalk');
|
|
19
|
-
const
|
|
20
|
-
const {
|
|
21
|
-
const { resolveApplicationConfigPath } = require('../utils/app-config-resolver');
|
|
19
|
+
const { detectAppType, getDeployJsonPath } = require('../utils/paths');
|
|
20
|
+
const { resolveApplicationConfigPath, resolveRbacPath } = require('../utils/app-config-resolver');
|
|
22
21
|
const { loadConfigFile, writeConfigFile, writeYamlPreservingComments, isYamlPath } = require('../utils/config-format');
|
|
23
22
|
const { systemKeyToKvPrefix, securityKeyToVar } = require('../utils/credential-secrets-env');
|
|
24
23
|
const logger = require('../utils/logger');
|
|
25
24
|
const generator = require('../generator');
|
|
26
25
|
const { repairEnvTemplate, normalizeSystemFileAuthAndConfig } = require('./repair-env-template');
|
|
26
|
+
const { generateReadmeFromDeployJson } = require('../generator/split-readme');
|
|
27
27
|
const { repairDatasourceFile } = require('./repair-datasource');
|
|
28
28
|
const { mergeRbacFromDatasources } = require('./repair-rbac');
|
|
29
29
|
const { discoverIntegrationFiles, buildEffectiveDatasourceFiles } = require('./repair-internal');
|
|
@@ -249,6 +249,11 @@ function normalizedAuthPartFromConfigName(name, systemKey) {
|
|
|
249
249
|
const rest = n.slice(kvPrefix.length);
|
|
250
250
|
return rest.toUpperCase().replace(/_/g, '');
|
|
251
251
|
}
|
|
252
|
+
// Old-style or other KV_* names: treat last segment as var (e.g. KV_HUBSPOT_CLIENTID → CLIENTID)
|
|
253
|
+
if (n.toUpperCase().startsWith('KV_')) {
|
|
254
|
+
const parts = n.split('_').filter(Boolean);
|
|
255
|
+
if (parts.length >= 2) return parts[parts.length - 1].toUpperCase();
|
|
256
|
+
}
|
|
252
257
|
return n.toUpperCase().replace(/_/g, '');
|
|
253
258
|
}
|
|
254
259
|
|
|
@@ -273,21 +278,20 @@ function removeAuthVarsFromConfiguration(systemParsed, systemKey, dryRun, change
|
|
|
273
278
|
return true;
|
|
274
279
|
}
|
|
275
280
|
|
|
276
|
-
function createRbacFromSystemIfNeeded(appPath, systemFilePath, systemParsed, dryRun, changes) {
|
|
277
|
-
|
|
278
|
-
const rbacYmlPath = path.join(appPath, 'rbac.yml');
|
|
279
|
-
if (fs.existsSync(rbacPath) || fs.existsSync(rbacYmlPath)) return false;
|
|
281
|
+
function createRbacFromSystemIfNeeded(appPath, systemFilePath, systemParsed, dryRun, changes, format) {
|
|
282
|
+
if (resolveRbacPath(appPath)) return false;
|
|
280
283
|
const rbacFromSystem = extractRbacFromSystem(systemParsed);
|
|
281
284
|
if (!rbacFromSystem) return false;
|
|
282
285
|
if (!dryRun) {
|
|
283
|
-
const
|
|
284
|
-
|
|
286
|
+
const rbacFormat = format === 'json' ? 'json' : 'yaml';
|
|
287
|
+
const defaultRbacPath = path.join(appPath, rbacFormat === 'json' ? 'rbac.json' : 'rbac.yaml');
|
|
288
|
+
writeConfigFile(defaultRbacPath, rbacFromSystem, rbacFormat);
|
|
285
289
|
delete systemParsed.roles;
|
|
286
290
|
delete systemParsed.permissions;
|
|
287
291
|
writeConfigFile(systemFilePath, systemParsed);
|
|
288
292
|
}
|
|
289
293
|
changes.push('Created rbac.yaml from system roles/permissions');
|
|
290
|
-
changes.push('Removed roles/permissions from system file (now in rbac
|
|
294
|
+
changes.push('Removed roles/permissions from system file (now in rbac file)');
|
|
291
295
|
return true;
|
|
292
296
|
}
|
|
293
297
|
|
|
@@ -338,6 +342,36 @@ async function regenerateManifest(appName, appPath, changes) {
|
|
|
338
342
|
}
|
|
339
343
|
}
|
|
340
344
|
|
|
345
|
+
/**
|
|
346
|
+
* Regenerates README.md from deployment manifest when options.doc is set.
|
|
347
|
+
* @param {string} appName - Application name
|
|
348
|
+
* @param {string} appPath - Application path
|
|
349
|
+
* @param {Object} options - Options (doc, dryRun)
|
|
350
|
+
* @param {string[]} changes - Array to append change messages to
|
|
351
|
+
* @returns {Promise<boolean>} True if README was regenerated
|
|
352
|
+
*/
|
|
353
|
+
async function regenerateReadmeIfRequested(appName, appPath, options, changes) {
|
|
354
|
+
if (!options.doc) return false;
|
|
355
|
+
const deployJsonPath = getDeployJsonPath(appName, 'external', true);
|
|
356
|
+
if (!fs.existsSync(deployJsonPath) && !options.dryRun) {
|
|
357
|
+
await regenerateManifest(appName, appPath, changes);
|
|
358
|
+
}
|
|
359
|
+
if (!fs.existsSync(deployJsonPath)) return false;
|
|
360
|
+
try {
|
|
361
|
+
const deployment = JSON.parse(fs.readFileSync(deployJsonPath, 'utf8'));
|
|
362
|
+
const readmeContent = generateReadmeFromDeployJson(deployment);
|
|
363
|
+
const readmePath = path.join(appPath, 'README.md');
|
|
364
|
+
if (!options.dryRun) {
|
|
365
|
+
fs.writeFileSync(readmePath, readmeContent, { mode: 0o644, encoding: 'utf8' });
|
|
366
|
+
}
|
|
367
|
+
changes.push('Regenerated README.md from deployment manifest');
|
|
368
|
+
return true;
|
|
369
|
+
} catch (err) {
|
|
370
|
+
logger.log(chalk.yellow(`⚠ Could not regenerate README: ${err.message}`));
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
341
375
|
function persistChangesAndRegenerate(configPath, variables, appName, appPath, changes, originalYamlContent) {
|
|
342
376
|
if (originalYamlContent !== null && originalYamlContent !== undefined && typeof originalYamlContent === 'string' && isYamlPath(configPath)) {
|
|
343
377
|
writeYamlPreservingComments(configPath, originalYamlContent, variables);
|
|
@@ -410,7 +444,7 @@ function runRepairSteps(ctx) {
|
|
|
410
444
|
ctx.appPath, ctx.datasourceFiles, ctx.systemKey, ctx.dryRun, ctx.changes
|
|
411
445
|
);
|
|
412
446
|
const rbacFileCreated = createRbacFromSystemIfNeeded(
|
|
413
|
-
ctx.appPath, ctx.systemFilePath, ctx.systemParsed, ctx.dryRun, ctx.changes
|
|
447
|
+
ctx.appPath, ctx.systemFilePath, ctx.systemParsed, ctx.dryRun, ctx.changes, ctx.format
|
|
414
448
|
);
|
|
415
449
|
const envTemplateRepaired = repairEnvTemplate(
|
|
416
450
|
ctx.appPath, ctx.systemParsed, ctx.systemKey, ctx.dryRun, ctx.changes
|
|
@@ -431,7 +465,8 @@ function runRepairSteps(ctx) {
|
|
|
431
465
|
* @param {string} appName - Application/integration name
|
|
432
466
|
* @param {Object} [options] - Options
|
|
433
467
|
* @param {boolean} [options.dryRun] - If true, only report changes; do not write
|
|
434
|
-
* @
|
|
468
|
+
* @param {boolean} [options.doc] - If true, regenerate README.md from deployment manifest
|
|
469
|
+
* @returns {Promise<{ updated: boolean, changes: string[], systemFiles: string[], datasourceFiles: string[], appKeyFixed?: boolean, datasourceKeysFixed?: boolean, rbacFileCreated?: boolean, envTemplateRepaired?: boolean, manifestRegenerated?: boolean, readmeRegenerated?: boolean }>}
|
|
435
470
|
*/
|
|
436
471
|
/**
|
|
437
472
|
* Loads application config and discovers integration files; validates at least one system file exists.
|
|
@@ -453,7 +488,36 @@ function loadConfigAndDiscover(appPath, configPath) {
|
|
|
453
488
|
return { variables, originalYamlContent, systemFiles, datasourceFiles };
|
|
454
489
|
}
|
|
455
490
|
|
|
456
|
-
|
|
491
|
+
/**
|
|
492
|
+
* Builds the repair result object from steps and flags.
|
|
493
|
+
* @param {Object} steps - Result of runRepairSteps
|
|
494
|
+
* @param {boolean} anyUpdated - Whether any repair made changes
|
|
495
|
+
* @param {boolean} manifestRegenerated - Whether manifest was regenerated
|
|
496
|
+
* @param {boolean} readmeRegenerated - Whether README was regenerated
|
|
497
|
+
* @param {{ changes: string[], systemFiles: string[], datasourceFiles: string[] }} ctx - changes and file lists
|
|
498
|
+
* @returns {Object} Combined result object
|
|
499
|
+
*/
|
|
500
|
+
function buildRepairResult(steps, anyUpdated, manifestRegenerated, readmeRegenerated, ctx) {
|
|
501
|
+
return Object.assign(
|
|
502
|
+
{ updated: anyUpdated, changes: ctx.changes, systemFiles: ctx.systemFiles, datasourceFiles: ctx.datasourceFiles },
|
|
503
|
+
{
|
|
504
|
+
appKeyFixed: steps.appKeyFixed,
|
|
505
|
+
datasourceKeysFixed: steps.datasourceKeysFixed,
|
|
506
|
+
rbacFileCreated: steps.rbacFileCreated,
|
|
507
|
+
envTemplateRepaired: steps.envTemplateRepaired,
|
|
508
|
+
manifestRegenerated,
|
|
509
|
+
readmeRegenerated
|
|
510
|
+
}
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Validates repair inputs and resolves app path and config path.
|
|
516
|
+
* @param {string} appName - Application name
|
|
517
|
+
* @param {Object} options - Command options (auth)
|
|
518
|
+
* @returns {Promise<{ appPath: string, configPath: string, dryRun: boolean, authOption: string|undefined }>}
|
|
519
|
+
*/
|
|
520
|
+
async function validateAndResolveRepairPaths(appName, options) {
|
|
457
521
|
if (!appName || typeof appName !== 'string') throw new Error('App name is required');
|
|
458
522
|
const { dryRun = false, auth: authOption } = options;
|
|
459
523
|
if (authOption !== undefined && authOption !== null && typeof authOption !== 'string') {
|
|
@@ -463,6 +527,11 @@ async function repairExternalIntegration(appName, options = {}) {
|
|
|
463
527
|
if (!isExternal) throw new Error(`App '${appName}' is not an external integration`);
|
|
464
528
|
const configPath = resolveApplicationConfigPath(appPath);
|
|
465
529
|
if (!fs.existsSync(configPath)) throw new Error(`Application config not found: ${configPath}`);
|
|
530
|
+
return { appPath, configPath, dryRun, authOption };
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
async function repairExternalIntegration(appName, options = {}) {
|
|
534
|
+
const { appPath, configPath, dryRun, authOption } = await validateAndResolveRepairPaths(appName, options);
|
|
466
535
|
|
|
467
536
|
const { variables, originalYamlContent, systemFiles, datasourceFiles: initialDatasourceFiles } = loadConfigAndDiscover(appPath, configPath);
|
|
468
537
|
const changes = [];
|
|
@@ -486,30 +555,27 @@ async function repairExternalIntegration(appName, options = {}) {
|
|
|
486
555
|
datasourceFiles,
|
|
487
556
|
dryRun,
|
|
488
557
|
changes,
|
|
489
|
-
auth: authOption
|
|
558
|
+
auth: authOption,
|
|
559
|
+
format: options.format === 'json' ? 'json' : 'yaml'
|
|
490
560
|
});
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
561
|
+
const datasourceRepairUpdated = runDatasourceRepairs(appPath, datasourceFiles, {
|
|
562
|
+
expose: Boolean(options.expose),
|
|
563
|
+
sync: Boolean(options.sync),
|
|
564
|
+
test: Boolean(options.test)
|
|
565
|
+
}, dryRun, changes);
|
|
494
566
|
const rbacMergeUpdated = options.rbac
|
|
495
|
-
? mergeRbacFromDatasources(appPath, systemParsed, datasourceFiles, extractRbacFromSystem,
|
|
567
|
+
? mergeRbacFromDatasources(appPath, systemParsed, datasourceFiles, extractRbacFromSystem, {
|
|
568
|
+
format: options.format === 'json' ? 'json' : 'yaml',
|
|
569
|
+
dryRun,
|
|
570
|
+
changes
|
|
571
|
+
})
|
|
496
572
|
: false;
|
|
497
573
|
const anyUpdated = keysNormalized || steps.updated || datasourceRepairUpdated || rbacMergeUpdated;
|
|
498
574
|
const manifestRegenerated = (anyUpdated && !dryRun)
|
|
499
575
|
? await persistChangesAndRegenerate(configPath, variables, appName, appPath, changes, originalYamlContent)
|
|
500
576
|
: false;
|
|
501
|
-
|
|
502
|
-
return {
|
|
503
|
-
updated: anyUpdated,
|
|
504
|
-
changes,
|
|
505
|
-
systemFiles,
|
|
506
|
-
datasourceFiles,
|
|
507
|
-
appKeyFixed: steps.appKeyFixed,
|
|
508
|
-
datasourceKeysFixed: steps.datasourceKeysFixed,
|
|
509
|
-
rbacFileCreated: steps.rbacFileCreated,
|
|
510
|
-
envTemplateRepaired: steps.envTemplateRepaired,
|
|
511
|
-
manifestRegenerated
|
|
512
|
-
};
|
|
577
|
+
const readmeRegenerated = await regenerateReadmeIfRequested(appName, appPath, options, changes);
|
|
578
|
+
return buildRepairResult(steps, anyUpdated, manifestRegenerated, readmeRegenerated, { changes, systemFiles, datasourceFiles });
|
|
513
579
|
}
|
|
514
580
|
|
|
515
581
|
module.exports = {
|
|
@@ -33,7 +33,7 @@ function removeKeyFromFile(key, filePath) {
|
|
|
33
33
|
throw new Error(`Secret '${key}' not found.`);
|
|
34
34
|
}
|
|
35
35
|
delete data[key];
|
|
36
|
-
const yamlContent = yaml.dump(data, { indent: 2, lineWidth:
|
|
36
|
+
const yamlContent = yaml.dump(data, { indent: 2, lineWidth: -1, noRefs: true, sortKeys: false });
|
|
37
37
|
fs.writeFileSync(filePath, yamlContent, { mode: 0o600 });
|
|
38
38
|
}
|
|
39
39
|
|
|
@@ -12,6 +12,7 @@ const chalk = require('chalk');
|
|
|
12
12
|
const path = require('path');
|
|
13
13
|
const logger = require('../utils/logger');
|
|
14
14
|
const { validateSecretsFile } = require('../utils/secrets-validation');
|
|
15
|
+
const { validateDataplaneSecrets } = require('../utils/token-manager');
|
|
15
16
|
const pathsUtil = require('../utils/paths');
|
|
16
17
|
const secretsEnsure = require('../core/secrets-ensure');
|
|
17
18
|
|
|
@@ -38,13 +39,25 @@ async function handleSecretsValidate(pathArg, options = {}) {
|
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
const result = validateSecretsFile(filePath, { checkNaming: Boolean(options.naming) });
|
|
42
|
+
const dataplaneResult = validateDataplaneSecrets(filePath);
|
|
43
|
+
const allValid = result.valid && dataplaneResult.valid;
|
|
41
44
|
if (result.valid) {
|
|
42
45
|
logger.log(chalk.green(`✓ Secrets file is valid: ${result.path}`));
|
|
43
|
-
|
|
46
|
+
} else {
|
|
47
|
+
logger.log(chalk.red(`✗ Validation failed: ${result.path}`));
|
|
48
|
+
result.errors.forEach((err) => logger.log(chalk.yellow(` • ${err}`)));
|
|
44
49
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
if (!dataplaneResult.valid) {
|
|
51
|
+
logger.log(chalk.yellow(`⚠ ${dataplaneResult.hint}`));
|
|
52
|
+
if (result.valid) {
|
|
53
|
+
logger.log(chalk.yellow(' Wizard/dataplane calls may fail until dataplane credentials are present.'));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
valid: allValid,
|
|
58
|
+
errors: allValid ? [] : [...result.errors, ...(dataplaneResult.valid ? [] : [dataplaneResult.hint])],
|
|
59
|
+
dataplaneValid: dataplaneResult.valid
|
|
60
|
+
};
|
|
48
61
|
}
|
|
49
62
|
|
|
50
63
|
module.exports = { handleSecretsValidate };
|