@aifabrix/builder 2.44.6 → 2.45.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/cli-layout.mdc +7 -3
- package/jest.projects.js +56 -0
- package/lib/app/helpers.js +3 -3
- package/lib/app/index.js +3 -3
- package/lib/app/register.js +7 -6
- package/lib/app/restart-display.js +52 -21
- package/lib/app/rotate-secret.js +7 -6
- package/lib/app/run-helpers.js +15 -8
- package/lib/app/run.js +57 -9
- package/lib/app/show-display.js +7 -0
- package/lib/app/show.js +87 -5
- package/lib/build/index.js +9 -5
- package/lib/cli/infra-guided.js +42 -27
- package/lib/cli/installation-log-command.js +73 -0
- package/lib/cli/setup-app.js +11 -1
- package/lib/cli/setup-auth.js +94 -49
- package/lib/cli/setup-infra-up-dataplane-action.js +111 -0
- package/lib/cli/setup-infra-up-platform-action.js +131 -0
- package/lib/cli/setup-infra.js +60 -119
- package/lib/cli/setup-platform.js +1 -1
- package/lib/cli/setup-utility-resolve.js +132 -0
- package/lib/cli/setup-utility.js +65 -51
- package/lib/commands/app-logs.js +81 -33
- package/lib/commands/auth-config.js +116 -18
- package/lib/commands/setup-modes.js +19 -6
- package/lib/commands/setup-prompts.js +41 -8
- package/lib/commands/setup.js +114 -9
- package/lib/commands/teardown.js +54 -5
- package/lib/commands/up-common.js +48 -14
- package/lib/commands/up-dataplane.js +21 -18
- package/lib/commands/up-miso.js +12 -8
- package/lib/commands/upload.js +5 -3
- package/lib/core/audit-logger.js +1 -34
- package/lib/core/config-admin-email.js +56 -0
- package/lib/core/config-normalize.js +60 -0
- package/lib/core/config-registered-controller-urls.js +54 -0
- package/lib/core/config.js +33 -50
- package/lib/core/secrets-ensure-infra.js +1 -1
- package/lib/core/secrets-env-content.js +86 -90
- package/lib/core/secrets-env-declarative-expand.js +170 -0
- package/lib/core/secrets-env-write.js +2 -0
- package/lib/core/secrets-load.js +106 -102
- package/lib/external-system/deploy.js +5 -1
- package/lib/internal/node-fs.js +2 -0
- package/lib/schema/application-schema.json +4 -0
- package/lib/schema/infra.parameter.yaml +10 -0
- package/lib/utils/app-config-resolver.js +24 -1
- package/lib/utils/applications-config-defaults.js +206 -0
- package/lib/utils/auth-config-validator.js +2 -12
- package/lib/utils/bash-secret-env.js +1 -1
- package/lib/utils/compose-generate-docker-compose.js +111 -6
- package/lib/utils/compose-generator.js +17 -8
- package/lib/utils/controller-url.js +50 -7
- package/lib/utils/env-copy.js +99 -14
- package/lib/utils/env-template.js +5 -1
- package/lib/utils/health-check-url.js +18 -15
- package/lib/utils/health-check.js +7 -5
- package/lib/utils/infra-optional-service-flags.js +69 -0
- package/lib/utils/installation-log-core.js +282 -0
- package/lib/utils/installation-log-record.js +237 -0
- package/lib/utils/installation-log.js +123 -0
- package/lib/utils/log-redaction.js +105 -0
- package/lib/utils/manifest-location.js +164 -0
- package/lib/utils/manifest-source-emit.js +162 -0
- package/lib/utils/paths.js +238 -89
- package/lib/utils/remote-secrets-loader.js +7 -1
- package/lib/utils/run-cli-flags.js +29 -0
- package/lib/utils/secrets-canonical.js +10 -3
- package/lib/utils/secrets-path.js +3 -4
- package/lib/utils/secrets-utils.js +20 -10
- package/lib/utils/system-builder-root.js +10 -2
- package/lib/utils/url-declarative-public-base.js +80 -12
- package/lib/utils/url-declarative-resolve-build-urls.js +238 -0
- package/lib/utils/url-declarative-resolve-build.js +24 -393
- package/lib/utils/url-declarative-resolve-expand-token.js +189 -0
- package/lib/utils/url-declarative-resolve-load-doc.js +12 -3
- package/lib/utils/url-declarative-resolve-surface-state.js +102 -0
- package/lib/utils/url-declarative-resolve.js +47 -7
- package/lib/utils/url-declarative-runtime-base-path.js +21 -1
- package/lib/utils/urls-local-registry-scan.js +103 -0
- package/lib/utils/urls-local-registry.js +161 -90
- package/package.json +3 -1
- package/templates/applications/dataplane/application.yaml +4 -0
- package/templates/applications/miso-controller/application.yaml +2 -0
- package/templates/applications/miso-controller/env.template +27 -29
- package/.npmrc.token +0 -1
package/lib/core/secrets-load.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Secrets loading
|
|
2
|
+
* Secrets loading: primary user file plus configured `aifabrix-secrets` (shared YAML or remote API).
|
|
3
3
|
*
|
|
4
|
-
* @fileoverview
|
|
4
|
+
* @fileoverview Default `kv://` resolution uses only `~/.aifabrix/secrets.local.yaml` and `aifabrix-secrets`
|
|
5
|
+
* from config — not cwd-ancestor `.aifabrix/secrets.local.yaml`, not `builder/secrets.local.yaml`,
|
|
6
|
+
* and not `~/.aifabrix/secrets.yaml`.
|
|
5
7
|
* @author AI Fabrix Team
|
|
6
8
|
* @version 1.0.0
|
|
7
9
|
*/
|
|
@@ -13,27 +15,75 @@ const path = require('path');
|
|
|
13
15
|
const config = require('./config');
|
|
14
16
|
const { readYamlAtPath, applyCanonicalSecretsOverride } = require('../utils/secrets-canonical');
|
|
15
17
|
const { ensureSecureFilePermissions } = require('../utils/secure-file-permissions');
|
|
18
|
+
const { resolveSecretsPath } = require('../utils/secrets-path');
|
|
16
19
|
const {
|
|
17
|
-
resolveSecretsPath
|
|
18
|
-
} = require('../utils/secrets-path');
|
|
19
|
-
const {
|
|
20
|
-
loadUserSecrets,
|
|
21
20
|
loadPrimaryUserSecrets,
|
|
22
21
|
loadDefaultSecrets,
|
|
23
22
|
ensurePrimaryUserSecretsFileExists
|
|
24
23
|
} = require('../utils/secrets-utils');
|
|
25
|
-
const { decryptSecret, isEncrypted } = require('../utils/secrets-encryption');
|
|
26
24
|
const pathsUtil = require('../utils/paths');
|
|
27
|
-
const {
|
|
25
|
+
const { decryptSecret, isEncrypted } = require('../utils/secrets-encryption');
|
|
26
|
+
const logger = require('../utils/logger');
|
|
27
|
+
const { isSecretKeyAllowedEmpty } = require('./secrets-ensure-infra');
|
|
28
|
+
|
|
29
|
+
/** Dedupe optional decrypt warnings across repeated `loadSecrets` (and duplicate module instances). */
|
|
30
|
+
function optionalDecryptWarnSeen(key) {
|
|
31
|
+
const g = globalThis;
|
|
32
|
+
if (!g.__aifabrixOptionalDecryptWarnOnce) {
|
|
33
|
+
g.__aifabrixOptionalDecryptWarnOnce = new Set();
|
|
34
|
+
}
|
|
35
|
+
const set = g.__aifabrixOptionalDecryptWarnOnce;
|
|
36
|
+
if (set.has(key)) return true;
|
|
37
|
+
set.add(key);
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {string} key
|
|
43
|
+
* @param {unknown} value
|
|
44
|
+
* @param {string} encryptionKey
|
|
45
|
+
* @param {Record<string, string>} decryptedSecrets
|
|
46
|
+
* @param {string} [sourceHint] - File path or API label for decrypt errors / warnings
|
|
47
|
+
*/
|
|
48
|
+
function mergeDecryptedEntry(key, value, encryptionKey, decryptedSecrets, sourceHint) {
|
|
49
|
+
if (!isEncrypted(value)) {
|
|
50
|
+
decryptedSecrets[key] = value;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
decryptedSecrets[key] = decryptSecret(value, encryptionKey);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
const msg = error && error.message ? error.message : String(error);
|
|
57
|
+
if (!isSecretKeyAllowedEmpty(key)) {
|
|
58
|
+
const where =
|
|
59
|
+
sourceHint && String(sourceHint).trim().length > 0
|
|
60
|
+
? ` (encrypted value loaded from: ${sourceHint})`
|
|
61
|
+
: ' (encrypted value source not recorded; check ~/.aifabrix/secrets.local.yaml and aifabrix-secrets)';
|
|
62
|
+
throw new Error(`Failed to decrypt secret '${key}'${where}: ${msg}`);
|
|
63
|
+
}
|
|
64
|
+
if (!optionalDecryptWarnSeen(key)) {
|
|
65
|
+
const where =
|
|
66
|
+
sourceHint && String(sourceHint).trim().length > 0
|
|
67
|
+
? ` Encrypted value loaded from: ${sourceHint}.`
|
|
68
|
+
: '';
|
|
69
|
+
logger.warn(
|
|
70
|
+
`Optional secret '${key}' could not be decrypted (${msg}). Treating as empty.${where} ` +
|
|
71
|
+
'Remove the stale key from shared or local secrets, or fix secrets-encryption alignment.'
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
decryptedSecrets[key] = '';
|
|
75
|
+
}
|
|
76
|
+
}
|
|
28
77
|
|
|
29
78
|
/**
|
|
30
79
|
* Decrypts encrypted values in secrets object
|
|
31
80
|
*
|
|
32
81
|
* @async
|
|
33
82
|
* @param {Object} secrets - Secrets object with potentially encrypted values
|
|
83
|
+
* @param {{ keySources?: Record<string, string>, defaultSourceLabel?: string }} [options] - Per-key file/API path for errors
|
|
34
84
|
* @returns {Promise<Object>} Secrets object with decrypted values
|
|
35
85
|
*/
|
|
36
|
-
async function decryptSecretsObject(secrets) {
|
|
86
|
+
async function decryptSecretsObject(secrets, options) {
|
|
37
87
|
if (!secrets || typeof secrets !== 'object') {
|
|
38
88
|
return secrets;
|
|
39
89
|
}
|
|
@@ -49,17 +99,13 @@ async function decryptSecretsObject(secrets) {
|
|
|
49
99
|
return secrets;
|
|
50
100
|
}
|
|
51
101
|
|
|
102
|
+
const keySources = options && options.keySources ? options.keySources : null;
|
|
103
|
+
const defaultLabel = options && options.defaultSourceLabel ? options.defaultSourceLabel : '';
|
|
104
|
+
|
|
52
105
|
const decryptedSecrets = {};
|
|
53
106
|
for (const [key, value] of Object.entries(secrets)) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
decryptedSecrets[key] = decryptSecret(value, encryptionKey);
|
|
57
|
-
} catch (error) {
|
|
58
|
-
throw new Error(`Failed to decrypt secret '${key}': ${error.message}`);
|
|
59
|
-
}
|
|
60
|
-
} else {
|
|
61
|
-
decryptedSecrets[key] = value;
|
|
62
|
-
}
|
|
107
|
+
const sourceHint = (keySources && keySources[key]) || defaultLabel || '';
|
|
108
|
+
mergeDecryptedEntry(key, value, encryptionKey, decryptedSecrets, sourceHint);
|
|
63
109
|
}
|
|
64
110
|
|
|
65
111
|
return decryptedSecrets;
|
|
@@ -67,8 +113,9 @@ async function decryptSecretsObject(secrets) {
|
|
|
67
113
|
|
|
68
114
|
/**
|
|
69
115
|
* Merges config file secrets into user secrets (user wins). Returns null if path missing or config empty.
|
|
116
|
+
* @param {Record<string, string>} [keySources] - Mutated: path for keys filled from this file
|
|
70
117
|
*/
|
|
71
|
-
function mergeUserWithConfigFile(userSecrets, resolvedConfigPath) {
|
|
118
|
+
function mergeUserWithConfigFile(userSecrets, resolvedConfigPath, keySources) {
|
|
72
119
|
if (!fs.existsSync(resolvedConfigPath)) {
|
|
73
120
|
return null;
|
|
74
121
|
}
|
|
@@ -86,6 +133,9 @@ function mergeUserWithConfigFile(userSecrets, resolvedConfigPath) {
|
|
|
86
133
|
for (const key of Object.keys(configSecrets)) {
|
|
87
134
|
if (!(key in merged) || merged[key] === undefined || merged[key] === null || merged[key] === '') {
|
|
88
135
|
merged[key] = configSecrets[key];
|
|
136
|
+
if (keySources) {
|
|
137
|
+
keySources[key] = resolvedConfigPath;
|
|
138
|
+
}
|
|
89
139
|
}
|
|
90
140
|
}
|
|
91
141
|
return merged;
|
|
@@ -99,122 +149,74 @@ function createMergeHelpers(userSecrets) {
|
|
|
99
149
|
};
|
|
100
150
|
}
|
|
101
151
|
|
|
102
|
-
async function mergeFromConfiguredSecretsPath(configSecretsPath, userSecrets, helpers) {
|
|
152
|
+
async function mergeFromConfiguredSecretsPath(configSecretsPath, userSecrets, helpers, keySources) {
|
|
103
153
|
const remoteDevAuth = require('../utils/remote-dev-auth');
|
|
104
154
|
const effectiveShared = await remoteDevAuth.resolveSharedSecretsEndpoint(configSecretsPath);
|
|
105
155
|
|
|
106
156
|
if (remoteDevAuth.isRemoteSecretsUrl(effectiveShared)) {
|
|
107
157
|
const { loadRemoteSharedSecrets, mergeUserWithRemoteSecrets } = require('../utils/remote-secrets-loader');
|
|
108
158
|
const remoteSecrets = await loadRemoteSharedSecrets();
|
|
109
|
-
const
|
|
159
|
+
const remoteLabel = `shared secrets API (${effectiveShared})`;
|
|
160
|
+
const merged = mergeUserWithRemoteSecrets(userSecrets, remoteSecrets, keySources, remoteLabel);
|
|
110
161
|
return helpers.hasKeys(merged) ? merged : helpers.userOrNull();
|
|
111
162
|
}
|
|
112
163
|
|
|
113
164
|
const resolvedConfigPath = path.isAbsolute(configSecretsPath)
|
|
114
165
|
? configSecretsPath
|
|
115
166
|
: path.resolve(process.cwd(), configSecretsPath);
|
|
116
|
-
const merged = mergeUserWithConfigFile(userSecrets, resolvedConfigPath);
|
|
167
|
+
const merged = mergeUserWithConfigFile(userSecrets, resolvedConfigPath, keySources);
|
|
117
168
|
return merged !== null ? merged : helpers.userOrNull();
|
|
118
169
|
}
|
|
119
170
|
|
|
120
171
|
async function loadMergedConfigAndUserSecrets() {
|
|
121
172
|
const userSecrets = loadPrimaryUserSecrets();
|
|
122
173
|
const helpers = createMergeHelpers(userSecrets);
|
|
174
|
+
const userPath = pathsUtil.getPrimaryUserSecretsLocalPath();
|
|
175
|
+
/** @type {Record<string, string>} */
|
|
176
|
+
const keySources = {};
|
|
177
|
+
for (const k of Object.keys(userSecrets || {})) {
|
|
178
|
+
keySources[k] = userPath;
|
|
179
|
+
}
|
|
123
180
|
|
|
124
181
|
try {
|
|
125
182
|
const configSecretsPath = await config.getSecretsPath();
|
|
126
183
|
if (!configSecretsPath) {
|
|
127
|
-
return helpers.userOrNull();
|
|
184
|
+
return { merged: helpers.userOrNull(), keySources };
|
|
128
185
|
}
|
|
129
|
-
|
|
186
|
+
const merged = await mergeFromConfiguredSecretsPath(configSecretsPath, userSecrets, helpers, keySources);
|
|
187
|
+
return { merged, keySources };
|
|
130
188
|
} catch (error) {
|
|
131
189
|
if (error.message && error.message.startsWith('Failed to load secrets file')) {
|
|
132
190
|
throw error;
|
|
133
191
|
}
|
|
134
|
-
return null;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function collectBuilderSecretsYamlPaths() {
|
|
139
|
-
const projectRoot = pathsUtil.getProjectRoot();
|
|
140
|
-
const candidates = [];
|
|
141
|
-
if (projectRoot) {
|
|
142
|
-
candidates.push(path.join(projectRoot, 'builder', 'secrets.local.yaml'));
|
|
143
|
-
}
|
|
144
|
-
try {
|
|
145
|
-
const alt = path.join(pathsUtil.getBuilderRoot(), 'secrets.local.yaml');
|
|
146
|
-
if (!candidates.length || path.resolve(candidates[0]) !== path.resolve(alt)) {
|
|
147
|
-
candidates.push(alt);
|
|
148
|
-
}
|
|
149
|
-
} catch {
|
|
150
|
-
/* ignore */
|
|
151
|
-
}
|
|
152
|
-
return candidates;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function mergeBuilderSecretsLocalFiles(merged) {
|
|
156
|
-
try {
|
|
157
|
-
const seen = new Set();
|
|
158
|
-
let out = merged;
|
|
159
|
-
for (const builderPath of collectBuilderSecretsYamlPaths()) {
|
|
160
|
-
if (!builderPath || seen.has(path.resolve(builderPath))) {
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
163
|
-
seen.add(path.resolve(builderPath));
|
|
164
|
-
if (fs.existsSync(builderPath)) {
|
|
165
|
-
ensureSecureFilePermissions(builderPath);
|
|
166
|
-
const builderSecrets = mergeUserWithConfigFile(out || {}, builderPath);
|
|
167
|
-
if (builderSecrets) out = builderSecrets;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
return out;
|
|
171
|
-
} catch {
|
|
172
|
-
return merged;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Merge each `cwd/.aifabrix/secrets.local.yaml` found walking up to filesystem root.
|
|
178
|
-
* Later files only fill keys missing or empty on the merged object (same as shared merge).
|
|
179
|
-
* Skips the primary user file path when it appears on the walk (already loaded in merge chain).
|
|
180
|
-
*
|
|
181
|
-
* @param {Object|null|undefined} merged - Current secrets map
|
|
182
|
-
* @returns {Object}
|
|
183
|
-
*/
|
|
184
|
-
function mergeAncestorAifabrixSecretsLocalFiles(merged) {
|
|
185
|
-
try {
|
|
186
|
-
const primaryUser = path.resolve(pathsUtil.getPrimaryUserSecretsLocalPath());
|
|
187
|
-
let out = merged && typeof merged === 'object' ? merged : {};
|
|
188
|
-
const ancestorPaths = collectAncestorAifabrixSecretsLocalYamlPaths(process.cwd(), fs.existsSync);
|
|
189
|
-
for (const secretsPath of ancestorPaths) {
|
|
190
|
-
if (path.resolve(secretsPath) === primaryUser) {
|
|
191
|
-
continue;
|
|
192
|
-
}
|
|
193
|
-
ensureSecureFilePermissions(secretsPath);
|
|
194
|
-
const next = mergeUserWithConfigFile(out, secretsPath);
|
|
195
|
-
if (next) out = next;
|
|
196
|
-
}
|
|
197
|
-
return out;
|
|
198
|
-
} catch {
|
|
199
|
-
return merged && typeof merged === 'object' ? merged : {};
|
|
192
|
+
return { merged: null, keySources };
|
|
200
193
|
}
|
|
201
194
|
}
|
|
202
195
|
|
|
203
196
|
async function loadSecretsWithFallbacks() {
|
|
204
|
-
|
|
197
|
+
const mergedResult = await loadMergedConfigAndUserSecrets();
|
|
198
|
+
let merged = mergedResult.merged;
|
|
199
|
+
const keySources = mergedResult.keySources;
|
|
205
200
|
if (!merged || Object.keys(merged).length === 0) {
|
|
206
201
|
merged = loadPrimaryUserSecrets();
|
|
207
|
-
|
|
208
|
-
|
|
202
|
+
const userPath = pathsUtil.getPrimaryUserSecretsLocalPath();
|
|
203
|
+
for (const k of Object.keys(merged || {})) {
|
|
204
|
+
keySources[k] = userPath;
|
|
209
205
|
}
|
|
210
|
-
merged = await applyCanonicalSecretsOverride(merged);
|
|
206
|
+
merged = await applyCanonicalSecretsOverride(merged || {}, keySources);
|
|
211
207
|
}
|
|
212
|
-
|
|
213
|
-
merged
|
|
214
|
-
if (Object.keys(merged).length === 0) {
|
|
208
|
+
const configuredShared = await config.getSecretsPath();
|
|
209
|
+
if ((!merged || Object.keys(merged).length === 0) && !configuredShared) {
|
|
215
210
|
merged = loadDefaultSecrets();
|
|
211
|
+
const defaultPath = path.join(pathsUtil.getAifabrixHome(), 'secrets.yaml');
|
|
212
|
+
for (const k of Object.keys(merged || {})) {
|
|
213
|
+
keySources[k] = defaultPath;
|
|
214
|
+
}
|
|
216
215
|
}
|
|
217
|
-
|
|
216
|
+
if (!merged || Object.keys(merged).length === 0) {
|
|
217
|
+
return { merged: {}, keySources };
|
|
218
|
+
}
|
|
219
|
+
return { merged, keySources };
|
|
218
220
|
}
|
|
219
221
|
|
|
220
222
|
/**
|
|
@@ -232,14 +234,16 @@ async function loadSecrets(secretsPath, _appName) {
|
|
|
232
234
|
if (!explicitSecrets || typeof explicitSecrets !== 'object') {
|
|
233
235
|
throw new Error(`Invalid secrets file format: ${resolvedPath}`);
|
|
234
236
|
}
|
|
235
|
-
return await decryptSecretsObject(explicitSecrets);
|
|
237
|
+
return await decryptSecretsObject(explicitSecrets, { defaultSourceLabel: resolvedPath });
|
|
236
238
|
}
|
|
237
|
-
let mergedSecrets = await loadSecretsWithFallbacks();
|
|
239
|
+
let { merged: mergedSecrets, keySources } = await loadSecretsWithFallbacks();
|
|
238
240
|
if (!mergedSecrets || Object.keys(mergedSecrets).length === 0) {
|
|
239
241
|
ensurePrimaryUserSecretsFileExists();
|
|
240
|
-
|
|
242
|
+
const again = await loadSecretsWithFallbacks();
|
|
243
|
+
mergedSecrets = again.merged;
|
|
244
|
+
keySources = again.keySources;
|
|
241
245
|
}
|
|
242
|
-
return await decryptSecretsObject(mergedSecrets || {});
|
|
246
|
+
return await decryptSecretsObject(mergedSecrets || {}, { keySources });
|
|
243
247
|
}
|
|
244
248
|
|
|
245
249
|
module.exports = {
|
|
@@ -169,7 +169,7 @@ function logImmediateControllerDeploymentOutcome(deploymentOutcome) {
|
|
|
169
169
|
);
|
|
170
170
|
return;
|
|
171
171
|
}
|
|
172
|
-
logger.log(chalk.gray('
|
|
172
|
+
logger.log(chalk.gray(' Check the controller deployment job or logs for details.'));
|
|
173
173
|
}
|
|
174
174
|
|
|
175
175
|
async function executeDeployAndDisplay(manifest, controllerUrl, environment, authConfig, options) {
|
|
@@ -192,6 +192,10 @@ async function executeDeployAndDisplay(manifest, controllerUrl, environment, aut
|
|
|
192
192
|
const deploymentOutcome = parseControllerDeploymentOutcome(result);
|
|
193
193
|
logImmediateControllerDeploymentOutcome(deploymentOutcome);
|
|
194
194
|
|
|
195
|
+
if (!deploymentOutcome.ok) {
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
|
|
195
199
|
const ctx = await fetchDataplaneDeployReadiness(
|
|
196
200
|
controllerUrl,
|
|
197
201
|
environment,
|
package/lib/internal/node-fs.js
CHANGED
|
@@ -72,6 +72,7 @@ function buildBoundFs() {
|
|
|
72
72
|
mkdirSync: snap.mkdirSync,
|
|
73
73
|
readdirSync: snap.readdirSync,
|
|
74
74
|
statSync: snap.statSync,
|
|
75
|
+
renameSync: snap.renameSync,
|
|
75
76
|
watch: (...args) => live.watch(...args),
|
|
76
77
|
promises: boundPromises
|
|
77
78
|
};
|
|
@@ -83,6 +84,7 @@ function buildBoundFs() {
|
|
|
83
84
|
mkdirSync: bindSync(rf, 'mkdirSync'),
|
|
84
85
|
readdirSync: bindSync(rf, 'readdirSync'),
|
|
85
86
|
statSync: bindSync(rf, 'statSync'),
|
|
87
|
+
renameSync: bindSync(rf, 'renameSync'),
|
|
86
88
|
watch: (...args) => live.watch(...args),
|
|
87
89
|
promises: boundPromises
|
|
88
90
|
};
|
|
@@ -534,6 +534,10 @@
|
|
|
534
534
|
"pattern": "^[a-zA-Z.$\\{\\}_-]+$",
|
|
535
535
|
"default": true
|
|
536
536
|
},
|
|
537
|
+
"internalDockerUseOriginOnly": {
|
|
538
|
+
"type": "boolean",
|
|
539
|
+
"description": "When true, declarative url://*-internal full URLs in Docker profile use http://service:containerPort only (omit frontDoorRouting.pattern). Use when the ingress path (e.g. /miso) is not part of in-container routes. Omit or false to keep pattern on internal URLs (e.g. Keycloak /auth)."
|
|
540
|
+
},
|
|
537
541
|
"certStore": {
|
|
538
542
|
"type": "string",
|
|
539
543
|
"description": "Certificate store name for wildcard certificates. Optional - only needed when using a pre-configured certificate store in Traefik.",
|
|
@@ -342,6 +342,16 @@ parameters:
|
|
|
342
342
|
azure:
|
|
343
343
|
notes: Empty until set; user-supplied Azure OpenAI API key (not auto-generated).
|
|
344
344
|
|
|
345
|
+
# Legacy unprefixed name (scaffold / old env.template); prefer *KeyVault suffix or {appKey}-secrets-apiKeyVault (keyvault.md).
|
|
346
|
+
- key: api-key
|
|
347
|
+
scope: app
|
|
348
|
+
generator:
|
|
349
|
+
type: randomBytes32
|
|
350
|
+
ensureOn: [resolveApp]
|
|
351
|
+
azure:
|
|
352
|
+
notes: >-
|
|
353
|
+
Legacy kv://api-key; new apps should use kv://api-keyKeyVault or {appKey}-secrets-apiKeyVault.
|
|
354
|
+
|
|
345
355
|
# Legacy unprefixed name; prefer kv://{appKey}-secrets-apiKeyVault in env.template (keyvault.md secrets.apiKeyVault).
|
|
346
356
|
- key: miso-controller-secrets-apiKeyVault
|
|
347
357
|
scope: app
|
|
@@ -12,7 +12,28 @@
|
|
|
12
12
|
'use strict';
|
|
13
13
|
|
|
14
14
|
const path = require('path');
|
|
15
|
-
const
|
|
15
|
+
const defaultFs = require('fs');
|
|
16
|
+
const { nodeFs } = require('../internal/node-fs');
|
|
17
|
+
|
|
18
|
+
const realFs = nodeFs();
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Prefer real disk when `fs` is not mocked (avoids `jest.mock('fs')` bleed in other suites).
|
|
22
|
+
* Use `require('fs')` when Jest replaced it so tests with virtual file stores still work.
|
|
23
|
+
*
|
|
24
|
+
* @returns {Pick<import('fs'), 'existsSync'|'renameSync'>}
|
|
25
|
+
*/
|
|
26
|
+
function getResolverFs() {
|
|
27
|
+
if (
|
|
28
|
+
typeof jest !== 'undefined' &&
|
|
29
|
+
defaultFs.existsSync &&
|
|
30
|
+
typeof jest.isMockFunction === 'function' &&
|
|
31
|
+
jest.isMockFunction(defaultFs.existsSync)
|
|
32
|
+
) {
|
|
33
|
+
return defaultFs;
|
|
34
|
+
}
|
|
35
|
+
return realFs;
|
|
36
|
+
}
|
|
16
37
|
|
|
17
38
|
/**
|
|
18
39
|
* Resolves path to application config file (application.yaml, application.json, or legacy variables.yaml).
|
|
@@ -23,6 +44,7 @@ const fs = require('fs');
|
|
|
23
44
|
* @throws {Error} If no config file found
|
|
24
45
|
*/
|
|
25
46
|
function resolveApplicationConfigPath(appPath) {
|
|
47
|
+
const fs = getResolverFs();
|
|
26
48
|
if (!appPath || typeof appPath !== 'string') {
|
|
27
49
|
throw new Error('App path is required and must be a string');
|
|
28
50
|
}
|
|
@@ -59,6 +81,7 @@ const RBAC_NAMES = ['rbac.yaml', 'rbac.yml', 'rbac.json'];
|
|
|
59
81
|
* @returns {string|null} Absolute path to RBAC file, or null if none exist
|
|
60
82
|
*/
|
|
61
83
|
function resolveRbacPath(appPath) {
|
|
84
|
+
const fs = getResolverFs();
|
|
62
85
|
if (!appPath || typeof appPath !== 'string') {
|
|
63
86
|
throw new Error('App path is required and must be a string');
|
|
64
87
|
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User `applications` entries in config.yaml (read by `aifabrix resolve`, written by `aifabrix run` dev).
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Per-app reload and `proxy` (Traefik/public URL hints) in ~/.aifabrix/config.yaml
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 1.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const config = require('../core/config');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Persist `applications.<appKey>.reload` from the last `aifabrix run` (dev only; CLI is source of truth).
|
|
15
|
+
*
|
|
16
|
+
* @async
|
|
17
|
+
* @param {string} appKey - Application key
|
|
18
|
+
* @param {boolean} reload - Same as passing `--reload` on run
|
|
19
|
+
* @returns {Promise<void>}
|
|
20
|
+
*/
|
|
21
|
+
async function persistApplicationReloadFlag(appKey, reload) {
|
|
22
|
+
if (!appKey || typeof appKey !== 'string') {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const cfg = await config.getConfig();
|
|
26
|
+
if (!cfg.applications || typeof cfg.applications !== 'object') {
|
|
27
|
+
cfg.applications = {};
|
|
28
|
+
}
|
|
29
|
+
const prev = cfg.applications[appKey];
|
|
30
|
+
const nextEntry =
|
|
31
|
+
prev && typeof prev === 'object' && !Array.isArray(prev) ? { ...prev, reload: Boolean(reload) } : { reload: Boolean(reload) };
|
|
32
|
+
cfg.applications[appKey] = nextEntry;
|
|
33
|
+
await config.saveConfig(cfg);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* True when `applications.<appKey>.reload` is explicitly **true** (bind-mount + hot reload for `aifabrix run`).
|
|
38
|
+
* Does **not** control other compose overrides (e.g. Python image `command` when the image lacks `uvicorn` on PATH).
|
|
39
|
+
*
|
|
40
|
+
* @param {Object|null|undefined} userConfig - Parsed config.yaml root
|
|
41
|
+
* @param {string} appKey - Application key (folder / app.key)
|
|
42
|
+
* @returns {boolean} True when `applications.<appKey>.reload` is explicitly true
|
|
43
|
+
*/
|
|
44
|
+
function isApplicationsReloadDefaultOn(userConfig, appKey) {
|
|
45
|
+
if (!userConfig || !appKey || typeof appKey !== 'string') {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
const apps = userConfig.applications;
|
|
49
|
+
if (!apps || typeof apps !== 'object') {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
const entry = apps[appKey];
|
|
53
|
+
if (!entry || typeof entry !== 'object') {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
return entry.reload === true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* True when `remote-server` is a non-empty string (after trim).
|
|
61
|
+
*
|
|
62
|
+
* @param {string|null|undefined} remoteServer
|
|
63
|
+
* @returns {boolean}
|
|
64
|
+
*/
|
|
65
|
+
function isRemoteServerConfigured(remoteServer) {
|
|
66
|
+
return Boolean(remoteServer && String(remoteServer).trim());
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Whether `build.envOutputPath` should be regenerated with the **local** profile (`generateEnvContent(..., 'local')`).
|
|
71
|
+
* **False** only when **both** `remote-server` is set and `applications.<appKey>.reload` is true — then the host file
|
|
72
|
+
* uses the same **docker**-flavor expansion as `builder/<app>/.env` (copy path). All other cases use **local**.
|
|
73
|
+
*
|
|
74
|
+
* @param {Object|null|undefined} userConfig - Parsed config.yaml root
|
|
75
|
+
* @param {string} appKey - Application key
|
|
76
|
+
* @param {string|null|undefined} remoteServer - `remote-server` value (or null)
|
|
77
|
+
* @returns {boolean}
|
|
78
|
+
*/
|
|
79
|
+
function isPreferLocalEnvOutputPath(userConfig, appKey, remoteServer) {
|
|
80
|
+
if (!isRemoteServerConfigured(remoteServer)) {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
return !isApplicationsReloadDefaultOn(userConfig, appKey);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* For `aifabrix resolve`: reads `remote-server` and returns {@link isPreferLocalEnvOutputPath}.
|
|
88
|
+
*
|
|
89
|
+
* @async
|
|
90
|
+
* @param {Object|null|undefined} userConfig
|
|
91
|
+
* @param {string} appKey
|
|
92
|
+
* @returns {Promise<boolean>}
|
|
93
|
+
*/
|
|
94
|
+
async function resolvePreferLocalEnvOutputPathFlag(userConfig, appKey) {
|
|
95
|
+
let remoteServer = null;
|
|
96
|
+
try {
|
|
97
|
+
const rs = await config.getRemoteServer();
|
|
98
|
+
if (rs && String(rs).trim()) {
|
|
99
|
+
remoteServer = String(rs).trim();
|
|
100
|
+
}
|
|
101
|
+
} catch {
|
|
102
|
+
remoteServer = null;
|
|
103
|
+
}
|
|
104
|
+
return isPreferLocalEnvOutputPath(userConfig, appKey, remoteServer);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Normalize YAML boolean-ish values.
|
|
109
|
+
* @param {unknown} v
|
|
110
|
+
* @returns {boolean|undefined} undefined if absent / not coercible
|
|
111
|
+
*/
|
|
112
|
+
function coerceTriStateBool(v) {
|
|
113
|
+
if (v === true || v === 'true' || v === 'yes' || v === 'on' || v === 1 || v === '1') {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
if (v === false || v === 'false' || v === 'no' || v === 'off' || v === 0 || v === '0') {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Whether `aifabrix run` / resolve should use Traefik-style public URL hints for this app when infra allows.
|
|
124
|
+
* Default **false** when unset. Legacy `noProxy: true` ⇒ false; legacy `noProxy: false` with no `proxy` ⇒ true.
|
|
125
|
+
*
|
|
126
|
+
* @param {Object|null|undefined} userConfig - Parsed config.yaml root
|
|
127
|
+
* @param {string} appKey - Application key
|
|
128
|
+
* @returns {boolean}
|
|
129
|
+
*/
|
|
130
|
+
function getApplicationsRunProxyHint(userConfig, appKey) {
|
|
131
|
+
if (!userConfig || !appKey || typeof appKey !== 'string') {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
const apps = userConfig.applications;
|
|
135
|
+
if (!apps || typeof apps !== 'object') {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
const entry = apps[appKey];
|
|
139
|
+
if (!entry || typeof entry !== 'object') {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
const proxyCoerced = coerceTriStateBool(entry.proxy);
|
|
143
|
+
if (proxyCoerced !== undefined) {
|
|
144
|
+
return proxyCoerced;
|
|
145
|
+
}
|
|
146
|
+
const legacyNo = coerceTriStateBool(entry.noProxy);
|
|
147
|
+
if (legacyNo === true) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
if (legacyNo === false) {
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Persist `applications.<appKey>.proxy` from each `aifabrix run` (same pattern as `reload` for dev-only reload flag).
|
|
158
|
+
* `proxy: true` means use Traefik/front-door URL hints when infra + that app's `application.yaml` allow; `false` means docker `url://` **public** bases for **that app** use localhost + published port. During `resolve`/`run`, each `url://` token uses the **target** app's `applications.<targetKey>.proxy`, not only the app whose `env.template` is being expanded.
|
|
159
|
+
* Removes legacy `noProxy` on the same entry when present.
|
|
160
|
+
*
|
|
161
|
+
* @async
|
|
162
|
+
* @param {string} appKey - Application key
|
|
163
|
+
* @param {boolean} proxyEnabled - Same as a normal `aifabrix run` with proxy hints on (`!isRunCliNoProxy(options)`)
|
|
164
|
+
* @returns {Promise<void>}
|
|
165
|
+
*/
|
|
166
|
+
async function persistApplicationRunProxyFlag(appKey, proxyEnabled) {
|
|
167
|
+
if (!appKey || typeof appKey !== 'string') {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const cfg = await config.getConfig();
|
|
171
|
+
if (!cfg.applications || typeof cfg.applications !== 'object') {
|
|
172
|
+
cfg.applications = {};
|
|
173
|
+
}
|
|
174
|
+
const prev = cfg.applications[appKey];
|
|
175
|
+
const nextEntry =
|
|
176
|
+
prev && typeof prev === 'object' && !Array.isArray(prev)
|
|
177
|
+
? { ...prev, proxy: Boolean(proxyEnabled) }
|
|
178
|
+
: { proxy: Boolean(proxyEnabled) };
|
|
179
|
+
if (Object.prototype.hasOwnProperty.call(nextEntry, 'noProxy')) {
|
|
180
|
+
delete nextEntry.noProxy;
|
|
181
|
+
}
|
|
182
|
+
cfg.applications[appKey] = nextEntry;
|
|
183
|
+
await config.saveConfig(cfg);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Whether declarative `url://*` expansion should treat Traefik as on (infra Traefik + per-app `proxy: true`).
|
|
188
|
+
*
|
|
189
|
+
* @param {Object|null|undefined} userConfig
|
|
190
|
+
* @param {string} appKey
|
|
191
|
+
* @returns {boolean}
|
|
192
|
+
*/
|
|
193
|
+
function isDeclarativeTraefikUrlsEnabled(userConfig, appKey) {
|
|
194
|
+
return Boolean(userConfig && userConfig.traefik === true) && getApplicationsRunProxyHint(userConfig, appKey);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
module.exports = {
|
|
198
|
+
getApplicationsRunProxyHint,
|
|
199
|
+
isApplicationsReloadDefaultOn,
|
|
200
|
+
isDeclarativeTraefikUrlsEnabled,
|
|
201
|
+
isPreferLocalEnvOutputPath,
|
|
202
|
+
isRemoteServerConfigured,
|
|
203
|
+
persistApplicationRunProxyFlag,
|
|
204
|
+
persistApplicationReloadFlag,
|
|
205
|
+
resolvePreferLocalEnvOutputPathFlag
|
|
206
|
+
};
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* @version 2.0.0
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
const {
|
|
11
|
+
const { hasStoredDeviceTokenForController } = require('./controller-url');
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Validate controller URL format
|
|
@@ -72,17 +72,7 @@ async function checkUserLoggedIn(controllerUrl) {
|
|
|
72
72
|
if (!controllerUrl) {
|
|
73
73
|
return false;
|
|
74
74
|
}
|
|
75
|
-
|
|
76
|
-
const normalizedUrl = controllerUrl.trim().replace(/\/+$/, '');
|
|
77
|
-
const loggedInControllerUrl = await getControllerUrlFromLoggedInUser();
|
|
78
|
-
|
|
79
|
-
if (!loggedInControllerUrl) {
|
|
80
|
-
return false;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Normalize both URLs for comparison
|
|
84
|
-
const normalizedLoggedIn = loggedInControllerUrl.trim().replace(/\/+$/, '');
|
|
85
|
-
return normalizedLoggedIn === normalizedUrl;
|
|
75
|
+
return hasStoredDeviceTokenForController(controllerUrl);
|
|
86
76
|
}
|
|
87
77
|
|
|
88
78
|
module.exports = {
|
|
@@ -41,7 +41,7 @@ function collectBashPrefixedEnv(secrets) {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
|
-
* Overlay for `child_process` `env`: values from user
|
|
44
|
+
* Overlay for `child_process` `env`: values from primary user `secrets.local.yaml` plus `aifabrix-secrets`
|
|
45
45
|
* for every `BASH_*` key (merged via {@link secretsLoad.loadSecrets}).
|
|
46
46
|
*
|
|
47
47
|
* @param {string|null} [secretsPath] - Optional explicit secrets file (same as resolve)
|