@aifabrix/builder 2.37.9 → 2.38.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 +19 -0
- package/integration/hubspot/hubspot-deploy.json +1 -2
- package/lib/api/applications.api.js +23 -1
- package/lib/api/credentials.api.js +34 -0
- package/lib/api/deployments.api.js +27 -0
- package/lib/api/types/applications.types.js +1 -1
- package/lib/api/types/deployments.types.js +1 -1
- package/lib/api/types/pipeline.types.js +1 -1
- package/lib/api/wizard.api.js +21 -1
- package/lib/app/run-helpers.js +30 -2
- package/lib/cli/index.js +2 -0
- package/lib/cli/setup-app.js +32 -0
- package/lib/cli/setup-credential-deployment.js +72 -0
- package/lib/cli/setup-utility.js +1 -25
- package/lib/commands/app-down.js +80 -0
- package/lib/commands/app-logs.js +146 -0
- package/lib/commands/app.js +22 -0
- package/lib/commands/credential-list.js +104 -0
- package/lib/commands/deployment-list.js +184 -0
- package/lib/core/templates.js +2 -1
- package/lib/generator/builders.js +8 -3
- package/lib/generator/external-controller-manifest.js +5 -4
- package/lib/generator/index.js +16 -14
- package/lib/generator/split.js +1 -0
- package/lib/generator/wizard.js +4 -1
- package/lib/schema/application-schema.json +6 -2
- package/lib/schema/deployment-rules.yaml +121 -0
- package/lib/utils/app-run-containers.js +2 -1
- package/lib/utils/compose-generator.js +2 -1
- package/lib/utils/help-builder.js +0 -1
- package/lib/utils/image-version.js +209 -0
- package/lib/utils/schema-loader.js +1 -1
- package/lib/utils/variable-transformer.js +1 -19
- package/lib/validation/external-manifest-validator.js +1 -1
- package/package.json +1 -1
- package/templates/applications/README.md.hbs +1 -3
- package/templates/applications/dataplane/Dockerfile +2 -2
- package/templates/applications/dataplane/README.md +1 -3
- package/templates/applications/dataplane/variables.yaml +5 -3
- package/templates/applications/keycloak/Dockerfile +3 -3
- package/templates/applications/keycloak/README.md +14 -4
- package/templates/applications/keycloak/env.template +14 -2
- package/templates/applications/keycloak/variables.yaml +1 -1
- package/templates/applications/miso-controller/README.md +1 -3
- package/templates/applications/miso-controller/env.template +64 -11
package/README.md
CHANGED
|
@@ -9,6 +9,25 @@ Install the AI Fabrix platform and test it locally. Then add external integratio
|
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
12
|
+
## Why AI Fabrix Builder?
|
|
13
|
+
|
|
14
|
+
- **Build perspective:** Everything is driven by declarative config and JSON schemas—no hidden logic, AI assistant–friendly.
|
|
15
|
+
- **Industry standards and security:** Follow industry standards and high security (e.g. ISO 27k); no secrets in version control.
|
|
16
|
+
- **Full lifecycle in your version control:** Configuration, apps, and integrations live in your own VCS (GitHub, GitLab, Azure DevOps).
|
|
17
|
+
- **One tool from day one:** Single CLI for local infra, app and integration creation, build, run, and deploy—same workflow for apps and integrations.
|
|
18
|
+
- **Consistency and production readiness:** Schema-driven; deploy apps and integrations to the same controller/dataplane; production-ready secrets with `kv://` and Azure Key Vault.
|
|
19
|
+
- **Application development:** Use **[miso-client](https://github.com/esystemsdev/aifabrix-miso-client)** for TypeScript and Python to talk to the dataplane and controller (see [templates/applications/dataplane/README.md](templates/applications/dataplane/README.md) and the repo for usage).
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Prerequisites
|
|
24
|
+
|
|
25
|
+
- **Node.js 18+** – Recommended for running the CLI.
|
|
26
|
+
- **AI Fabrix Azure / platform:** Install from **Azure Marketplace** or run via **Docker** (e.g. `aifabrix up-platform`). You need **full access to Docker** (docker commands) where applicable.
|
|
27
|
+
- **Secrets before platform:** Add secrets (e.g. OpenAI or Azure OpenAI) **before** running `aifabrix up-platform`; the platform reads them from the place you configure. See [Infrastructure](docs/infrastructure.md) and secrets configuration.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
12
31
|
## Install
|
|
13
32
|
|
|
14
33
|
```bash
|
|
@@ -153,6 +153,27 @@ async function rotateApplicationSecret(controllerUrl, envKey, appKey, authConfig
|
|
|
153
153
|
return await client.post(`/api/v1/environments/${envKey}/applications/${appKey}/rotate-secret`);
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Get application status (metadata only, no configuration)
|
|
158
|
+
* GET /api/v1/environments/{envKey}/applications/{appKey}/status
|
|
159
|
+
* Auth: bearer token or pipeline client credentials for that application.
|
|
160
|
+
*
|
|
161
|
+
* @async
|
|
162
|
+
* @function getApplicationStatus
|
|
163
|
+
* @param {string} controllerUrl - Controller base URL
|
|
164
|
+
* @param {string} envKey - Environment key
|
|
165
|
+
* @param {string} appKey - Application key
|
|
166
|
+
* @param {Object} authConfig - Authentication configuration
|
|
167
|
+
* @returns {Promise<Object>} Response with data: { id, key, displayName, url, internalUrl, port, status, runtimeStatus, environmentId, createdAt, updatedAt, image, description }
|
|
168
|
+
* @throws {Error} If request fails
|
|
169
|
+
*/
|
|
170
|
+
async function getApplicationStatus(controllerUrl, envKey, appKey, authConfig) {
|
|
171
|
+
const client = new ApiClient(controllerUrl, authConfig);
|
|
172
|
+
return await client.get(
|
|
173
|
+
`/api/v1/environments/${envKey}/applications/${appKey}/status`
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
156
177
|
module.exports = {
|
|
157
178
|
listApplications,
|
|
158
179
|
createApplication,
|
|
@@ -160,6 +181,7 @@ module.exports = {
|
|
|
160
181
|
updateApplication,
|
|
161
182
|
deleteApplication,
|
|
162
183
|
registerApplication,
|
|
163
|
-
rotateApplicationSecret
|
|
184
|
+
rotateApplicationSecret,
|
|
185
|
+
getApplicationStatus
|
|
164
186
|
};
|
|
165
187
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Credentials API functions (controller/dataplane credential list)
|
|
3
|
+
* @author AI Fabrix Team
|
|
4
|
+
* @version 2.0.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { ApiClient } = require('./index');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* List credentials from controller or dataplane
|
|
11
|
+
* GET /api/v1/credential
|
|
12
|
+
* Used by `aifabrix credential list`. Call with controller or dataplane base URL per deployment.
|
|
13
|
+
*
|
|
14
|
+
* @async
|
|
15
|
+
* @function listCredentials
|
|
16
|
+
* @param {string} baseUrl - Controller or dataplane base URL
|
|
17
|
+
* @param {Object} authConfig - Authentication configuration
|
|
18
|
+
* @param {Object} [options] - List options
|
|
19
|
+
* @param {boolean} [options.activeOnly] - If true, return only active credentials
|
|
20
|
+
* @param {number} [options.page] - Page number
|
|
21
|
+
* @param {number} [options.pageSize] - Items per page
|
|
22
|
+
* @returns {Promise<Object>} Response with credentials (e.g. data.credentials or data.items)
|
|
23
|
+
* @throws {Error} If request fails
|
|
24
|
+
*/
|
|
25
|
+
async function listCredentials(baseUrl, authConfig, options = {}) {
|
|
26
|
+
const client = new ApiClient(baseUrl, authConfig);
|
|
27
|
+
return await client.get('/api/v1/credential', {
|
|
28
|
+
params: options
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = {
|
|
33
|
+
listCredentials
|
|
34
|
+
};
|
|
@@ -116,10 +116,37 @@ async function getDeploymentLogs(controllerUrl, envKey, deploymentId, authConfig
|
|
|
116
116
|
});
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
/**
|
|
120
|
+
* List deployments for a single application in an environment
|
|
121
|
+
* GET /api/v1/environments/{envKey}/applications/{appKey}/deployments
|
|
122
|
+
*
|
|
123
|
+
* @async
|
|
124
|
+
* @function listApplicationDeployments
|
|
125
|
+
* @param {string} controllerUrl - Controller base URL
|
|
126
|
+
* @param {string} envKey - Environment key
|
|
127
|
+
* @param {string} appKey - Application key
|
|
128
|
+
* @param {Object} authConfig - Authentication configuration
|
|
129
|
+
* @param {Object} [options] - List options
|
|
130
|
+
* @param {number} [options.page] - Page number
|
|
131
|
+
* @param {number} [options.pageSize] - Items per page (default 50)
|
|
132
|
+
* @param {string} [options.sort] - Sort parameter
|
|
133
|
+
* @param {string} [options.filter] - Filter parameter
|
|
134
|
+
* @returns {Promise<Object>} Paginated list of deployments for the application
|
|
135
|
+
* @throws {Error} If request fails
|
|
136
|
+
*/
|
|
137
|
+
async function listApplicationDeployments(controllerUrl, envKey, appKey, authConfig, options = {}) {
|
|
138
|
+
const client = new ApiClient(controllerUrl, authConfig);
|
|
139
|
+
return await client.get(
|
|
140
|
+
`/api/v1/environments/${envKey}/applications/${appKey}/deployments`,
|
|
141
|
+
{ params: options }
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
119
145
|
module.exports = {
|
|
120
146
|
deployApplication,
|
|
121
147
|
deployEnvironment,
|
|
122
148
|
listDeployments,
|
|
149
|
+
listApplicationDeployments,
|
|
123
150
|
getDeployment,
|
|
124
151
|
getDeploymentLogs
|
|
125
152
|
};
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* @property {string} displayName - Human-readable application name
|
|
31
31
|
* @property {string} description - Application description
|
|
32
32
|
* @property {string} type - Azure application type ('webapp' | 'functionapp' | 'api' | 'service' | 'external')
|
|
33
|
-
* @property {string} deploymentKey - SHA256 hash of deployment manifest
|
|
33
|
+
* @property {string} [deploymentKey] - SHA256 hash of deployment manifest (Controller adds internally)
|
|
34
34
|
* @property {string} [image] - Container image reference
|
|
35
35
|
* @property {string} [registryMode] - Registry mode ('acr' | 'external' | 'public')
|
|
36
36
|
* @property {number} [port] - Application port number
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* @property {string} displayName - Human-readable application name
|
|
31
31
|
* @property {string} description - Application description
|
|
32
32
|
* @property {string} type - Azure application type
|
|
33
|
-
* @property {string} deploymentKey - SHA256 hash of deployment manifest
|
|
33
|
+
* @property {string} [deploymentKey] - SHA256 hash of deployment manifest (Controller adds internally)
|
|
34
34
|
* @property {*} [additionalProperties] - Additional configuration properties
|
|
35
35
|
*/
|
|
36
36
|
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* @property {string} displayName - Human-readable application name
|
|
12
12
|
* @property {string} description - Application description
|
|
13
13
|
* @property {string} type - Azure application type
|
|
14
|
-
* @property {string} deploymentKey - SHA256 hash of deployment manifest
|
|
14
|
+
* @property {string} [deploymentKey] - SHA256 hash of deployment manifest (Controller adds internally)
|
|
15
15
|
* @property {*} [additionalProperties] - Additional configuration properties
|
|
16
16
|
*/
|
|
17
17
|
|
package/lib/api/wizard.api.js
CHANGED
|
@@ -370,6 +370,25 @@ async function getWizardPlatforms(dataplaneUrl, authConfig) {
|
|
|
370
370
|
}
|
|
371
371
|
}
|
|
372
372
|
|
|
373
|
+
/**
|
|
374
|
+
* List credentials for wizard selection (Step 3)
|
|
375
|
+
* GET /api/v1/wizard/credentials
|
|
376
|
+
* @async
|
|
377
|
+
* @function listWizardCredentials
|
|
378
|
+
* @param {string} dataplaneUrl - Dataplane base URL
|
|
379
|
+
* @param {Object} authConfig - Authentication configuration
|
|
380
|
+
* @param {Object} [options] - Query options
|
|
381
|
+
* @param {boolean} [options.activeOnly] - If true, return only active credentials
|
|
382
|
+
* @returns {Promise<Object>} Response with credentials list (e.g. data.credentials or data.items)
|
|
383
|
+
* @throws {Error} If request fails
|
|
384
|
+
*/
|
|
385
|
+
async function listWizardCredentials(dataplaneUrl, authConfig, options = {}) {
|
|
386
|
+
const client = new ApiClient(dataplaneUrl, authConfig);
|
|
387
|
+
return await client.get('/api/v1/wizard/credentials', {
|
|
388
|
+
params: options
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
373
392
|
module.exports = {
|
|
374
393
|
createWizardSession,
|
|
375
394
|
getWizardSession,
|
|
@@ -388,5 +407,6 @@ module.exports = {
|
|
|
388
407
|
testMcpConnection,
|
|
389
408
|
getDeploymentDocs,
|
|
390
409
|
postDeploymentDocs,
|
|
391
|
-
getWizardPlatforms
|
|
410
|
+
getWizardPlatforms,
|
|
411
|
+
listWizardCredentials
|
|
392
412
|
};
|
package/lib/app/run-helpers.js
CHANGED
|
@@ -27,6 +27,7 @@ const composeGenerator = require('../utils/compose-generator');
|
|
|
27
27
|
const dockerUtils = require('../utils/docker');
|
|
28
28
|
const containerHelpers = require('../utils/app-run-containers');
|
|
29
29
|
const pathsUtil = require('../utils/paths');
|
|
30
|
+
const { resolveVersionForApp } = require('../utils/image-version');
|
|
30
31
|
|
|
31
32
|
const execAsync = promisify(exec);
|
|
32
33
|
|
|
@@ -138,6 +139,23 @@ async function validateAppConfiguration(appName) {
|
|
|
138
139
|
return appConfig;
|
|
139
140
|
}
|
|
140
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Resolves version from image and updates builder/variables.yaml when running
|
|
144
|
+
* @async
|
|
145
|
+
* @param {string} appName - Application name
|
|
146
|
+
* @param {Object} appConfig - Application configuration
|
|
147
|
+
* @param {boolean} debug - Enable debug logging
|
|
148
|
+
*/
|
|
149
|
+
async function resolveAndUpdateVersion(appName, appConfig, debug) {
|
|
150
|
+
const resolved = await resolveVersionForApp(appName, appConfig, {
|
|
151
|
+
updateBuilder: true,
|
|
152
|
+
builderPath: pathsUtil.getBuilderPath(appName)
|
|
153
|
+
});
|
|
154
|
+
if (resolved.fromImage && resolved.updated && debug) {
|
|
155
|
+
logger.log(chalk.gray(`[DEBUG] Updated app.version to ${resolved.version} from image`));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
141
159
|
/**
|
|
142
160
|
* Checks prerequisites: Docker image and (optionally) infrastructure
|
|
143
161
|
* @async
|
|
@@ -163,10 +181,20 @@ async function checkPrerequisites(appName, appConfig, debug = false, skipInfraCh
|
|
|
163
181
|
}
|
|
164
182
|
logger.log(chalk.green(`✓ Image ${fullImageName} found`));
|
|
165
183
|
|
|
166
|
-
|
|
167
|
-
|
|
184
|
+
await resolveAndUpdateVersion(appName, appConfig, debug);
|
|
185
|
+
|
|
186
|
+
if (!skipInfraCheck) {
|
|
187
|
+
await checkInfraHealthOrThrow(debug);
|
|
168
188
|
}
|
|
189
|
+
}
|
|
169
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Checks infrastructure health and throws if unhealthy
|
|
193
|
+
* @async
|
|
194
|
+
* @param {boolean} debug - Enable debug logging
|
|
195
|
+
* @throws {Error} If infrastructure is not healthy
|
|
196
|
+
*/
|
|
197
|
+
async function checkInfraHealthOrThrow(debug) {
|
|
170
198
|
logger.log(chalk.blue('Checking infrastructure health...'));
|
|
171
199
|
const infraHealth = await infra.checkInfraHealth();
|
|
172
200
|
if (debug) {
|
package/lib/cli/index.js
CHANGED
|
@@ -20,6 +20,7 @@ const { setupSecretsCommands } = require('./setup-secrets');
|
|
|
20
20
|
const { setupExternalSystemCommands } = require('./setup-external-system');
|
|
21
21
|
const { setupAppCommands: setupAppManagementCommands } = require('../commands/app');
|
|
22
22
|
const { setupDatasourceCommands } = require('../commands/datasource');
|
|
23
|
+
const { setupCredentialDeploymentCommands } = require('./setup-credential-deployment');
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* Sets up all CLI commands on the Commander program instance
|
|
@@ -33,6 +34,7 @@ function setupCommands(program) {
|
|
|
33
34
|
setupAppManagementCommands(program);
|
|
34
35
|
setupDatasourceCommands(program);
|
|
35
36
|
setupUtilityCommands(program);
|
|
37
|
+
setupCredentialDeploymentCommands(program);
|
|
36
38
|
setupExternalSystemCommands(program);
|
|
37
39
|
setupDevCommands(program);
|
|
38
40
|
setupSecretsCommands(program);
|
package/lib/cli/setup-app.js
CHANGED
|
@@ -173,6 +173,7 @@ See integration/hubspot/wizard-hubspot-e2e.yaml for an example.`)
|
|
|
173
173
|
.description('Run application locally')
|
|
174
174
|
.option('-p, --port <port>', 'Override local port')
|
|
175
175
|
.option('-d, --debug', 'Enable debug output with detailed container information')
|
|
176
|
+
.option('-t, --tag <tag>', 'Image tag to run (e.g. v1.0.0); overrides variables.yaml image.tag')
|
|
176
177
|
.action(async(appName, options) => {
|
|
177
178
|
try {
|
|
178
179
|
await app.runApp(appName, options);
|
|
@@ -182,6 +183,37 @@ See integration/hubspot/wizard-hubspot-e2e.yaml for an example.`)
|
|
|
182
183
|
}
|
|
183
184
|
});
|
|
184
185
|
|
|
186
|
+
program.command('logs <app>')
|
|
187
|
+
.description('Show application container logs (and optional env summary with secrets masked)')
|
|
188
|
+
.option('-f', 'Follow log stream')
|
|
189
|
+
.option('-t, --tail <lines>', 'Number of lines (default: 100); 0 = full list', '100')
|
|
190
|
+
.action(async(appName, options) => {
|
|
191
|
+
try {
|
|
192
|
+
const { runAppLogs } = require('../commands/app-logs');
|
|
193
|
+
const tailNum = parseInt(options.tail, 10);
|
|
194
|
+
await runAppLogs(appName, {
|
|
195
|
+
follow: options.f,
|
|
196
|
+
tail: Number.isNaN(tailNum) ? 100 : tailNum
|
|
197
|
+
});
|
|
198
|
+
} catch (error) {
|
|
199
|
+
handleCommandError(error, 'logs');
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
program.command('down-app <app>')
|
|
205
|
+
.description('Stop and remove application container; optionally remove volume and image')
|
|
206
|
+
.option('--volumes', 'Remove application Docker volume')
|
|
207
|
+
.action(async(appName, options) => {
|
|
208
|
+
try {
|
|
209
|
+
const { runDownAppWithImageRemoval } = require('../commands/app-down');
|
|
210
|
+
await runDownAppWithImageRemoval(appName, { volumes: options.volumes });
|
|
211
|
+
} catch (error) {
|
|
212
|
+
handleCommandError(error, 'down-app');
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
185
217
|
program.command('push <app>')
|
|
186
218
|
.description('Push image to Azure Container Registry')
|
|
187
219
|
.option('-r, --registry <registry>', 'ACR registry URL (overrides variables.yaml)')
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI credential and deployment list command setup.
|
|
3
|
+
* Commands: credential list, deployment list.
|
|
4
|
+
*
|
|
5
|
+
* @fileoverview Credential and deployment list 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 { runCredentialList } = require('../commands/credential-list');
|
|
14
|
+
const { runDeploymentList } = require('../commands/deployment-list');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Sets up credential and deployment list commands
|
|
18
|
+
* @param {Command} program - Commander program instance
|
|
19
|
+
*/
|
|
20
|
+
function setupCredentialDeploymentCommands(program) {
|
|
21
|
+
const credential = program
|
|
22
|
+
.command('credential')
|
|
23
|
+
.description('Manage credentials');
|
|
24
|
+
|
|
25
|
+
credential
|
|
26
|
+
.command('list')
|
|
27
|
+
.description('List credentials from controller/dataplane (GET /api/v1/credential)')
|
|
28
|
+
.option('--controller <url>', 'Controller URL (default: from config)')
|
|
29
|
+
.option('--active-only', 'List only active credentials')
|
|
30
|
+
.option('--page-size <n>', 'Items per page', '50')
|
|
31
|
+
.action(async(options) => {
|
|
32
|
+
try {
|
|
33
|
+
const opts = {
|
|
34
|
+
controller: options.controller,
|
|
35
|
+
activeOnly: options.activeOnly,
|
|
36
|
+
pageSize: parseInt(options.pageSize, 10) || 50
|
|
37
|
+
};
|
|
38
|
+
await runCredentialList(opts);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
logger.error(chalk.red(`Error: ${error.message}`));
|
|
41
|
+
handleCommandError(error, 'credential list');
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const deployment = program
|
|
47
|
+
.command('deployment')
|
|
48
|
+
.description('List deployments');
|
|
49
|
+
|
|
50
|
+
deployment
|
|
51
|
+
.command('list')
|
|
52
|
+
.description('List last N deployments for current environment (default pageSize=50)')
|
|
53
|
+
.option('--controller <url>', 'Controller URL (default: from config)')
|
|
54
|
+
.option('--environment <env>', 'Environment key (default: from config)')
|
|
55
|
+
.option('--page-size <n>', 'Items per page', '50')
|
|
56
|
+
.action(async(options) => {
|
|
57
|
+
try {
|
|
58
|
+
const opts = {
|
|
59
|
+
controller: options.controller,
|
|
60
|
+
environment: options.environment,
|
|
61
|
+
pageSize: parseInt(options.pageSize, 10) || 50
|
|
62
|
+
};
|
|
63
|
+
await runDeploymentList(opts);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
logger.error(chalk.red(`Error: ${error.message}`));
|
|
66
|
+
handleCommandError(error, 'deployment list');
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = { setupCredentialDeploymentCommands };
|
package/lib/cli/setup-utility.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CLI utility command setup (resolve, json, split-json,
|
|
2
|
+
* CLI utility command setup (resolve, json, split-json, show, validate, diff).
|
|
3
3
|
*
|
|
4
4
|
* @fileoverview Utility command definitions for AI Fabrix Builder CLI
|
|
5
5
|
* @author AI Fabrix Team
|
|
@@ -127,30 +127,6 @@ function setupUtilityCommands(program) {
|
|
|
127
127
|
}
|
|
128
128
|
});
|
|
129
129
|
|
|
130
|
-
program.command('genkey <app>')
|
|
131
|
-
.description('Generate deployment key')
|
|
132
|
-
.action(async(appName) => {
|
|
133
|
-
try {
|
|
134
|
-
const jsonPath = await generator.generateDeployJson(appName);
|
|
135
|
-
|
|
136
|
-
const jsonContent = fs.readFileSync(jsonPath, 'utf8');
|
|
137
|
-
const deployment = JSON.parse(jsonContent);
|
|
138
|
-
|
|
139
|
-
const key = deployment.deploymentKey;
|
|
140
|
-
|
|
141
|
-
if (!key) {
|
|
142
|
-
throw new Error('deploymentKey not found in generated JSON');
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
logger.log(`\nDeployment key for ${appName}:`);
|
|
146
|
-
logger.log(key);
|
|
147
|
-
logger.log(chalk.gray(`\nGenerated from: ${jsonPath}`));
|
|
148
|
-
} catch (error) {
|
|
149
|
-
handleCommandError(error, 'genkey');
|
|
150
|
-
process.exit(1);
|
|
151
|
-
}
|
|
152
|
-
});
|
|
153
|
-
|
|
154
130
|
program.command('show <appKey>')
|
|
155
131
|
.description('Show application info from local builder/ or integration/ (offline) or from controller (--online)')
|
|
156
132
|
.option('--online', 'Fetch application data from the controller')
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Down-app command – stop container, optionally volumes, then remove image if unused
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Down-app command implementation
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 2.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const chalk = require('chalk');
|
|
10
|
+
const { exec } = require('child_process');
|
|
11
|
+
const { promisify } = require('util');
|
|
12
|
+
const logger = require('../utils/logger');
|
|
13
|
+
const config = require('../core/config');
|
|
14
|
+
const containerHelpers = require('../utils/app-run-containers');
|
|
15
|
+
const { downApp } = require('../app/down');
|
|
16
|
+
|
|
17
|
+
const execAsync = promisify(exec);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get image ID of a running container (sha or name:tag)
|
|
21
|
+
* @async
|
|
22
|
+
* @param {string} containerName - Docker container name
|
|
23
|
+
* @returns {Promise<string|null>} Image ID or null if container not found / not running
|
|
24
|
+
*/
|
|
25
|
+
async function getContainerImageId(containerName) {
|
|
26
|
+
try {
|
|
27
|
+
const { stdout } = await execAsync(
|
|
28
|
+
`docker inspect --format='{{.Image}}' ${containerName}`,
|
|
29
|
+
{ encoding: 'utf8' }
|
|
30
|
+
);
|
|
31
|
+
const id = (stdout && stdout.trim()) || null;
|
|
32
|
+
return id || null;
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Remove Docker image by ID; ignore "in use" or "no such image"
|
|
40
|
+
* @async
|
|
41
|
+
* @param {string} imageId - Image ID (sha or name:tag)
|
|
42
|
+
*/
|
|
43
|
+
async function removeImageIfUnused(imageId) {
|
|
44
|
+
if (!imageId) return;
|
|
45
|
+
try {
|
|
46
|
+
await execAsync(`docker rmi ${imageId}`);
|
|
47
|
+
logger.log(chalk.green(`✓ Image ${imageId} removed`));
|
|
48
|
+
} catch (err) {
|
|
49
|
+
const msg = (err && err.message) || '';
|
|
50
|
+
if (msg.includes('in use') || msg.includes('is being used')) {
|
|
51
|
+
logger.log(chalk.gray(`Image ${imageId} still in use by another container; not removed`));
|
|
52
|
+
} else if (msg.includes('No such image')) {
|
|
53
|
+
logger.log(chalk.gray(`Image ${imageId} not found (already removed)`));
|
|
54
|
+
} else {
|
|
55
|
+
logger.log(chalk.yellow(`Could not remove image: ${msg}`));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Run down-app: get image from container, stop/remove container (and optionally volume), then remove image if unused
|
|
62
|
+
* @async
|
|
63
|
+
* @param {string} appName - Application name
|
|
64
|
+
* @param {Object} options - { volumes: boolean }
|
|
65
|
+
* @returns {Promise<void>}
|
|
66
|
+
*/
|
|
67
|
+
async function runDownAppWithImageRemoval(appName, options = {}) {
|
|
68
|
+
const developerId = await config.getDeveloperId();
|
|
69
|
+
const containerName = containerHelpers.getContainerName(appName, developerId);
|
|
70
|
+
const imageId = await getContainerImageId(containerName);
|
|
71
|
+
|
|
72
|
+
await downApp(appName, options);
|
|
73
|
+
await removeImageIfUnused(imageId);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = {
|
|
77
|
+
runDownAppWithImageRemoval,
|
|
78
|
+
getContainerImageId,
|
|
79
|
+
removeImageIfUnused
|
|
80
|
+
};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App logs command – show container env (masked) and docker logs for an app
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview App logs command implementation
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 2.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const chalk = require('chalk');
|
|
10
|
+
const { exec, spawn } = require('child_process');
|
|
11
|
+
const { promisify } = require('util');
|
|
12
|
+
const logger = require('../utils/logger');
|
|
13
|
+
const config = require('../core/config');
|
|
14
|
+
const containerHelpers = require('../utils/app-run-containers');
|
|
15
|
+
const { validateAppName } = require('../app/push');
|
|
16
|
+
|
|
17
|
+
const execAsync = promisify(exec);
|
|
18
|
+
|
|
19
|
+
/** Default number of log lines */
|
|
20
|
+
const DEFAULT_TAIL_LINES = 100;
|
|
21
|
+
|
|
22
|
+
/** Env key patterns that indicate a secret (mask value) */
|
|
23
|
+
const SECRET_KEY_PATTERN = /password|secret|token|credential|api[_-]?key/i;
|
|
24
|
+
|
|
25
|
+
/** Prefixes to strip before checking key (avoids masking KEYCLOAK_SERVER_URL etc.) */
|
|
26
|
+
const KEY_PREFIXES_TO_STRIP = /^KEYCLOAK_|^KEY_VAULT_/;
|
|
27
|
+
|
|
28
|
+
/** URL with embedded credentials: scheme://user:password@host → scheme://user:***@host */
|
|
29
|
+
const URL_CREDENTIAL_PATTERN = /(\w+:\/\/)([^:@]*):([^@]+)@/g;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Masks a single env line if the key looks like a secret or value contains URL credentials
|
|
33
|
+
* @param {string} line - Line in form KEY=value
|
|
34
|
+
* @returns {string} Same line or KEY=*** or value with masked URL credentials
|
|
35
|
+
*/
|
|
36
|
+
function maskEnvLine(line) {
|
|
37
|
+
const eq = line.indexOf('=');
|
|
38
|
+
if (eq <= 0) return line;
|
|
39
|
+
const key = line.slice(0, eq);
|
|
40
|
+
const value = line.slice(eq + 1);
|
|
41
|
+
|
|
42
|
+
const keyForCheck = key.replace(KEY_PREFIXES_TO_STRIP, '');
|
|
43
|
+
const isSecretKey = SECRET_KEY_PATTERN.test(keyForCheck);
|
|
44
|
+
|
|
45
|
+
const maskedValue = value.replace(URL_CREDENTIAL_PATTERN, '$1$2:***@');
|
|
46
|
+
const hasUrlCredentials = maskedValue !== value;
|
|
47
|
+
|
|
48
|
+
if (isSecretKey) return `${key}=***`;
|
|
49
|
+
if (hasUrlCredentials) return `${key}=${maskedValue}`;
|
|
50
|
+
return line;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Dump container env (masked) and print to logger
|
|
55
|
+
* @async
|
|
56
|
+
* @param {string} containerName - Docker container name
|
|
57
|
+
* @returns {Promise<void>}
|
|
58
|
+
*/
|
|
59
|
+
async function dumpMaskedEnv(containerName) {
|
|
60
|
+
try {
|
|
61
|
+
const { stdout } = await execAsync(`docker exec ${containerName} env`, { encoding: 'utf8', timeout: 5000 });
|
|
62
|
+
const lines = stdout.split('\n').filter((l) => l.trim());
|
|
63
|
+
if (lines.length === 0) return;
|
|
64
|
+
logger.log(chalk.bold('\n--- Environment (sensitive values masked) ---\n'));
|
|
65
|
+
lines.sort((a, b) => {
|
|
66
|
+
const keyA = a.indexOf('=') > 0 ? a.slice(0, a.indexOf('=')) : a;
|
|
67
|
+
const keyB = b.indexOf('=') > 0 ? b.slice(0, b.indexOf('=')) : b;
|
|
68
|
+
return keyA.localeCompare(keyB);
|
|
69
|
+
});
|
|
70
|
+
lines.forEach((line) => logger.log(maskEnvLine(line)));
|
|
71
|
+
logger.log(chalk.gray('\n--- Logs ---\n'));
|
|
72
|
+
} catch (err) {
|
|
73
|
+
logger.log(chalk.gray('(Could not read container env; container may be stopped)\n'));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Run docker logs (non-follow): tail N lines or full (tail 0)
|
|
79
|
+
* @async
|
|
80
|
+
* @param {string} containerName - Docker container name
|
|
81
|
+
* @param {Object} options - { tail: number } (0 = full, no limit)
|
|
82
|
+
* @returns {Promise<void>}
|
|
83
|
+
*/
|
|
84
|
+
async function runDockerLogs(containerName, options) {
|
|
85
|
+
const args = options.tail === 0 ? ['logs', containerName] : ['logs', '--tail', String(options.tail), containerName];
|
|
86
|
+
return new Promise((resolve, reject) => {
|
|
87
|
+
const proc = spawn('docker', args, { stdio: 'inherit' });
|
|
88
|
+
proc.on('error', reject);
|
|
89
|
+
proc.on('close', (code) => (code === 0 ? resolve() : reject(new Error(`docker logs exited with ${code}`))));
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Run docker logs --follow (stream), optionally with tail
|
|
95
|
+
* @param {string} containerName - Docker container name
|
|
96
|
+
* @param {number} [tail] - Lines to show (0 = full, omit --tail)
|
|
97
|
+
*/
|
|
98
|
+
function runDockerLogsFollow(containerName, tail) {
|
|
99
|
+
const args = tail === 0 ? ['logs', '-f', containerName] : ['logs', '-f', '--tail', String(tail), containerName];
|
|
100
|
+
const proc = spawn('docker', args, { stdio: 'inherit' });
|
|
101
|
+
proc.on('error', (err) => {
|
|
102
|
+
logger.log(chalk.red(`Error: ${err.message}`));
|
|
103
|
+
process.exit(1);
|
|
104
|
+
});
|
|
105
|
+
proc.on('close', (code) => {
|
|
106
|
+
if (code !== 0 && code !== null) process.exit(code);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Run app logs command: optional env dump (masked), then docker logs
|
|
112
|
+
* @async
|
|
113
|
+
* @param {string} appKey - Application key (app name)
|
|
114
|
+
* @param {Object} options - CLI options
|
|
115
|
+
* @param {boolean} [options.follow] - Follow log stream (-f)
|
|
116
|
+
* @param {number} [options.tail] - Number of lines (default 100; 0 = full list)
|
|
117
|
+
* @returns {Promise<void>}
|
|
118
|
+
*/
|
|
119
|
+
async function runAppLogs(appKey, options = {}) {
|
|
120
|
+
validateAppName(appKey);
|
|
121
|
+
const developerId = await config.getDeveloperId();
|
|
122
|
+
const containerName = containerHelpers.getContainerName(appKey, developerId);
|
|
123
|
+
|
|
124
|
+
const follow = !!options.follow;
|
|
125
|
+
const tail = typeof options.tail === 'number' ? options.tail : DEFAULT_TAIL_LINES;
|
|
126
|
+
|
|
127
|
+
logger.log(chalk.blue(`Container: ${containerName}\n`));
|
|
128
|
+
|
|
129
|
+
if (!follow) {
|
|
130
|
+
await dumpMaskedEnv(containerName);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (follow) {
|
|
134
|
+
runDockerLogsFollow(containerName, tail);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
await runDockerLogs(containerName, { tail });
|
|
140
|
+
} catch (err) {
|
|
141
|
+
logger.log(chalk.red(`Error: ${err.message}`));
|
|
142
|
+
throw new Error(`Failed to show logs: ${err.message}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
module.exports = { runAppLogs, maskEnvLine };
|