@aifabrix/builder 2.1.6 → 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/secrets.js +68 -99
- package/lib/utils/secrets-path.js +18 -21
- package/lib/utils/secrets-utils.js +206 -0
- package/package.json +1 -1
package/lib/secrets.js
CHANGED
|
@@ -23,6 +23,13 @@ const {
|
|
|
23
23
|
resolveSecretsPath,
|
|
24
24
|
getActualSecretsPath
|
|
25
25
|
} = require('./utils/secrets-path');
|
|
26
|
+
const {
|
|
27
|
+
loadUserSecrets,
|
|
28
|
+
loadBuildSecrets,
|
|
29
|
+
loadDefaultSecrets,
|
|
30
|
+
buildHostnameToServiceMap,
|
|
31
|
+
resolveUrlPort
|
|
32
|
+
} = require('./utils/secrets-utils');
|
|
26
33
|
|
|
27
34
|
/**
|
|
28
35
|
* Loads environment configuration for docker/local context
|
|
@@ -34,35 +41,6 @@ function loadEnvConfig() {
|
|
|
34
41
|
return yaml.load(content);
|
|
35
42
|
}
|
|
36
43
|
|
|
37
|
-
/**
|
|
38
|
-
* Loads secrets from file with cascading lookup support
|
|
39
|
-
* First checks ~/.aifabrix/secrets.local.yaml, then build.secrets from variables.yaml
|
|
40
|
-
*
|
|
41
|
-
* @async
|
|
42
|
-
* @function loadSecretsFromFile
|
|
43
|
-
* @param {string} filePath - Path to secrets file
|
|
44
|
-
* @returns {Promise<Object>} Loaded secrets object or empty object if file doesn't exist
|
|
45
|
-
*/
|
|
46
|
-
async function loadSecretsFromFile(filePath) {
|
|
47
|
-
if (!fs.existsSync(filePath)) {
|
|
48
|
-
return {};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
try {
|
|
52
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
53
|
-
const secrets = yaml.load(content);
|
|
54
|
-
|
|
55
|
-
if (!secrets || typeof secrets !== 'object') {
|
|
56
|
-
return {};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return secrets;
|
|
60
|
-
} catch (error) {
|
|
61
|
-
logger.warn(`Warning: Could not read secrets file ${filePath}: ${error.message}`);
|
|
62
|
-
return {};
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
44
|
/**
|
|
67
45
|
* Loads secrets with cascading lookup
|
|
68
46
|
* Supports both user secrets (~/.aifabrix/secrets.local.yaml) and project overrides
|
|
@@ -98,76 +76,16 @@ async function loadSecrets(secretsPath, appName) {
|
|
|
98
76
|
}
|
|
99
77
|
|
|
100
78
|
// Cascading lookup: user's file first
|
|
101
|
-
|
|
102
|
-
let mergedSecrets;
|
|
103
|
-
if (fs.existsSync(userSecretsPath)) {
|
|
104
|
-
try {
|
|
105
|
-
const content = fs.readFileSync(userSecretsPath, 'utf8');
|
|
106
|
-
const secrets = yaml.load(content);
|
|
107
|
-
if (!secrets || typeof secrets !== 'object') {
|
|
108
|
-
throw new Error(`Invalid secrets file format: ${userSecretsPath}`);
|
|
109
|
-
}
|
|
110
|
-
mergedSecrets = secrets;
|
|
111
|
-
} catch (error) {
|
|
112
|
-
// If it's a format error, throw it; otherwise log warning and continue
|
|
113
|
-
if (error.message.includes('Invalid secrets file format')) {
|
|
114
|
-
throw error;
|
|
115
|
-
}
|
|
116
|
-
logger.warn(`Warning: Could not read secrets file ${userSecretsPath}: ${error.message}`);
|
|
117
|
-
mergedSecrets = {};
|
|
118
|
-
}
|
|
119
|
-
} else {
|
|
120
|
-
mergedSecrets = {};
|
|
121
|
-
}
|
|
79
|
+
let mergedSecrets = loadUserSecrets();
|
|
122
80
|
|
|
123
81
|
// Then check build.secrets from variables.yaml if appName provided
|
|
124
82
|
if (appName) {
|
|
125
|
-
|
|
126
|
-
if (fs.existsSync(variablesPath)) {
|
|
127
|
-
try {
|
|
128
|
-
const variablesContent = fs.readFileSync(variablesPath, 'utf8');
|
|
129
|
-
const variables = yaml.load(variablesContent);
|
|
130
|
-
|
|
131
|
-
if (variables?.build?.secrets) {
|
|
132
|
-
const buildSecretsPath = path.resolve(
|
|
133
|
-
path.dirname(variablesPath),
|
|
134
|
-
variables.build.secrets
|
|
135
|
-
);
|
|
136
|
-
|
|
137
|
-
const buildSecrets = await loadSecretsFromFile(buildSecretsPath);
|
|
138
|
-
|
|
139
|
-
// Merge: user's file takes priority, but use build.secrets for missing/empty values
|
|
140
|
-
for (const [key, value] of Object.entries(buildSecrets)) {
|
|
141
|
-
if (!(key in mergedSecrets) || !mergedSecrets[key] || mergedSecrets[key] === '') {
|
|
142
|
-
mergedSecrets[key] = value;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
} catch (error) {
|
|
147
|
-
logger.warn(`Warning: Could not load build.secrets from variables.yaml: ${error.message}`);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
83
|
+
mergedSecrets = await loadBuildSecrets(mergedSecrets, appName);
|
|
150
84
|
}
|
|
151
85
|
|
|
152
86
|
// If still no secrets found, try default location
|
|
153
87
|
if (Object.keys(mergedSecrets).length === 0) {
|
|
154
|
-
|
|
155
|
-
if (fs.existsSync(defaultPath)) {
|
|
156
|
-
try {
|
|
157
|
-
const content = fs.readFileSync(defaultPath, 'utf8');
|
|
158
|
-
const secrets = yaml.load(content);
|
|
159
|
-
if (!secrets || typeof secrets !== 'object') {
|
|
160
|
-
throw new Error(`Invalid secrets file format: ${defaultPath}`);
|
|
161
|
-
}
|
|
162
|
-
mergedSecrets = secrets;
|
|
163
|
-
} catch (error) {
|
|
164
|
-
// If it's a format error, throw it; otherwise log warning
|
|
165
|
-
if (error.message.includes('Invalid secrets file format')) {
|
|
166
|
-
throw error;
|
|
167
|
-
}
|
|
168
|
-
logger.warn(`Warning: Could not read secrets file ${defaultPath}: ${error.message}`);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
88
|
+
mergedSecrets = loadDefaultSecrets();
|
|
171
89
|
}
|
|
172
90
|
|
|
173
91
|
// If still empty, throw error
|
|
@@ -187,7 +105,9 @@ async function loadSecrets(secretsPath, appName) {
|
|
|
187
105
|
* @param {string} envTemplate - Environment template content
|
|
188
106
|
* @param {Object} secrets - Secrets object from loadSecrets()
|
|
189
107
|
* @param {string} [environment='local'] - Environment context (docker/local)
|
|
190
|
-
* @param {string} [
|
|
108
|
+
* @param {Object|string|null} [secretsFilePaths] - Paths object with userPath and buildPath, or string path (for backward compatibility)
|
|
109
|
+
* @param {string} [secretsFilePaths.userPath] - User's secrets file path
|
|
110
|
+
* @param {string|null} [secretsFilePaths.buildPath] - App's build.secrets file path (if configured)
|
|
191
111
|
* @returns {Promise<string>} Resolved environment content
|
|
192
112
|
* @throws {Error} If kv:// reference cannot be resolved
|
|
193
113
|
*
|
|
@@ -195,7 +115,7 @@ async function loadSecrets(secretsPath, appName) {
|
|
|
195
115
|
* const resolved = await resolveKvReferences(template, secrets, 'local');
|
|
196
116
|
* // Returns: 'DATABASE_URL=postgresql://user:pass@localhost:5432/db'
|
|
197
117
|
*/
|
|
198
|
-
async function resolveKvReferences(envTemplate, secrets, environment = 'local',
|
|
118
|
+
async function resolveKvReferences(envTemplate, secrets, environment = 'local', secretsFilePaths = null) {
|
|
199
119
|
const envConfig = loadEnvConfig();
|
|
200
120
|
const envVars = envConfig.environments[environment] || envConfig.environments.local;
|
|
201
121
|
|
|
@@ -216,7 +136,20 @@ async function resolveKvReferences(envTemplate, secrets, environment = 'local',
|
|
|
216
136
|
}
|
|
217
137
|
|
|
218
138
|
if (missingSecrets.length > 0) {
|
|
219
|
-
|
|
139
|
+
let fileInfo = '';
|
|
140
|
+
if (secretsFilePaths) {
|
|
141
|
+
// Handle backward compatibility: if it's a string, use it as-is
|
|
142
|
+
if (typeof secretsFilePaths === 'string') {
|
|
143
|
+
fileInfo = `\n\nSecrets file location: ${secretsFilePaths}`;
|
|
144
|
+
} else if (typeof secretsFilePaths === 'object' && secretsFilePaths.userPath) {
|
|
145
|
+
// New format: show both paths if buildPath is configured
|
|
146
|
+
const paths = [secretsFilePaths.userPath];
|
|
147
|
+
if (secretsFilePaths.buildPath) {
|
|
148
|
+
paths.push(secretsFilePaths.buildPath);
|
|
149
|
+
}
|
|
150
|
+
fileInfo = `\n\nSecrets file location: ${paths.join(' and ')}`;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
220
153
|
throw new Error(`Missing secrets: ${missingSecrets.join(', ')}${fileInfo}`);
|
|
221
154
|
}
|
|
222
155
|
|
|
@@ -235,6 +168,36 @@ async function resolveKvReferences(envTemplate, secrets, environment = 'local',
|
|
|
235
168
|
return resolved;
|
|
236
169
|
}
|
|
237
170
|
|
|
171
|
+
/**
|
|
172
|
+
* Resolves service ports in URLs within .env content for Docker environment
|
|
173
|
+
* Replaces ports in URLs with containerPort from service's variables.yaml
|
|
174
|
+
*
|
|
175
|
+
* @function resolveServicePortsInEnvContent
|
|
176
|
+
* @param {string} envContent - Resolved .env file content
|
|
177
|
+
* @param {string} environment - Environment context (docker/local)
|
|
178
|
+
* @returns {string} Content with resolved ports
|
|
179
|
+
*/
|
|
180
|
+
function resolveServicePortsInEnvContent(envContent, environment) {
|
|
181
|
+
// Only process docker environment
|
|
182
|
+
if (environment !== 'docker') {
|
|
183
|
+
return envContent;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const envConfig = loadEnvConfig();
|
|
187
|
+
const dockerHosts = envConfig.environments.docker || {};
|
|
188
|
+
const hostnameToService = buildHostnameToServiceMap(dockerHosts);
|
|
189
|
+
|
|
190
|
+
// Pattern to match URLs: http://hostname:port or https://hostname:port
|
|
191
|
+
// Matches: protocol://hostname:port/path?query
|
|
192
|
+
// Captures: protocol, hostname, port, and optional path/query
|
|
193
|
+
// Note: [^\s\n]* matches any non-whitespace characters except newline (stops at end of line)
|
|
194
|
+
const urlPattern = /(https?:\/\/)([a-zA-Z0-9-]+):(\d+)([^\s\n]*)?/g;
|
|
195
|
+
|
|
196
|
+
return envContent.replace(urlPattern, (match, protocol, hostname, port, urlPath = '') => {
|
|
197
|
+
return resolveUrlPort(protocol, hostname, port, urlPath || '', hostnameToService);
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
238
201
|
/**
|
|
239
202
|
* Loads environment template from file
|
|
240
203
|
* @function loadEnvTemplate
|
|
@@ -324,15 +287,21 @@ async function generateEnvFile(appName, secretsPath, environment = 'local', forc
|
|
|
324
287
|
|
|
325
288
|
const template = loadEnvTemplate(templatePath);
|
|
326
289
|
|
|
327
|
-
// Resolve secrets
|
|
328
|
-
const
|
|
290
|
+
// Resolve secrets paths to show in error messages (use actual paths that loadSecrets would use)
|
|
291
|
+
const secretsPaths = getActualSecretsPath(secretsPath, appName);
|
|
329
292
|
|
|
330
293
|
if (force) {
|
|
331
|
-
|
|
294
|
+
// Use userPath for generating missing secrets (priority file)
|
|
295
|
+
await generateMissingSecrets(template, secretsPaths.userPath);
|
|
332
296
|
}
|
|
333
297
|
|
|
334
298
|
const secrets = await loadSecrets(secretsPath, appName);
|
|
335
|
-
|
|
299
|
+
let resolved = await resolveKvReferences(template, secrets, environment, secretsPaths);
|
|
300
|
+
|
|
301
|
+
// Resolve service ports in URLs for docker environment
|
|
302
|
+
if (environment === 'docker') {
|
|
303
|
+
resolved = resolveServicePortsInEnvContent(resolved, environment);
|
|
304
|
+
}
|
|
336
305
|
|
|
337
306
|
fs.writeFileSync(envPath, resolved, { mode: 0o600 });
|
|
338
307
|
|
|
@@ -54,26 +54,30 @@ function resolveSecretsPath(secretsPath) {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
/**
|
|
57
|
-
* Determines the actual secrets file
|
|
57
|
+
* Determines the actual secrets file paths that loadSecrets would use
|
|
58
58
|
* Mirrors the cascading lookup logic from loadSecrets
|
|
59
59
|
* @function getActualSecretsPath
|
|
60
60
|
* @param {string} [secretsPath] - Path to secrets file (optional)
|
|
61
61
|
* @param {string} [appName] - Application name (optional, for variables.yaml lookup)
|
|
62
|
-
* @returns {
|
|
62
|
+
* @returns {Object} Object with userPath and buildPath (if configured)
|
|
63
|
+
* @returns {string} returns.userPath - User's secrets file path (~/.aifabrix/secrets.local.yaml)
|
|
64
|
+
* @returns {string|null} returns.buildPath - App's build.secrets file path (if configured in variables.yaml)
|
|
63
65
|
*/
|
|
64
66
|
function getActualSecretsPath(secretsPath, appName) {
|
|
65
67
|
// If explicit path provided, use it (backward compatibility)
|
|
66
68
|
if (secretsPath) {
|
|
67
|
-
|
|
69
|
+
const resolvedPath = resolveSecretsPath(secretsPath);
|
|
70
|
+
return {
|
|
71
|
+
userPath: resolvedPath,
|
|
72
|
+
buildPath: null
|
|
73
|
+
};
|
|
68
74
|
}
|
|
69
75
|
|
|
70
76
|
// Cascading lookup: user's file first
|
|
71
77
|
const userSecretsPath = path.join(os.homedir(), '.aifabrix', 'secrets.local.yaml');
|
|
72
|
-
if (fs.existsSync(userSecretsPath)) {
|
|
73
|
-
return userSecretsPath;
|
|
74
|
-
}
|
|
75
78
|
|
|
76
|
-
//
|
|
79
|
+
// Check build.secrets from variables.yaml if appName provided
|
|
80
|
+
let buildSecretsPath = null;
|
|
77
81
|
if (appName) {
|
|
78
82
|
const variablesPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
|
|
79
83
|
if (fs.existsSync(variablesPath)) {
|
|
@@ -82,29 +86,22 @@ function getActualSecretsPath(secretsPath, appName) {
|
|
|
82
86
|
const variables = yaml.load(variablesContent);
|
|
83
87
|
|
|
84
88
|
if (variables?.build?.secrets) {
|
|
85
|
-
|
|
89
|
+
buildSecretsPath = path.resolve(
|
|
86
90
|
path.dirname(variablesPath),
|
|
87
91
|
variables.build.secrets
|
|
88
92
|
);
|
|
89
|
-
|
|
90
|
-
if (fs.existsSync(buildSecretsPath)) {
|
|
91
|
-
return buildSecretsPath;
|
|
92
|
-
}
|
|
93
93
|
}
|
|
94
94
|
} catch (error) {
|
|
95
|
-
// Ignore errors, continue
|
|
95
|
+
// Ignore errors, continue
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Return user's file path as default (even if it doesn't exist) for error messages
|
|
107
|
-
return userSecretsPath;
|
|
100
|
+
// Return both paths (even if files don't exist) for error messages
|
|
101
|
+
return {
|
|
102
|
+
userPath: userSecretsPath,
|
|
103
|
+
buildPath: buildSecretsPath
|
|
104
|
+
};
|
|
108
105
|
}
|
|
109
106
|
|
|
110
107
|
module.exports = {
|
|
@@ -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
|
+
|