@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/api/pipeline.api.js
CHANGED
|
@@ -9,6 +9,7 @@ const { ApiClient } = require('./index');
|
|
|
9
9
|
/**
|
|
10
10
|
* Validate deployment configuration
|
|
11
11
|
* POST /api/v1/pipeline/{envKey}/validate
|
|
12
|
+
* @requiresPermission {Controller} applications:deploy
|
|
12
13
|
* @async
|
|
13
14
|
* @function validatePipeline
|
|
14
15
|
* @param {string} controllerUrl - Controller base URL
|
|
@@ -31,6 +32,7 @@ async function validatePipeline(controllerUrl, envKey, authConfig, validationDat
|
|
|
31
32
|
/**
|
|
32
33
|
* Deploy application using validateToken
|
|
33
34
|
* POST /api/v1/pipeline/{envKey}/deploy
|
|
35
|
+
* @requiresPermission {Controller} applications:deploy
|
|
34
36
|
* @async
|
|
35
37
|
* @function deployPipeline
|
|
36
38
|
* @param {string} controllerUrl - Controller base URL
|
|
@@ -52,6 +54,7 @@ async function deployPipeline(controllerUrl, envKey, authConfig, deployData) {
|
|
|
52
54
|
/**
|
|
53
55
|
* Get deployment status for CI/CD
|
|
54
56
|
* GET /api/v1/pipeline/{envKey}/deployments/{deploymentId}
|
|
57
|
+
* @requiresPermission {Controller} applications:deploy
|
|
55
58
|
* @async
|
|
56
59
|
* @function getPipelineDeployment
|
|
57
60
|
* @param {string} controllerUrl - Controller base URL
|
|
@@ -69,6 +72,7 @@ async function getPipelineDeployment(controllerUrl, envKey, deploymentId, authCo
|
|
|
69
72
|
/**
|
|
70
73
|
* Pipeline health check
|
|
71
74
|
* GET /api/v1/pipeline/{envKey}/health
|
|
75
|
+
* @requiresPermission {Controller} Public (no auth required)
|
|
72
76
|
* @async
|
|
73
77
|
* @function getPipelineHealth
|
|
74
78
|
* @param {string} controllerUrl - Controller base URL
|
|
@@ -87,7 +91,7 @@ async function getPipelineHealth(controllerUrl, envKey) {
|
|
|
87
91
|
* Request body: external system JSON (external-system.schema.json). Optional field in body:
|
|
88
92
|
* generateMcpContract (boolean, default true). Optional: generateOpenApiContract (boolean).
|
|
89
93
|
* Do not use query parameters for MCP; use the field in the body only.
|
|
90
|
-
*
|
|
94
|
+
* @requiresPermission {Dataplane} Authenticated (oauth2: [])
|
|
91
95
|
* @async
|
|
92
96
|
* @function publishSystemViaPipeline
|
|
93
97
|
* @param {string} dataplaneUrl - Dataplane base URL
|
|
@@ -107,7 +111,7 @@ async function publishSystemViaPipeline(dataplaneUrl, authConfig, systemConfig)
|
|
|
107
111
|
* Publish datasource via dataplane pipeline endpoint
|
|
108
112
|
* POST /api/v1/pipeline/{systemKey}/publish
|
|
109
113
|
* No generateMcpContract for this endpoint; dataplane always uses default (MCP generated).
|
|
110
|
-
*
|
|
114
|
+
* @requiresPermission {Dataplane} Authenticated (oauth2: [])
|
|
111
115
|
* @async
|
|
112
116
|
* @function publishDatasourceViaPipeline
|
|
113
117
|
* @param {string} dataplaneUrl - Dataplane base URL
|
|
@@ -127,6 +131,7 @@ async function publishDatasourceViaPipeline(dataplaneUrl, systemKey, authConfig,
|
|
|
127
131
|
/**
|
|
128
132
|
* Test datasource via dataplane pipeline endpoint
|
|
129
133
|
* POST /api/v1/pipeline/{systemKey}/{datasourceKey}/test
|
|
134
|
+
* @requiresPermission {Dataplane} Authenticated (oauth2: [])
|
|
130
135
|
* @async
|
|
131
136
|
* @function testDatasourceViaPipeline
|
|
132
137
|
* @param {Object} params - Function parameters
|
|
@@ -156,6 +161,7 @@ async function testDatasourceViaPipeline({ dataplaneUrl, systemKey, datasourceKe
|
|
|
156
161
|
/**
|
|
157
162
|
* Deploy external system via dataplane pipeline endpoint
|
|
158
163
|
* POST /api/v1/pipeline/deploy
|
|
164
|
+
* @requiresPermission {Dataplane} Authenticated (oauth2: [])
|
|
159
165
|
* @async
|
|
160
166
|
* @function deployExternalSystemViaPipeline
|
|
161
167
|
* @param {string} dataplaneUrl - Dataplane base URL
|
|
@@ -174,6 +180,7 @@ async function deployExternalSystemViaPipeline(dataplaneUrl, authConfig, systemC
|
|
|
174
180
|
/**
|
|
175
181
|
* Deploy datasource via dataplane pipeline endpoint
|
|
176
182
|
* POST /api/v1/pipeline/{systemKey}/deploy
|
|
183
|
+
* @requiresPermission {Dataplane} Authenticated (oauth2: [])
|
|
177
184
|
* @async
|
|
178
185
|
* @function deployDatasourceViaPipeline
|
|
179
186
|
* @param {string} dataplaneUrl - Dataplane base URL
|
|
@@ -196,7 +203,7 @@ async function deployDatasourceViaPipeline(dataplaneUrl, systemKey, authConfig,
|
|
|
196
203
|
* Body: { version, application, dataSources }. Include application.generateMcpContract
|
|
197
204
|
* and/or application.generateOpenApiContract to control contract generation when
|
|
198
205
|
* publishing this upload (publish reads from stored config; no query param on publish).
|
|
199
|
-
*
|
|
206
|
+
* @requiresPermission {Dataplane} Authenticated (oauth2: [])
|
|
200
207
|
* @async
|
|
201
208
|
* @function uploadApplicationViaPipeline
|
|
202
209
|
* @param {string} dataplaneUrl - Dataplane base URL
|
|
@@ -215,6 +222,7 @@ async function uploadApplicationViaPipeline(dataplaneUrl, authConfig, applicatio
|
|
|
215
222
|
/**
|
|
216
223
|
* Validate upload via dataplane pipeline endpoint
|
|
217
224
|
* POST /api/v1/pipeline/upload/{uploadId}/validate
|
|
225
|
+
* @requiresPermission {Dataplane} Authenticated (oauth2: [])
|
|
218
226
|
* @async
|
|
219
227
|
* @function validateUploadViaPipeline
|
|
220
228
|
* @param {string} dataplaneUrl - Dataplane base URL
|
|
@@ -235,7 +243,7 @@ async function validateUploadViaPipeline(dataplaneUrl, uploadId, authConfig) {
|
|
|
235
243
|
* that was uploaded (application.generateMcpContract, application.generateOpenApiContract).
|
|
236
244
|
* To control MCP/OpenAPI, include those fields in the application object when calling
|
|
237
245
|
* uploadApplicationViaPipeline.
|
|
238
|
-
*
|
|
246
|
+
* @requiresPermission {Dataplane} Authenticated (oauth2: [])
|
|
239
247
|
* @async
|
|
240
248
|
* @function publishUploadViaPipeline
|
|
241
249
|
* @param {string} dataplaneUrl - Dataplane base URL
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Service users API functions (create service user with one-time secret)
|
|
3
|
+
* @author AI Fabrix Team
|
|
4
|
+
* @version 2.0.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { ApiClient } = require('./index');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Create a service user (username, email, redirectUris, groupIds); return one-time secret
|
|
11
|
+
* POST /api/v1/service-users
|
|
12
|
+
* @requiresPermission {Controller} service-user:create
|
|
13
|
+
* @async
|
|
14
|
+
* @function createServiceUser
|
|
15
|
+
* @param {string} controllerUrl - Controller base URL
|
|
16
|
+
* @param {Object} authConfig - Authentication configuration (bearer or client-credentials)
|
|
17
|
+
* @param {Object} body - Request body
|
|
18
|
+
* @param {string} body.username - Username (required)
|
|
19
|
+
* @param {string} body.email - Email (required)
|
|
20
|
+
* @param {string[]} body.redirectUris - Redirect URIs for OAuth2 (required, min 1)
|
|
21
|
+
* @param {string[]} body.groupNames - Group names (required, e.g. AI-Fabrix-Developers)
|
|
22
|
+
* @param {string} [body.description] - Optional description
|
|
23
|
+
* @returns {Promise<Object>} Response with clientId and one-time clientSecret
|
|
24
|
+
* @throws {Error} If request fails (400/401/403 or network)
|
|
25
|
+
*/
|
|
26
|
+
async function createServiceUser(controllerUrl, authConfig, body) {
|
|
27
|
+
const client = new ApiClient(controllerUrl, authConfig);
|
|
28
|
+
return await client.post('/api/v1/service-users', {
|
|
29
|
+
body: {
|
|
30
|
+
username: body.username,
|
|
31
|
+
email: body.email,
|
|
32
|
+
redirectUris: body.redirectUris,
|
|
33
|
+
groupNames: body.groupNames,
|
|
34
|
+
description: body.description
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = {
|
|
40
|
+
createServiceUser
|
|
41
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Service users API type definitions
|
|
3
|
+
* @author AI Fabrix Team
|
|
4
|
+
* @version 2.0.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Service user create request body (builder sends this to controller)
|
|
9
|
+
* @typedef {Object} ServiceUserCreateRequest
|
|
10
|
+
* @property {string} username - Username (1ā100 chars, e.g. api-client-001)
|
|
11
|
+
* @property {string} email - Email address
|
|
12
|
+
* @property {string[]} redirectUris - Allowed redirect URIs for OAuth2 (min 1)
|
|
13
|
+
* @property {string[]} groupNames - Group names to assign (e.g. AI-Fabrix-Developers)
|
|
14
|
+
* @property {string} [description] - Optional description (max 500 chars)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Service user create response (clientSecret is one-time-only; store at creation time)
|
|
19
|
+
* @typedef {Object} ServiceUserCreateResponse
|
|
20
|
+
* @property {string} clientId - Stable identifier for the service user
|
|
21
|
+
* @property {string} clientSecret - One-time-only secret; not returned by any other endpoint
|
|
22
|
+
* @property {boolean} [success] - Optional wrapper flag
|
|
23
|
+
* @property {string} [createdAt] - Optional creation timestamp (ISO 8601)
|
|
24
|
+
*/
|
package/lib/api/wizard.api.js
CHANGED
|
@@ -10,6 +10,7 @@ const { uploadFile } = require('../utils/file-upload');
|
|
|
10
10
|
/**
|
|
11
11
|
* Create wizard session
|
|
12
12
|
* POST /api/v1/wizard/sessions
|
|
13
|
+
* @requiresPermission {Dataplane} external-system:create
|
|
13
14
|
* @async
|
|
14
15
|
* @function createWizardSession
|
|
15
16
|
* @param {string} dataplaneUrl - Dataplane base URL
|
|
@@ -33,6 +34,7 @@ async function createWizardSession(dataplaneUrl, authConfig, mode, systemIdOrKey
|
|
|
33
34
|
/**
|
|
34
35
|
* Get wizard session
|
|
35
36
|
* GET /api/v1/wizard/sessions/{sessionId}
|
|
37
|
+
* @requiresPermission {Dataplane} external-system:read or external-system:create
|
|
36
38
|
* @async
|
|
37
39
|
* @function getWizardSession
|
|
38
40
|
* @param {string} dataplaneUrl - Dataplane base URL
|
|
@@ -49,6 +51,7 @@ async function getWizardSession(dataplaneUrl, sessionId, authConfig) {
|
|
|
49
51
|
/**
|
|
50
52
|
* Update wizard session
|
|
51
53
|
* PUT /api/v1/wizard/sessions/{sessionId}
|
|
54
|
+
* @requiresPermission {Dataplane} external-system:create
|
|
52
55
|
* @async
|
|
53
56
|
* @function updateWizardSession
|
|
54
57
|
* @param {string} dataplaneUrl - Dataplane base URL
|
|
@@ -80,6 +83,7 @@ async function updateWizardSession(dataplaneUrl, sessionId, authConfig, updateDa
|
|
|
80
83
|
/**
|
|
81
84
|
* Delete wizard session
|
|
82
85
|
* DELETE /api/v1/wizard/sessions/{sessionId}
|
|
86
|
+
* @requiresPermission {Dataplane} external-system:create
|
|
83
87
|
* @async
|
|
84
88
|
* @function deleteWizardSession
|
|
85
89
|
* @param {string} dataplaneUrl - Dataplane base URL
|
|
@@ -96,6 +100,7 @@ async function deleteWizardSession(dataplaneUrl, sessionId, authConfig) {
|
|
|
96
100
|
/**
|
|
97
101
|
* Get wizard session progress
|
|
98
102
|
* GET /api/v1/wizard/sessions/{sessionId}/progress
|
|
103
|
+
* @requiresPermission {Dataplane} external-system:create
|
|
99
104
|
* @async
|
|
100
105
|
* @function getWizardProgress
|
|
101
106
|
* @param {string} dataplaneUrl - Dataplane base URL
|
|
@@ -112,6 +117,7 @@ async function getWizardProgress(dataplaneUrl, sessionId, authConfig) {
|
|
|
112
117
|
/**
|
|
113
118
|
* Parse OpenAPI file or URL
|
|
114
119
|
* POST /api/v1/wizard/parse-openapi
|
|
120
|
+
* @requiresPermission {Dataplane} external-system:create
|
|
115
121
|
* @async
|
|
116
122
|
* @function parseOpenApi
|
|
117
123
|
* @param {string} dataplaneUrl - Dataplane base URL
|
|
@@ -133,6 +139,7 @@ async function parseOpenApi(dataplaneUrl, authConfig, openApiFilePathOrUrl, isUr
|
|
|
133
139
|
/**
|
|
134
140
|
* Credential selection for wizard
|
|
135
141
|
* POST /api/v1/wizard/credential-selection
|
|
142
|
+
* @requiresPermission {Dataplane} external-system:create
|
|
136
143
|
* @async
|
|
137
144
|
* @function credentialSelection
|
|
138
145
|
* @param {string} dataplaneUrl - Dataplane base URL
|
|
@@ -154,6 +161,7 @@ async function credentialSelection(dataplaneUrl, authConfig, selectionData) {
|
|
|
154
161
|
/**
|
|
155
162
|
* Detect API type from OpenAPI spec
|
|
156
163
|
* POST /api/v1/wizard/detect-type
|
|
164
|
+
* @requiresPermission {Dataplane} external-system:create
|
|
157
165
|
* @async
|
|
158
166
|
* @function detectType
|
|
159
167
|
* @param {string} dataplaneUrl - Dataplane base URL
|
|
@@ -172,6 +180,7 @@ async function detectType(dataplaneUrl, authConfig, openapiSpec) {
|
|
|
172
180
|
/**
|
|
173
181
|
* Generate configuration via AI
|
|
174
182
|
* POST /api/v1/wizard/generate-config
|
|
183
|
+
* @requiresPermission {Dataplane} external-system:create
|
|
175
184
|
* @async
|
|
176
185
|
* @function generateConfig
|
|
177
186
|
* @param {string} dataplaneUrl - Dataplane base URL
|
|
@@ -202,6 +211,7 @@ async function generateConfig(dataplaneUrl, authConfig, config) {
|
|
|
202
211
|
/**
|
|
203
212
|
* Generate configuration via AI (streaming)
|
|
204
213
|
* POST /api/v1/wizard/generate-config-stream
|
|
214
|
+
* @requiresPermission {Dataplane} external-system:create
|
|
205
215
|
* @async
|
|
206
216
|
* @function generateConfigStream
|
|
207
217
|
* @param {string} dataplaneUrl - Dataplane base URL
|
|
@@ -220,6 +230,7 @@ async function generateConfigStream(dataplaneUrl, authConfig, config) {
|
|
|
220
230
|
/**
|
|
221
231
|
* Validate wizard configuration
|
|
222
232
|
* POST /api/v1/wizard/validate
|
|
233
|
+
* @requiresPermission {Dataplane} external-system:create
|
|
223
234
|
* @async
|
|
224
235
|
* @function validateWizardConfig
|
|
225
236
|
* @param {string} dataplaneUrl - Dataplane base URL
|
|
@@ -242,6 +253,7 @@ async function validateWizardConfig(dataplaneUrl, authConfig, systemConfig, data
|
|
|
242
253
|
/**
|
|
243
254
|
* Validate all completed wizard steps
|
|
244
255
|
* GET /api/v1/wizard/sessions/{sessionId}/validate
|
|
256
|
+
* @requiresPermission {Dataplane} external-system:create
|
|
245
257
|
* @async
|
|
246
258
|
* @function validateAllSteps
|
|
247
259
|
* @param {string} dataplaneUrl - Dataplane base URL
|
|
@@ -258,6 +270,7 @@ async function validateAllSteps(dataplaneUrl, sessionId, authConfig) {
|
|
|
258
270
|
/**
|
|
259
271
|
* Validate specific wizard step
|
|
260
272
|
* POST /api/v1/wizard/sessions/{sessionId}/validate-step
|
|
273
|
+
* @requiresPermission {Dataplane} external-system:create
|
|
261
274
|
* @async
|
|
262
275
|
* @function validateStep
|
|
263
276
|
* @param {string} dataplaneUrl - Dataplane base URL
|
|
@@ -275,6 +288,7 @@ async function validateStep(dataplaneUrl, sessionId, authConfig, stepNumber) {
|
|
|
275
288
|
/**
|
|
276
289
|
* Get configuration preview with summaries
|
|
277
290
|
* GET /api/v1/wizard/preview/{sessionId}
|
|
291
|
+
* @requiresPermission {Dataplane} external-system:create
|
|
278
292
|
* @async
|
|
279
293
|
* @function getPreview
|
|
280
294
|
* @param {string} dataplaneUrl - Dataplane base URL
|
|
@@ -291,6 +305,7 @@ async function getPreview(dataplaneUrl, sessionId, authConfig) {
|
|
|
291
305
|
/**
|
|
292
306
|
* Test MCP server connection
|
|
293
307
|
* POST /api/v1/wizard/test-mcp-connection
|
|
308
|
+
* @requiresPermission {Dataplane} external-system:create
|
|
294
309
|
* @async
|
|
295
310
|
* @function testMcpConnection
|
|
296
311
|
* @param {string} dataplaneUrl - Dataplane base URL
|
|
@@ -313,6 +328,7 @@ async function testMcpConnection(dataplaneUrl, authConfig, serverUrl, token) {
|
|
|
313
328
|
/**
|
|
314
329
|
* Get deployment documentation for a system (from dataplane DB only)
|
|
315
330
|
* GET /api/v1/wizard/deployment-docs/{systemKey}
|
|
331
|
+
* @requiresPermission {Dataplane} external-system:read
|
|
316
332
|
* @async
|
|
317
333
|
* @function getDeploymentDocs
|
|
318
334
|
* @param {string} dataplaneUrl - Dataplane base URL
|
|
@@ -330,6 +346,7 @@ async function getDeploymentDocs(dataplaneUrl, authConfig, systemKey) {
|
|
|
330
346
|
* Generate deployment documentation with variables.yaml and deploy JSON for better quality
|
|
331
347
|
* POST /api/v1/wizard/deployment-docs/{systemKey}
|
|
332
348
|
* Sends deployJson and variablesYaml in the request body so the dataplane can align README with the integration folder.
|
|
349
|
+
* @requiresPermission {Dataplane} external-system:update
|
|
333
350
|
* @async
|
|
334
351
|
* @function postDeploymentDocs
|
|
335
352
|
* @param {string} dataplaneUrl - Dataplane base URL
|
|
@@ -353,6 +370,7 @@ async function postDeploymentDocs(dataplaneUrl, authConfig, systemKey, body = nu
|
|
|
353
370
|
* GET /api/v1/wizard/platforms
|
|
354
371
|
* Expected response: { platforms: [ { key: string, displayName?: string } ] } or equivalent.
|
|
355
372
|
* On 404 or error, returns empty array (caller should hide "Known platform" choice).
|
|
373
|
+
* @requiresPermission {Dataplane} external-system:read or unauthenticated
|
|
356
374
|
* @async
|
|
357
375
|
* @function getWizardPlatforms
|
|
358
376
|
* @param {string} dataplaneUrl - Dataplane base URL
|
|
@@ -373,6 +391,7 @@ async function getWizardPlatforms(dataplaneUrl, authConfig) {
|
|
|
373
391
|
/**
|
|
374
392
|
* List credentials for wizard selection (Step 3)
|
|
375
393
|
* GET /api/v1/wizard/credentials
|
|
394
|
+
* @requiresPermission {Dataplane} credential:read or external-system:create
|
|
376
395
|
* @async
|
|
377
396
|
* @function listWizardCredentials
|
|
378
397
|
* @param {string} dataplaneUrl - Dataplane base URL
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deploy status display helpers: build app URL from controller/port and show after deploy.
|
|
3
|
+
* @fileoverview Status URL display for application deployment
|
|
4
|
+
* @author AI Fabrix Team
|
|
5
|
+
* @version 2.0.0
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const chalk = require('chalk');
|
|
9
|
+
const logger = require('../utils/logger');
|
|
10
|
+
const { getApplicationStatus } = require('../api/applications.api');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Builds app URL from controller base URL and app port (e.g. http://localhost:3600 + 3601 -> http://localhost:3601).
|
|
14
|
+
* @param {string} controllerUrl - Controller base URL
|
|
15
|
+
* @param {number} port - Application port
|
|
16
|
+
* @returns {string|null} App URL or null if parsing fails
|
|
17
|
+
*/
|
|
18
|
+
function buildAppUrlFromControllerAndPort(controllerUrl, port) {
|
|
19
|
+
if (!controllerUrl || (port === null || port === undefined) || typeof port !== 'number') return null;
|
|
20
|
+
try {
|
|
21
|
+
const u = new URL(controllerUrl);
|
|
22
|
+
u.port = String(port);
|
|
23
|
+
return u.toString();
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Parses URL and port from application status response body.
|
|
31
|
+
* @param {Object} body - Response body (may have data wrapper or top-level url/port)
|
|
32
|
+
* @returns {{ url: string|null, port: number|null }}
|
|
33
|
+
*/
|
|
34
|
+
function parseUrlAndPortFromStatusBody(body) {
|
|
35
|
+
const data = body?.data ?? body;
|
|
36
|
+
const url = (data && typeof data.url === 'string' && data.url.trim() !== '')
|
|
37
|
+
? data.url
|
|
38
|
+
: (body?.url && typeof body.url === 'string' && body.url.trim() !== '')
|
|
39
|
+
? body.url
|
|
40
|
+
: null;
|
|
41
|
+
const port = (data && typeof data.port === 'number')
|
|
42
|
+
? data.port
|
|
43
|
+
: (body && typeof body.port === 'number')
|
|
44
|
+
? body.port
|
|
45
|
+
: null;
|
|
46
|
+
return { url, port };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Fetches app URL from controller application status and displays it.
|
|
51
|
+
* Uses status API url when present; otherwise derives from status port and controller host.
|
|
52
|
+
* @param {string} controllerUrl - Controller base URL (used only to derive host when status returns port)
|
|
53
|
+
* @param {string} envKey - Environment key
|
|
54
|
+
* @param {string} appKey - Application key (manifest.key)
|
|
55
|
+
* @param {Object} authConfig - Auth used for deployment (same as for status)
|
|
56
|
+
*/
|
|
57
|
+
async function displayAppUrlFromController(controllerUrl, envKey, appKey, authConfig) {
|
|
58
|
+
let url = null;
|
|
59
|
+
let port = null;
|
|
60
|
+
try {
|
|
61
|
+
const res = await getApplicationStatus(controllerUrl, envKey, appKey, authConfig);
|
|
62
|
+
const parsed = parseUrlAndPortFromStatusBody(res?.data);
|
|
63
|
+
url = parsed.url;
|
|
64
|
+
port = parsed.port;
|
|
65
|
+
} catch (_) {
|
|
66
|
+
// Show fallback message below
|
|
67
|
+
}
|
|
68
|
+
if (!url && (port !== null && port !== undefined)) {
|
|
69
|
+
url = buildAppUrlFromControllerAndPort(controllerUrl, port);
|
|
70
|
+
}
|
|
71
|
+
if (url) {
|
|
72
|
+
logger.log(chalk.green(` ā App running at ${url}`));
|
|
73
|
+
} else {
|
|
74
|
+
logger.log(chalk.blue(' ā App deployed. Get URL from controller dashboard.'));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = { displayAppUrlFromController, buildAppUrlFromControllerAndPort, parseUrlAndPortFromStatusBody };
|
package/lib/app/deploy.js
CHANGED
|
@@ -18,6 +18,7 @@ const logger = require('../utils/logger');
|
|
|
18
18
|
const { detectAppType, getBuilderPath, getIntegrationPath } = require('../utils/paths');
|
|
19
19
|
const { checkApplicationExists } = require('../utils/app-existence');
|
|
20
20
|
const { loadDeploymentConfig } = require('./deploy-config');
|
|
21
|
+
const { displayAppUrlFromController } = require('./deploy-status-display');
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Validate application name format
|
|
@@ -272,6 +273,59 @@ function validateImageIsPullable(imageRef, appName) {
|
|
|
272
273
|
}
|
|
273
274
|
}
|
|
274
275
|
|
|
276
|
+
/**
|
|
277
|
+
* Throws an error when deployment status is failed or cancelled
|
|
278
|
+
* @param {string} status - Status value
|
|
279
|
+
* @param {Object} statusObj - Full status object from result
|
|
280
|
+
* @throws {Error}
|
|
281
|
+
*/
|
|
282
|
+
function throwIfDeploymentFailed(status, statusObj) {
|
|
283
|
+
if (status !== 'failed' && status !== 'cancelled') return;
|
|
284
|
+
const msg =
|
|
285
|
+
statusObj.message ||
|
|
286
|
+
statusObj.error ||
|
|
287
|
+
(status === 'cancelled' ? 'Deployment cancelled' : 'Deployment failed');
|
|
288
|
+
const err = new Error(`Deployment ${status}: ${msg}`);
|
|
289
|
+
err.formatted = `Deployment ${status}.\n\n${msg}`;
|
|
290
|
+
err.deploymentStatus = statusObj;
|
|
291
|
+
throw err;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Enhances 401 error with rotate-secret hint when app exists
|
|
296
|
+
* @param {Error} error - Caught error
|
|
297
|
+
* @param {boolean} appExists - Whether app exists in controller
|
|
298
|
+
* @param {string} appName - Application name
|
|
299
|
+
* @param {string} envKey - Environment key
|
|
300
|
+
* @throws {Error} Enhanced or original error
|
|
301
|
+
*/
|
|
302
|
+
function enhanceAuthErrorIfNeeded(error, appExists, appName, envKey) {
|
|
303
|
+
if (!appExists || error.status !== 401 || error.message.includes('rotate-secret')) {
|
|
304
|
+
throw error;
|
|
305
|
+
}
|
|
306
|
+
const enhancedError = new Error(
|
|
307
|
+
`${error.message}\n\nš” The application '${appName}' exists in environment '${envKey}'. ` +
|
|
308
|
+
`To fix invalid credentials, rotate the application secret:\n aifabrix app rotate-secret ${appName}`
|
|
309
|
+
);
|
|
310
|
+
enhancedError.status = 401;
|
|
311
|
+
enhancedError.formatted = error.formatted || enhancedError.message;
|
|
312
|
+
throw enhancedError;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Apply image/registry overrides from options to manifest
|
|
317
|
+
* @param {Object} manifest - Deployment manifest
|
|
318
|
+
* @param {Object} options - Deployment options
|
|
319
|
+
*/
|
|
320
|
+
function applyManifestOverrides(manifest, options) {
|
|
321
|
+
if (options.imageOverride || options.image) {
|
|
322
|
+
manifest.image = options.imageOverride || options.image;
|
|
323
|
+
}
|
|
324
|
+
if (options.registryMode) {
|
|
325
|
+
manifest.registryMode = options.registryMode;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
275
329
|
/**
|
|
276
330
|
* Execute standard application deployment flow
|
|
277
331
|
* @async
|
|
@@ -283,38 +337,29 @@ function validateImageIsPullable(imageRef, appName) {
|
|
|
283
337
|
async function executeStandardDeployment(appName, options) {
|
|
284
338
|
const config = await loadDeploymentConfig(appName, options);
|
|
285
339
|
const controllerUrl = config.controllerUrl || 'unknown';
|
|
286
|
-
|
|
287
|
-
// Check if application exists before deployment
|
|
288
340
|
const appExists = await checkApplicationExists(appName, controllerUrl, config.envKey, config.auth);
|
|
289
341
|
|
|
290
342
|
const { manifest, manifestPath } = await generateAndValidateManifest(appName);
|
|
291
|
-
|
|
292
|
-
manifest.image = options.imageOverride || options.image;
|
|
293
|
-
}
|
|
294
|
-
if (options.registryMode) {
|
|
295
|
-
manifest.registryMode = options.registryMode;
|
|
296
|
-
}
|
|
297
|
-
|
|
343
|
+
applyManifestOverrides(manifest, options);
|
|
298
344
|
validateImageIsPullable(manifest.image, appName);
|
|
299
|
-
|
|
300
345
|
displayDeploymentInfo(manifest, manifestPath);
|
|
301
346
|
|
|
302
347
|
try {
|
|
303
348
|
const result = await executeDeployment(manifest, config);
|
|
304
349
|
displayDeploymentResults(result);
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
350
|
+
const status = result.status?.status;
|
|
351
|
+
throwIfDeploymentFailed(status, result.status || {});
|
|
352
|
+
if (status === 'completed') {
|
|
353
|
+
await displayAppUrlFromController(
|
|
354
|
+
config.controllerUrl,
|
|
355
|
+
config.envKey,
|
|
356
|
+
manifest.key,
|
|
357
|
+
config.auth
|
|
312
358
|
);
|
|
313
|
-
enhancedError.status = 401;
|
|
314
|
-
enhancedError.formatted = error.formatted || enhancedError.message;
|
|
315
|
-
throw enhancedError;
|
|
316
359
|
}
|
|
317
|
-
|
|
360
|
+
return { result, controllerUrl, appExists };
|
|
361
|
+
} catch (error) {
|
|
362
|
+
enhanceAuthErrorIfNeeded(error, appExists, appName, config.envKey);
|
|
318
363
|
}
|
|
319
364
|
}
|
|
320
365
|
|
package/lib/app/rotate-secret.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
const chalk = require('chalk');
|
|
12
|
-
const { getConfig, normalizeControllerUrl, resolveEnvironment } = require('../core/config');
|
|
12
|
+
const { getConfig, normalizeControllerUrl, resolveEnvironment, clearClientToken } = require('../core/config');
|
|
13
13
|
const { resolveControllerUrl } = require('../utils/controller-url');
|
|
14
14
|
const { getOrRefreshDeviceToken } = require('../utils/token-manager');
|
|
15
15
|
const { rotateApplicationSecret } = require('../api/applications.api');
|
|
@@ -324,6 +324,8 @@ async function executeRotation(appKey, actualControllerUrl, environment, token)
|
|
|
324
324
|
const { credentials, message } = validateResponse(response);
|
|
325
325
|
await saveCredentialsLocally(appKey, credentials, actualControllerUrl);
|
|
326
326
|
displayRotationResults(appKey, environment, credentials, actualControllerUrl, message);
|
|
327
|
+
// Clear cached client token so next deploy uses new credentials (avoids "Invalid or expired credentials" when running deploy/up-dataplane immediately after rotate)
|
|
328
|
+
await clearClientToken(environment, appKey);
|
|
327
329
|
}
|
|
328
330
|
|
|
329
331
|
/**
|
package/lib/app/run-helpers.js
CHANGED
|
@@ -31,6 +31,9 @@ const { resolveVersionForApp } = require('../utils/image-version');
|
|
|
31
31
|
|
|
32
32
|
const execAsync = promisify(exec);
|
|
33
33
|
|
|
34
|
+
/** Template apps (keycloak, miso-controller, dataplane) - never update variables.yaml when running */
|
|
35
|
+
const TEMPLATE_APP_KEYS = ['keycloak', 'miso-controller', 'dataplane'];
|
|
36
|
+
|
|
34
37
|
// Re-export container helper functions
|
|
35
38
|
const checkImageExists = containerHelpers.checkImageExists;
|
|
36
39
|
const checkContainerRunning = containerHelpers.checkContainerRunning;
|
|
@@ -140,15 +143,17 @@ async function validateAppConfiguration(appName) {
|
|
|
140
143
|
}
|
|
141
144
|
|
|
142
145
|
/**
|
|
143
|
-
* Resolves version from image and updates builder/variables.yaml when running
|
|
146
|
+
* Resolves version from image and updates builder/variables.yaml when running.
|
|
147
|
+
* Template apps (keycloak, miso-controller, dataplane) are never updated - variables.yaml stays pristine.
|
|
144
148
|
* @async
|
|
145
149
|
* @param {string} appName - Application name
|
|
146
150
|
* @param {Object} appConfig - Application configuration
|
|
147
151
|
* @param {boolean} debug - Enable debug logging
|
|
148
152
|
*/
|
|
149
153
|
async function resolveAndUpdateVersion(appName, appConfig, debug) {
|
|
154
|
+
const isTemplateApp = TEMPLATE_APP_KEYS.includes(appName);
|
|
150
155
|
const resolved = await resolveVersionForApp(appName, appConfig, {
|
|
151
|
-
updateBuilder:
|
|
156
|
+
updateBuilder: !isTemplateApp,
|
|
152
157
|
builderPath: pathsUtil.getBuilderPath(appName)
|
|
153
158
|
});
|
|
154
159
|
if (resolved.fromImage && resolved.updated && debug) {
|
package/lib/app/show-display.js
CHANGED
|
@@ -31,14 +31,21 @@ function logApplicationRequired(a) {
|
|
|
31
31
|
logger.log(` Port: ${port}`);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
const APPLICATION_OPTIONAL_FIELDS = [
|
|
35
|
+
{ key: 'deploymentKey', label: 'Deployment' },
|
|
36
|
+
{ key: 'image', label: 'Image' },
|
|
37
|
+
{ key: 'registryMode', label: 'Registry' },
|
|
38
|
+
{ key: 'healthCheck', label: 'Health' },
|
|
39
|
+
{ key: 'build', label: 'Build' },
|
|
40
|
+
{ key: 'status', label: 'Status' },
|
|
41
|
+
{ key: 'url', label: 'URL' },
|
|
42
|
+
{ key: 'internalUrl', label: 'Internal URL' }
|
|
43
|
+
];
|
|
44
|
+
|
|
34
45
|
function logApplicationOptional(a) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (a.healthCheck !== undefined) logger.log(` Health: ${a.healthCheck ?? 'ā'}`);
|
|
39
|
-
if (a.build !== undefined) logger.log(` Build: ${a.build ?? 'ā'}`);
|
|
40
|
-
if (a.status !== undefined) logger.log(` Status: ${a.status ?? 'ā'}`);
|
|
41
|
-
if (a.url !== undefined) logger.log(` URL: ${a.url ?? 'ā'}`);
|
|
46
|
+
APPLICATION_OPTIONAL_FIELDS.forEach(({ key, label }) => {
|
|
47
|
+
if (a[key] !== undefined) logger.log(` ${(label + ':').padEnd(16)} ${a[key] ?? 'ā'}`);
|
|
48
|
+
});
|
|
42
49
|
}
|
|
43
50
|
|
|
44
51
|
function logApplicationFields(a) {
|
|
@@ -75,10 +82,15 @@ function logRolesSection(roles) {
|
|
|
75
82
|
});
|
|
76
83
|
}
|
|
77
84
|
|
|
78
|
-
function logPermissionsSection(permissions) {
|
|
79
|
-
|
|
85
|
+
function logPermissionsSection(permissions, opts = {}) {
|
|
86
|
+
const showWhenEmpty = opts.showWhenEmpty || false;
|
|
87
|
+
if (permissions.length === 0 && !showWhenEmpty) return;
|
|
80
88
|
logger.log('');
|
|
81
89
|
logger.log('š”ļø Permissions');
|
|
90
|
+
if (permissions.length === 0) {
|
|
91
|
+
logger.log(' (none)');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
82
94
|
permissions.forEach((p) => {
|
|
83
95
|
const name = p.name ?? 'ā';
|
|
84
96
|
const roleList = (p.roles || []).join(', ');
|
|
@@ -186,8 +198,10 @@ function logExternalSystemSection(ext) {
|
|
|
186
198
|
/**
|
|
187
199
|
* Format and print human-readable show output (offline or online).
|
|
188
200
|
* @param {Object} summary - Unified summary (buildOfflineSummaryFromDeployJson or buildOnlineSummary)
|
|
201
|
+
* @param {Object} [options] - Display options
|
|
202
|
+
* @param {boolean} [options.permissionsOnly] - When true, output only source and Permissions section
|
|
189
203
|
*/
|
|
190
|
-
function display(summary) {
|
|
204
|
+
function display(summary, options = {}) {
|
|
191
205
|
const a = summary.application;
|
|
192
206
|
const roles = summary.roles ?? a.roles ?? [];
|
|
193
207
|
const permissions = summary.permissions ?? a.permissions ?? [];
|
|
@@ -197,9 +211,14 @@ function display(summary) {
|
|
|
197
211
|
const dbNames = Array.isArray(databases) ? databases.map((d) => (d && d.name) || d).filter(Boolean) : [];
|
|
198
212
|
|
|
199
213
|
logSourceAndHeader(summary);
|
|
214
|
+
if (options.permissionsOnly) {
|
|
215
|
+
logPermissionsSection(permissions, { showWhenEmpty: true });
|
|
216
|
+
logger.log('');
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
200
219
|
logApplicationSection(a, summary.isExternal);
|
|
201
220
|
logRolesSection(roles);
|
|
202
|
-
|
|
221
|
+
/* Permissions section shown only when --permissions is set (permissionsOnly mode) */
|
|
203
222
|
logAuthSection(authentication);
|
|
204
223
|
logConfigurationsSection(portalInputConfigurations);
|
|
205
224
|
logDatabasesSection(dbNames);
|