@aifabrix/builder 2.38.0 → 2.39.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 +3 -0
- package/integration/hubspot/hubspot-deploy.json +0 -3
- package/integration/hubspot/hubspot-system.json +0 -3
- package/lib/api/applications.api.js +8 -2
- package/lib/api/auth.api.js +14 -0
- package/lib/api/credentials.api.js +1 -1
- package/lib/api/datasources-core.api.js +16 -1
- package/lib/api/datasources-extended.api.js +18 -1
- package/lib/api/deployments.api.js +6 -1
- package/lib/api/environments.api.js +11 -0
- package/lib/api/external-systems.api.js +16 -1
- package/lib/api/pipeline.api.js +12 -4
- package/lib/api/service-users.api.js +41 -0
- package/lib/api/types/service-users.types.js +24 -0
- package/lib/api/wizard.api.js +19 -0
- package/lib/app/deploy-status-display.js +78 -0
- package/lib/app/deploy.js +66 -21
- package/lib/app/rotate-secret.js +3 -1
- package/lib/app/run-helpers.js +7 -2
- package/lib/app/show-display.js +30 -11
- package/lib/app/show.js +34 -8
- package/lib/cli/index.js +2 -0
- package/lib/cli/setup-app.js +8 -0
- package/lib/cli/setup-infra.js +3 -3
- package/lib/cli/setup-service-user.js +61 -0
- package/lib/commands/app.js +2 -1
- package/lib/commands/service-user.js +199 -0
- package/lib/commands/up-common.js +74 -5
- package/lib/commands/up-dataplane.js +13 -7
- package/lib/commands/up-miso.js +17 -24
- package/lib/core/templates.js +0 -1
- package/lib/external-system/deploy.js +79 -15
- package/lib/generator/builders.js +0 -24
- package/lib/schema/application-schema.json +0 -12
- package/lib/schema/external-system.schema.json +0 -16
- package/lib/utils/app-register-config.js +10 -12
- package/lib/utils/deployment-errors.js +10 -0
- package/lib/utils/environment-checker.js +25 -6
- package/lib/utils/variable-transformer.js +6 -14
- package/package.json +1 -1
- package/templates/applications/dataplane/README.md +23 -7
- package/templates/applications/dataplane/env.template +31 -2
- package/templates/applications/dataplane/rbac.yaml +1 -1
- package/templates/applications/dataplane/variables.yaml +2 -1
- package/templates/applications/keycloak/env.template +6 -3
- package/templates/applications/keycloak/variables.yaml +1 -0
- package/templates/applications/miso-controller/env.template +22 -15
- package/templates/applications/miso-controller/rbac.yaml +15 -0
- package/templates/applications/miso-controller/variables.yaml +24 -23
package/lib/app/show.js
CHANGED
|
@@ -418,6 +418,7 @@ function buildApplicationFromAppCfg(app, cfg, portalInputConfigurations) {
|
|
|
418
418
|
build: formatBuildForDisplay(cfg.build ?? app.build),
|
|
419
419
|
status: pickAppCfg('status', app, cfg, '—'),
|
|
420
420
|
url: pickAppCfg('url', app, cfg, '—'),
|
|
421
|
+
internalUrl: pickAppCfg('internalUrl', app, cfg, '—'),
|
|
421
422
|
roles: cfg.roles ?? app.roles,
|
|
422
423
|
permissions: cfg.permissions ?? app.permissions,
|
|
423
424
|
authentication: cfg.authentication ?? app.authentication,
|
|
@@ -485,9 +486,10 @@ function buildOnlineSummary(apiApp, controllerUrl, externalSystem) {
|
|
|
485
486
|
* Run show in offline mode: generate manifest (same as aifabrix json) and use it; else fall back to variables.yaml.
|
|
486
487
|
* @param {string} appKey - Application key
|
|
487
488
|
* @param {boolean} json - Output as JSON
|
|
489
|
+
* @param {boolean} [permissionsOnly] - When true, output only permissions
|
|
488
490
|
* @throws {Error} If variables.yaml not found or invalid YAML
|
|
489
491
|
*/
|
|
490
|
-
async function runOffline(appKey, json) {
|
|
492
|
+
async function runOffline(appKey, json, permissionsOnly) {
|
|
491
493
|
let summary;
|
|
492
494
|
|
|
493
495
|
try {
|
|
@@ -503,6 +505,16 @@ async function runOffline(appKey, json) {
|
|
|
503
505
|
}
|
|
504
506
|
|
|
505
507
|
if (json) {
|
|
508
|
+
if (permissionsOnly) {
|
|
509
|
+
const out = {
|
|
510
|
+
source: summary.source,
|
|
511
|
+
path: summary.path,
|
|
512
|
+
appKey: summary.appKey,
|
|
513
|
+
permissions: summary.permissions || []
|
|
514
|
+
};
|
|
515
|
+
logger.log(JSON.stringify(out, null, 2));
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
506
518
|
const out = {
|
|
507
519
|
source: summary.source,
|
|
508
520
|
path: summary.path,
|
|
@@ -519,7 +531,7 @@ async function runOffline(appKey, json) {
|
|
|
519
531
|
logger.log(JSON.stringify(out, null, 2));
|
|
520
532
|
return;
|
|
521
533
|
}
|
|
522
|
-
displayShow(summary);
|
|
534
|
+
displayShow(summary, { permissionsOnly: !!permissionsOnly });
|
|
523
535
|
}
|
|
524
536
|
|
|
525
537
|
async function resolveOnlineAuth(controllerUrl) {
|
|
@@ -550,7 +562,17 @@ async function fetchExternalSystemForOnline(controllerUrl, appKey, authConfig) {
|
|
|
550
562
|
}
|
|
551
563
|
}
|
|
552
564
|
|
|
553
|
-
function outputOnlineJson(summary) {
|
|
565
|
+
function outputOnlineJson(summary, permissionsOnly) {
|
|
566
|
+
if (permissionsOnly) {
|
|
567
|
+
const out = {
|
|
568
|
+
source: summary.source,
|
|
569
|
+
controllerUrl: summary.controllerUrl,
|
|
570
|
+
appKey: summary.appKey,
|
|
571
|
+
permissions: summary.permissions || []
|
|
572
|
+
};
|
|
573
|
+
logger.log(JSON.stringify(out, null, 2));
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
554
576
|
const out = {
|
|
555
577
|
source: summary.source,
|
|
556
578
|
controllerUrl: summary.controllerUrl,
|
|
@@ -562,6 +584,7 @@ function outputOnlineJson(summary) {
|
|
|
562
584
|
type: summary.application.type,
|
|
563
585
|
status: summary.application.status,
|
|
564
586
|
url: summary.application.url,
|
|
587
|
+
internalUrl: summary.application.internalUrl,
|
|
565
588
|
port: summary.application.port,
|
|
566
589
|
configuration: summary.application.configuration,
|
|
567
590
|
roles: summary.application.roles,
|
|
@@ -583,9 +606,10 @@ function outputOnlineJson(summary) {
|
|
|
583
606
|
* Run show in online mode: getApplication, optionally dataplane for external, display or JSON.
|
|
584
607
|
* @param {string} appKey - Application key
|
|
585
608
|
* @param {boolean} json - Output as JSON
|
|
609
|
+
* @param {boolean} [permissionsOnly] - When true, output only permissions
|
|
586
610
|
* @throws {Error} On auth failure, 404, or API error
|
|
587
611
|
*/
|
|
588
|
-
async function runOnline(appKey, json) {
|
|
612
|
+
async function runOnline(appKey, json, permissionsOnly) {
|
|
589
613
|
const controllerUrl = await resolveControllerUrl();
|
|
590
614
|
if (!controllerUrl) {
|
|
591
615
|
throw new Error('Controller URL is required for --online. Run aifabrix login to set the controller URL in config.yaml.');
|
|
@@ -599,10 +623,10 @@ async function runOnline(appKey, json) {
|
|
|
599
623
|
: null;
|
|
600
624
|
const summary = buildOnlineSummary(apiApp, authResult.actualControllerUrl, externalSystem);
|
|
601
625
|
if (json) {
|
|
602
|
-
outputOnlineJson(summary);
|
|
626
|
+
outputOnlineJson(summary, permissionsOnly);
|
|
603
627
|
return;
|
|
604
628
|
}
|
|
605
|
-
displayShow(summary);
|
|
629
|
+
displayShow(summary, { permissionsOnly: !!permissionsOnly });
|
|
606
630
|
}
|
|
607
631
|
|
|
608
632
|
/**
|
|
@@ -612,6 +636,7 @@ async function runOnline(appKey, json) {
|
|
|
612
636
|
* @param {Object} options - Options
|
|
613
637
|
* @param {boolean} [options.online] - Fetch from controller
|
|
614
638
|
* @param {boolean} [options.json] - Output as JSON
|
|
639
|
+
* @param {boolean} [options.permissions] - When true, output only permissions (app show --permissions)
|
|
615
640
|
* @throws {Error} If file missing/invalid (offline) or API/auth error (online)
|
|
616
641
|
*/
|
|
617
642
|
async function showApp(appKey, options = {}) {
|
|
@@ -621,11 +646,12 @@ async function showApp(appKey, options = {}) {
|
|
|
621
646
|
|
|
622
647
|
const online = Boolean(options.online);
|
|
623
648
|
const json = Boolean(options.json);
|
|
649
|
+
const permissions = Boolean(options.permissions);
|
|
624
650
|
|
|
625
651
|
if (online) {
|
|
626
|
-
await runOnline(appKey, json);
|
|
652
|
+
await runOnline(appKey, json, permissions);
|
|
627
653
|
} else {
|
|
628
|
-
await runOffline(appKey, json);
|
|
654
|
+
await runOffline(appKey, json, permissions);
|
|
629
655
|
}
|
|
630
656
|
}
|
|
631
657
|
|
package/lib/cli/index.js
CHANGED
|
@@ -21,6 +21,7 @@ const { setupExternalSystemCommands } = require('./setup-external-system');
|
|
|
21
21
|
const { setupAppCommands: setupAppManagementCommands } = require('../commands/app');
|
|
22
22
|
const { setupDatasourceCommands } = require('../commands/datasource');
|
|
23
23
|
const { setupCredentialDeploymentCommands } = require('./setup-credential-deployment');
|
|
24
|
+
const { setupServiceUserCommands } = require('./setup-service-user');
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* Sets up all CLI commands on the Commander program instance
|
|
@@ -35,6 +36,7 @@ function setupCommands(program) {
|
|
|
35
36
|
setupDatasourceCommands(program);
|
|
36
37
|
setupUtilityCommands(program);
|
|
37
38
|
setupCredentialDeploymentCommands(program);
|
|
39
|
+
setupServiceUserCommands(program);
|
|
38
40
|
setupExternalSystemCommands(program);
|
|
39
41
|
setupDevCommands(program);
|
|
40
42
|
setupSecretsCommands(program);
|
package/lib/cli/setup-app.js
CHANGED
|
@@ -229,6 +229,7 @@ See integration/hubspot/wizard-hubspot-e2e.yaml for an example.`)
|
|
|
229
229
|
|
|
230
230
|
program.command('deploy <app>')
|
|
231
231
|
.description('Deploy to Azure via Miso Controller')
|
|
232
|
+
.option('--deployment <target>', 'Deployment target: \'local\' (send manifest to controller, then run app locally) or \'cloud\' (deploy via Miso Controller only)', 'cloud')
|
|
232
233
|
.option('--type <type>', 'Application type: external to deploy from integration/<app> (no app register needed)')
|
|
233
234
|
.option('--client-id <id>', 'Client ID (overrides config)')
|
|
234
235
|
.option('--client-secret <secret>', 'Client Secret (overrides config)')
|
|
@@ -236,7 +237,14 @@ See integration/hubspot/wizard-hubspot-e2e.yaml for an example.`)
|
|
|
236
237
|
.option('--no-poll', 'Do not poll for status')
|
|
237
238
|
.action(async(appName, options) => {
|
|
238
239
|
try {
|
|
240
|
+
const target = (options.deployment || 'cloud').toLowerCase();
|
|
241
|
+
if (target !== 'local' && target !== 'cloud') {
|
|
242
|
+
throw new Error('Deployment target must be \'local\' or \'cloud\'');
|
|
243
|
+
}
|
|
239
244
|
await app.deployApp(appName, options);
|
|
245
|
+
if (target === 'local') {
|
|
246
|
+
await app.runApp(appName, options);
|
|
247
|
+
}
|
|
240
248
|
} catch (error) {
|
|
241
249
|
handleCommandError(error, 'deploy');
|
|
242
250
|
process.exit(1);
|
package/lib/cli/setup-infra.js
CHANGED
|
@@ -83,10 +83,10 @@ function setupInfraCommands(program) {
|
|
|
83
83
|
});
|
|
84
84
|
|
|
85
85
|
program.command('up-miso')
|
|
86
|
-
.description('Install keycloak
|
|
86
|
+
.description('Install keycloak and miso-controller from images (no build). Infra must be up. For dataplane use up-dataplane. Uses auto-generated secrets for testing.')
|
|
87
87
|
.option('-r, --registry <url>', 'Override registry for all apps (e.g. myacr.azurecr.io)')
|
|
88
88
|
.option('--registry-mode <mode>', 'Override registry mode (acr|external)')
|
|
89
|
-
.option('-i, --image <key>=<value>', 'Override image (e.g. keycloak=myreg/k:v1, miso-controller=myreg/m:v1
|
|
89
|
+
.option('-i, --image <key>=<value>', 'Override image (e.g. keycloak=myreg/k:v1, miso-controller=myreg/m:v1); can be repeated', (v, prev) => (prev || []).concat([v]))
|
|
90
90
|
.action(async(options) => {
|
|
91
91
|
try {
|
|
92
92
|
await handleUpMiso(options);
|
|
@@ -97,7 +97,7 @@ function setupInfraCommands(program) {
|
|
|
97
97
|
});
|
|
98
98
|
|
|
99
99
|
program.command('up-dataplane')
|
|
100
|
-
.description('Register
|
|
100
|
+
.description('Register, deploy, then run dataplane app locally in dev (always local deployment; requires login, environment must be dev)')
|
|
101
101
|
.option('-r, --registry <url>', 'Override registry for dataplane image')
|
|
102
102
|
.option('--registry-mode <mode>', 'Override registry mode (acr|external)')
|
|
103
103
|
.option('-i, --image <ref>', 'Override dataplane image reference (e.g. myreg/dataplane:latest)')
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI service-user command setup.
|
|
3
|
+
* Command: service-user create (create service user, get one-time secret).
|
|
4
|
+
*
|
|
5
|
+
* @fileoverview Service user CLI definitions
|
|
6
|
+
* @author AI Fabrix Team
|
|
7
|
+
* @version 2.0.0
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const chalk = require('chalk');
|
|
11
|
+
const logger = require('../utils/logger');
|
|
12
|
+
const { handleCommandError } = require('../utils/cli-utils');
|
|
13
|
+
const { runServiceUserCreate } = require('../commands/service-user');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Sets up service-user commands
|
|
17
|
+
* @param {Command} program - Commander program instance
|
|
18
|
+
*/
|
|
19
|
+
function setupServiceUserCommands(program) {
|
|
20
|
+
const serviceUser = program
|
|
21
|
+
.command('service-user')
|
|
22
|
+
.description('Create and manage service users (API clients) for integrations and CI')
|
|
23
|
+
.addHelpText('after', `
|
|
24
|
+
Service users are dedicated accounts for integrations, CI pipelines, or API clients.
|
|
25
|
+
The controller returns a one-time clientSecret on create—save it immediately; it cannot be retrieved again.
|
|
26
|
+
|
|
27
|
+
Example:
|
|
28
|
+
$ aifabrix service-user create -u api-client-001 -e api@example.com \\
|
|
29
|
+
--redirect-uris "https://app.example.com/callback" --group-names "AI-Fabrix-Developers"
|
|
30
|
+
|
|
31
|
+
Required: permission service-user:create on the controller. Run "aifabrix login" first.`);
|
|
32
|
+
|
|
33
|
+
serviceUser
|
|
34
|
+
.command('create')
|
|
35
|
+
.description('Create a service user and receive a one-time clientSecret (save it now; it will not be shown again)')
|
|
36
|
+
.option('--controller <url>', 'Controller base URL (default: from config)')
|
|
37
|
+
.option('-u, --username <username>', 'Service user username (required)')
|
|
38
|
+
.option('-e, --email <email>', 'Email address (required)')
|
|
39
|
+
.option('--redirect-uris <uris>', 'Comma-separated OAuth2 redirect URIs (required)')
|
|
40
|
+
.option('--group-names <names>', 'Comma-separated group names to assign (required)')
|
|
41
|
+
.option('-d, --description <description>', 'Description for the service user')
|
|
42
|
+
.action(async(options) => {
|
|
43
|
+
try {
|
|
44
|
+
const opts = {
|
|
45
|
+
controller: options.controller,
|
|
46
|
+
username: options.username,
|
|
47
|
+
email: options.email,
|
|
48
|
+
redirectUris: options.redirectUris,
|
|
49
|
+
groupNames: options.groupNames,
|
|
50
|
+
description: options.description
|
|
51
|
+
};
|
|
52
|
+
await runServiceUserCreate(opts);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
logger.error(chalk.red(`Error: ${error.message}`));
|
|
55
|
+
handleCommandError(error, 'service-user create');
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = { setupServiceUserCommands };
|
package/lib/commands/app.js
CHANGED
|
@@ -74,9 +74,10 @@ function setupAppCommands(program) {
|
|
|
74
74
|
.command('show <appKey>')
|
|
75
75
|
.description('Show application from controller (online). Same as aifabrix show <appKey> --online')
|
|
76
76
|
.option('--json', 'Output as JSON')
|
|
77
|
+
.option('--permissions', 'Show only list of permissions')
|
|
77
78
|
.action(async(appKey, options) => {
|
|
78
79
|
try {
|
|
79
|
-
await showApp(appKey, { online: true, json: !!options.json });
|
|
80
|
+
await showApp(appKey, { online: true, json: !!options.json, permissions: !!options.permissions });
|
|
80
81
|
} catch (error) {
|
|
81
82
|
logger.error(chalk.red(`Error: ${error.message}`));
|
|
82
83
|
process.exit(1);
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service user create command – create service user and get one-time secret
|
|
3
|
+
* POST /api/v1/service-users. Used by `aifabrix service-user create`.
|
|
4
|
+
*
|
|
5
|
+
* @fileoverview Service user create command implementation
|
|
6
|
+
* @author AI Fabrix Team
|
|
7
|
+
* @version 2.0.0
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const chalk = require('chalk');
|
|
11
|
+
const logger = require('../utils/logger');
|
|
12
|
+
const { resolveControllerUrl } = require('../utils/controller-url');
|
|
13
|
+
const { getOrRefreshDeviceToken } = require('../utils/token-manager');
|
|
14
|
+
const { normalizeControllerUrl } = require('../core/config');
|
|
15
|
+
const { createServiceUser } = require('../api/service-users.api');
|
|
16
|
+
|
|
17
|
+
const ONE_TIME_WARNING =
|
|
18
|
+
'Save this secret now; it will not be shown again.';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Get auth token for service-user (device token from config)
|
|
22
|
+
* @async
|
|
23
|
+
* @param {string} controllerUrl - Controller base URL
|
|
24
|
+
* @returns {Promise<{token: string, controllerUrl: string}|null>}
|
|
25
|
+
*/
|
|
26
|
+
async function getServiceUserAuth(controllerUrl) {
|
|
27
|
+
const normalizedUrl = normalizeControllerUrl(controllerUrl);
|
|
28
|
+
const deviceToken = await getOrRefreshDeviceToken(normalizedUrl);
|
|
29
|
+
if (deviceToken && deviceToken.token) {
|
|
30
|
+
return {
|
|
31
|
+
token: deviceToken.token,
|
|
32
|
+
controllerUrl: deviceToken.controller || normalizedUrl
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Extract clientId and clientSecret from API response.
|
|
40
|
+
* Controller returns { data: { user, clientSecret } }; API client puts body in response.data.
|
|
41
|
+
* So payload is at response.data.data. clientId may be on user.clientId or user.federatedIdentity.keycloakClientId.
|
|
42
|
+
* @param {Object} response - API response (success: true, data: body)
|
|
43
|
+
* @returns {{ clientId: string, clientSecret: string }}
|
|
44
|
+
*/
|
|
45
|
+
function extractCreateResponse(response) {
|
|
46
|
+
const payload = response?.data?.data ?? response?.data ?? response;
|
|
47
|
+
const user = payload?.user;
|
|
48
|
+
const clientId =
|
|
49
|
+
user?.clientId ??
|
|
50
|
+
user?.federatedIdentity?.keycloakClientId ??
|
|
51
|
+
payload?.clientId ??
|
|
52
|
+
'';
|
|
53
|
+
const clientSecret = payload?.clientSecret ?? '';
|
|
54
|
+
return { clientId, clientSecret };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Log error for failed create response and exit
|
|
59
|
+
* @param {Object} response - API response with success: false
|
|
60
|
+
*/
|
|
61
|
+
function handleCreateError(response) {
|
|
62
|
+
const status = response.status;
|
|
63
|
+
const msg = response.formattedError || response.error || 'Request failed';
|
|
64
|
+
if (status === 400) {
|
|
65
|
+
logger.error(chalk.red(`❌ Validation error: ${msg}`));
|
|
66
|
+
} else if (status === 401) {
|
|
67
|
+
logger.error(chalk.red('❌ Unauthorized. Run "aifabrix login" and try again.'));
|
|
68
|
+
} else if (status === 403) {
|
|
69
|
+
logger.error(chalk.red('❌ Missing permission: service-user:create'));
|
|
70
|
+
logger.error(chalk.gray('Your account needs the service-user:create permission on the controller.'));
|
|
71
|
+
} else {
|
|
72
|
+
logger.error(chalk.red(`❌ Failed to create service user: ${msg}`));
|
|
73
|
+
}
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Display success output with clientId, clientSecret and one-time warning
|
|
79
|
+
* @param {string} clientId - Service user client ID
|
|
80
|
+
* @param {string} clientSecret - One-time client secret
|
|
81
|
+
*/
|
|
82
|
+
function displayCreateSuccess(clientId, clientSecret) {
|
|
83
|
+
logger.log(chalk.bold('\n✓ Service user created\n'));
|
|
84
|
+
logger.log(chalk.cyan(' clientId: ') + clientId);
|
|
85
|
+
logger.log(chalk.cyan(' clientSecret: ') + clientSecret);
|
|
86
|
+
logger.log('');
|
|
87
|
+
logger.log(chalk.yellow('⚠ ' + ONE_TIME_WARNING));
|
|
88
|
+
logger.log('');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Parse comma-separated string into non-empty trimmed array
|
|
93
|
+
* @param {string} [val] - Comma-separated value
|
|
94
|
+
* @returns {string[]}
|
|
95
|
+
*/
|
|
96
|
+
function parseList(val) {
|
|
97
|
+
if (val === undefined || val === null || String(val).trim() === '') {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
return String(val)
|
|
101
|
+
.split(',')
|
|
102
|
+
.map(s => s.trim())
|
|
103
|
+
.filter(Boolean);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Validate username, email, redirectUris, groupIds; exit on failure
|
|
108
|
+
* @param {Object} options - CLI options
|
|
109
|
+
* @returns {{ username: string, email: string, redirectUris: string[], groupNames: string[], description?: string }}
|
|
110
|
+
*/
|
|
111
|
+
function validateServiceUserOptions(options) {
|
|
112
|
+
const username = options.username?.trim();
|
|
113
|
+
const email = options.email?.trim();
|
|
114
|
+
const redirectUris = parseList(options.redirectUris);
|
|
115
|
+
const groupNames = parseList(options.groupNames);
|
|
116
|
+
if (!username) {
|
|
117
|
+
logger.error(chalk.red('❌ Username is required. Use --username <username>.'));
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
if (!email) {
|
|
121
|
+
logger.error(chalk.red('❌ Email is required. Use --email <email>.'));
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
if (redirectUris.length === 0) {
|
|
125
|
+
logger.error(chalk.red('❌ At least one redirect URI is required. Use --redirect-uris <uri1,uri2,...>.'));
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
if (groupNames.length === 0) {
|
|
129
|
+
logger.error(chalk.red('❌ At least one group name is required. Use --group-names <name1,name2,...>.'));
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
username,
|
|
134
|
+
email,
|
|
135
|
+
redirectUris,
|
|
136
|
+
groupNames,
|
|
137
|
+
description: options.description?.trim() || undefined
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Resolve controller URL and auth; exit on failure
|
|
143
|
+
* @async
|
|
144
|
+
* @param {Object} options - CLI options
|
|
145
|
+
* @returns {Promise<{ username: string, email: string, redirectUris: string[], groupNames: string[], description?: string, controllerUrl: string, authConfig: Object }>}
|
|
146
|
+
*/
|
|
147
|
+
async function resolveOptionsAndAuth(options) {
|
|
148
|
+
const validated = validateServiceUserOptions(options);
|
|
149
|
+
const controllerUrl = options.controller || (await resolveControllerUrl());
|
|
150
|
+
if (!controllerUrl) {
|
|
151
|
+
logger.error(chalk.red('❌ Controller URL is required. Run "aifabrix login" first.'));
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
const authResult = await getServiceUserAuth(controllerUrl);
|
|
155
|
+
if (!authResult || !authResult.token) {
|
|
156
|
+
logger.error(chalk.red(`❌ No authentication token for controller: ${controllerUrl}`));
|
|
157
|
+
logger.error(chalk.gray('Run: aifabrix login'));
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
...validated,
|
|
162
|
+
controllerUrl: authResult.controllerUrl,
|
|
163
|
+
authConfig: { type: 'bearer', token: authResult.token }
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Run service-user create: call POST /api/v1/service-users and display one-time secret with warning
|
|
169
|
+
* @async
|
|
170
|
+
* @param {Object} options - CLI options
|
|
171
|
+
* @param {string} [options.controller] - Controller URL override
|
|
172
|
+
* @param {string} options.username - Username (required)
|
|
173
|
+
* @param {string} options.email - Email (required)
|
|
174
|
+
* @param {string} options.redirectUris - Comma-separated redirect URIs (required, min 1)
|
|
175
|
+
* @param {string} options.groupNames - Comma-separated group names (required, e.g. AI-Fabrix-Developers)
|
|
176
|
+
* @param {string} [options.description] - Optional description
|
|
177
|
+
* @returns {Promise<void>}
|
|
178
|
+
*/
|
|
179
|
+
async function runServiceUserCreate(options = {}) {
|
|
180
|
+
const ctx = await resolveOptionsAndAuth(options);
|
|
181
|
+
const body = {
|
|
182
|
+
username: ctx.username,
|
|
183
|
+
email: ctx.email,
|
|
184
|
+
redirectUris: ctx.redirectUris,
|
|
185
|
+
groupNames: ctx.groupNames,
|
|
186
|
+
description: ctx.description
|
|
187
|
+
};
|
|
188
|
+
const response = await createServiceUser(ctx.controllerUrl, ctx.authConfig, body);
|
|
189
|
+
if (!response.success) {
|
|
190
|
+
handleCreateError(response);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const { clientId, clientSecret } = extractCreateResponse(response);
|
|
194
|
+
displayCreateSuccess(clientId, clientSecret);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
module.exports = {
|
|
198
|
+
runServiceUserCreate
|
|
199
|
+
};
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
const path = require('path');
|
|
12
12
|
const fs = require('fs');
|
|
13
|
+
const yaml = require('js-yaml');
|
|
13
14
|
const chalk = require('chalk');
|
|
14
15
|
const logger = require('../utils/logger');
|
|
15
16
|
const pathsUtil = require('../utils/paths');
|
|
@@ -30,8 +31,74 @@ async function ensureTemplateAtPath(appName, targetAppPath) {
|
|
|
30
31
|
return true;
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Resolve the directory (folder) that would contain the .env file for envOutputPath.
|
|
36
|
+
* @param {string} envOutputPath - Value from build.envOutputPath (e.g. ../../.env)
|
|
37
|
+
* @param {string} variablesPath - Path to variables.yaml
|
|
38
|
+
* @returns {string} Absolute path to the folder that would contain the output .env file
|
|
39
|
+
*/
|
|
40
|
+
function getEnvOutputPathFolder(envOutputPath, variablesPath) {
|
|
41
|
+
const variablesDir = path.dirname(variablesPath);
|
|
42
|
+
const resolvedFile = path.resolve(variablesDir, envOutputPath);
|
|
43
|
+
return path.dirname(resolvedFile);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Validates envOutputPath: if the target folder does not exist, patches variables.yaml to set envOutputPath to null.
|
|
48
|
+
* Used by up-platform, up-miso, up-dataplane so we do not keep a path that points outside an existing tree.
|
|
49
|
+
*
|
|
50
|
+
* @param {string} appName - Application name (e.g. keycloak, miso-controller, dataplane)
|
|
51
|
+
*/
|
|
52
|
+
function validateEnvOutputPathFolderOrNull(appName) {
|
|
53
|
+
if (!appName || typeof appName !== 'string') return;
|
|
54
|
+
const pathsToPatch = [pathsUtil.getBuilderPath(appName)];
|
|
55
|
+
const cwdBuilderPath = path.join(process.cwd(), 'builder', appName);
|
|
56
|
+
if (path.resolve(cwdBuilderPath) !== path.resolve(pathsToPatch[0])) {
|
|
57
|
+
pathsToPatch.push(cwdBuilderPath);
|
|
58
|
+
}
|
|
59
|
+
const envOutputPathLine = /^(\s*envOutputPath:)\s*.*$/m;
|
|
60
|
+
const replacement = '$1 null # deploy only, no copy';
|
|
61
|
+
for (const appPath of pathsToPatch) {
|
|
62
|
+
const variablesPath = path.join(appPath, 'variables.yaml');
|
|
63
|
+
if (!fs.existsSync(variablesPath)) continue;
|
|
64
|
+
try {
|
|
65
|
+
const content = fs.readFileSync(variablesPath, 'utf8');
|
|
66
|
+
const variables = yaml.load(content);
|
|
67
|
+
const value = variables?.build?.envOutputPath;
|
|
68
|
+
if (value === null || value === undefined || value === '') continue;
|
|
69
|
+
const folder = getEnvOutputPathFolder(String(value).trim(), variablesPath);
|
|
70
|
+
if (fs.existsSync(folder)) continue;
|
|
71
|
+
const newContent = content.replace(envOutputPathLine, replacement);
|
|
72
|
+
fs.writeFileSync(variablesPath, newContent, 'utf8');
|
|
73
|
+
} catch (err) {
|
|
74
|
+
logger.warn(chalk.yellow(`Could not validate envOutputPath in ${variablesPath}: ${err.message}`));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Patches a single variables.yaml to set build.envOutputPath to null for deploy-only.
|
|
81
|
+
*
|
|
82
|
+
* @param {string} variablesPath - Path to variables.yaml
|
|
83
|
+
* @param {RegExp} envOutputPathLine - Regex for envOutputPath line
|
|
84
|
+
* @param {string} replacement - Replacement string
|
|
85
|
+
*/
|
|
86
|
+
function patchOneVariablesFileForDeployOnly(variablesPath, envOutputPathLine, replacement) {
|
|
87
|
+
const content = fs.readFileSync(variablesPath, 'utf8');
|
|
88
|
+
if (!envOutputPathLine.test(content)) return;
|
|
89
|
+
const variables = yaml.load(content);
|
|
90
|
+
const value = variables?.build?.envOutputPath;
|
|
91
|
+
if (value !== null && value !== undefined && value !== '') {
|
|
92
|
+
const folder = getEnvOutputPathFolder(String(value).trim(), variablesPath);
|
|
93
|
+
if (fs.existsSync(folder)) return;
|
|
94
|
+
}
|
|
95
|
+
const newContent = content.replace(envOutputPathLine, replacement);
|
|
96
|
+
fs.writeFileSync(variablesPath, newContent, 'utf8');
|
|
97
|
+
}
|
|
98
|
+
|
|
33
99
|
/**
|
|
34
100
|
* Patches variables.yaml to set build.envOutputPath to null for deploy-only (no local code).
|
|
101
|
+
* Only patches when the target folder does NOT exist; when folder exists, keeps the value.
|
|
35
102
|
* Use when running up-miso/up-platform so we do not copy .env to repo paths or show that message.
|
|
36
103
|
* Patches both primary builder path and cwd/builder if different.
|
|
37
104
|
*
|
|
@@ -50,10 +117,7 @@ function patchEnvOutputPathForDeployOnly(appName) {
|
|
|
50
117
|
const variablesPath = path.join(appPath, 'variables.yaml');
|
|
51
118
|
if (!fs.existsSync(variablesPath)) continue;
|
|
52
119
|
try {
|
|
53
|
-
|
|
54
|
-
if (!envOutputPathLine.test(content)) continue;
|
|
55
|
-
content = content.replace(envOutputPathLine, replacement);
|
|
56
|
-
fs.writeFileSync(variablesPath, content, 'utf8');
|
|
120
|
+
patchOneVariablesFileForDeployOnly(variablesPath, envOutputPathLine, replacement);
|
|
57
121
|
} catch (err) {
|
|
58
122
|
logger.warn(chalk.yellow(`Could not patch envOutputPath in ${variablesPath}: ${err.message}`));
|
|
59
123
|
}
|
|
@@ -99,4 +163,9 @@ async function ensureAppFromTemplate(appName) {
|
|
|
99
163
|
return primaryCopied;
|
|
100
164
|
}
|
|
101
165
|
|
|
102
|
-
module.exports = {
|
|
166
|
+
module.exports = {
|
|
167
|
+
ensureAppFromTemplate,
|
|
168
|
+
patchEnvOutputPathForDeployOnly,
|
|
169
|
+
validateEnvOutputPathFolderOrNull,
|
|
170
|
+
getEnvOutputPathFolder
|
|
171
|
+
};
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* AI Fabrix Builder - Up Dataplane Command
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Always local deployment: registers or rotates dataplane app in dev, sends
|
|
5
|
+
* deployment manifest to Miso Controller, then runs the dataplane app locally
|
|
6
|
+
* (same as aifabrix deploy dataplane --deployment=local). If app is already
|
|
7
|
+
* registered, uses rotate-secret; otherwise registers.
|
|
7
8
|
*
|
|
8
9
|
* @fileoverview up-dataplane command implementation
|
|
9
10
|
* @author AI Fabrix Team
|
|
@@ -23,7 +24,7 @@ const { registerApplication } = require('../app/register');
|
|
|
23
24
|
const { rotateSecret } = require('../app/rotate-secret');
|
|
24
25
|
const { checkApplicationExists } = require('../utils/app-existence');
|
|
25
26
|
const app = require('../app');
|
|
26
|
-
const { ensureAppFromTemplate } = require('./up-common');
|
|
27
|
+
const { ensureAppFromTemplate, validateEnvOutputPathFolderOrNull } = require('./up-common');
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* Register or rotate dataplane: if app exists in controller, rotate secret; otherwise register.
|
|
@@ -64,7 +65,8 @@ function buildDataplaneImageRef(registry) {
|
|
|
64
65
|
|
|
65
66
|
/**
|
|
66
67
|
* Handle up-dataplane command: ensure logged in, environment dev, ensure dataplane,
|
|
67
|
-
* register or rotate (if already registered),
|
|
68
|
+
* register or rotate (if already registered), deploy (send manifest to controller),
|
|
69
|
+
* then run dataplane app locally (always local deployment).
|
|
68
70
|
*
|
|
69
71
|
* @async
|
|
70
72
|
* @function handleUpDataplane
|
|
@@ -80,7 +82,7 @@ async function handleUpDataplane(options = {}) {
|
|
|
80
82
|
if (builderDir) {
|
|
81
83
|
process.env.AIFABRIX_BUILDER_DIR = builderDir;
|
|
82
84
|
}
|
|
83
|
-
logger.log(chalk.blue('Starting up-dataplane (register/rotate, deploy dataplane
|
|
85
|
+
logger.log(chalk.blue('Starting up-dataplane (register/rotate, deploy, then run dataplane locally)...\n'));
|
|
84
86
|
|
|
85
87
|
const [controllerUrl, environmentKey] = await Promise.all([resolveControllerUrl(), resolveEnvironment()]);
|
|
86
88
|
const authConfig = await checkAuthentication(controllerUrl, environmentKey);
|
|
@@ -95,6 +97,8 @@ async function handleUpDataplane(options = {}) {
|
|
|
95
97
|
logger.log(chalk.green('✓ Logged in and environment is dev'));
|
|
96
98
|
|
|
97
99
|
await ensureAppFromTemplate('dataplane');
|
|
100
|
+
// If envOutputPath target folder does not exist, set envOutputPath to null
|
|
101
|
+
validateEnvOutputPathFolderOrNull('dataplane');
|
|
98
102
|
|
|
99
103
|
await registerOrRotateDataplane(options, controllerUrl, environmentKey, authConfig);
|
|
100
104
|
|
|
@@ -102,8 +106,10 @@ async function handleUpDataplane(options = {}) {
|
|
|
102
106
|
const deployOpts = { imageOverride, image: imageOverride, registryMode: options.registryMode };
|
|
103
107
|
|
|
104
108
|
await app.deployApp('dataplane', deployOpts);
|
|
109
|
+
logger.log('');
|
|
110
|
+
await app.runApp('dataplane', {});
|
|
105
111
|
|
|
106
|
-
logger.log(chalk.green('\n✓ up-dataplane complete. Dataplane is registered
|
|
112
|
+
logger.log(chalk.green('\n✓ up-dataplane complete. Dataplane is registered, deployed in dev, and running locally.'));
|
|
107
113
|
}
|
|
108
114
|
|
|
109
115
|
module.exports = { handleUpDataplane, buildDataplaneImageRef };
|