@aifabrix/builder 2.37.5 → 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/commands/up-miso.js +2 -2
- package/lib/commands/wizard-core.js +39 -27
- package/lib/core/config.js +16 -1
- package/lib/core/secrets.js +42 -50
- package/lib/core/templates.js +2 -1
- package/lib/deployment/environment.js +32 -21
- 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/paths.js +6 -3
- 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
|
@@ -82,20 +82,8 @@ async function getEnvironmentAuth(controllerUrl) {
|
|
|
82
82
|
* @returns {Promise<Object>} Deployment result
|
|
83
83
|
* @throws {Error} If deployment fails
|
|
84
84
|
*/
|
|
85
|
-
/**
|
|
86
|
-
|
|
87
|
-
* @param {string} configPath - Absolute or relative path to config JSON
|
|
88
|
-
* @returns {Object} Valid deploy request { environmentConfig, dryRun? }
|
|
89
|
-
* @throws {Error} If file missing, invalid JSON, or validation fails
|
|
90
|
-
*/
|
|
91
|
-
function loadAndValidateEnvironmentDeployConfig(configPath) {
|
|
92
|
-
const resolvedPath = path.isAbsolute(configPath) ? configPath : path.resolve(process.cwd(), configPath);
|
|
93
|
-
if (!fs.existsSync(resolvedPath)) {
|
|
94
|
-
throw new Error(
|
|
95
|
-
`Environment config file not found: ${resolvedPath}\n` +
|
|
96
|
-
'Use --config <file> with a JSON file containing "environmentConfig" (e.g. templates/infra/environment-dev.json).'
|
|
97
|
-
);
|
|
98
|
-
}
|
|
85
|
+
/** Reads and parses config file; throws if missing, unreadable, or invalid structure. */
|
|
86
|
+
function parseEnvironmentConfigFile(resolvedPath) {
|
|
99
87
|
let raw;
|
|
100
88
|
try {
|
|
101
89
|
raw = fs.readFileSync(resolvedPath, 'utf8');
|
|
@@ -124,14 +112,21 @@ function loadAndValidateEnvironmentDeployConfig(configPath) {
|
|
|
124
112
|
);
|
|
125
113
|
}
|
|
126
114
|
if (typeof parsed.environmentConfig !== 'object' || parsed.environmentConfig === null) {
|
|
127
|
-
throw new Error(
|
|
128
|
-
`"environmentConfig" must be an object. File: ${resolvedPath}`
|
|
129
|
-
);
|
|
115
|
+
throw new Error(`"environmentConfig" must be an object. File: ${resolvedPath}`);
|
|
130
116
|
}
|
|
117
|
+
return parsed;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Validates parsed config against schema and returns deploy request.
|
|
122
|
+
* @param {Object} parsed - Parsed config object
|
|
123
|
+
* @param {string} resolvedPath - Path for error messages
|
|
124
|
+
* @returns {Object} { environmentConfig, dryRun? }
|
|
125
|
+
*/
|
|
126
|
+
function validateEnvironmentDeployParsed(parsed, resolvedPath) {
|
|
131
127
|
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
132
128
|
const validate = ajv.compile(environmentDeployRequestSchema);
|
|
133
|
-
|
|
134
|
-
if (!valid) {
|
|
129
|
+
if (!validate(parsed)) {
|
|
135
130
|
const messages = formatValidationErrors(validate.errors);
|
|
136
131
|
throw new Error(
|
|
137
132
|
`Environment config validation failed (${resolvedPath}):\n • ${messages.join('\n • ')}\n` +
|
|
@@ -144,6 +139,24 @@ function loadAndValidateEnvironmentDeployConfig(configPath) {
|
|
|
144
139
|
};
|
|
145
140
|
}
|
|
146
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Loads and validates environment deploy config from a JSON file
|
|
144
|
+
* @param {string} configPath - Absolute or relative path to config JSON
|
|
145
|
+
* @returns {Object} Valid deploy request { environmentConfig, dryRun? }
|
|
146
|
+
* @throws {Error} If file missing, invalid JSON, or validation fails
|
|
147
|
+
*/
|
|
148
|
+
function loadAndValidateEnvironmentDeployConfig(configPath) {
|
|
149
|
+
const resolvedPath = path.isAbsolute(configPath) ? configPath : path.resolve(process.cwd(), configPath);
|
|
150
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
151
|
+
throw new Error(
|
|
152
|
+
`Environment config file not found: ${resolvedPath}\n` +
|
|
153
|
+
'Use --config <file> with a JSON file containing "environmentConfig" (e.g. templates/infra/environment-dev.json).'
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
const parsed = parseEnvironmentConfigFile(resolvedPath);
|
|
157
|
+
return validateEnvironmentDeployParsed(parsed, resolvedPath);
|
|
158
|
+
}
|
|
159
|
+
|
|
147
160
|
/**
|
|
148
161
|
* Builds environment deployment request from options (config file required)
|
|
149
162
|
* @function buildEnvironmentDeploymentRequest
|
|
@@ -479,8 +492,6 @@ async function deployEnvironment(envKey, options = {}) {
|
|
|
479
492
|
throw error;
|
|
480
493
|
}
|
|
481
494
|
}
|
|
482
|
-
|
|
483
495
|
module.exports = {
|
|
484
496
|
deployEnvironment
|
|
485
497
|
};
|
|
486
|
-
|
|
@@ -126,11 +126,17 @@ function buildAuthentication(rbac) {
|
|
|
126
126
|
* @returns {Object} App metadata
|
|
127
127
|
*/
|
|
128
128
|
function buildAppMetadata(appName, variables) {
|
|
129
|
+
const rawVersion = variables.app?.version;
|
|
130
|
+
const version =
|
|
131
|
+
rawVersion !== undefined && rawVersion !== null && String(rawVersion).trim()
|
|
132
|
+
? String(rawVersion).trim()
|
|
133
|
+
: '1.0.0';
|
|
129
134
|
return {
|
|
130
135
|
key: variables.app?.key || appName,
|
|
131
136
|
displayName: variables.app?.displayName || appName,
|
|
132
137
|
description: variables.app?.description || '',
|
|
133
|
-
type: variables.app?.type || 'webapp'
|
|
138
|
+
type: variables.app?.type || 'webapp',
|
|
139
|
+
version
|
|
134
140
|
};
|
|
135
141
|
}
|
|
136
142
|
|
|
@@ -375,12 +381,11 @@ function buildOptionalFields(deployment, variables, rbac) {
|
|
|
375
381
|
* Builds deployment manifest structure
|
|
376
382
|
* @param {string} appName - Application name
|
|
377
383
|
* @param {Object} variables - Variables configuration
|
|
378
|
-
* @param {string} deploymentKey - Deployment key
|
|
379
384
|
* @param {Array} configuration - Environment configuration
|
|
380
385
|
* @param {Object|null} rbac - RBAC configuration
|
|
381
386
|
* @returns {Object} Deployment manifest
|
|
382
387
|
*/
|
|
383
|
-
function buildManifestStructure(appName, variables,
|
|
388
|
+
function buildManifestStructure(appName, variables, configuration, rbac) {
|
|
384
389
|
const registryMode = variables.image?.registryMode || 'external';
|
|
385
390
|
const filteredConfiguration = filterConfigurationByRegistryMode(configuration, registryMode);
|
|
386
391
|
const deployment = buildBaseDeployment(appName, variables, filteredConfiguration);
|
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
|
|
12
12
|
const path = require('path');
|
|
13
13
|
const { detectAppType } = require('../utils/paths');
|
|
14
|
-
const { generateDeploymentKeyFromJson } = require('../core/key-generator');
|
|
15
14
|
const { loadSystemFile, loadDatasourceFiles } = require('./external');
|
|
16
15
|
const { loadVariables, loadRbac } = require('./helpers');
|
|
17
16
|
|
|
@@ -97,7 +96,7 @@ async function loadSystemWithRbac(appPath, schemaBasePath, systemFile) {
|
|
|
97
96
|
*
|
|
98
97
|
* @example
|
|
99
98
|
* const manifest = await generateControllerManifest('my-hubspot');
|
|
100
|
-
* // Returns: { key, displayName, description, type: "external", system: {...}, dataSources: [...]
|
|
99
|
+
* // Returns: { key, displayName, description, type: "external", system: {...}, dataSources: [...] }
|
|
101
100
|
*/
|
|
102
101
|
async function generateControllerManifest(appName, options = {}) {
|
|
103
102
|
if (!appName || typeof appName !== 'string') {
|
|
@@ -124,13 +123,15 @@ async function generateControllerManifest(appName, options = {}) {
|
|
|
124
123
|
const datasourceFiles = variables.externalIntegration.dataSources || [];
|
|
125
124
|
const datasourceJsons = await loadDatasourceFiles(appPath, schemaBasePath, datasourceFiles);
|
|
126
125
|
|
|
126
|
+
const appVersion = variables.app?.version || variables.externalIntegration?.version || '1.0.0';
|
|
127
|
+
|
|
127
128
|
// Build externalIntegration block (required by application schema for type: "external")
|
|
128
129
|
const externalIntegration = {
|
|
129
130
|
schemaBasePath: schemaBasePath,
|
|
130
131
|
systems: systemFiles,
|
|
131
132
|
dataSources: datasourceFiles,
|
|
132
133
|
autopublish: variables.externalIntegration.autopublish !== false, // default true
|
|
133
|
-
version:
|
|
134
|
+
version: appVersion
|
|
134
135
|
};
|
|
135
136
|
|
|
136
137
|
const manifest = {
|
|
@@ -138,6 +139,7 @@ async function generateControllerManifest(appName, options = {}) {
|
|
|
138
139
|
displayName: metadata.displayName,
|
|
139
140
|
description: metadata.description,
|
|
140
141
|
type: 'external',
|
|
142
|
+
version: appVersion,
|
|
141
143
|
externalIntegration: externalIntegration,
|
|
142
144
|
// Inline system and dataSources for atomic deployment (optional but recommended)
|
|
143
145
|
system: systemJson,
|
|
@@ -148,7 +150,6 @@ async function generateControllerManifest(appName, options = {}) {
|
|
|
148
150
|
requiresStorage: false
|
|
149
151
|
};
|
|
150
152
|
|
|
151
|
-
manifest.deploymentKey = generateDeploymentKeyFromJson(manifest);
|
|
152
153
|
return manifest;
|
|
153
154
|
}
|
|
154
155
|
|
package/lib/generator/index.js
CHANGED
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
|
|
12
12
|
const fs = require('fs');
|
|
13
13
|
const path = require('path');
|
|
14
|
-
const _keyGenerator = require('../core/key-generator');
|
|
15
14
|
const _validator = require('../validation/validator');
|
|
16
15
|
const builders = require('./builders');
|
|
17
16
|
const { detectAppType, getDeployJsonPath } = require('../utils/paths');
|
|
@@ -19,6 +18,7 @@ const splitFunctions = require('./split');
|
|
|
19
18
|
const { loadVariables, loadEnvTemplate, loadRbac, parseEnvironmentVariables } = require('./helpers');
|
|
20
19
|
const { generateExternalSystemApplicationSchema, splitExternalApplicationSchema } = require('./external');
|
|
21
20
|
const { generateControllerManifest } = require('./external-controller-manifest');
|
|
21
|
+
const { resolveVersionForApp } = require('../utils/image-version');
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Generates deployment JSON from application configuration files
|
|
@@ -65,21 +65,15 @@ function loadDeploymentConfigFiles(appPath, appType, appName) {
|
|
|
65
65
|
* @param {Object} variables - Variables configuration
|
|
66
66
|
* @param {Object} envTemplate - Environment template
|
|
67
67
|
* @param {Object} rbac - RBAC configuration
|
|
68
|
-
* @returns {Object} Deployment manifest
|
|
68
|
+
* @returns {Object} Deployment manifest
|
|
69
69
|
* @throws {Error} If validation fails
|
|
70
70
|
*/
|
|
71
71
|
function buildAndValidateDeployment(appName, variables, envTemplate, rbac) {
|
|
72
72
|
// Parse environment variables from template and merge portalInput from variables.yaml
|
|
73
73
|
const configuration = parseEnvironmentVariables(envTemplate, variables);
|
|
74
74
|
|
|
75
|
-
// Build deployment manifest
|
|
76
|
-
const deployment = builders.buildManifestStructure(appName, variables,
|
|
77
|
-
|
|
78
|
-
// Generate deploymentKey from the manifest object (excluding deploymentKey field)
|
|
79
|
-
const deploymentKey = _keyGenerator.generateDeploymentKeyFromJson(deployment);
|
|
80
|
-
|
|
81
|
-
// Add deploymentKey to manifest
|
|
82
|
-
deployment.deploymentKey = deploymentKey;
|
|
75
|
+
// Build deployment manifest (Controller computes deploymentKey from schema)
|
|
76
|
+
const deployment = builders.buildManifestStructure(appName, variables, configuration, rbac);
|
|
83
77
|
|
|
84
78
|
// Validate deployment JSON against schema
|
|
85
79
|
const validation = _validator.validateDeploymentJson(deployment);
|
|
@@ -114,10 +108,13 @@ async function buildDeploymentManifestInMemory(appName, options = {}) {
|
|
|
114
108
|
}
|
|
115
109
|
|
|
116
110
|
const { variables, envTemplate, rbac } = loadDeploymentConfigFiles(appPath, appType, appName);
|
|
111
|
+
const resolved = await resolveVersionForApp(appName, variables, { updateBuilder: false });
|
|
112
|
+
const variablesWithVersion = {
|
|
113
|
+
...variables,
|
|
114
|
+
app: { ...variables.app, version: resolved.version }
|
|
115
|
+
};
|
|
117
116
|
const configuration = parseEnvironmentVariables(envTemplate, variables);
|
|
118
|
-
const deployment = builders.buildManifestStructure(appName,
|
|
119
|
-
const deploymentKey = _keyGenerator.generateDeploymentKeyFromJson(deployment);
|
|
120
|
-
deployment.deploymentKey = deploymentKey;
|
|
117
|
+
const deployment = builders.buildManifestStructure(appName, variablesWithVersion, configuration, rbac);
|
|
121
118
|
|
|
122
119
|
return { deployment, appPath };
|
|
123
120
|
}
|
|
@@ -145,7 +142,12 @@ async function generateDeployJson(appName, options = {}) {
|
|
|
145
142
|
|
|
146
143
|
// Regular app: generate deployment manifest
|
|
147
144
|
const { variables, envTemplate, rbac, jsonPath } = loadDeploymentConfigFiles(appPath, appType, appName);
|
|
148
|
-
const
|
|
145
|
+
const resolved = await resolveVersionForApp(appName, variables, { updateBuilder: false });
|
|
146
|
+
const variablesWithVersion = {
|
|
147
|
+
...variables,
|
|
148
|
+
app: { ...variables.app, version: resolved.version }
|
|
149
|
+
};
|
|
150
|
+
const deployment = buildAndValidateDeployment(appName, variablesWithVersion, envTemplate, rbac);
|
|
149
151
|
|
|
150
152
|
// Write deployment JSON
|
|
151
153
|
const jsonContent = JSON.stringify(deployment, null, 2);
|
package/lib/generator/split.js
CHANGED
|
@@ -98,6 +98,7 @@ function extractAppSection(deployment) {
|
|
|
98
98
|
if (deployment.displayName) app.displayName = deployment.displayName;
|
|
99
99
|
if (deployment.description) app.description = deployment.description;
|
|
100
100
|
if (deployment.type) app.type = deployment.type;
|
|
101
|
+
if (deployment.version) app.version = deployment.version;
|
|
101
102
|
return app;
|
|
102
103
|
}
|
|
103
104
|
|
package/lib/generator/wizard.js
CHANGED
|
@@ -234,8 +234,11 @@ async function generateOrUpdateVariablesYaml(params) {
|
|
|
234
234
|
key: appName,
|
|
235
235
|
displayName: systemConfig.displayName || appName.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase()),
|
|
236
236
|
description: systemConfig.description || `External system integration for ${appName}`,
|
|
237
|
-
type: 'external'
|
|
237
|
+
type: 'external',
|
|
238
|
+
version: '1.0.0'
|
|
238
239
|
};
|
|
240
|
+
} else {
|
|
241
|
+
variables.app.version = variables.app.version || '1.0.0';
|
|
239
242
|
}
|
|
240
243
|
|
|
241
244
|
// Set deployment config if not present
|
|
@@ -57,8 +57,7 @@
|
|
|
57
57
|
"key",
|
|
58
58
|
"displayName",
|
|
59
59
|
"description",
|
|
60
|
-
"type"
|
|
61
|
-
"deploymentKey"
|
|
60
|
+
"type"
|
|
62
61
|
],
|
|
63
62
|
"properties": {
|
|
64
63
|
"key": {
|
|
@@ -91,6 +90,11 @@
|
|
|
91
90
|
"external"
|
|
92
91
|
]
|
|
93
92
|
},
|
|
93
|
+
"version": {
|
|
94
|
+
"type": "string",
|
|
95
|
+
"description": "Application version (semantic); default 1.0.0 when empty",
|
|
96
|
+
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+(-[a-zA-Z0-9.-]+)?$"
|
|
97
|
+
},
|
|
94
98
|
"image": {
|
|
95
99
|
"type": "string",
|
|
96
100
|
"description": "Container image reference",
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Deployment rules – Central mapping for Controller
|
|
2
|
+
#
|
|
3
|
+
# Defines which manifest paths trigger deployment and which can differ per environment.
|
|
4
|
+
# Controller uses this file (or equivalent) for deployment key computation and value merge.
|
|
5
|
+
# Schemas (application, external-system, external-datasource) remain clean; no x-* annotations.
|
|
6
|
+
#
|
|
7
|
+
# Semantics:
|
|
8
|
+
# triggerPaths: Change affects deployment key / requires deploy
|
|
9
|
+
# overridablePaths: Value can differ per environment (preserve on promote)
|
|
10
|
+
# A path may appear in both (e.g. authentication.endpoints triggers deploy and is overridable).
|
|
11
|
+
#
|
|
12
|
+
# Path format: Dot notation. Child paths override parent when both match.
|
|
13
|
+
# Schema keys: application | externalSystem | externalDataSource
|
|
14
|
+
|
|
15
|
+
application:
|
|
16
|
+
triggerPaths:
|
|
17
|
+
- key
|
|
18
|
+
- displayName
|
|
19
|
+
- description
|
|
20
|
+
- type
|
|
21
|
+
- version
|
|
22
|
+
- image
|
|
23
|
+
- registryMode
|
|
24
|
+
- port
|
|
25
|
+
- requiresDatabase
|
|
26
|
+
- databases
|
|
27
|
+
- requiresRedis
|
|
28
|
+
- requiresStorage
|
|
29
|
+
- configuration
|
|
30
|
+
- configuration.items
|
|
31
|
+
- configuration.items.required
|
|
32
|
+
- configuration.items.portalInput
|
|
33
|
+
- healthCheck
|
|
34
|
+
- healthCheck.path
|
|
35
|
+
- healthCheck.probePath
|
|
36
|
+
- healthCheck.probeRequestType
|
|
37
|
+
- healthCheck.probeProtocol
|
|
38
|
+
- frontDoorRouting
|
|
39
|
+
- authentication
|
|
40
|
+
- roles
|
|
41
|
+
- permissions
|
|
42
|
+
- repository
|
|
43
|
+
- startupCommand
|
|
44
|
+
- runtimeVersion
|
|
45
|
+
- scaling
|
|
46
|
+
- build
|
|
47
|
+
- deployment
|
|
48
|
+
- externalIntegration
|
|
49
|
+
overridablePaths:
|
|
50
|
+
- configuration.items.value
|
|
51
|
+
- authentication.endpoints
|
|
52
|
+
- deployment.controllerUrl
|
|
53
|
+
- healthCheck.interval
|
|
54
|
+
- healthCheck.probeIntervalInSeconds
|
|
55
|
+
|
|
56
|
+
externalSystem:
|
|
57
|
+
triggerPaths:
|
|
58
|
+
- key
|
|
59
|
+
- displayName
|
|
60
|
+
- description
|
|
61
|
+
- type
|
|
62
|
+
- enabled
|
|
63
|
+
- environment
|
|
64
|
+
- authentication
|
|
65
|
+
- openapi
|
|
66
|
+
- mcp
|
|
67
|
+
- dataSources
|
|
68
|
+
- configuration
|
|
69
|
+
- configuration.items
|
|
70
|
+
- tags
|
|
71
|
+
- roles
|
|
72
|
+
- permissions
|
|
73
|
+
- endpoints
|
|
74
|
+
- endpointsActive
|
|
75
|
+
- generateMcpContract
|
|
76
|
+
- generateOpenApiContract
|
|
77
|
+
overridablePaths:
|
|
78
|
+
- environment.baseUrl
|
|
79
|
+
- environment.region
|
|
80
|
+
- authentication.oauth2
|
|
81
|
+
- authentication.apikey
|
|
82
|
+
- authentication.basic
|
|
83
|
+
- authentication.aad
|
|
84
|
+
- openapi.specUrl
|
|
85
|
+
- openapi.documentKey
|
|
86
|
+
- mcp.serverUrl
|
|
87
|
+
- mcp.toolPrefix
|
|
88
|
+
- configuration.items.value
|
|
89
|
+
- credentialIdOrKey
|
|
90
|
+
|
|
91
|
+
externalDataSource:
|
|
92
|
+
triggerPaths:
|
|
93
|
+
- key
|
|
94
|
+
- displayName
|
|
95
|
+
- description
|
|
96
|
+
- enabled
|
|
97
|
+
- systemKey
|
|
98
|
+
- entityType
|
|
99
|
+
- resourceType
|
|
100
|
+
- version
|
|
101
|
+
- metadataSchema
|
|
102
|
+
- fieldMappings
|
|
103
|
+
- exposed
|
|
104
|
+
- validation
|
|
105
|
+
- quality
|
|
106
|
+
- indexing
|
|
107
|
+
- context
|
|
108
|
+
- documentStorage
|
|
109
|
+
- portalInput
|
|
110
|
+
- capabilities
|
|
111
|
+
- execution
|
|
112
|
+
- config
|
|
113
|
+
- openapi
|
|
114
|
+
overridablePaths:
|
|
115
|
+
- sync
|
|
116
|
+
- sync.mode
|
|
117
|
+
- sync.schedule
|
|
118
|
+
- sync.batchSize
|
|
119
|
+
- sync.maxParallelRequests
|
|
120
|
+
- openapi.baseUrl
|
|
121
|
+
- openapi.resourcePath
|
|
@@ -458,7 +458,8 @@ async function generateDockerCompose(appName, appConfig, options) {
|
|
|
458
458
|
const language = appConfig.build?.language || appConfig.language || 'typescript';
|
|
459
459
|
const template = loadDockerComposeTemplate(language);
|
|
460
460
|
const port = options.port || appConfig.port || 3000;
|
|
461
|
-
const imageOverride = options.image || options.imageOverride
|
|
461
|
+
const imageOverride = options.image || options.imageOverride ||
|
|
462
|
+
(options.tag ? `${getImageName(appConfig, appName)}:${options.tag}` : null);
|
|
462
463
|
const { devId, idNum } = await getDeveloperIdAndNumeric();
|
|
463
464
|
const { networkName, containerName } = buildNetworkAndContainerNames(appName, devId, idNum);
|
|
464
465
|
const serviceConfig = buildServiceConfig(appName, appConfig, port, devId, imageOverride);
|
|
@@ -75,7 +75,6 @@ const CATEGORIES = [
|
|
|
75
75
|
{ name: 'resolve', term: 'resolve <app>' },
|
|
76
76
|
{ name: 'json', term: 'json <app>' },
|
|
77
77
|
{ name: 'split-json', term: 'split-json <app>' },
|
|
78
|
-
{ name: 'genkey', term: 'genkey <app>' },
|
|
79
78
|
{ name: 'validate', term: 'validate <appOrFile>' },
|
|
80
79
|
{ name: 'diff', term: 'diff <file1> <file2>' }
|
|
81
80
|
]
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Fabrix Builder - Image Version Resolution
|
|
3
|
+
*
|
|
4
|
+
* Resolves application version from Docker image (OCI label or semver tag).
|
|
5
|
+
* When template is empty or image version is greater, uses image version.
|
|
6
|
+
*
|
|
7
|
+
* @fileoverview Image version resolution utilities
|
|
8
|
+
* @author AI Fabrix Team
|
|
9
|
+
* @version 2.0.0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { exec } = require('child_process');
|
|
13
|
+
const { promisify } = require('util');
|
|
14
|
+
const fs = require('fs').promises;
|
|
15
|
+
const fsSync = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const yaml = require('js-yaml');
|
|
18
|
+
const { getBuilderPath } = require('./paths');
|
|
19
|
+
const composeGenerator = require('./compose-generator');
|
|
20
|
+
const containerHelpers = require('./app-run-containers');
|
|
21
|
+
|
|
22
|
+
const execAsync = promisify(exec);
|
|
23
|
+
|
|
24
|
+
const OCI_VERSION_LABEL = 'org.opencontainers.image.version';
|
|
25
|
+
const SEMVER_REGEX = /^v?(\d+\.\d+\.\d+)(?:-[-.\w]+)?(?:\+[-.\w]+)?$/i;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Gets version from Docker image via OCI label or semver tag
|
|
29
|
+
* @async
|
|
30
|
+
* @param {string} imageName - Image name (e.g. aifabrix/dataplane)
|
|
31
|
+
* @param {string} imageTag - Image tag (e.g. v1.0.0, latest)
|
|
32
|
+
* @returns {Promise<string|null>} Version string or null if not found
|
|
33
|
+
*/
|
|
34
|
+
async function getVersionFromImage(imageName, imageTag) {
|
|
35
|
+
if (!imageName || typeof imageName !== 'string') {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const tag = imageTag || 'latest';
|
|
39
|
+
const fullImage = `${imageName}:${tag}`;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const labelFormat = `{{index .Config.Labels "${OCI_VERSION_LABEL}"}}`;
|
|
43
|
+
const { stdout } = await execAsync(
|
|
44
|
+
`docker inspect --format '${labelFormat}' "${fullImage}" 2>/dev/null || true`,
|
|
45
|
+
{ timeout: 10000 }
|
|
46
|
+
);
|
|
47
|
+
const labelValue = (stdout || '').trim();
|
|
48
|
+
if (labelValue && labelValue !== '<no value>') {
|
|
49
|
+
return labelValue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const tagMatch = tag.match(SEMVER_REGEX);
|
|
53
|
+
if (tagMatch) {
|
|
54
|
+
return tagMatch[1];
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Compares two semantic versions
|
|
64
|
+
* @param {string} a - First version
|
|
65
|
+
* @param {string} b - Second version
|
|
66
|
+
* @returns {number} -1 if a < b, 0 if a === b, 1 if a > b
|
|
67
|
+
*/
|
|
68
|
+
function compareSemver(a, b) {
|
|
69
|
+
if (!a || !b) {
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
const parse = (v) => {
|
|
73
|
+
const m = String(v).match(SEMVER_REGEX);
|
|
74
|
+
if (!m) return null;
|
|
75
|
+
const parts = m[1].split('.').map(Number);
|
|
76
|
+
return { major: parts[0] || 0, minor: parts[1] || 0, patch: parts[2] || 0 };
|
|
77
|
+
};
|
|
78
|
+
const pa = parse(a);
|
|
79
|
+
const pb = parse(b);
|
|
80
|
+
if (!pa || !pb) return 0;
|
|
81
|
+
if (pa.major !== pb.major) return pa.major > pb.major ? 1 : -1;
|
|
82
|
+
if (pa.minor !== pb.minor) return pa.minor > pb.minor ? 1 : -1;
|
|
83
|
+
if (pa.patch !== pb.patch) return pa.patch > pb.patch ? 1 : -1;
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Resolves version for external app (app.version or externalIntegration.version)
|
|
89
|
+
* @param {Object} variables - Parsed variables.yaml
|
|
90
|
+
* @returns {string}
|
|
91
|
+
*/
|
|
92
|
+
function resolveExternalVersion(variables) {
|
|
93
|
+
const version =
|
|
94
|
+
variables.app?.version ||
|
|
95
|
+
variables.externalIntegration?.version ||
|
|
96
|
+
'1.0.0';
|
|
97
|
+
return String(version).trim() || '1.0.0';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Resolves version for regular app when image exists
|
|
102
|
+
* @async
|
|
103
|
+
* @param {string} imageName - Image name
|
|
104
|
+
* @param {string} imageTag - Image tag
|
|
105
|
+
* @param {string} templateVersion - Template version (may be empty)
|
|
106
|
+
* @returns {Promise<{ version: string, fromImage: boolean }>}
|
|
107
|
+
*/
|
|
108
|
+
async function resolveRegularVersion(imageName, imageTag, templateVersion) {
|
|
109
|
+
const templateEmpty =
|
|
110
|
+
templateVersion === undefined ||
|
|
111
|
+
templateVersion === null ||
|
|
112
|
+
String(templateVersion).trim() === '';
|
|
113
|
+
const templateStr = String(templateVersion || '').trim();
|
|
114
|
+
|
|
115
|
+
const imageVersion = await getVersionFromImage(imageName, imageTag);
|
|
116
|
+
const useImage =
|
|
117
|
+
imageVersion &&
|
|
118
|
+
(templateEmpty || compareSemver(imageVersion, templateStr) >= 0);
|
|
119
|
+
|
|
120
|
+
const version = useImage ? imageVersion : (templateEmpty ? '1.0.0' : templateStr);
|
|
121
|
+
return { version, fromImage: Boolean(useImage) };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Resolves version for an app: from image when image exists and template empty or smaller
|
|
126
|
+
* @async
|
|
127
|
+
* @param {string} appName - Application name
|
|
128
|
+
* @param {Object} variables - Parsed variables.yaml
|
|
129
|
+
* @param {Object} [options] - Options
|
|
130
|
+
* @param {boolean} [options.updateBuilder] - When true, update builder/variables.yaml if fromImage
|
|
131
|
+
* @param {string} [options.builderPath] - Builder path (defaults to getBuilderPath(appName))
|
|
132
|
+
* @returns {Promise<{ version: string, fromImage: boolean, updated: boolean }>}
|
|
133
|
+
*/
|
|
134
|
+
async function resolveVersionForApp(appName, variables, options = {}) {
|
|
135
|
+
if (!appName || typeof appName !== 'string') {
|
|
136
|
+
return { version: '1.0.0', fromImage: false, updated: false };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (variables?.externalIntegration) {
|
|
140
|
+
const version = resolveExternalVersion(variables);
|
|
141
|
+
return { version, fromImage: false, updated: false };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const imageName = composeGenerator.getImageName(variables, appName);
|
|
145
|
+
const imageTag = variables?.image?.tag || 'latest';
|
|
146
|
+
const imageExists = await containerHelpers.checkImageExists(imageName, imageTag);
|
|
147
|
+
|
|
148
|
+
if (!imageExists) {
|
|
149
|
+
const templateVersion = variables?.app?.version;
|
|
150
|
+
const templateEmpty =
|
|
151
|
+
templateVersion === undefined ||
|
|
152
|
+
templateVersion === null ||
|
|
153
|
+
String(templateVersion).trim() === '';
|
|
154
|
+
const fallback = templateEmpty ? '1.0.0' : String(templateVersion).trim();
|
|
155
|
+
return { version: fallback, fromImage: false, updated: false };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const { version, fromImage } = await resolveRegularVersion(
|
|
159
|
+
imageName,
|
|
160
|
+
imageTag,
|
|
161
|
+
variables?.app?.version
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
let updated = false;
|
|
165
|
+
if (fromImage && options.updateBuilder) {
|
|
166
|
+
const builderPath = options.builderPath || getBuilderPath(appName);
|
|
167
|
+
updated = await updateAppVersionInVariablesYaml(builderPath, version);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return { version, fromImage, updated };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Updates app.version in builder variables.yaml
|
|
175
|
+
* @async
|
|
176
|
+
* @param {string} builderPath - Path to builder app directory
|
|
177
|
+
* @param {string} version - Version to set
|
|
178
|
+
* @returns {Promise<boolean>} True if file was updated
|
|
179
|
+
*/
|
|
180
|
+
async function updateAppVersionInVariablesYaml(builderPath, version) {
|
|
181
|
+
if (!builderPath || !version || typeof version !== 'string') {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
const variablesPath = path.join(builderPath, 'variables.yaml');
|
|
185
|
+
if (!fsSync.existsSync(variablesPath)) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
const content = await fs.readFile(variablesPath, 'utf8');
|
|
191
|
+
const parsed = yaml.load(content) || {};
|
|
192
|
+
if (!parsed.app) {
|
|
193
|
+
parsed.app = {};
|
|
194
|
+
}
|
|
195
|
+
parsed.app.version = version;
|
|
196
|
+
const dumped = yaml.dump(parsed, { lineWidth: -1 });
|
|
197
|
+
await fs.writeFile(variablesPath, dumped, { mode: 0o644, encoding: 'utf8' });
|
|
198
|
+
return true;
|
|
199
|
+
} catch {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
module.exports = {
|
|
205
|
+
getVersionFromImage,
|
|
206
|
+
compareSemver,
|
|
207
|
+
resolveVersionForApp,
|
|
208
|
+
updateAppVersionInVariablesYaml
|
|
209
|
+
};
|
package/lib/utils/paths.js
CHANGED
|
@@ -31,11 +31,15 @@ function safeHomedir() {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
|
-
* Returns the path to the config
|
|
35
|
-
*
|
|
34
|
+
* Returns the path to the config directory (same precedence as config.js so both read the same config).
|
|
35
|
+
* Priority: AIFABRIX_CONFIG (dirname) → AIFABRIX_HOME → ~/.aifabrix.
|
|
36
36
|
* @returns {string} Absolute path to config directory
|
|
37
37
|
*/
|
|
38
38
|
function getConfigDirForPaths() {
|
|
39
|
+
const configFile = process.env.AIFABRIX_CONFIG && typeof process.env.AIFABRIX_CONFIG === 'string';
|
|
40
|
+
if (configFile) {
|
|
41
|
+
return path.dirname(path.resolve(process.env.AIFABRIX_CONFIG.trim()));
|
|
42
|
+
}
|
|
39
43
|
if (process.env.AIFABRIX_HOME && typeof process.env.AIFABRIX_HOME === 'string') {
|
|
40
44
|
return path.resolve(process.env.AIFABRIX_HOME.trim());
|
|
41
45
|
}
|
|
@@ -480,7 +484,6 @@ async function detectAppType(appName, options = {}) {
|
|
|
480
484
|
// Check builder folder (backward compatibility)
|
|
481
485
|
return checkBuilderFolder(appName);
|
|
482
486
|
}
|
|
483
|
-
|
|
484
487
|
module.exports = {
|
|
485
488
|
getAifabrixHome,
|
|
486
489
|
getConfigDirForPaths,
|
|
@@ -199,7 +199,7 @@ function detectFromDatasourceFields(parsed) {
|
|
|
199
199
|
* @returns {string|null} Schema type or null if not detected
|
|
200
200
|
*/
|
|
201
201
|
function detectFromApplicationFields(parsed) {
|
|
202
|
-
if (parsed.
|
|
202
|
+
if (parsed.image && parsed.registryMode && parsed.port) {
|
|
203
203
|
return 'application';
|
|
204
204
|
}
|
|
205
205
|
return null;
|