@aifabrix/builder 2.1.5 → 2.1.7
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/lib/commands/app.js +43 -0
- package/lib/secrets.js +104 -57
- package/lib/utils/env-template.js +70 -0
- package/lib/utils/local-secrets.js +98 -0
- package/lib/utils/secrets-path.js +111 -0
- package/lib/utils/secrets-utils.js +206 -0
- package/package.json +1 -1
- package/templates/applications/keycloak/Dockerfile +3 -2
- package/templates/applications/keycloak/env.template +5 -0
- package/templates/applications/keycloak/variables.yaml +3 -3
- package/templates/applications/miso-controller/Dockerfile +3 -0
- package/templates/applications/miso-controller/env.template +3 -3
- package/test-output.txt +5431 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Fabrix Builder Secrets Utilities
|
|
3
|
+
*
|
|
4
|
+
* This module provides utility functions for loading and processing secrets.
|
|
5
|
+
* Helper functions for secrets.js module.
|
|
6
|
+
*
|
|
7
|
+
* @fileoverview Secrets utility functions for AI Fabrix Builder
|
|
8
|
+
* @author AI Fabrix Team
|
|
9
|
+
* @version 2.0.0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const yaml = require('js-yaml');
|
|
15
|
+
const os = require('os');
|
|
16
|
+
const logger = require('./logger');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Loads secrets from file with cascading lookup support
|
|
20
|
+
* First checks ~/.aifabrix/secrets.local.yaml, then build.secrets from variables.yaml
|
|
21
|
+
*
|
|
22
|
+
* @async
|
|
23
|
+
* @function loadSecretsFromFile
|
|
24
|
+
* @param {string} filePath - Path to secrets file
|
|
25
|
+
* @returns {Promise<Object>} Loaded secrets object or empty object if file doesn't exist
|
|
26
|
+
*/
|
|
27
|
+
async function loadSecretsFromFile(filePath) {
|
|
28
|
+
if (!fs.existsSync(filePath)) {
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
34
|
+
const secrets = yaml.load(content);
|
|
35
|
+
|
|
36
|
+
if (!secrets || typeof secrets !== 'object') {
|
|
37
|
+
return {};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return secrets;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
logger.warn(`Warning: Could not read secrets file ${filePath}: ${error.message}`);
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Loads user secrets from ~/.aifabrix/secrets.local.yaml
|
|
49
|
+
* @function loadUserSecrets
|
|
50
|
+
* @returns {Object} Loaded secrets object or empty object
|
|
51
|
+
*/
|
|
52
|
+
function loadUserSecrets() {
|
|
53
|
+
const userSecretsPath = path.join(os.homedir(), '.aifabrix', 'secrets.local.yaml');
|
|
54
|
+
if (!fs.existsSync(userSecretsPath)) {
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const content = fs.readFileSync(userSecretsPath, 'utf8');
|
|
60
|
+
const secrets = yaml.load(content);
|
|
61
|
+
if (!secrets || typeof secrets !== 'object') {
|
|
62
|
+
throw new Error(`Invalid secrets file format: ${userSecretsPath}`);
|
|
63
|
+
}
|
|
64
|
+
return secrets;
|
|
65
|
+
} catch (error) {
|
|
66
|
+
if (error.message.includes('Invalid secrets file format')) {
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
logger.warn(`Warning: Could not read secrets file ${userSecretsPath}: ${error.message}`);
|
|
70
|
+
return {};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Loads build secrets from variables.yaml and merges with existing secrets
|
|
76
|
+
* @async
|
|
77
|
+
* @function loadBuildSecrets
|
|
78
|
+
* @param {Object} mergedSecrets - Existing secrets to merge with
|
|
79
|
+
* @param {string} appName - Application name
|
|
80
|
+
* @returns {Promise<Object>} Merged secrets object
|
|
81
|
+
*/
|
|
82
|
+
async function loadBuildSecrets(mergedSecrets, appName) {
|
|
83
|
+
const variablesPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
|
|
84
|
+
if (!fs.existsSync(variablesPath)) {
|
|
85
|
+
return mergedSecrets;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const variablesContent = fs.readFileSync(variablesPath, 'utf8');
|
|
90
|
+
const variables = yaml.load(variablesContent);
|
|
91
|
+
|
|
92
|
+
if (variables?.build?.secrets) {
|
|
93
|
+
const buildSecretsPath = path.resolve(
|
|
94
|
+
path.dirname(variablesPath),
|
|
95
|
+
variables.build.secrets
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const buildSecrets = await loadSecretsFromFile(buildSecretsPath);
|
|
99
|
+
|
|
100
|
+
// Merge: user's file takes priority, but use build.secrets for missing/empty values
|
|
101
|
+
for (const [key, value] of Object.entries(buildSecrets)) {
|
|
102
|
+
if (!(key in mergedSecrets) || !mergedSecrets[key] || mergedSecrets[key] === '') {
|
|
103
|
+
mergedSecrets[key] = value;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
} catch (error) {
|
|
108
|
+
logger.warn(`Warning: Could not load build.secrets from variables.yaml: ${error.message}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return mergedSecrets;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Loads default secrets from ~/.aifabrix/secrets.yaml
|
|
116
|
+
* @function loadDefaultSecrets
|
|
117
|
+
* @returns {Object} Loaded secrets object or empty object
|
|
118
|
+
*/
|
|
119
|
+
function loadDefaultSecrets() {
|
|
120
|
+
const defaultPath = path.join(os.homedir(), '.aifabrix', 'secrets.yaml');
|
|
121
|
+
if (!fs.existsSync(defaultPath)) {
|
|
122
|
+
return {};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const content = fs.readFileSync(defaultPath, 'utf8');
|
|
127
|
+
const secrets = yaml.load(content);
|
|
128
|
+
if (!secrets || typeof secrets !== 'object') {
|
|
129
|
+
throw new Error(`Invalid secrets file format: ${defaultPath}`);
|
|
130
|
+
}
|
|
131
|
+
return secrets;
|
|
132
|
+
} catch (error) {
|
|
133
|
+
if (error.message.includes('Invalid secrets file format')) {
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
logger.warn(`Warning: Could not read secrets file ${defaultPath}: ${error.message}`);
|
|
137
|
+
return {};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Builds a map of hostname to service name from environment config
|
|
143
|
+
* @function buildHostnameToServiceMap
|
|
144
|
+
* @param {Object} dockerHosts - Docker environment hosts configuration
|
|
145
|
+
* @returns {Object} Map of hostname to service name
|
|
146
|
+
*/
|
|
147
|
+
function buildHostnameToServiceMap(dockerHosts) {
|
|
148
|
+
const hostnameToService = {};
|
|
149
|
+
for (const [key, hostname] of Object.entries(dockerHosts)) {
|
|
150
|
+
if (key.endsWith('_HOST')) {
|
|
151
|
+
// Use hostname directly as service name (e.g., 'keycloak', 'miso-controller')
|
|
152
|
+
hostnameToService[hostname] = hostname;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return hostnameToService;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Resolves port for a single URL by looking up service's variables.yaml
|
|
160
|
+
* @function resolveUrlPort
|
|
161
|
+
* @param {string} protocol - URL protocol (http:// or https://)
|
|
162
|
+
* @param {string} hostname - Service hostname
|
|
163
|
+
* @param {string} port - Current port
|
|
164
|
+
* @param {string} urlPath - URL path and query string
|
|
165
|
+
* @param {Object} hostnameToService - Map of hostname to service name
|
|
166
|
+
* @returns {string} URL with resolved port
|
|
167
|
+
*/
|
|
168
|
+
function resolveUrlPort(protocol, hostname, port, urlPath, hostnameToService) {
|
|
169
|
+
const serviceName = hostnameToService[hostname];
|
|
170
|
+
if (!serviceName) {
|
|
171
|
+
// Not a service hostname, keep original
|
|
172
|
+
return `${protocol}${hostname}:${port}${urlPath}`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Try to load service's variables.yaml
|
|
176
|
+
const serviceVariablesPath = path.join(process.cwd(), 'builder', serviceName, 'variables.yaml');
|
|
177
|
+
if (!fs.existsSync(serviceVariablesPath)) {
|
|
178
|
+
// Service variables.yaml not found, keep original port
|
|
179
|
+
return `${protocol}${hostname}:${port}${urlPath}`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
const variablesContent = fs.readFileSync(serviceVariablesPath, 'utf8');
|
|
184
|
+
const variables = yaml.load(variablesContent);
|
|
185
|
+
|
|
186
|
+
// Get containerPort or fall back to port
|
|
187
|
+
const containerPort = variables?.build?.containerPort || variables?.port || port;
|
|
188
|
+
|
|
189
|
+
// Replace port in URL
|
|
190
|
+
return `${protocol}${hostname}:${containerPort}${urlPath}`;
|
|
191
|
+
} catch (error) {
|
|
192
|
+
// Error loading variables.yaml, keep original port
|
|
193
|
+
logger.warn(`Warning: Could not load variables.yaml for service ${serviceName}: ${error.message}`);
|
|
194
|
+
return `${protocol}${hostname}:${port}${urlPath}`;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
module.exports = {
|
|
199
|
+
loadSecretsFromFile,
|
|
200
|
+
loadUserSecrets,
|
|
201
|
+
loadBuildSecrets,
|
|
202
|
+
loadDefaultSecrets,
|
|
203
|
+
buildHostnameToServiceMap,
|
|
204
|
+
resolveUrlPort
|
|
205
|
+
};
|
|
206
|
+
|
package/package.json
CHANGED
|
@@ -21,9 +21,10 @@ RUN if [ -d "/tmp/build-context/themes" ] && [ "$(ls -A /tmp/build-context/theme
|
|
|
21
21
|
fi && \
|
|
22
22
|
rm -rf /tmp/build-context
|
|
23
23
|
|
|
24
|
-
# Build Keycloak with health checks enabled
|
|
24
|
+
# Build Keycloak with health checks enabled (similar to Keycloak 24.0)
|
|
25
25
|
# This enables the /health, /health/live, /health/ready, and /health/started endpoints
|
|
26
|
-
|
|
26
|
+
# Expose health endpoints on main HTTP port (like 24.0) instead of management port
|
|
27
|
+
RUN /opt/keycloak/bin/kc.sh build --health-enabled=true --http-management-health-enabled=false
|
|
27
28
|
|
|
28
29
|
# Switch back to keycloak user
|
|
29
30
|
USER keycloak
|
|
@@ -15,8 +15,13 @@ KC_HTTP_ENABLED=true
|
|
|
15
15
|
# HEALTH CHECK CONFIGURATION
|
|
16
16
|
# =============================================================================
|
|
17
17
|
# Enable health check endpoints: /health, /health/live, /health/ready, /health/started
|
|
18
|
+
# Keycloak 26.4.0+: Health endpoints are exposed on main HTTP(S) port (8080) by default
|
|
19
|
+
# Set http-management-health-enabled=false to expose on main port instead of management port
|
|
18
20
|
|
|
19
21
|
KC_HEALTH_ENABLED=true
|
|
22
|
+
# Expose health endpoints on main HTTP port (like Keycloak 24.0)
|
|
23
|
+
# Set to false to expose on main port instead of management port (9000)
|
|
24
|
+
KC_HTTP_MANAGEMENT_HEALTH_ENABLED=false
|
|
20
25
|
|
|
21
26
|
# =============================================================================
|
|
22
27
|
# DATABASE CONFIGURATION
|
|
@@ -25,9 +25,9 @@ requires:
|
|
|
25
25
|
|
|
26
26
|
# Health Check
|
|
27
27
|
healthCheck:
|
|
28
|
-
path: /health
|
|
28
|
+
path: /health/ready
|
|
29
29
|
interval: 30
|
|
30
|
-
probePath: /health
|
|
30
|
+
probePath: /health/ready
|
|
31
31
|
probeRequestType: GET
|
|
32
32
|
probeProtocol: Https
|
|
33
33
|
probeIntervalInSeconds: 120
|
|
@@ -49,4 +49,4 @@ build:
|
|
|
49
49
|
# Docker Compose
|
|
50
50
|
compose:
|
|
51
51
|
file: docker-compose.yaml
|
|
52
|
-
service: keycloak
|
|
52
|
+
service: keycloak
|
|
@@ -62,6 +62,9 @@ RUN pnpm exec tsc -p tsconfig.docker.json || true
|
|
|
62
62
|
# Copy sensitive-fields.config.json to dist folder
|
|
63
63
|
RUN mkdir -p dist/src/services/logging && \
|
|
64
64
|
cp src/services/logging/sensitive-fields.config.json dist/src/services/logging/ || true
|
|
65
|
+
# Copy generated Prisma logs client to dist folder (needed at runtime)
|
|
66
|
+
RUN mkdir -p dist/database/generated && \
|
|
67
|
+
cp -r src/database/generated/logs-client dist/database/generated/ || true
|
|
65
68
|
|
|
66
69
|
# Return to root to prune correctly (needed to keep workspace dependencies)
|
|
67
70
|
WORKDIR /app
|
|
@@ -121,14 +121,14 @@ MISO_CONTROLLER_URL=kv://miso-controller-url
|
|
|
121
121
|
|
|
122
122
|
# Web Server URL (for OpenAPI documentation server URLs)
|
|
123
123
|
# Used to generate correct server URLs in OpenAPI spec
|
|
124
|
-
WEB_SERVER_URL=kv://miso-web-server-url
|
|
124
|
+
WEB_SERVER_URL=kv://miso-controller-web-server-url
|
|
125
125
|
|
|
126
126
|
# MISO Environment Configuration (miso, dev, tst, pro)
|
|
127
127
|
MISO_ENVIRONMENT=miso
|
|
128
128
|
|
|
129
129
|
# MISO Application Client Credentials (per application)
|
|
130
|
-
MISO_CLIENTID=kv://miso-client-idKeyVault
|
|
131
|
-
MISO_CLIENTSECRET=kv://miso-client-secretKeyVault
|
|
130
|
+
MISO_CLIENTID=kv://miso-controller-client-idKeyVault
|
|
131
|
+
MISO_CLIENTSECRET=kv://miso-controller-client-secretKeyVault
|
|
132
132
|
|
|
133
133
|
# =============================================================================
|
|
134
134
|
# MORI SERVICE CONFIGURATION
|