@aifabrix/builder 2.61.2 → 2.63.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/api/applications.api.js +22 -2
- package/lib/api/bootstrap-snapshot.api.js +115 -0
- package/lib/api/onboarding-status.api.js +117 -0
- package/lib/app/deploy.js +14 -5
- package/lib/app/register.js +24 -2
- package/lib/app/run-env-compose.js +44 -8
- package/lib/app/run-env-recovery.js +1 -0
- package/lib/app/run-helpers.js +1 -1
- package/lib/app/run.js +6 -4
- package/lib/build/standard-docker-build.js +4 -0
- package/lib/channels/channel-artifact-generator.js +13 -3
- package/lib/cli/infra-guided-footers.js +10 -8
- package/lib/cli/infra-guided.js +60 -12
- package/lib/cli/setup-infra-up-platform-action.js +14 -1
- package/lib/cli/setup-platform.js +20 -3
- package/lib/commands/auth-config.js +2 -0
- package/lib/commands/login.js +4 -0
- package/lib/commands/repair-datasource-apply.js +3 -1
- package/lib/commands/repair-datasource-legacy.js +94 -0
- package/lib/commands/setup-image-refresh.js +26 -0
- package/lib/commands/setup-modes.js +9 -2
- package/lib/commands/setup-onboarding-readiness.js +83 -0
- package/lib/commands/setup-prompts-platform-mode.js +34 -0
- package/lib/commands/setup.js +35 -10
- package/lib/commands/up-builder-api.js +24 -8
- package/lib/commands/up-common.js +16 -1
- package/lib/commands/up-dataplane-bootstrap.js +149 -16
- package/lib/commands/up-dataplane-credentials.js +21 -0
- package/lib/commands/up-dataplane.js +26 -34
- package/lib/commands/up-integration-server.js +112 -0
- package/lib/commands/wizard-config-normalizer.js +5 -1
- package/lib/core/admin-secrets-env-overlay.js +8 -3
- package/lib/core/environment-access-policy.js +121 -0
- package/lib/core/environment-mode-policy.js +177 -0
- package/lib/core/local-env-overrides.js +149 -0
- package/lib/core/secrets-ensure.js +4 -2
- package/lib/core/secrets-env-content.js +12 -9
- package/lib/core/secrets-env-write.js +1 -1
- package/lib/datasource/capability/capability-manifest.js +1 -0
- package/lib/deployment/installation/index.js +14 -1
- package/lib/deployment/installation/infra-catalog.js +3 -0
- package/lib/deployment/installation/local-environment-stage.js +285 -0
- package/lib/deployment/installation/local-installation.js +76 -0
- package/lib/deployment/installation/stage-input.js +5 -1
- package/lib/generator/builders.js +3 -1
- package/lib/generator/index.js +32 -9
- package/lib/generator/split-variables.js +2 -2
- package/lib/parameters/database-adoption.js +125 -0
- package/lib/parameters/physical-database-naming.js +136 -0
- package/lib/parameters/physical-secret-name.js +194 -0
- package/lib/parameters/rsa-secret-values.js +165 -0
- package/lib/programmatic/run-channel-install-artifacts.js +28 -2
- package/lib/programmatic/run-channel-package.js +3 -1
- package/lib/programmatic/workspace-context.js +37 -2
- package/lib/schema/infra.parameter.yaml +61 -27
- package/lib/schema/infrastructure-schema.json +213 -120
- package/lib/utils/compose-generate-docker-compose.js +2 -2
- package/lib/utils/compose-generator.js +4 -7
- package/lib/utils/compose-miso-env.js +2 -2
- package/lib/utils/compose-traefik-ingress-base.js +3 -4
- package/lib/utils/dataplane-dimension-abac-setting.js +120 -0
- package/lib/utils/declarative-url-ports.js +3 -0
- package/lib/utils/env-environment-file-paths.js +105 -0
- package/lib/utils/environment-scoped-resources.js +48 -14
- package/lib/utils/log-redaction.js +4 -1
- package/lib/utils/paths-system-builder-keys.js +7 -1
- package/lib/utils/paths.js +7 -2
- package/lib/utils/postgres-platform-bootstrap.js +35 -1
- package/lib/utils/redis-env-scope.js +21 -1
- package/lib/utils/registry-auth-sources.js +154 -0
- package/lib/utils/registry-credentials.js +131 -14
- package/lib/utils/secrets-generator.js +30 -21
- package/lib/utils/secrets-helpers.js +5 -7
- package/lib/utils/secrets-kv-scope.js +127 -28
- package/lib/utils/secrets-materialize-local.js +9 -0
- package/lib/utils/secrets-missing-error.js +35 -1
- package/lib/utils/token-manager.js +14 -8
- package/lib/utils/url-public-path-prefix.js +6 -10
- package/package.json +4 -2
- package/templates/README.md +6 -0
- package/templates/applications/builder-api/application.yaml +1 -1
- package/templates/applications/dataplane/application.yaml +2 -2
- package/templates/applications/dataplane/env.template +1 -1
- package/templates/applications/integration-server/application.yaml +91 -0
- package/templates/applications/integration-server/env.template +215 -0
- package/templates/applications/mirrored-templates.json +52 -0
- package/templates/applications/miso-controller/application.yaml +43 -1
- package/templates/applications/miso-controller/env.template +45 -39
- package/templates/marketplace/createUiDefinition.json +52 -4
- package/templates/marketplace/main.json +85 -7
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a registry credential comes from when none is stored.
|
|
3
|
+
*
|
|
4
|
+
* Builder is registry-agnostic: Docker Hub, GHCR and ACR are all a host, a username and a
|
|
5
|
+
* password, and an individual identity, a registry token and an admin key all fit that
|
|
6
|
+
* shape. The only special case is that an `*.azurecr.io` host can also be authenticated by
|
|
7
|
+
* the Azure CLI, which is worth trying before asking a person to paste a secret.
|
|
8
|
+
*
|
|
9
|
+
* Order: the stored pair (handled by the caller), then `az acr login` for ACR, then a
|
|
10
|
+
* prompt. A non-interactive run never prompts — it fails naming the host and how to
|
|
11
|
+
* configure it, because a hung prompt in CI is worse than a clear failure.
|
|
12
|
+
*
|
|
13
|
+
* @fileoverview Credential source strategy for registries without a stored pair
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
/** Azure Container Registry login server suffix. */
|
|
19
|
+
const ACR_HOST_SUFFIX = '.azurecr.io';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {string} host - Registry host
|
|
23
|
+
* @returns {boolean} True for an Azure Container Registry login server
|
|
24
|
+
*/
|
|
25
|
+
function isAzureContainerRegistryHost(host) {
|
|
26
|
+
return String(host || '').trim().toLowerCase().endsWith(ACR_HOST_SUFFIX);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Message for a run that cannot ask for credentials.
|
|
31
|
+
*
|
|
32
|
+
* @param {string} host - Registry host
|
|
33
|
+
* @returns {string}
|
|
34
|
+
*/
|
|
35
|
+
function nonInteractiveCredentialMessage(host) {
|
|
36
|
+
const acrHint = isAzureContainerRegistryHost(host)
|
|
37
|
+
? ` For Azure Container Registry you can instead run "az login" so "az acr login --name ${String(host).split('.')[0]}" succeeds.`
|
|
38
|
+
: '';
|
|
39
|
+
return (
|
|
40
|
+
`No credentials for ${host} and nothing to ask: this run is not interactive. ` +
|
|
41
|
+
`Store a pair with "aifabrix dev set-registry ${host} --username <username> --password-stdin".${acrHint}`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Build the fallback `ensureRegistryLogin` uses when no pair is stored.
|
|
47
|
+
*
|
|
48
|
+
* Dependencies are injected so the chain can be tested without a terminal, an Azure CLI or
|
|
49
|
+
* a Docker daemon.
|
|
50
|
+
*
|
|
51
|
+
* @param {Object} [deps] - Injectable dependencies
|
|
52
|
+
* @param {boolean} [deps.interactive] - Whether a person can be asked; defaults to TTY
|
|
53
|
+
* @param {Function} [deps.acrLogin] - async (host) => void, defaults to `az acr login`
|
|
54
|
+
* @param {Function} [deps.promptCredentials] - async (host) => { username, password }
|
|
55
|
+
* @param {Function} [deps.saveCredentials] - async (host, username, password) => void
|
|
56
|
+
* @param {Function} [deps.dockerLogin] - async (host, username, password) => void
|
|
57
|
+
* @returns {(host: string) => Promise<'acr'|'prompt'>} Fallback for ensureRegistryLogin
|
|
58
|
+
*/
|
|
59
|
+
/**
|
|
60
|
+
* An error that names the host it is about.
|
|
61
|
+
*
|
|
62
|
+
* Callers read `registryHost` off it to report which registry refused, so the property is
|
|
63
|
+
* attached in one place rather than at each throw.
|
|
64
|
+
*
|
|
65
|
+
* @param {string} message - Message
|
|
66
|
+
* @param {string} host - Registry host
|
|
67
|
+
* @returns {Error}
|
|
68
|
+
*/
|
|
69
|
+
function hostError(message, host) {
|
|
70
|
+
const err = new Error(message);
|
|
71
|
+
err.registryHost = host;
|
|
72
|
+
return err;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function buildRegistryAuthFallback(deps = {}) {
|
|
76
|
+
const interactive = deps.interactive === undefined
|
|
77
|
+
? Boolean(process.stdin.isTTY && process.stdout.isTTY)
|
|
78
|
+
: Boolean(deps.interactive);
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Try `az acr login` for an ACR host.
|
|
82
|
+
*
|
|
83
|
+
* Extracted so the fallback itself stays readable. A failure here is deliberately not
|
|
84
|
+
* fatal when a prompt is still possible: an expired or absent `az login` is an ordinary
|
|
85
|
+
* state, and a stored pair or an answered prompt can still authenticate the host.
|
|
86
|
+
*
|
|
87
|
+
* @param {string} host - Registry host
|
|
88
|
+
* @returns {Promise<boolean>} True when ACR login succeeded
|
|
89
|
+
*/
|
|
90
|
+
async function tryAcrLogin(host) {
|
|
91
|
+
if (!isAzureContainerRegistryHost(host)) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
const acrLogin = deps.acrLogin
|
|
95
|
+
|| ((target) => require('../deployment/push').authenticateACR(target));
|
|
96
|
+
try {
|
|
97
|
+
await acrLogin(host);
|
|
98
|
+
return true;
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (!interactive) {
|
|
101
|
+
throw hostError(`${nonInteractiveCredentialMessage(host)} (az acr login failed: ${error.message})`, host);
|
|
102
|
+
}
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return async function registryAuthFallback(host) {
|
|
108
|
+
if (await tryAcrLogin(host)) {
|
|
109
|
+
return 'acr';
|
|
110
|
+
}
|
|
111
|
+
if (!interactive) {
|
|
112
|
+
throw hostError(nonInteractiveCredentialMessage(host), host);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const promptCredentials = deps.promptCredentials || promptForRegistryCredentials;
|
|
116
|
+
const { username, password } = await promptCredentials(host);
|
|
117
|
+
if (!username || !password) {
|
|
118
|
+
throw new Error(`Registry credentials for ${host} are required: both a username and a password.`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const saveCredentials = deps.saveCredentials
|
|
122
|
+
|| ((target, user, pass) => require('./registry-credentials').saveRegistryCredentials(target, user, pass));
|
|
123
|
+
// Store before logging in, so the answer is not asked for again on the next command
|
|
124
|
+
// even if the login itself fails for an unrelated reason.
|
|
125
|
+
await saveCredentials(host, username, password);
|
|
126
|
+
|
|
127
|
+
const dockerLogin = deps.dockerLogin
|
|
128
|
+
|| ((target, user, pass) => require('../deployment/push').authenticateExternalRegistry(target, user, pass));
|
|
129
|
+
await dockerLogin(host, username, password);
|
|
130
|
+
return 'prompt';
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Ask for a registry username and password. The password is never echoed.
|
|
136
|
+
*
|
|
137
|
+
* @param {string} host - Registry host
|
|
138
|
+
* @returns {Promise<{ username: string, password: string }>}
|
|
139
|
+
*/
|
|
140
|
+
async function promptForRegistryCredentials(host) {
|
|
141
|
+
const inquirer = require('inquirer');
|
|
142
|
+
return inquirer.prompt([
|
|
143
|
+
{ type: 'input', name: 'username', message: `Username for ${host}:` },
|
|
144
|
+
{ type: 'password', name: 'password', mask: '*', message: `Password or token for ${host}:` }
|
|
145
|
+
]);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
module.exports = {
|
|
149
|
+
ACR_HOST_SUFFIX,
|
|
150
|
+
isAzureContainerRegistryHost,
|
|
151
|
+
nonInteractiveCredentialMessage,
|
|
152
|
+
buildRegistryAuthFallback,
|
|
153
|
+
promptForRegistryCredentials
|
|
154
|
+
};
|
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Host-scoped container registry credentials in the primary user secrets file.
|
|
3
3
|
*
|
|
4
|
-
* Keys are
|
|
5
|
-
* `secrets.local.yaml` (never the shared store)
|
|
6
|
-
*
|
|
4
|
+
* Keys are `registry-<slug>-username` and `registry-<slug>-password` in the user's
|
|
5
|
+
* `secrets.local.yaml` (never the shared store), where the slug is the host with every
|
|
6
|
+
* character outside `[a-z0-9]` replaced by `-`. The vault naming contract accepts
|
|
7
|
+
* alphanumerics and dashes only, so the earlier `<host>-username` form — which keeps the
|
|
8
|
+
* dots of `myreg.azurecr.io` — was local-only by accident rather than by design. The
|
|
9
|
+
* `registry-` prefix keeps a host that begins with an application key distinguishable from
|
|
10
|
+
* that application's own secrets in the same flat file.
|
|
11
|
+
*
|
|
12
|
+
* Pairs written under the old keys are still read, so an existing `dev set-registry` does
|
|
13
|
+
* not silently stop working, and removal clears both forms.
|
|
14
|
+
*
|
|
15
|
+
* Values are encrypted through the existing local-secret writer and reach Docker only via
|
|
16
|
+
* `docker login --password-stdin`.
|
|
7
17
|
*
|
|
8
18
|
* @fileoverview Registry credential storage, lookup, removal, and per-command Docker login
|
|
9
19
|
* @author AI Fabrix Team
|
|
@@ -25,12 +35,41 @@ const { normalizeRegistryHost } = require('./config-registry-preference');
|
|
|
25
35
|
const authenticatedHosts = new Set();
|
|
26
36
|
|
|
27
37
|
/**
|
|
38
|
+
* Host reduced to the vault naming charset: lowercase alphanumerics and dashes.
|
|
39
|
+
*
|
|
40
|
+
* @param {string} host - Normalized registry host
|
|
41
|
+
* @returns {string} Slug safe for a secret key
|
|
42
|
+
*/
|
|
43
|
+
function registryKeySlug(host) {
|
|
44
|
+
return String(host)
|
|
45
|
+
.toLowerCase()
|
|
46
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
47
|
+
.replace(/^-+|-+$/g, '');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Secret keys for a registry, including the previous form so an existing pair still loads.
|
|
52
|
+
*
|
|
53
|
+
* `hostKey` records which host owns the slug. Two hosts can normalise to one slug
|
|
54
|
+
* (`a.b-c` and `a-b.c` both give `a-b-c`), and silently reusing another registry's
|
|
55
|
+
* credentials is worse than refusing, so a write compares against it.
|
|
56
|
+
*
|
|
28
57
|
* @param {string} registry - Registry host
|
|
29
|
-
* @returns {{ host: string, usernameKey: string, passwordKey: string
|
|
58
|
+
* @returns {{ host: string, slug: string, usernameKey: string, passwordKey: string,
|
|
59
|
+
* hostKey: string, legacyUsernameKey: string, legacyPasswordKey: string }}
|
|
30
60
|
*/
|
|
31
61
|
function registryCredentialKeys(registry) {
|
|
32
62
|
const host = normalizeRegistryHost(registry);
|
|
33
|
-
|
|
63
|
+
const slug = registryKeySlug(host);
|
|
64
|
+
return {
|
|
65
|
+
host,
|
|
66
|
+
slug,
|
|
67
|
+
usernameKey: `registry-${slug}-username`,
|
|
68
|
+
passwordKey: `registry-${slug}-password`,
|
|
69
|
+
hostKey: `registry-${slug}-host`,
|
|
70
|
+
legacyUsernameKey: `${host}-username`,
|
|
71
|
+
legacyPasswordKey: `${host}-password`
|
|
72
|
+
};
|
|
34
73
|
}
|
|
35
74
|
|
|
36
75
|
function hasValue(value) {
|
|
@@ -40,7 +79,18 @@ function hasValue(value) {
|
|
|
40
79
|
function readRawPair(keys) {
|
|
41
80
|
const file = pathsUtil.getPrimaryUserSecretsLocalPath();
|
|
42
81
|
const raw = loadExistingSecrets(file);
|
|
43
|
-
|
|
82
|
+
const current = { username: raw[keys.usernameKey], password: raw[keys.passwordKey] };
|
|
83
|
+
if (hasValue(current.username) || hasValue(current.password)) {
|
|
84
|
+
return { file, ...current, legacy: false, owner: raw[keys.hostKey] };
|
|
85
|
+
}
|
|
86
|
+
// Written before the keys were namespaced; read it rather than prompting again.
|
|
87
|
+
return {
|
|
88
|
+
file,
|
|
89
|
+
username: raw[keys.legacyUsernameKey],
|
|
90
|
+
password: raw[keys.legacyPasswordKey],
|
|
91
|
+
legacy: true,
|
|
92
|
+
owner: undefined
|
|
93
|
+
};
|
|
44
94
|
}
|
|
45
95
|
|
|
46
96
|
function partialPairError(keys) {
|
|
@@ -106,13 +156,36 @@ function removeKeysFromFile(file, keyNames) {
|
|
|
106
156
|
return true;
|
|
107
157
|
}
|
|
108
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Refuse to overwrite a different host's credentials when two hosts share a slug.
|
|
161
|
+
*
|
|
162
|
+
* @param {Object} keys - From {@link registryCredentialKeys}
|
|
163
|
+
* @param {Object} snapshot - From readRawPair
|
|
164
|
+
* @throws {Error} When the slug already belongs to another host
|
|
165
|
+
*/
|
|
166
|
+
function assertNoSlugCollision(keys, snapshot) {
|
|
167
|
+
const owner = typeof snapshot.owner === 'string' ? snapshot.owner.trim() : '';
|
|
168
|
+
if (!owner || owner === keys.host) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
throw new Error(
|
|
172
|
+
`Registry key collision: "${keys.host}" and "${owner}" both reduce to "${keys.slug}", ` +
|
|
173
|
+
`so storing this pair would overwrite the credentials for ${owner}. ` +
|
|
174
|
+
`Remove the other registry first with "aifabrix dev set-registry ${owner} --clear-credentials".`
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
109
178
|
function restorePair(keys, snapshot) {
|
|
179
|
+
// Always clear what a save writes, then put back what the snapshot actually held — which
|
|
180
|
+
// may be under the old keys, since a pair written before namespacing is read from there.
|
|
110
181
|
const restore = {};
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
182
|
+
const [usernameKey, passwordKey] = snapshot.legacy
|
|
183
|
+
? [keys.legacyUsernameKey, keys.legacyPasswordKey]
|
|
184
|
+
: [keys.usernameKey, keys.passwordKey];
|
|
185
|
+
if (snapshot.username !== undefined) restore[usernameKey] = snapshot.username;
|
|
186
|
+
if (snapshot.password !== undefined) restore[passwordKey] = snapshot.password;
|
|
187
|
+
const remove = [keys.usernameKey, keys.passwordKey, keys.hostKey]
|
|
188
|
+
.filter((key) => !Object.prototype.hasOwnProperty.call(restore, key));
|
|
116
189
|
removeKeysFromFile(snapshot.file, remove);
|
|
117
190
|
mergeSecretsIntoFile(snapshot.file, restore);
|
|
118
191
|
}
|
|
@@ -130,14 +203,21 @@ async function saveRegistryCredentials(registry, username, password) {
|
|
|
130
203
|
throw new Error('Registry username and password must both be non-empty');
|
|
131
204
|
}
|
|
132
205
|
const snapshot = readRawPair(keys);
|
|
206
|
+
assertNoSlugCollision(keys, snapshot);
|
|
133
207
|
await config.ensureSecretsEncryptionKey();
|
|
134
208
|
try {
|
|
135
209
|
await saveLocalSecret(keys.usernameKey, String(username));
|
|
136
210
|
await saveLocalSecret(keys.passwordKey, String(password));
|
|
211
|
+
// Plain, not through saveLocalSecret: the host is not a secret, and encrypting it would
|
|
212
|
+
// make every later read compare a ciphertext against the host and report a collision.
|
|
213
|
+
mergeSecretsIntoFile(snapshot.file, { [keys.hostKey]: keys.host });
|
|
137
214
|
} catch (error) {
|
|
138
215
|
restorePair(keys, snapshot);
|
|
139
216
|
throw new Error(`Could not save registry credentials for ${keys.host}; previous values were restored (${error.message})`);
|
|
140
217
|
}
|
|
218
|
+
// The pair now lives under the namespaced keys; leaving the old one behind would keep a
|
|
219
|
+
// stale password readable and let a later read pick whichever it found first.
|
|
220
|
+
removeKeysFromFile(snapshot.file, [keys.legacyUsernameKey, keys.legacyPasswordKey]);
|
|
141
221
|
authenticatedHosts.delete(keys.host);
|
|
142
222
|
return { host: keys.host, rollback: () => restorePair(keys, snapshot) };
|
|
143
223
|
}
|
|
@@ -150,12 +230,24 @@ async function saveRegistryCredentials(registry, username, password) {
|
|
|
150
230
|
function removeRegistryCredentials(registry) {
|
|
151
231
|
const keys = registryCredentialKeys(registry);
|
|
152
232
|
authenticatedHosts.delete(keys.host);
|
|
153
|
-
return removeKeysFromFile(pathsUtil.getPrimaryUserSecretsLocalPath(), [
|
|
233
|
+
return removeKeysFromFile(pathsUtil.getPrimaryUserSecretsLocalPath(), [
|
|
234
|
+
keys.usernameKey,
|
|
235
|
+
keys.passwordKey,
|
|
236
|
+
keys.hostKey,
|
|
237
|
+
keys.legacyUsernameKey,
|
|
238
|
+
keys.legacyPasswordKey
|
|
239
|
+
]);
|
|
154
240
|
}
|
|
155
241
|
|
|
156
242
|
/**
|
|
157
|
-
* Authenticates Docker to the registry once per process: stored pair first, then the
|
|
158
|
-
* fallback
|
|
243
|
+
* Authenticates Docker to the registry once per process: stored pair first, then the
|
|
244
|
+
* optional fallback. The password is sent only on stdin.
|
|
245
|
+
*
|
|
246
|
+
* There is deliberately no default fallback. Most pulls are of public images and must stay
|
|
247
|
+
* anonymous; asking for a credential there would prompt for something the pull does not
|
|
248
|
+
* need. A caller that genuinely requires a credential passes one — see
|
|
249
|
+
* {@link ensureRegistryLoginWithSources}.
|
|
250
|
+
*
|
|
159
251
|
* @param {string} registry - Registry host
|
|
160
252
|
* @param {Object} [options] - Options
|
|
161
253
|
* @param {Function} [options.fallback] - async (host) => void when no pair is stored
|
|
@@ -180,6 +272,29 @@ async function ensureRegistryLogin(registry, options = {}) {
|
|
|
180
272
|
return 'none';
|
|
181
273
|
}
|
|
182
274
|
|
|
275
|
+
/**
|
|
276
|
+
* Authenticate a registry that a command cannot proceed without.
|
|
277
|
+
*
|
|
278
|
+
* Adds the credential-source chain on top of {@link ensureRegistryLogin}: the stored pair,
|
|
279
|
+
* then `az acr login` for an `*.azurecr.io` host, then a prompt. A non-interactive run
|
|
280
|
+
* fails naming the host instead of hanging on a question nobody can answer.
|
|
281
|
+
*
|
|
282
|
+
* @param {string} registry - Registry host
|
|
283
|
+
* @param {Object} [sources] - Overrides for {@link buildRegistryAuthFallback}
|
|
284
|
+
* @returns {Promise<'cached'|'credentials'|'acr'|'prompt'>}
|
|
285
|
+
*/
|
|
286
|
+
async function ensureRegistryLoginWithSources(registry, sources = {}) {
|
|
287
|
+
const { buildRegistryAuthFallback } = require('./registry-auth-sources');
|
|
288
|
+
const fallback = buildRegistryAuthFallback(sources);
|
|
289
|
+
let outcome = null;
|
|
290
|
+
const result = await ensureRegistryLogin(registry, {
|
|
291
|
+
fallback: async(host) => {
|
|
292
|
+
outcome = await fallback(host);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
return result === 'fallback' ? outcome : result;
|
|
296
|
+
}
|
|
297
|
+
|
|
183
298
|
/**
|
|
184
299
|
* Registry host of an image reference, or '' for Docker Hub style refs.
|
|
185
300
|
* @param {string} imageRef - Image reference (`host/repo:tag`, `repo:tag`, ...)
|
|
@@ -224,12 +339,14 @@ function resetRegistryLoginCache(registry) {
|
|
|
224
339
|
}
|
|
225
340
|
|
|
226
341
|
module.exports = {
|
|
342
|
+
registryKeySlug,
|
|
227
343
|
registryCredentialKeys,
|
|
228
344
|
getRegistryCredentialStatus,
|
|
229
345
|
loadRegistryCredentials,
|
|
230
346
|
saveRegistryCredentials,
|
|
231
347
|
removeRegistryCredentials,
|
|
232
348
|
ensureRegistryLogin,
|
|
349
|
+
ensureRegistryLoginWithSources,
|
|
233
350
|
ensureLoginForImageRefs,
|
|
234
351
|
registryHostFromImageRef,
|
|
235
352
|
resetRegistryLoginCache
|
|
@@ -16,6 +16,7 @@ const yaml = require('js-yaml');
|
|
|
16
16
|
const crypto = require('crypto');
|
|
17
17
|
const logger = require('./logger');
|
|
18
18
|
const pathsUtil = require('./paths');
|
|
19
|
+
const { generateRsaSecretValues } = require('../parameters/rsa-secret-values');
|
|
19
20
|
const { mergeFlatSecretsYamlPreservingComments } = require('./secrets-yaml-preserve');
|
|
20
21
|
const {
|
|
21
22
|
generateDatabasePasswordValueForKey,
|
|
@@ -165,32 +166,40 @@ function generateDbUrlValue(key) {
|
|
|
165
166
|
* @param {Record<string, string>|undefined} placeholderContext - Merged infra.parameter.yaml defaults + CLI overrides
|
|
166
167
|
* @returns {string|undefined} Value if catalog handled key; undefined to use legacy rules
|
|
167
168
|
*/
|
|
168
|
-
|
|
169
|
-
|
|
169
|
+
/** Generators whose value comes straight from the catalog entry. */
|
|
170
|
+
const CATALOG_DIRECT_GENERATORS = Object.freeze([
|
|
171
|
+
'randomBytes32', 'randomAlphanumeric', 'password', 'emptyString', 'emptyAllowed', 'literal'
|
|
172
|
+
]);
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The catalog entry for a key, or nothing when the catalog cannot be read.
|
|
176
|
+
*
|
|
177
|
+
* @param {string} key - Secret key
|
|
178
|
+
* @returns {Object|undefined}
|
|
179
|
+
*/
|
|
180
|
+
function catalogEntryForKey(key) {
|
|
170
181
|
try {
|
|
171
|
-
|
|
182
|
+
return infraParameterCatalogModule().getInfraParameterCatalog().findEntryForKey(key);
|
|
172
183
|
} catch (err) {
|
|
173
184
|
logger.warn(`infra parameter catalog unavailable (${err.message}); using legacy generators.`);
|
|
174
185
|
return undefined;
|
|
175
186
|
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function tryGenerateSecretValueFromCatalog(key, placeholderContext) {
|
|
190
|
+
const catalogEntry = catalogEntryForKey(key);
|
|
176
191
|
if (!catalogEntry || !catalogEntry.generator) return undefined;
|
|
177
192
|
const g = catalogEntry.generator.type;
|
|
193
|
+
if (g === 'rsaKeyPair') {
|
|
194
|
+
return require('../parameters/rsa-secret-values').generateRsaValueFromCatalog(key, catalogEntry);
|
|
195
|
+
}
|
|
178
196
|
if (g === 'databasePassword') {
|
|
179
|
-
|
|
180
|
-
return v !== null ? v : undefined;
|
|
197
|
+
return generateDbPasswordValue(key) ?? undefined;
|
|
181
198
|
}
|
|
182
199
|
if (g === 'databaseUrl') {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
if (
|
|
187
|
-
g === 'randomBytes32' ||
|
|
188
|
-
g === 'randomAlphanumeric' ||
|
|
189
|
-
g === 'password' ||
|
|
190
|
-
g === 'emptyString' ||
|
|
191
|
-
g === 'emptyAllowed' ||
|
|
192
|
-
g === 'literal'
|
|
193
|
-
) {
|
|
200
|
+
return generateDbUrlValue(key) ?? undefined;
|
|
201
|
+
}
|
|
202
|
+
if (CATALOG_DIRECT_GENERATORS.includes(g)) {
|
|
194
203
|
return infraParameterCatalogModule().generateValueFromCatalogEntry(key, catalogEntry, crypto, placeholderContext);
|
|
195
204
|
}
|
|
196
205
|
return undefined;
|
|
@@ -411,15 +420,16 @@ async function generateMissingSecrets(envTemplate, secretsPath) {
|
|
|
411
420
|
return [];
|
|
412
421
|
}
|
|
413
422
|
|
|
414
|
-
const newSecrets =
|
|
423
|
+
const newSecrets = generateRsaSecretValues(missingKeys, existingSecrets);
|
|
415
424
|
for (const key of missingKeys) {
|
|
416
|
-
newSecrets[key] = generateSecretValue(key);
|
|
425
|
+
if (!(key in newSecrets)) newSecrets[key] = generateSecretValue(key);
|
|
417
426
|
}
|
|
418
427
|
|
|
419
428
|
appendSecretsToFile(resolvedPath, newSecrets);
|
|
420
429
|
|
|
421
|
-
|
|
422
|
-
|
|
430
|
+
const generatedKeys = Object.keys(newSecrets);
|
|
431
|
+
logger.log(`✔ Generated ${generatedKeys.length} missing secret key(s): ${generatedKeys.join(', ')}`);
|
|
432
|
+
return generatedKeys;
|
|
423
433
|
}
|
|
424
434
|
|
|
425
435
|
/**
|
|
@@ -479,4 +489,3 @@ module.exports = {
|
|
|
479
489
|
generateMissingSecrets,
|
|
480
490
|
createDefaultSecrets
|
|
481
491
|
};
|
|
482
|
-
|
|
@@ -136,7 +136,7 @@ function getValueByPath(secrets, pathStr) {
|
|
|
136
136
|
return getValueByNestedPath(secrets, pathStr.split('/'));
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
const { getValueByPathWithEnvScope, mergeSecretsWithPrefixedCopies } =
|
|
139
|
+
const { getValueByPathWithEnvScope, mergeSecretsWithPrefixedCopies, prepareScopedKv } =
|
|
140
140
|
require('./secrets-kv-scope').createScopedKvHelpers(getValueByPath);
|
|
141
141
|
|
|
142
142
|
function resolveKvRefValue(secretsMap, pathStr, envKey, effective) {
|
|
@@ -155,8 +155,7 @@ function resolveKvRefValue(secretsMap, pathStr, envKey, effective) {
|
|
|
155
155
|
* @returns {string[]} Array of missing kv://<path> references (unique)
|
|
156
156
|
*/
|
|
157
157
|
function collectMissingSecrets(content, secrets, scopedKv = null) {
|
|
158
|
-
const effective =
|
|
159
|
-
const secretsMap = effective ? mergeSecretsWithPrefixedCopies(secrets, scopedKv.envKey) : secrets;
|
|
158
|
+
const { secretsMap, envKey, effective } = prepareScopedKv(secrets, scopedKv);
|
|
160
159
|
const seen = new Set();
|
|
161
160
|
const missing = [];
|
|
162
161
|
for (const line of content.split('\n')) {
|
|
@@ -168,7 +167,7 @@ function collectMissingSecrets(content, secrets, scopedKv = null) {
|
|
|
168
167
|
const pathStr = match[1];
|
|
169
168
|
if (seen.has(pathStr)) continue;
|
|
170
169
|
seen.add(pathStr);
|
|
171
|
-
const value = resolveKvRefValue(secretsMap, pathStr,
|
|
170
|
+
const value = resolveKvRefValue(secretsMap, pathStr, envKey, effective);
|
|
172
171
|
if ((value === undefined || value === null) && !isKvKeyAllowedEmptyWhenAbsent(pathStr)) {
|
|
173
172
|
missing.push(`kv://${pathStr}`);
|
|
174
173
|
}
|
|
@@ -216,8 +215,7 @@ function formatMissingSecretsFileInfo(secretsFilePaths) {
|
|
|
216
215
|
* @returns {string} Content with kv:// references replaced
|
|
217
216
|
*/
|
|
218
217
|
function replaceKvInContent(content, secrets, envVars, scopedKv = null) {
|
|
219
|
-
const effective =
|
|
220
|
-
const secretsMap = effective ? mergeSecretsWithPrefixedCopies(secrets, scopedKv.envKey) : secrets;
|
|
218
|
+
const { secretsMap, envKey, effective } = prepareScopedKv(secrets, scopedKv);
|
|
221
219
|
const lines = content.split('\n');
|
|
222
220
|
const result = lines.map(line => {
|
|
223
221
|
if (isCommentOrEmptyLine(line)) return line;
|
|
@@ -225,7 +223,7 @@ function replaceKvInContent(content, secrets, envVars, scopedKv = null) {
|
|
|
225
223
|
if (!scanLine.trim()) return line;
|
|
226
224
|
const suffix = line.slice(scanLine.length);
|
|
227
225
|
const replaced = scanLine.replace(KV_REF_PATTERN, (match, pathStr) => {
|
|
228
|
-
let value = resolveKvRefValue(secretsMap, pathStr,
|
|
226
|
+
let value = resolveKvRefValue(secretsMap, pathStr, envKey, effective);
|
|
229
227
|
if ((value === undefined || value === null) && isKvKeyAllowedEmptyWhenAbsent(pathStr)) {
|
|
230
228
|
value = '';
|
|
231
229
|
}
|