@aifabrix/builder 2.57.0 → 2.58.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/lib/app/push.js +98 -41
- package/lib/build/standard-docker-build.js +13 -11
- package/lib/cli/setup-app.js +16 -5
- package/lib/cli/setup-dev-path-commands.js +49 -2
- package/lib/cli/setup-environment.js +17 -4
- package/lib/commands/dev-registry.js +155 -0
- package/lib/commands/dev-show-display.js +10 -2
- package/lib/commands/setup-image-refresh.js +26 -0
- package/lib/core/config-attach-extensions.js +4 -1
- package/lib/deployment/installation/azure-infra-stage.js +40 -9
- package/lib/deployment/installation/azure-preflight.js +53 -8
- package/lib/deployment/installation/azure-readiness.js +35 -5
- package/lib/deployment/installation/infra-catalog.js +50 -4
- package/lib/deployment/installation/infra-manifest.js +17 -2
- package/lib/deployment/installation/infra-projection.js +14 -3
- package/lib/deployment/installation/registry-auth-mode.js +98 -0
- package/lib/deployment/installation/stage-input.js +21 -6
- package/lib/generator/builders.js +8 -9
- package/lib/schema/infrastructure-schema.json +55 -2
- package/lib/utils/app-register-config.js +2 -4
- package/lib/utils/build-resolve-image.js +2 -21
- package/lib/utils/cli-utils.js +8 -1
- package/lib/utils/config-registry-preference.js +106 -0
- package/lib/utils/docker-build.js +53 -19
- package/lib/utils/env-map.js +23 -6
- package/lib/utils/image-tags.js +88 -0
- package/lib/utils/promote-dev-docker-image.js +6 -0
- package/lib/utils/registry-credentials.js +236 -0
- package/lib/utils/resolve-docker-image-ref.js +78 -9
- package/lib/utils/secrets-helpers.js +9 -1
- package/lib/utils/variable-transformer.js +2 -4
- package/package.json +2 -1
- package/templates/marketplace/main.json +2 -2
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-scoped container registry credentials in the primary user secrets file.
|
|
3
|
+
*
|
|
4
|
+
* Keys are exactly `<registry>-username` and `<registry>-password` in the user's
|
|
5
|
+
* `secrets.local.yaml` (never the shared store). Values are encrypted through the existing
|
|
6
|
+
* local-secret writer and reach Docker only via `docker login --password-stdin`.
|
|
7
|
+
*
|
|
8
|
+
* @fileoverview Registry credential storage, lookup, removal, and per-command Docker login
|
|
9
|
+
* @author AI Fabrix Team
|
|
10
|
+
* @version 1.0.0
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const yaml = require('js-yaml');
|
|
17
|
+
const config = require('../core/config');
|
|
18
|
+
const pathsUtil = require('./paths');
|
|
19
|
+
const { loadExistingSecrets, mergeSecretsIntoFile } = require('./secrets-generator');
|
|
20
|
+
const { saveLocalSecret } = require('./local-secrets');
|
|
21
|
+
const { decryptSecret, isEncrypted } = require('./secrets-encryption');
|
|
22
|
+
const { normalizeRegistryHost } = require('./config-registry-preference');
|
|
23
|
+
|
|
24
|
+
/** Hosts already authenticated during this CLI process. */
|
|
25
|
+
const authenticatedHosts = new Set();
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {string} registry - Registry host
|
|
29
|
+
* @returns {{ host: string, usernameKey: string, passwordKey: string }}
|
|
30
|
+
*/
|
|
31
|
+
function registryCredentialKeys(registry) {
|
|
32
|
+
const host = normalizeRegistryHost(registry);
|
|
33
|
+
return { host, usernameKey: `${host}-username`, passwordKey: `${host}-password` };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function hasValue(value) {
|
|
37
|
+
return typeof value === 'string' ? value.trim() !== '' : value !== undefined && value !== null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function readRawPair(keys) {
|
|
41
|
+
const file = pathsUtil.getPrimaryUserSecretsLocalPath();
|
|
42
|
+
const raw = loadExistingSecrets(file);
|
|
43
|
+
return { file, username: raw[keys.usernameKey], password: raw[keys.passwordKey] };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function partialPairError(keys) {
|
|
47
|
+
return new Error(
|
|
48
|
+
`Registry credentials for ${keys.host} are incomplete: both ${keys.usernameKey} and ${keys.passwordKey} ` +
|
|
49
|
+
`must be set. Run "aifabrix dev set-registry ${keys.host} --username <username> --password-stdin" ` +
|
|
50
|
+
`or "aifabrix dev set-registry ${keys.host} --clear-credentials".`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Credential presence for a registry without decrypting anything.
|
|
56
|
+
* @param {string} registry - Registry host
|
|
57
|
+
* @returns {'complete'|'absent'|'partial'}
|
|
58
|
+
*/
|
|
59
|
+
function getRegistryCredentialStatus(registry) {
|
|
60
|
+
const keys = registryCredentialKeys(registry);
|
|
61
|
+
const pair = readRawPair(keys);
|
|
62
|
+
const count = [pair.username, pair.password].filter(hasValue).length;
|
|
63
|
+
if (count === 2) return 'complete';
|
|
64
|
+
return count === 0 ? 'absent' : 'partial';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function decryptStoredValue(value, label) {
|
|
68
|
+
const text = String(value);
|
|
69
|
+
if (!isEncrypted(text)) return text;
|
|
70
|
+
const key = await config.getSecretsEncryptionKey();
|
|
71
|
+
if (!key) {
|
|
72
|
+
throw new Error(`Cannot decrypt the stored ${label}: the local secrets encryption key is not configured`);
|
|
73
|
+
}
|
|
74
|
+
return decryptSecret(text, key);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Loads a complete credential pair for the registry.
|
|
79
|
+
* @param {string} registry - Registry host
|
|
80
|
+
* @returns {Promise<{ username: string, password: string }|null>} null when no pair is stored
|
|
81
|
+
* @throws {Error} When only one of the two keys is stored
|
|
82
|
+
*/
|
|
83
|
+
async function loadRegistryCredentials(registry) {
|
|
84
|
+
const keys = registryCredentialKeys(registry);
|
|
85
|
+
const pair = readRawPair(keys);
|
|
86
|
+
const present = [pair.username, pair.password].filter(hasValue).length;
|
|
87
|
+
if (present === 0) return null;
|
|
88
|
+
if (present === 1) throw partialPairError(keys);
|
|
89
|
+
return {
|
|
90
|
+
username: await decryptStoredValue(pair.username, `${keys.host} username`),
|
|
91
|
+
password: await decryptStoredValue(pair.password, `${keys.host} password`)
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function removeKeysFromFile(file, keyNames) {
|
|
96
|
+
if (!fs.existsSync(file)) return false;
|
|
97
|
+
const data = loadExistingSecrets(file);
|
|
98
|
+
const present = keyNames.filter((key) => Object.prototype.hasOwnProperty.call(data, key));
|
|
99
|
+
if (present.length === 0) return false;
|
|
100
|
+
for (const key of present) delete data[key];
|
|
101
|
+
const { assertWritableSecretsPathForTests } = require('./aifabrix-test-runtime-guard');
|
|
102
|
+
assertWritableSecretsPathForTests(file);
|
|
103
|
+
fs.writeFileSync(file, yaml.dump(data, { indent: 2, lineWidth: -1, noRefs: true, sortKeys: false }), { mode: 0o600 });
|
|
104
|
+
// `mode` applies only on creation; keep an existing file owner-only too.
|
|
105
|
+
fs.chmodSync(file, 0o600);
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function restorePair(keys, snapshot) {
|
|
110
|
+
const restore = {};
|
|
111
|
+
const remove = [];
|
|
112
|
+
for (const [key, value] of [[keys.usernameKey, snapshot.username], [keys.passwordKey, snapshot.password]]) {
|
|
113
|
+
if (value === undefined) remove.push(key);
|
|
114
|
+
else restore[key] = value;
|
|
115
|
+
}
|
|
116
|
+
removeKeysFromFile(snapshot.file, remove);
|
|
117
|
+
mergeSecretsIntoFile(snapshot.file, restore);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Saves a complete pair. On any write failure the previous entries are restored.
|
|
122
|
+
* @param {string} registry - Registry host
|
|
123
|
+
* @param {string} username - Registry username
|
|
124
|
+
* @param {string} password - Registry password or token
|
|
125
|
+
* @returns {Promise<{ host: string, rollback: Function }>} rollback restores the prior entries
|
|
126
|
+
*/
|
|
127
|
+
async function saveRegistryCredentials(registry, username, password) {
|
|
128
|
+
const keys = registryCredentialKeys(registry);
|
|
129
|
+
if (!hasValue(username) || !hasValue(password)) {
|
|
130
|
+
throw new Error('Registry username and password must both be non-empty');
|
|
131
|
+
}
|
|
132
|
+
const snapshot = readRawPair(keys);
|
|
133
|
+
await config.ensureSecretsEncryptionKey();
|
|
134
|
+
try {
|
|
135
|
+
await saveLocalSecret(keys.usernameKey, String(username));
|
|
136
|
+
await saveLocalSecret(keys.passwordKey, String(password));
|
|
137
|
+
} catch (error) {
|
|
138
|
+
restorePair(keys, snapshot);
|
|
139
|
+
throw new Error(`Could not save registry credentials for ${keys.host}; previous values were restored (${error.message})`);
|
|
140
|
+
}
|
|
141
|
+
authenticatedHosts.delete(keys.host);
|
|
142
|
+
return { host: keys.host, rollback: () => restorePair(keys, snapshot) };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Removes both credential keys for the registry (idempotent).
|
|
147
|
+
* @param {string} registry - Registry host
|
|
148
|
+
* @returns {boolean} true when at least one key was removed
|
|
149
|
+
*/
|
|
150
|
+
function removeRegistryCredentials(registry) {
|
|
151
|
+
const keys = registryCredentialKeys(registry);
|
|
152
|
+
authenticatedHosts.delete(keys.host);
|
|
153
|
+
return removeKeysFromFile(pathsUtil.getPrimaryUserSecretsLocalPath(), [keys.usernameKey, keys.passwordKey]);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Authenticates Docker to the registry once per process: stored pair first, then the optional
|
|
158
|
+
* fallback (for example Azure CLI for ACR). The password is sent only on stdin.
|
|
159
|
+
* @param {string} registry - Registry host
|
|
160
|
+
* @param {Object} [options] - Options
|
|
161
|
+
* @param {Function} [options.fallback] - async (host) => void when no pair is stored
|
|
162
|
+
* @param {Function} [options.login] - async (host, username, password) => void (tests)
|
|
163
|
+
* @returns {Promise<'cached'|'credentials'|'fallback'|'none'>}
|
|
164
|
+
*/
|
|
165
|
+
async function ensureRegistryLogin(registry, options = {}) {
|
|
166
|
+
const host = normalizeRegistryHost(registry);
|
|
167
|
+
if (authenticatedHosts.has(host)) return 'cached';
|
|
168
|
+
const credentials = await loadRegistryCredentials(host);
|
|
169
|
+
if (credentials) {
|
|
170
|
+
const login = options.login || require('../deployment/push').authenticateExternalRegistry;
|
|
171
|
+
await login(host, credentials.username, credentials.password);
|
|
172
|
+
authenticatedHosts.add(host);
|
|
173
|
+
return 'credentials';
|
|
174
|
+
}
|
|
175
|
+
if (typeof options.fallback === 'function') {
|
|
176
|
+
await options.fallback(host);
|
|
177
|
+
authenticatedHosts.add(host);
|
|
178
|
+
return 'fallback';
|
|
179
|
+
}
|
|
180
|
+
return 'none';
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Registry host of an image reference, or '' for Docker Hub style refs.
|
|
185
|
+
* @param {string} imageRef - Image reference (`host/repo:tag`, `repo:tag`, ...)
|
|
186
|
+
* @returns {string}
|
|
187
|
+
*/
|
|
188
|
+
function registryHostFromImageRef(imageRef) {
|
|
189
|
+
const ref = String(imageRef || '').trim();
|
|
190
|
+
const slash = ref.indexOf('/');
|
|
191
|
+
if (slash <= 0) return '';
|
|
192
|
+
const first = ref.slice(0, slash);
|
|
193
|
+
if (!(first.includes('.') || first.includes(':') || first === 'localhost')) return '';
|
|
194
|
+
try {
|
|
195
|
+
return normalizeRegistryHost(first);
|
|
196
|
+
} catch {
|
|
197
|
+
return '';
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Logs in once per distinct registry host of the given image refs when a stored pair exists.
|
|
203
|
+
* Hosts without stored credentials are pulled anonymously (or with the user's own Docker login).
|
|
204
|
+
* @param {string[]} imageRefs - Image references
|
|
205
|
+
* @returns {Promise<string[]>} Hosts logged in with stored credentials
|
|
206
|
+
*/
|
|
207
|
+
async function ensureLoginForImageRefs(imageRefs) {
|
|
208
|
+
const hosts = [...new Set((imageRefs || []).map(registryHostFromImageRef).filter(Boolean))];
|
|
209
|
+
const used = [];
|
|
210
|
+
for (const host of hosts) {
|
|
211
|
+
const outcome = await ensureRegistryLogin(host);
|
|
212
|
+
if (outcome === 'credentials') used.push(host);
|
|
213
|
+
}
|
|
214
|
+
return used;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Forgets cached logins (used before re-authentication and in tests).
|
|
219
|
+
* @param {string} [registry] - Host to forget; all when omitted
|
|
220
|
+
*/
|
|
221
|
+
function resetRegistryLoginCache(registry) {
|
|
222
|
+
if (registry) authenticatedHosts.delete(normalizeRegistryHost(registry));
|
|
223
|
+
else authenticatedHosts.clear();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
module.exports = {
|
|
227
|
+
registryCredentialKeys,
|
|
228
|
+
getRegistryCredentialStatus,
|
|
229
|
+
loadRegistryCredentials,
|
|
230
|
+
saveRegistryCredentials,
|
|
231
|
+
removeRegistryCredentials,
|
|
232
|
+
ensureRegistryLogin,
|
|
233
|
+
ensureLoginForImageRefs,
|
|
234
|
+
registryHostFromImageRef,
|
|
235
|
+
resetRegistryLoginCache
|
|
236
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Resolve Docker repository path and tag from application config and optional CLI overrides.
|
|
3
|
-
* Precedence: --image (full ref), --registry CLI,
|
|
3
|
+
* Precedence: --image (full ref), --registry CLI, installation `registry` in config.yaml,
|
|
4
|
+
* image.registry in manifest, else unqualified name.
|
|
4
5
|
*
|
|
5
6
|
* For refs like localhost:5000/repo without an explicit tag, prefer --image with :tag (parse ambiguity).
|
|
6
7
|
*
|
|
@@ -12,6 +13,7 @@
|
|
|
12
13
|
'use strict';
|
|
13
14
|
|
|
14
15
|
const { parseImageOverride } = require('./parse-image-ref');
|
|
16
|
+
const { readInstallationRegistrySync } = require('./config-registry-preference');
|
|
15
17
|
const DOCKER_REPOSITORY_RE = /^[a-z0-9][a-z0-9._/-]*$/i;
|
|
16
18
|
const DOCKER_TAG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
17
19
|
|
|
@@ -92,6 +94,73 @@ function normalizeDockerRegistryPrefix(registry) {
|
|
|
92
94
|
return t.slice(0, end);
|
|
93
95
|
}
|
|
94
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Remove a registry host prefix from a Docker repository path.
|
|
99
|
+
* The first segment is a host when it contains `.` or `:` or equals `localhost`.
|
|
100
|
+
* @param {string} repository
|
|
101
|
+
* @returns {string}
|
|
102
|
+
*/
|
|
103
|
+
function stripRegistryHostFromRepository(repository) {
|
|
104
|
+
if (!repository || typeof repository !== 'string') {
|
|
105
|
+
return repository;
|
|
106
|
+
}
|
|
107
|
+
const parts = repository.split('/');
|
|
108
|
+
if (parts.length <= 1) {
|
|
109
|
+
return repository;
|
|
110
|
+
}
|
|
111
|
+
const host = parts[0];
|
|
112
|
+
if (host.includes('.') || host.includes(':') || host === 'localhost') {
|
|
113
|
+
return parts.slice(1).join('/');
|
|
114
|
+
}
|
|
115
|
+
return repository;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Effective registry for an application image.
|
|
120
|
+
* Precedence: `options.registry` (CLI) > installation `registry` > manifest `image.registry`.
|
|
121
|
+
* @param {Object} [appConfig] - Application manifest
|
|
122
|
+
* @param {Object} [options] - `registry` (CLI) and optional `installationRegistry` (preloaded; '' = none)
|
|
123
|
+
* @returns {{ registry: string, source: 'cli'|'installation'|'manifest'|'none' }}
|
|
124
|
+
*/
|
|
125
|
+
function resolveEffectiveRegistry(appConfig, options = {}) {
|
|
126
|
+
const opts = options || {};
|
|
127
|
+
const cli = normalizeDockerRegistryPrefix(opts.registry);
|
|
128
|
+
if (cli) return { registry: cli, source: 'cli' };
|
|
129
|
+
const installation = opts.installationRegistry !== undefined
|
|
130
|
+
? normalizeDockerRegistryPrefix(opts.installationRegistry)
|
|
131
|
+
: readInstallationRegistrySync();
|
|
132
|
+
if (installation) return { registry: installation, source: 'installation' };
|
|
133
|
+
const manifest = normalizeDockerRegistryPrefix(appConfig?.image?.registry ?? '');
|
|
134
|
+
if (manifest) return { registry: manifest, source: 'manifest' };
|
|
135
|
+
return { registry: '', source: 'none' };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Prefix a repository with a registry exactly once (replacing any existing host).
|
|
140
|
+
* @param {string} registry - Registry prefix ('' keeps the repository unchanged)
|
|
141
|
+
* @param {string} repository - Repository path, possibly already qualified
|
|
142
|
+
* @returns {string}
|
|
143
|
+
*/
|
|
144
|
+
function qualifyRepository(registry, repository) {
|
|
145
|
+
const prefix = normalizeDockerRegistryPrefix(registry);
|
|
146
|
+
if (!prefix) return repository;
|
|
147
|
+
return `${prefix}/${stripRegistryHostFromRepository(repository)}`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Deployment/registration image reference (`registry/name:tag`) using the shared precedence.
|
|
152
|
+
* @param {Object} variables - Application manifest
|
|
153
|
+
* @param {string} fallbackName - Name when the manifest has no image name or app key
|
|
154
|
+
* @param {Object} [options] - Same options as {@link resolveEffectiveRegistry}
|
|
155
|
+
* @returns {string}
|
|
156
|
+
*/
|
|
157
|
+
function buildManifestImageReference(variables, fallbackName, options = {}) {
|
|
158
|
+
const imageName = variables?.image?.name || variables?.app?.key || fallbackName;
|
|
159
|
+
const tag = variables?.image?.tag || 'latest';
|
|
160
|
+
const { registry } = resolveEffectiveRegistry(variables, options);
|
|
161
|
+
return `${qualifyRepository(registry, imageName)}:${tag}`;
|
|
162
|
+
}
|
|
163
|
+
|
|
95
164
|
/**
|
|
96
165
|
* Effective image repository (may include registry prefix) and tag for Docker.
|
|
97
166
|
* @param {string} appName - Application name
|
|
@@ -118,13 +187,8 @@ function resolveDockerImageRef(appName, appConfig, runOptions = {}) {
|
|
|
118
187
|
|
|
119
188
|
const baseRepo = assertSafeRepository(getRepositoryPathFromConfig(appConfig, appName));
|
|
120
189
|
const imageTag = assertSafeTag(tagFromOpts || imageTagFromConfig(appConfig));
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
normalizeDockerRegistryPrefix(appConfig?.image?.registry ?? '');
|
|
124
|
-
if (prefix) {
|
|
125
|
-
return { imageName: assertSafeRepository(`${prefix}/${baseRepo}`), imageTag };
|
|
126
|
-
}
|
|
127
|
-
return { imageName: baseRepo, imageTag };
|
|
190
|
+
const { registry } = resolveEffectiveRegistry(appConfig, opts);
|
|
191
|
+
return { imageName: assertSafeRepository(qualifyRepository(registry, baseRepo)), imageTag };
|
|
128
192
|
}
|
|
129
193
|
|
|
130
194
|
/**
|
|
@@ -137,7 +201,7 @@ function resolveDockerImageRef(appName, appConfig, runOptions = {}) {
|
|
|
137
201
|
function resolveComposeImageOverrideString(appName, appConfig, options = {}) {
|
|
138
202
|
if (options.image) return options.image;
|
|
139
203
|
if (options.imageOverride) return options.imageOverride;
|
|
140
|
-
const runOpts = { registry: options.registry, image: undefined };
|
|
204
|
+
const runOpts = { registry: options.registry, installationRegistry: options.installationRegistry, image: undefined };
|
|
141
205
|
if (options.tag) {
|
|
142
206
|
const { imageName } = resolveDockerImageRef(appName, appConfig, runOpts);
|
|
143
207
|
return `${imageName}:${options.tag}`;
|
|
@@ -154,6 +218,11 @@ function resolveComposeImageOverrideString(appName, appConfig, options = {}) {
|
|
|
154
218
|
module.exports = {
|
|
155
219
|
resolveDockerImageRef,
|
|
156
220
|
resolveComposeImageOverrideString,
|
|
221
|
+
resolveEffectiveRegistry,
|
|
222
|
+
qualifyRepository,
|
|
223
|
+
buildManifestImageReference,
|
|
224
|
+
stripRegistryHostFromRepository,
|
|
225
|
+
assertSafeTag,
|
|
157
226
|
normalizeDockerRegistryPrefix,
|
|
158
227
|
getRepositoryPathFromConfig,
|
|
159
228
|
imageTagFromConfig
|
|
@@ -29,7 +29,15 @@ const { collectUniqueKvPathStrings, isCommentOrEmptyLine, activeEnvLineForKvScan
|
|
|
29
29
|
* @returns {string} Interpolated content
|
|
30
30
|
*/
|
|
31
31
|
function interpolateEnvVars(content, envVars) {
|
|
32
|
-
|
|
32
|
+
const hasOwn = (key) => Object.prototype.hasOwnProperty.call(envVars, key);
|
|
33
|
+
const interpolated = content.replace(
|
|
34
|
+
/\$\{DEV_USERNAME\}\.\$\{REMOTE_HOST\}/g,
|
|
35
|
+
(match) => {
|
|
36
|
+
if (!hasOwn('DEV_USERNAME') || !hasOwn('REMOTE_HOST')) return match;
|
|
37
|
+
return [envVars.DEV_USERNAME, envVars.REMOTE_HOST].filter(Boolean).join('.');
|
|
38
|
+
}
|
|
39
|
+
);
|
|
40
|
+
return interpolated.replace(/\$\{([A-Z_]+)\}/g, (match, envVar) => {
|
|
33
41
|
return envVars[envVar] || match;
|
|
34
42
|
});
|
|
35
43
|
}
|
|
@@ -126,10 +126,8 @@ function transformFlatStructure(variables, appName) {
|
|
|
126
126
|
* @returns {string} Image reference string
|
|
127
127
|
*/
|
|
128
128
|
function buildImageReference(variables, appName) {
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
const tag = variables.image?.tag || 'latest';
|
|
132
|
-
return registry ? `${registry}/${imageName}:${tag}` : `${imageName}:${tag}`;
|
|
129
|
+
const { buildManifestImageReference } = require('./resolve-docker-image-ref');
|
|
130
|
+
return buildManifestImageReference(variables, appName);
|
|
133
131
|
}
|
|
134
132
|
|
|
135
133
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aifabrix/builder",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.58.0",
|
|
4
4
|
"description": "AI Fabrix Builder — CLI and developer scripts (pnpm + af)",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"agent-skills:check": "node scripts/pnpm/agent-skills-sync.mjs --check",
|
|
29
29
|
"agent-skills:sync:codex": "node scripts/pnpm/agent-skills-sync.mjs --codex-home",
|
|
30
30
|
"agent-skills:test": "node --test scripts/pnpm/agent-skills-sync.test.mjs",
|
|
31
|
+
"dev-deploy:test": "node --test scripts/pnpm/dev-deploy.test.mjs",
|
|
31
32
|
"check": "npm run lint && npm run test",
|
|
32
33
|
"lint": "eslint . --ext .js",
|
|
33
34
|
"lint:fix": "eslint . --ext .js --fix",
|
|
@@ -2379,7 +2379,7 @@
|
|
|
2379
2379
|
"virtualNetworkSubnetId": "[if(not(empty(parameters('vnetSubnetId'))), parameters('vnetSubnetId'), null())]",
|
|
2380
2380
|
"siteConfig": {
|
|
2381
2381
|
"linuxFxVersion": "[format('DOCKER|{0}', parameters('image'))]",
|
|
2382
|
-
"acrUseManagedIdentityCreds":
|
|
2382
|
+
"acrUseManagedIdentityCreds": "[not(variables('registryPasswordConfigured'))]",
|
|
2383
2383
|
"appCommandLine": "start --optimized=false",
|
|
2384
2384
|
"healthCheckPath": "[variables('keycloakHealthCheckPath')]",
|
|
2385
2385
|
"alwaysOn": true,
|
|
@@ -3083,7 +3083,7 @@
|
|
|
3083
3083
|
"virtualNetworkSubnetId": "[if(not(empty(parameters('vnetSubnetId'))), parameters('vnetSubnetId'), null())]",
|
|
3084
3084
|
"siteConfig": {
|
|
3085
3085
|
"linuxFxVersion": "[format('DOCKER|{0}', parameters('image'))]",
|
|
3086
|
-
"acrUseManagedIdentityCreds":
|
|
3086
|
+
"acrUseManagedIdentityCreds": "[not(parameters('registryPasswordConfigured'))]",
|
|
3087
3087
|
"healthCheckPath": "/health",
|
|
3088
3088
|
"alwaysOn": true,
|
|
3089
3089
|
"appSettings": "[concat(createArray(createObject('name', 'WEBSITES_ENABLE_APP_SERVICE_STORAGE', 'value', 'false'), createObject('name', 'WEBSITES_CONTAINER_START_TIME_LIMIT', 'value', '1800'), createObject('name', 'DATABASE_URL', 'value', variables('databaseUrlAppSettingValue')), createObject('name', 'JWT_SECRET', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName={1}-secrets-jwtKeyVault)', last(split(parameters('keyVaultId'), '/')), parameters('prefix'))), createObject('name', 'NODE_ENV', 'value', 'production'), createObject('name', 'LOCATION', 'value', parameters('location')), createObject('name', 'PORT', 'value', '3000'), createObject('name', 'CORS_ORIGINS', 'value', format('https://{0}.azurewebsites.net', parameters('name'))), createObject('name', 'ALLOWED_ORIGINS', 'value', format('http://localhost:*,http://127.0.0.1:*,https://{0}.azurewebsites.net', parameters('name'))), createObject('name', 'WEBSITE_VNET_ROUTE_ALL', 'value', if(parameters('enablePostgresPublicAccess'), '0', '1')), createObject('name', 'WEBSITE_DNS_SERVER', 'value', '168.63.129.16'), createObject('name', 'DATABASELOG_URL', 'value', variables('databaseLogUrlAppSettingValue')), createObject('name', 'KEY_VAULT_ENABLED', 'value', 'true'), createObject('name', 'SECURITY_STORAGE_MODE', 'value', 'keyvault'), createObject('name', 'KEY_VAULT_URL', 'value', format('https://{0}.vault.azure.net/', last(split(parameters('keyVaultId'), '/')))), createObject('name', 'ENCRYPTION_KEY', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName={1}-encryption-keyKeyVault)', last(split(parameters('keyVaultId'), '/')), parameters('prefix'))), createObject('name', 'AUTO_CREATE_TABLES', 'value', if(parameters('autoCreateTables'), 'true', 'false')), createObject('name', 'KEYCLOAK_REALM', 'value', parameters('keycloakRealm')), createObject('name', 'KEYCLOAK_CLIENT_ID', 'value', parameters('keycloakClientId')), createObject('name', 'KEYCLOAK_ADMIN_USERNAME', 'value', parameters('keycloakAdminUsername')), createObject('name', 'KEYCLOAK_ADMIN_PASSWORD', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName={1}-keycloak-admin-password)', last(split(parameters('keyVaultId'), '/')), parameters('prefix'))), createObject('name', 'ONBOARDING_ADMIN_PASSWORD', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName={1}-onboarding-admin-password)', last(split(parameters('keyVaultId'), '/')), parameters('prefix'))), createObject('name', 'KEYCLOAK_SERVER_URL', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName=keycloak-web-server-url)', last(split(parameters('keyVaultId'), '/')))), createObject('name', 'KEYCLOAK_INTERNAL_SERVER_URL', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName=keycloak-internal-server-url)', last(split(parameters('keyVaultId'), '/')))), createObject('name', 'KEYCLOAK_TOKEN_TIMEOUT', 'value', '5000'), createObject('name', 'KEYCLOAK_VERIFY_AUDIENCE', 'value', if(parameters('keycloakVerifyAudience'), 'true', 'false')), createObject('name', 'DISABLE_RATE_LIMIT', 'value', variables('shouldDisableRateLimit')), createObject('name', 'SKIP_DB_REDEPLOY', 'value', 'true'), createObject('name', 'MISO_WEB_SERVER_URL', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName=miso-controller-web-server-url)', last(split(parameters('keyVaultId'), '/')))), createObject('name', 'MISO_CONTROLLER_URL', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName=miso-controller-internal-server-urlKeyVault)', last(split(parameters('keyVaultId'), '/')))), createObject('name', 'MISO_RELATIVE_PATH', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName=miso-controller-vdir-public)', last(split(parameters('keyVaultId'), '/')))), createObject('name', 'CONTROLLER_BASE_URL', 'value', if(not(empty(parameters('misoControllerUrl'))), parameters('misoControllerUrl'), format('https://{0}.azurewebsites.net', parameters('name')))), createObject('name', 'AZURE_SUBSCRIPTION_ID', 'value', subscription().subscriptionId), createObject('name', 'TENANT_ID', 'value', subscription().tenantId), createObject('name', 'AZURE_RESOURCE_GROUP_NAME', 'value', parameters('resourceGroupName')), createObject('name', 'INFRASTRUCTURE_NAME', 'value', parameters('prefix')), createObject('name', 'ENTRA_ID_CLIENT_ID', 'value', parameters('entraIdClientId')), createObject('name', 'ENTRA_ID_CLIENT_SECRET', 'value', parameters('entraIdClientSecret')), createObject('name', 'ENTRA_ID_TENANT_ID', 'value', parameters('entraIdTenantId')), createObject('name', 'MISO_CLIENTID', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName={1}-miso-clientIdKeyVault)', last(split(parameters('keyVaultId'), '/')), parameters('prefix'))), createObject('name', 'MISO_CLIENTSECRET', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName=miso-controller-client-secretKeyVault)', last(split(parameters('keyVaultId'), '/')))), createObject('name', 'MORI_BASE_URL', 'value', variables('moriBaseNormalized')), createObject('name', 'MORI_CALLBACK_API_KEY', 'value', parameters('moriCallbackApiKey')), createObject('name', 'ENTRA_GRAPH_DELEGATED_REDIRECT_URI', 'value', variables('moriEntraGraphDelegatedRedirectUri')), createObject('name', 'ENTRA_ID_CENTRAL_CALLBACK_URI', 'value', variables('moriEntraCentralCallbackUri')), createObject('name', 'ENTRA_CALLBACK_STATE_SECRET', 'value', parameters('entraCallbackStateSecret')), createObject('name', 'DOCKER_ENABLE_CI', 'value', 'true'), createObject('name', 'DOCKER_REGISTRY_SERVER_URL', 'value', if(not(empty(parameters('acrLoginServer'))), format('https://{0}', parameters('acrLoginServer')), '')), createObject('name', 'ONBOARDING_CREATE_PIPELINE_CREDENTIALS', 'value', 'true'), createObject('name', 'LICENSE_JWT', 'value', parameters('licenseJwt')), createObject('name', 'LICENSE_EDITION', 'value', parameters('licenseEdition')), createObject('name', 'ONBOARDING_MODE', 'value', parameters('onboardingMode'))), variables('registryCredentialAppSettings'))]",
|