@aifabrix/builder 2.37.9 → 2.39.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/.cursor/rules/project-rules.mdc +3 -0
- package/README.md +19 -0
- package/integration/hubspot/hubspot-deploy.json +1 -5
- package/integration/hubspot/hubspot-system.json +0 -3
- package/lib/api/applications.api.js +29 -1
- package/lib/api/auth.api.js +14 -0
- package/lib/api/credentials.api.js +34 -0
- package/lib/api/datasources-core.api.js +16 -1
- package/lib/api/datasources-extended.api.js +18 -1
- package/lib/api/deployments.api.js +32 -0
- 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/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/types/service-users.types.js +24 -0
- package/lib/api/wizard.api.js +40 -1
- package/lib/app/deploy.js +86 -21
- package/lib/app/rotate-secret.js +3 -1
- package/lib/app/run-helpers.js +35 -2
- package/lib/app/show-display.js +30 -11
- package/lib/app/show.js +34 -8
- package/lib/cli/index.js +4 -0
- package/lib/cli/setup-app.js +40 -0
- package/lib/cli/setup-credential-deployment.js +72 -0
- package/lib/cli/setup-infra.js +3 -3
- package/lib/cli/setup-service-user.js +52 -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 +24 -1
- package/lib/commands/credential-list.js +104 -0
- package/lib/commands/deployment-list.js +184 -0
- package/lib/commands/service-user.js +193 -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 +2 -2
- package/lib/external-system/deploy.js +79 -15
- package/lib/generator/builders.js +8 -27
- 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 -14
- package/lib/schema/deployment-rules.yaml +121 -0
- package/lib/schema/external-system.schema.json +0 -16
- package/lib/utils/app-register-config.js +10 -12
- package/lib/utils/app-run-containers.js +2 -1
- package/lib/utils/compose-generator.js +2 -1
- package/lib/utils/deployment-errors.js +10 -0
- package/lib/utils/environment-checker.js +25 -6
- 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 +7 -33
- 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 +20 -6
- package/templates/applications/dataplane/env.template +31 -2
- package/templates/applications/dataplane/rbac.yaml +1 -1
- package/templates/applications/dataplane/variables.yaml +7 -4
- package/templates/applications/keycloak/Dockerfile +3 -3
- package/templates/applications/keycloak/README.md +14 -4
- package/templates/applications/keycloak/env.template +17 -2
- package/templates/applications/keycloak/variables.yaml +2 -1
- package/templates/applications/miso-controller/README.md +1 -3
- package/templates/applications/miso-controller/env.template +85 -25
- package/templates/applications/miso-controller/rbac.yaml +15 -0
- package/templates/applications/miso-controller/variables.yaml +24 -23
|
@@ -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",
|
|
@@ -768,18 +772,6 @@
|
|
|
768
772
|
},
|
|
769
773
|
"additionalProperties": false
|
|
770
774
|
},
|
|
771
|
-
"deployment": {
|
|
772
|
-
"type": "object",
|
|
773
|
-
"description": "Deployment configuration for pipeline API",
|
|
774
|
-
"properties": {
|
|
775
|
-
"controllerUrl": {
|
|
776
|
-
"type": "string",
|
|
777
|
-
"description": "Controller API URL for deployment",
|
|
778
|
-
"pattern": "^https://.*$"
|
|
779
|
-
}
|
|
780
|
-
},
|
|
781
|
-
"additionalProperties": false
|
|
782
|
-
},
|
|
783
775
|
"system": {
|
|
784
776
|
"type": "object",
|
|
785
777
|
"description": "Optional: Inline external system configuration for atomic deployment. Uses external-system.schema.json structure via $ref. Alternative to externalIntegration.systems file-based approach.",
|
|
@@ -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
|
|
@@ -102,22 +102,6 @@
|
|
|
102
102
|
"type": "boolean",
|
|
103
103
|
"default": true
|
|
104
104
|
},
|
|
105
|
-
"environment": {
|
|
106
|
-
"type": "object",
|
|
107
|
-
"description": "Environment-level configuration values used by dataplane and external data sources.",
|
|
108
|
-
"properties": {
|
|
109
|
-
"baseUrl": {
|
|
110
|
-
"type": "string",
|
|
111
|
-
"description": "Base API URL or MCP server URL",
|
|
112
|
-
"pattern": "^(http|https)://.*$"
|
|
113
|
-
},
|
|
114
|
-
"region": {
|
|
115
|
-
"type": "string",
|
|
116
|
-
"description": "Optional region setting for API routing"
|
|
117
|
-
}
|
|
118
|
-
},
|
|
119
|
-
"additionalProperties": true
|
|
120
|
-
},
|
|
121
105
|
"authentication": {
|
|
122
106
|
"type": "object",
|
|
123
107
|
"description": "Authentication configuration for external system",
|
|
@@ -112,19 +112,14 @@ function resolveSystemFilePath(appPath, schemaBasePath, systemFileName) {
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
/**
|
|
115
|
-
*
|
|
115
|
+
* Base URL is no longer read from manifest; Controller resolves from configuration at deploy time.
|
|
116
116
|
* @function extractUrlFromSystemJson
|
|
117
|
-
* @param {Object}
|
|
118
|
-
* @param {string}
|
|
119
|
-
* @returns {
|
|
120
|
-
* @throws {Error} If URL is missing
|
|
117
|
+
* @param {Object} _systemJson - System JSON object (unused; kept for call-site compatibility)
|
|
118
|
+
* @param {string} _systemFileName - System file name (unused)
|
|
119
|
+
* @returns {undefined} URL is not supplied from manifest
|
|
121
120
|
*/
|
|
122
|
-
function extractUrlFromSystemJson(
|
|
123
|
-
|
|
124
|
-
if (!url) {
|
|
125
|
-
throw new Error(`Missing environment.baseUrl in ${systemFileName}`);
|
|
126
|
-
}
|
|
127
|
-
return url;
|
|
121
|
+
function extractUrlFromSystemJson(_systemJson, _systemFileName) {
|
|
122
|
+
return undefined;
|
|
128
123
|
}
|
|
129
124
|
|
|
130
125
|
/**
|
|
@@ -209,7 +204,10 @@ async function extractExternalIntegrationUrl(appKey, externalIntegration) {
|
|
|
209
204
|
*/
|
|
210
205
|
async function extractExternalAppConfiguration(appKey, variables, appKeyFromFile, displayName, description) {
|
|
211
206
|
const { url, apiKey } = await extractExternalIntegrationUrl(appKey, variables.externalIntegration);
|
|
212
|
-
const externalIntegration = {
|
|
207
|
+
const externalIntegration = {};
|
|
208
|
+
if (url !== undefined && url !== null) {
|
|
209
|
+
externalIntegration.url = url;
|
|
210
|
+
}
|
|
213
211
|
if (apiKey) {
|
|
214
212
|
externalIntegration.apiKey = apiKey;
|
|
215
213
|
}
|
|
@@ -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);
|
|
@@ -206,6 +206,16 @@ async function handleDeploymentErrors(error, appName, url, alreadyLogged = false
|
|
|
206
206
|
// Extract error message
|
|
207
207
|
const errorMessage = extractErrorMessage(error);
|
|
208
208
|
|
|
209
|
+
// Deployment failed/cancelled (from deploy.js): show message as-is, do not treat as HTTP error
|
|
210
|
+
if (errorMessage.startsWith('Deployment failed:') || errorMessage.startsWith('Deployment cancelled:')) {
|
|
211
|
+
if (!alreadyLogged) {
|
|
212
|
+
await logDeploymentFailure(appName, url, error);
|
|
213
|
+
}
|
|
214
|
+
const err = new Error(errorMessage);
|
|
215
|
+
err.formatted = (error.formatted || errorMessage).trim();
|
|
216
|
+
throw err;
|
|
217
|
+
}
|
|
218
|
+
|
|
209
219
|
// For validation errors (like URL validation), just re-throw them directly
|
|
210
220
|
if (isValidationError(errorMessage)) {
|
|
211
221
|
throwValidationError(error, errorMessage);
|
|
@@ -13,6 +13,8 @@ const fs = require('fs');
|
|
|
13
13
|
const path = require('path');
|
|
14
14
|
const dockerUtils = require('./docker');
|
|
15
15
|
const { getActualSecretsPath } = require('./secrets-path');
|
|
16
|
+
const config = require('../core/config');
|
|
17
|
+
const devConfig = require('./dev-config');
|
|
16
18
|
|
|
17
19
|
/**
|
|
18
20
|
* Checks if Docker is installed and available
|
|
@@ -33,17 +35,28 @@ async function checkDocker() {
|
|
|
33
35
|
|
|
34
36
|
/**
|
|
35
37
|
* Checks if required ports are available
|
|
38
|
+
* Uses developer-specific ports when developer-id greater than 0 (basePort + developerId * 100)
|
|
36
39
|
*
|
|
37
40
|
* @async
|
|
38
41
|
* @function checkPorts
|
|
42
|
+
* @param {number[]} [requiredPorts] - Ports to check. If omitted, uses ports from config developer-id
|
|
39
43
|
* @returns {Promise<string>} 'ok' if all ports are available, 'warning' otherwise
|
|
40
44
|
*/
|
|
41
|
-
async function checkPorts() {
|
|
42
|
-
const requiredPorts = [5432, 6379, 5050, 8081];
|
|
45
|
+
async function checkPorts(requiredPorts) {
|
|
43
46
|
const netstat = require('net');
|
|
47
|
+
let portsToCheck = requiredPorts;
|
|
48
|
+
|
|
49
|
+
if (!portsToCheck || portsToCheck.length === 0) {
|
|
50
|
+
const devId = await config.getDeveloperId();
|
|
51
|
+
const idNum = typeof devId === 'string' ? parseInt(devId, 10) : devId;
|
|
52
|
+
const id = Number.isNaN(idNum) || idNum < 0 ? 0 : idNum;
|
|
53
|
+
const ports = devConfig.getDevPorts(id);
|
|
54
|
+
portsToCheck = [ports.postgres, ports.redis, ports.pgadmin, ports.redisCommander];
|
|
55
|
+
}
|
|
56
|
+
|
|
44
57
|
let portIssues = 0;
|
|
45
58
|
|
|
46
|
-
for (const port of
|
|
59
|
+
for (const port of portsToCheck) {
|
|
47
60
|
try {
|
|
48
61
|
await new Promise((resolve, reject) => {
|
|
49
62
|
const server = netstat.createServer();
|
|
@@ -128,10 +141,16 @@ async function checkEnvironment() {
|
|
|
128
141
|
result.recommendations.push('Install Docker and Docker Compose');
|
|
129
142
|
}
|
|
130
143
|
|
|
131
|
-
// Check ports
|
|
132
|
-
|
|
144
|
+
// Check ports (developer-specific: dev 0 = base ports, dev N = base + N*100)
|
|
145
|
+
const devId = await config.getDeveloperId();
|
|
146
|
+
const idNum = typeof devId === 'string' ? parseInt(devId, 10) : devId;
|
|
147
|
+
const id = Number.isNaN(idNum) || idNum < 0 ? 0 : idNum;
|
|
148
|
+
const ports = devConfig.getDevPorts(id);
|
|
149
|
+
const requiredPorts = [ports.postgres, ports.redis, ports.pgadmin, ports.redisCommander];
|
|
150
|
+
|
|
151
|
+
result.ports = await checkPorts(requiredPorts);
|
|
133
152
|
if (result.ports === 'warning') {
|
|
134
|
-
result.recommendations.push(
|
|
153
|
+
result.recommendations.push(`Some required ports (${requiredPorts.join(', ')}) are in use`);
|
|
135
154
|
}
|
|
136
155
|
|
|
137
156
|
// Check secrets
|
|
@@ -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
|
+
};
|
|
@@ -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;
|
|
@@ -96,20 +96,6 @@ function handlePartialAuthentication(authentication) {
|
|
|
96
96
|
return auth;
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
/**
|
|
100
|
-
* Adds placeholder deployment key if missing
|
|
101
|
-
* @function addPlaceholderDeploymentKey
|
|
102
|
-
* @param {Object} result - Result object
|
|
103
|
-
* @returns {Object} Result object with deployment key
|
|
104
|
-
*/
|
|
105
|
-
function addPlaceholderDeploymentKey(result) {
|
|
106
|
-
if (!result.deploymentKey) {
|
|
107
|
-
// This is a 64-character hex string matching the SHA256 pattern
|
|
108
|
-
result.deploymentKey = '0000000000000000000000000000000000000000000000000000000000000000';
|
|
109
|
-
}
|
|
110
|
-
return result;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
99
|
/**
|
|
114
100
|
* Transforms flat structure to schema-compatible format
|
|
115
101
|
* @function transformFlatStructure
|
|
@@ -126,9 +112,6 @@ function transformFlatStructure(variables, appName) {
|
|
|
126
112
|
result.authentication = handlePartialAuthentication(result.authentication);
|
|
127
113
|
}
|
|
128
114
|
|
|
129
|
-
// Add placeholder deploymentKey for validation
|
|
130
|
-
addPlaceholderDeploymentKey(result);
|
|
131
|
-
|
|
132
115
|
return result;
|
|
133
116
|
}
|
|
134
117
|
|
|
@@ -215,22 +198,14 @@ function validateBuildConfig(build) {
|
|
|
215
198
|
}
|
|
216
199
|
|
|
217
200
|
/**
|
|
218
|
-
* Validates and transforms deployment configuration
|
|
201
|
+
* Validates and transforms deployment configuration.
|
|
202
|
+
* Manifest is generic: deployment URL/env are resolved outside the manifest (user/config).
|
|
219
203
|
* @function validateDeploymentConfig
|
|
220
|
-
* @param {Object}
|
|
221
|
-
* @returns {
|
|
204
|
+
* @param {Object} _deployment - Deployment configuration (unused; manifest is generic)
|
|
205
|
+
* @returns {null} No deployment block emitted from variables
|
|
222
206
|
*/
|
|
223
|
-
function validateDeploymentConfig(
|
|
224
|
-
|
|
225
|
-
return null;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
const deploymentConfig = {};
|
|
229
|
-
if (deployment.controllerUrl && deployment.controllerUrl.trim() !== '' && /^https:\/\/.*$/.test(deployment.controllerUrl)) {
|
|
230
|
-
deploymentConfig.controllerUrl = deployment.controllerUrl;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
return Object.keys(deploymentConfig).length > 0 ? deploymentConfig : null;
|
|
207
|
+
function validateDeploymentConfig(_deployment) {
|
|
208
|
+
return null;
|
|
234
209
|
}
|
|
235
210
|
|
|
236
211
|
/**
|
|
@@ -379,8 +354,7 @@ function transformVariablesForValidation(variables, appName) {
|
|
|
379
354
|
|
|
380
355
|
// Nested structure - transform it
|
|
381
356
|
const transformed = buildBaseTransformedStructure(variables, appName);
|
|
382
|
-
|
|
383
|
-
return addPlaceholderDeploymentKey(result);
|
|
357
|
+
return transformOptionalFields(variables, transformed);
|
|
384
358
|
}
|
|
385
359
|
|
|
386
360
|
module.exports = {
|
|
@@ -148,7 +148,7 @@ function validateConditionalRequirements(manifest, errors, warnings) {
|
|
|
148
148
|
* @returns {void}
|
|
149
149
|
*/
|
|
150
150
|
function validateRequiredFields(manifest, errors) {
|
|
151
|
-
const requiredFields = ['key', 'displayName', 'description', 'type'
|
|
151
|
+
const requiredFields = ['key', 'displayName', 'description', 'type'];
|
|
152
152
|
requiredFields.forEach(field => {
|
|
153
153
|
if (!manifest[field]) {
|
|
154
154
|
errors.push(`Required field "${field}" is missing`);
|
package/package.json
CHANGED
|
@@ -92,8 +92,7 @@ aifabrix dockerfile {{appName}} --force # Generate Dockerfile
|
|
|
92
92
|
aifabrix resolve {{appName}} # Generate .env file
|
|
93
93
|
|
|
94
94
|
# Deployment
|
|
95
|
-
aifabrix json {{appName}} #
|
|
96
|
-
aifabrix genkey {{appName}} # Generate deployment key
|
|
95
|
+
aifabrix json {{appName}} # Generate deployment manifest
|
|
97
96
|
aifabrix push {{appName}} --registry {{registry}} # Push to ACR
|
|
98
97
|
aifabrix deploy {{appName}} # Deploy (controller/env from config)
|
|
99
98
|
|
|
@@ -176,7 +175,6 @@ Controller URL and environment (for `deploy`, `app register`, etc.) are set via
|
|
|
176
175
|
```bash
|
|
177
176
|
aifabrix resolve {{appName}} --force
|
|
178
177
|
aifabrix json {{appName}}
|
|
179
|
-
aifabrix genkey {{appName}}
|
|
180
178
|
```
|
|
181
179
|
|
|
182
180
|
---
|
|
@@ -9,8 +9,8 @@ FROM aifabrix/dataplane:latest
|
|
|
9
9
|
EXPOSE 3001
|
|
10
10
|
|
|
11
11
|
# Health check (documentation; base image may already set this)
|
|
12
|
-
HEALTHCHECK --interval=30s --timeout=
|
|
13
|
-
|
|
12
|
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
|
13
|
+
CMD wget --no-verbose --tries=1 --spider http://localhost:3001/health || exit 1
|
|
14
14
|
|
|
15
15
|
# CMD inherited from base image; override only if needed
|
|
16
16
|
# CMD inherited: uvicorn app.main:app --host 0.0.0.0 --port 3001
|