cdk-local 0.166.1 → 0.166.3

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.
@@ -1,2234 +0,0 @@
1
- import { t as __exportAll } from "./rolldown-runtime-w6R9maHv.js";
2
- import { a as displayIdent, c as getEmbedConfig, s as displayToolOutput, t as getLogger } from "./logger-uzapot2X.js";
3
- import { F as namesTheSameDirectory, I as renderAssemblyPathEscape, L as resolveAssemblyPath, M as assemblyPathEscape, N as displayUntrustedValue, P as isPasteableCommandArg, V as flattenToOneLine, Y as defineOwnKey, i as buildProxyClientConfig, j as absoluteAssemblyPathEscape, n as awsEmulatorRealServices, o as deployArtifactClientConfig, s as deployArtifactsReadEmulator, z as describeAwsFailureForWarn } from "./aws-proxy-Ba_BOPyi.js";
4
- import { C as effectiveLoopbackPorts, F as resolveHostDefaultCredentials, O as loopbackForwarderPlan, U as RESIGN_SIDECAR_SCRIPT, d as workloadStsClientConfig, i as assumedRoleRecorded, o as readEnvCallerCredentials, s as recordAssumedRole, t as SECOND_ASSUME_MESSAGE, u as workloadClientConfig, w as getActiveWorkloadAwsWiring, y as applyWorkloadAwsWiringToEnv, z as workloadGetsEmulatorKeysOnly } from "./workload-identity-Blv2g1Jp.js";
5
- import { a as isDockerClientEnvKey, c as runDockerStreaming, d as dockerRegistryEnv, i as getDockerCmd, l as spawnStreaming, n as finchSecretArgvRefusal, o as isMalformedEnvKey, p as noImplicitEcrPullArgs, r as formatDockerLoginError, s as runDockerForeground, u as warnFinchArgvExposure } from "./docker-cmd-BnW-0c0e.js";
6
- import { existsSync, readlinkSync, realpathSync } from "node:fs";
7
- import { AssumeRoleCommand, GetCallerIdentityCommand, STSClient } from "@aws-sdk/client-sts";
8
- import { createHash, randomBytes } from "node:crypto";
9
- import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
10
- import { homedir } from "node:os";
11
- import { execFile, execFileSync, spawn } from "node:child_process";
12
- import { createServer } from "node:net";
13
- import { StringDecoder } from "node:string_decoder";
14
- import { promisify } from "node:util";
15
- import { ECRClient, GetAuthorizationTokenCommand } from "@aws-sdk/client-ecr";
16
-
17
- //#region src/utils/profile-resolver.ts
18
- async function resolveProfileCredentials(profile) {
19
- const sts = new STSClient(buildStsClientConfig({ profile }));
20
- try {
21
- const credsProvider = sts.config.credentials;
22
- const creds = typeof credsProvider === "function" ? await credsProvider() : credsProvider;
23
- if (!creds || !creds.accessKeyId || !creds.secretAccessKey) {
24
- const ssoHint = isPasteableCommandArg(profile) ? "Check `aws sso login --profile " + profile + "` for SSO profiles" : "For an SSO profile, run `aws sso login` for that profile (the command is not written out because the name as rendered may not be the one your shell would act on)";
25
- throw new Error(`--profile ${displayUntrustedValue(profile)}: credential provider chain resolved without usable credentials. ${ssoHint}, or check \`~/.aws/credentials\` / \`~/.aws/config\` for regular profiles.`);
26
- }
27
- let region;
28
- try {
29
- const regionProvider = sts.config.region;
30
- const resolved = typeof regionProvider === "function" ? await regionProvider() : regionProvider;
31
- if (typeof resolved === "string" && resolved.length > 0) region = resolved;
32
- } catch (err) {
33
- const msg = err instanceof Error ? err.message : String(err);
34
- if (!(/region is missing/i.test(msg) || /could not resolve region/i.test(msg) || /no region in config/i.test(msg) || /region.*not.*set/i.test(msg))) throw err;
35
- region = void 0;
36
- }
37
- return {
38
- accessKeyId: creds.accessKeyId,
39
- secretAccessKey: creds.secretAccessKey,
40
- ...creds.sessionToken && { sessionToken: creds.sessionToken },
41
- ...region && { region }
42
- };
43
- } finally {
44
- sts.destroy();
45
- }
46
- }
47
- function buildStsClientConfig(args) {
48
- return {
49
- ...buildProxyClientConfig({ profile: args.profile }),
50
- ...args.region && { region: args.region },
51
- ...args.profile && { profile: args.profile }
52
- };
53
- }
54
-
55
- //#endregion
56
- //#region src/utils/role-permission-emulation.ts
57
- const SESSION_POLICY_MAX_CHARS = 2048;
58
- const SESSION_POLICY_MAX_ARNS = 10;
59
- const DENY_ALL_POLICY = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Deny\",\"Action\":\"*\",\"Resource\":\"*\"}]}";
60
- function buildSessionPolicy(policies) {
61
- const { inline, managedArns } = policies;
62
- if (managedArns.length > 10) return {
63
- ok: false,
64
- reason: `the role has ${managedArns.length} managed policies, more than the ${10} STS accepts as session policies`
65
- };
66
- const versions = /* @__PURE__ */ new Set();
67
- const statements = [];
68
- for (const doc of inline) {
69
- if (doc === null || typeof doc !== "object" || Array.isArray(doc)) return {
70
- ok: false,
71
- reason: "an inline policy of the role is not a JSON object"
72
- };
73
- const record = doc;
74
- versions.add(typeof record["Version"] === "string" ? record["Version"] : "");
75
- const raw = record["Statement"];
76
- for (const statement of Array.isArray(raw) ? raw : raw === void 0 ? [] : [raw]) if (statement !== null && typeof statement === "object" && !Array.isArray(statement)) {
77
- const { Sid: _sid, ...rest } = statement;
78
- statements.push(rest);
79
- } else statements.push(statement);
80
- }
81
- if (versions.size > 1) return {
82
- ok: false,
83
- reason: "the inline policies of the role use different policy Version values and cannot be combined into one session policy"
84
- };
85
- const [version] = [...versions];
86
- let policy;
87
- if (statements.length === 0) policy = managedArns.length === 0 ? DENY_ALL_POLICY : "";
88
- else policy = JSON.stringify({
89
- ...version !== void 0 && version !== "" && { Version: version },
90
- Statement: statements
91
- });
92
- const chars = policy.length + managedArns.reduce((sum, arn) => sum + arn.length, 0);
93
- if (chars > 2048) return {
94
- ok: false,
95
- reason: `the role's policies take ${chars} characters as session policies, more than the ${SESSION_POLICY_MAX_CHARS} STS accepts`
96
- };
97
- return {
98
- ok: true,
99
- policy,
100
- policyArns: managedArns
101
- };
102
- }
103
- function isIamUserArn(arn) {
104
- return arn !== void 0 && /^arn:[^:]+:iam::\d{12}:user\//.test(arn);
105
- }
106
- function isAssumeRoleAccessDenied(err) {
107
- return err?.name === "AccessDenied";
108
- }
109
- function roleNameOf(roleArn) {
110
- const resource = roleArn.split(":").slice(5).join(":");
111
- return resource.slice(resource.lastIndexOf("/") + 1);
112
- }
113
- const noticedRoles = /* @__PURE__ */ new Set();
114
- const failedRoles = /* @__PURE__ */ new Set();
115
- async function emulateRoleAfterDeniedAssume(err, args) {
116
- if (!isAssumeRoleAccessDenied(err)) return void 0;
117
- try {
118
- return await emulate(args);
119
- } catch (unexpected) {
120
- const key = `${args.roleArn}\0unexpected`;
121
- if (failedRoles.has(key)) return void 0;
122
- failedRoles.add(key);
123
- getLogger().warn(`${args.flag}: emulating the role's permissions failed: ${describeAwsFailureForWarn(unexpected, "role permission emulation")}.`);
124
- return;
125
- }
126
- }
127
- async function emulate(args) {
128
- const logger = getLogger();
129
- const shownRole = flattenToOneLine(args.roleArn);
130
- const cannot = (reason) => {
131
- const key = `${args.roleArn}\0${reason}`;
132
- if (failedRoles.has(key)) return void 0;
133
- failedRoles.add(key);
134
- logger.warn(`${args.flag}: AssumeRole(${shownRole}) was refused, and the role's permissions cannot be emulated instead: ${reason}.`);
135
- };
136
- const { STSClient, GetCallerIdentityCommand, GetFederationTokenCommand, AssumeRoleCommand } = await import("@aws-sdk/client-sts");
137
- const sts = new STSClient(workloadStsClientConfig({
138
- region: args.region,
139
- profile: args.profile
140
- }));
141
- try {
142
- let callerArn;
143
- try {
144
- callerArn = (await sts.send(new GetCallerIdentityCommand({}))).Arn;
145
- } catch (idErr) {
146
- return cannot(describeAwsFailureForWarn(idErr, "STS GetCallerIdentity"));
147
- }
148
- const roleAccount = args.roleArn.split(":")[4];
149
- if (roleAccount === void 0 || roleAccount !== callerArn?.split(":")[4]) return cannot("the role is not in your credentials' account, so its policies cannot be read");
150
- if (args.emulateRoleVia !== void 0 && !isIamRoleArn(args.emulateRoleVia)) return cannot(`--emulate-role-via is not a well-formed IAM role ARN: ${describeRejectedRoleArn(args.emulateRoleVia)}`);
151
- if (args.emulateRoleVia === void 0 && !isIamUserArn(callerArn)) return cannot("your credentials are not an IAM user, and only an IAM user can call sts:GetFederationToken. Pass --emulate-role-via <arn> naming a role you can assume, and that role is assumed with the role's policies as session policies");
152
- let policies;
153
- try {
154
- policies = await readRolePolicies(args);
155
- } catch (readErr) {
156
- return cannot(`reading the role's policies failed (it needs iam:GetRole, iam:ListRolePolicies, iam:GetRolePolicy and iam:ListAttachedRolePolicies): ${describeAwsFailureForWarn(readErr, "IAM role policy read")}`);
157
- }
158
- if (policies.permissionsBoundaryArn !== void 0) return cannot(`the role has a permissions boundary (${flattenToOneLine(policies.permissionsBoundaryArn)}), which session policies cannot reproduce, so the emulation could allow what the role cannot`);
159
- const plan = buildSessionPolicy(policies);
160
- if (!plan.ok) return cannot(plan.reason);
161
- const sessionPolicies = {
162
- ...plan.policy !== "" && { Policy: plan.policy },
163
- ...plan.policyArns.length > 0 && { PolicyArns: plan.policyArns.map((arn) => ({ arn })) }
164
- };
165
- const prefix = getEmbedConfig().resourceNamePrefix;
166
- let via;
167
- let response;
168
- try {
169
- if (args.emulateRoleVia !== void 0) {
170
- via = `AssumeRole of --emulate-role-via ${flattenToOneLine(args.emulateRoleVia)}`;
171
- response = await sts.send(new AssumeRoleCommand({
172
- RoleArn: args.emulateRoleVia,
173
- RoleSessionName: `${prefix}-${args.sessionNameSuffix}-emulate-${Date.now()}`.slice(0, 64),
174
- DurationSeconds: 3600,
175
- ...sessionPolicies
176
- }));
177
- } else {
178
- via = "sts:GetFederationToken";
179
- response = await sts.send(new GetFederationTokenCommand({
180
- Name: `${prefix.replace(/[^\w+=,.@-]/g, "-")}-${Date.now()}`.slice(-32),
181
- DurationSeconds: 3600,
182
- ...sessionPolicies
183
- }));
184
- }
185
- } catch (stsErr) {
186
- const stsName = stsErr?.name;
187
- if (stsName === "PackedPolicyTooLargeException" || stsName === "PackedPolicyTooLarge") return cannot("the role's policies are larger than STS accepts as session policies (PackedPolicyTooLarge)");
188
- const rendered = describeAwsFailureForWarn(stsErr, "STS session-policy credentials");
189
- return cannot(args.emulateRoleVia === void 0 ? `${rendered}. Temporary IAM user credentials (GetSessionToken / MFA) cannot call sts:GetFederationToken; pass --emulate-role-via <arn> naming a role you can assume instead` : rendered);
190
- }
191
- const creds = response.Credentials;
192
- if (!creds?.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken) return cannot("STS returned no usable credentials");
193
- if (!noticedRoles.has(args.roleArn)) {
194
- noticedRoles.add(args.roleArn);
195
- const count = policies.inline.length + policies.managedArns.length;
196
- const who = args.emulateRoleVia !== void 0 ? "that role" : "you";
197
- const federationNote = args.emulateRoleVia === void 0 ? " A GetFederationToken session cannot call IAM, nor STS except GetCallerIdentity." : "";
198
- logger.warn(`${args.flag}: AssumeRole(${shownRole}) was refused (the role's trust policy does not allow you), so its permissions are EMULATED: credentials from ${via} with the role's ${policies.inline.length} inline and ${policies.managedArns.length} managed ${count === 1 ? "policy" : "policies"} as session policies, which allow only what both ${who} and the role allow. This is an APPROXIMATION: resource-based policies, KMS key policies and aws:PrincipalArn conditions that name the role see a different principal, and the role's session tags are not reproduced. Policy variables such as aws:username and aws:userid take this session's values, not the role's (a role session has no aws:username).${federationNote}`);
199
- }
200
- return {
201
- accessKeyId: creds.AccessKeyId,
202
- secretAccessKey: creds.SecretAccessKey,
203
- sessionToken: creds.SessionToken
204
- };
205
- } finally {
206
- sts.destroy();
207
- }
208
- }
209
- async function readRolePolicies(args) {
210
- const { IAMClient, ListRolePoliciesCommand, GetRolePolicyCommand, ListAttachedRolePoliciesCommand, GetRoleCommand } = await import("@aws-sdk/client-iam");
211
- const iam = new IAMClient({
212
- ...workloadClientConfig({ profile: args.profile }),
213
- ...args.region && { region: args.region },
214
- ...args.profile && { profile: args.profile }
215
- });
216
- const RoleName = roleNameOf(args.roleArn);
217
- try {
218
- const permissionsBoundaryArn = (await iam.send(new GetRoleCommand({ RoleName }))).Role?.PermissionsBoundary?.PermissionsBoundaryArn;
219
- const names = [];
220
- let marker;
221
- do {
222
- const page = await iam.send(new ListRolePoliciesCommand({
223
- RoleName,
224
- ...marker && { Marker: marker }
225
- }));
226
- names.push(...page.PolicyNames ?? []);
227
- marker = page.IsTruncated ? page.Marker : void 0;
228
- } while (marker);
229
- const inline = [];
230
- for (const PolicyName of names) {
231
- const got = await iam.send(new GetRolePolicyCommand({
232
- RoleName,
233
- PolicyName
234
- }));
235
- let doc;
236
- try {
237
- doc = JSON.parse(decodeURIComponent(got.PolicyDocument ?? "{}"));
238
- } catch {
239
- throw new Error("an inline policy document of the role is not valid JSON");
240
- }
241
- inline.push(doc);
242
- }
243
- const managedArns = [];
244
- let attachedMarker;
245
- do {
246
- const page = await iam.send(new ListAttachedRolePoliciesCommand({
247
- RoleName,
248
- ...attachedMarker && { Marker: attachedMarker }
249
- }));
250
- for (const attached of page.AttachedPolicies ?? []) if (attached.PolicyArn) managedArns.push(attached.PolicyArn);
251
- attachedMarker = page.IsTruncated ? page.Marker : void 0;
252
- } while (attachedMarker);
253
- return {
254
- inline,
255
- managedArns,
256
- ...permissionsBoundaryArn && { permissionsBoundaryArn }
257
- };
258
- } finally {
259
- iam.destroy();
260
- }
261
- }
262
-
263
- //#endregion
264
- //#region src/utils/role-arn.ts
265
- const IAM_ROLE_ARN_MAX_LENGTH = 2048;
266
- const IAM_ROLE_ARN_PATTERN = /^arn:[A-Za-z0-9-]+:iam::[0-9]+:role\/[!-~]+$/;
267
- function isIamRoleArn(value) {
268
- if (typeof value !== "string") return false;
269
- if (value.length > 2048) return false;
270
- return IAM_ROLE_ARN_PATTERN.test(value);
271
- }
272
- const REJECTED_ARN_PREVIEW_MAX = 128;
273
- function describeRejectedRoleArn(value) {
274
- if (typeof value !== "string") return `a non-string ${typeof value} value`;
275
- const points = [...flattenToOneLine(value)];
276
- return `"${points.length <= REJECTED_ARN_PREVIEW_MAX ? points.join("") : `${points.slice(0, REJECTED_ARN_PREVIEW_MAX).join("")}...`}" (${points.length} characters)`;
277
- }
278
- function refusedRoleArnMessage(value) {
279
- return `AssumeRole refused: the role ARN is not a well-formed IAM role ARN (expected arn:<partition>:iam::<account>:role/<name>, at most ${IAM_ROLE_ARN_MAX_LENGTH} characters): ${describeRejectedRoleArn(value)}. Nothing was sent to STS.`;
280
- }
281
- var AssumeRoleFailure = class AssumeRoleFailure extends Error {
282
- detail;
283
- constructor(message, detail) {
284
- super(message);
285
- this.name = "AssumeRoleFailure";
286
- this.detail = detail;
287
- Object.setPrototypeOf(this, AssumeRoleFailure.prototype);
288
- }
289
- };
290
- async function assumeRoleCredentials(opts) {
291
- if (!isIamRoleArn(opts.roleArn)) {
292
- const detail = `the role ARN is not well-formed: ${describeRejectedRoleArn(opts.roleArn)}`;
293
- const message = refusedRoleArnMessage(opts.roleArn);
294
- throw opts.makeError ? opts.makeError(message) : new AssumeRoleFailure(message, detail);
295
- }
296
- const sts = new STSClient(workloadStsClientConfig({
297
- region: opts.region,
298
- profile: opts.profile
299
- }));
300
- const shownArn = flattenToOneLine(opts.roleArn);
301
- let response;
302
- let refused;
303
- try {
304
- response = await sts.send(new AssumeRoleCommand({
305
- RoleArn: opts.roleArn,
306
- RoleSessionName: `${getEmbedConfig().resourceNamePrefix}-${opts.sessionNameSuffix}-${Date.now()}`,
307
- DurationSeconds: 3600
308
- }));
309
- } catch (err) {
310
- refused = { err };
311
- } finally {
312
- sts.destroy();
313
- }
314
- if (refused !== void 0) {
315
- const emulated = await emulateRoleAfterDeniedAssume(refused.err, {
316
- roleArn: opts.roleArn,
317
- region: opts.region,
318
- profile: opts.profile,
319
- emulateRoleVia: opts.emulateRoleVia,
320
- flag: opts.roleFlag ?? "--assume-role",
321
- sessionNameSuffix: opts.sessionNameSuffix
322
- });
323
- if (emulated !== void 0) return emulated;
324
- const detail = describeAwsFailureForWarn(refused.err, "STS AssumeRole");
325
- const message = `AssumeRole(${shownArn}) failed: ${detail}`;
326
- throw opts.makeError ? opts.makeError(message) : new AssumeRoleFailure(message, detail);
327
- }
328
- const creds = response?.Credentials;
329
- if (!creds?.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken) {
330
- const message = `AssumeRole(${shownArn}) returned no usable credentials.`;
331
- throw opts.makeError ? opts.makeError(message) : new AssumeRoleFailure(message, "the response carried no usable credentials");
332
- }
333
- return {
334
- accessKeyId: creds.AccessKeyId,
335
- secretAccessKey: creds.SecretAccessKey,
336
- sessionToken: creds.SessionToken
337
- };
338
- }
339
- async function applyRoleArnIfSet(opts) {
340
- const roleArn = opts.roleArn || process.env[`${getEmbedConfig().envPrefix}_ROLE_ARN`];
341
- if (!roleArn) return;
342
- if (!isIamRoleArn(roleArn)) throw new Error(refusedRoleArnMessage(roleArn));
343
- if (assumedRoleRecorded() && getEmbedConfig().workloadIdentity === "caller") throw new Error(SECOND_ASSUME_MESSAGE);
344
- const logger = getLogger().child("role-arn");
345
- const shownArn = flattenToOneLine(roleArn);
346
- logger.debug(`Assuming role ${shownArn}...`);
347
- const sts = new STSClient(buildStsClientConfig({
348
- region: opts.region,
349
- profile: opts.profile
350
- }));
351
- let response;
352
- try {
353
- response = await sts.send(new AssumeRoleCommand({
354
- RoleArn: roleArn,
355
- RoleSessionName: `${getEmbedConfig().binaryName}-${Date.now()}`,
356
- DurationSeconds: 3600
357
- }));
358
- } catch (err) {
359
- throw new Error(`AssumeRole(${shownArn}) failed: ${describeAwsFailureForWarn(err, "STS AssumeRole")}`);
360
- } finally {
361
- sts.destroy();
362
- }
363
- if (!response.Credentials) throw new Error(`AssumeRole returned no credentials for role ${shownArn}`);
364
- const { AccessKeyId, SecretAccessKey, SessionToken, Expiration } = response.Credentials;
365
- if (!AccessKeyId || !SecretAccessKey || !SessionToken) throw new Error(`AssumeRole response missing credentials fields for role ${shownArn}`);
366
- if (!assumedRoleRecorded()) recordAssumedRole(readEnvCallerCredentials(), {
367
- accessKeyId: AccessKeyId,
368
- secretAccessKey: SecretAccessKey,
369
- sessionToken: SessionToken
370
- });
371
- process.env["AWS_ACCESS_KEY_ID"] = AccessKeyId;
372
- process.env["AWS_SECRET_ACCESS_KEY"] = SecretAccessKey;
373
- process.env["AWS_SESSION_TOKEN"] = SessionToken;
374
- logger.info(`Assumed role ${shownArn} (session expires ${Expiration?.toISOString() ?? "unknown"})`);
375
- }
376
-
377
- //#endregion
378
- //#region src/utils/error-handler.ts
379
- var CdkLocalError = class CdkLocalError extends Error {
380
- code;
381
- cause;
382
- constructor(message, code, cause) {
383
- super(message);
384
- this.code = code;
385
- this.cause = cause;
386
- this.name = "CdkLocalError";
387
- Object.setPrototypeOf(this, CdkLocalError.prototype);
388
- }
389
- };
390
- var LocalInvokeBuildError = class LocalInvokeBuildError extends CdkLocalError {
391
- constructor(message, cause) {
392
- super(message, "LOCAL_INVOKE_BUILD_ERROR", cause);
393
- this.name = "LocalInvokeBuildError";
394
- Object.setPrototypeOf(this, LocalInvokeBuildError.prototype);
395
- }
396
- };
397
- var RouteDiscoveryError = class RouteDiscoveryError extends CdkLocalError {
398
- constructor(message, cause) {
399
- super(message, "ROUTE_DISCOVERY_ERROR", cause);
400
- this.name = "RouteDiscoveryError";
401
- Object.setPrototypeOf(this, RouteDiscoveryError.prototype);
402
- }
403
- };
404
- var LocalStartServiceError = class LocalStartServiceError extends CdkLocalError {
405
- constructor(message, cause) {
406
- super(message, "LOCAL_START_SERVICE_ERROR", cause);
407
- this.name = "LocalStartServiceError";
408
- Object.setPrototypeOf(this, LocalStartServiceError.prototype);
409
- }
410
- };
411
- var TargetSelectionCancelledError = class TargetSelectionCancelledError extends CdkLocalError {
412
- silent = true;
413
- exitCode = 130;
414
- constructor() {
415
- super("Target selection cancelled.", "TARGET_SELECTION_CANCELLED");
416
- this.name = "TargetSelectionCancelledError";
417
- Object.setPrototypeOf(this, TargetSelectionCancelledError.prototype);
418
- }
419
- };
420
- var InteractiveTtyRequiredError = class InteractiveTtyRequiredError extends CdkLocalError {
421
- constructor(message) {
422
- super(message, "INTERACTIVE_TTY_REQUIRED");
423
- this.name = "InteractiveTtyRequiredError";
424
- Object.setPrototypeOf(this, InteractiveTtyRequiredError.prototype);
425
- }
426
- };
427
- function isCdkLocalError(error) {
428
- return error instanceof CdkLocalError;
429
- }
430
- function formatError(error) {
431
- if (isCdkLocalError(error)) {
432
- let message = `${error.name}: ${error.message}`;
433
- if (error.cause) message += `\nCaused by: ${error.cause.message}`;
434
- return message;
435
- }
436
- if (error instanceof Error) return `${error.name}: ${error.message}`;
437
- return String(error);
438
- }
439
- function handleError(error) {
440
- const logger = getLogger();
441
- if (!(error instanceof CdkLocalError && error.silent)) logger.error(formatError(error));
442
- if (error instanceof Error && error.stack) logger.debug("Stack trace:", error.stack);
443
- const customExitCode = error instanceof CdkLocalError ? error.exitCode : void 0;
444
- const exitCode = typeof customExitCode === "number" ? customExitCode : 1;
445
- process.exit(exitCode);
446
- }
447
- function withErrorHandling(fn) {
448
- return async (...args) => {
449
- try {
450
- await fn(...args);
451
- } catch (error) {
452
- handleError(error);
453
- }
454
- };
455
- }
456
-
457
- //#endregion
458
- //#region src/local/intrinsic-image.ts
459
- function contextForContainer(context, containerName) {
460
- if (!context?.containerStates || !Object.hasOwn(context.containerStates, containerName)) return context;
461
- return {
462
- ...context.pseudoParameters && { pseudoParameters: context.pseudoParameters },
463
- ...context.containerStates[containerName]
464
- };
465
- }
466
- function formatStateRemedy(context) {
467
- if (context?.stateLoadFailureMessage) return `the state-source attempt failed: ${context.stateLoadFailureMessage}; verify the stack name (try --from-cfn-stack <deployed-name> if it differs from the synthesized name) and that --region / --profile target the right account`;
468
- return "pass --from-cfn-stack to load the deployed stack state";
469
- }
470
- const PARTITION_TABLE = [
471
- {
472
- regionPrefix: "cn-",
473
- partition: "aws-cn",
474
- urlSuffix: "amazonaws.com.cn"
475
- },
476
- {
477
- regionPrefix: "us-gov-",
478
- partition: "aws-us-gov",
479
- urlSuffix: "amazonaws.com"
480
- },
481
- {
482
- regionPrefix: "us-isob-",
483
- partition: "aws-iso-b",
484
- urlSuffix: "sc2s.sgov.gov"
485
- },
486
- {
487
- regionPrefix: "us-isof-",
488
- partition: "aws-iso-f",
489
- urlSuffix: "csp.hci.ic.gov"
490
- },
491
- {
492
- regionPrefix: "us-iso-",
493
- partition: "aws-iso",
494
- urlSuffix: "c2s.ic.gov"
495
- },
496
- {
497
- regionPrefix: "eu-isoe-",
498
- partition: "aws-iso-e",
499
- urlSuffix: "cloud.adc-e.uk"
500
- },
501
- {
502
- regionPrefix: "eusc-",
503
- partition: "aws-eusc",
504
- urlSuffix: "amazonaws.eu"
505
- }
506
- ];
507
- function derivePartitionAndUrlSuffix(region) {
508
- const normalized = region.toLowerCase();
509
- for (const row of PARTITION_TABLE) if (normalized.startsWith(row.regionPrefix)) return {
510
- partition: row.partition,
511
- urlSuffix: row.urlSuffix
512
- };
513
- return {
514
- partition: "aws",
515
- urlSuffix: "amazonaws.com"
516
- };
517
- }
518
- function derivePseudoParametersFromRegion(region, accountId) {
519
- if (!region || typeof region !== "string" || region.length === 0) return void 0;
520
- const { partition, urlSuffix } = derivePartitionAndUrlSuffix(region);
521
- return {
522
- ...accountId !== void 0 && { accountId },
523
- region,
524
- partition,
525
- urlSuffix
526
- };
527
- }
528
- function tryResolveImageFnJoin(raw, resources, context) {
529
- if (!raw || typeof raw !== "object") return { kind: "not-applicable" };
530
- const arg = raw["Fn::Join"];
531
- if (arg === void 0) return { kind: "not-applicable" };
532
- if (!Array.isArray(arg) || arg.length !== 2 || !Array.isArray(arg[1])) return {
533
- kind: "unsupported-join",
534
- reason: "Fn::Join must be [delimiter, [elements]]"
535
- };
536
- const [delimiter, elements] = arg;
537
- if (typeof delimiter !== "string") return {
538
- kind: "unsupported-join",
539
- reason: `Fn::Join delimiter must be a string, got ${typeof delimiter}`
540
- };
541
- const repoLogicalId = findEcrRepositoryRefInTree(elements, resources);
542
- const stateResources = context?.stateResources;
543
- if (repoLogicalId && !stateResources) return {
544
- kind: "needs-state",
545
- repoLogicalId
546
- };
547
- const parts = [];
548
- for (const element of elements) {
549
- const r = resolveImageIntrinsic(element, resources, context);
550
- if (r === void 0) {
551
- if (!repoLogicalId) return { kind: "not-applicable" };
552
- return {
553
- kind: "unsupported-join",
554
- reason: "one or more Fn::Join elements could not be resolved"
555
- };
556
- }
557
- parts.push(r);
558
- }
559
- return {
560
- kind: "resolved",
561
- uri: parts.join(delimiter)
562
- };
563
- }
564
- function findEcrRepositoryRefInTree(node, resources) {
565
- if (node === null || node === void 0) return void 0;
566
- if (typeof node === "string" || typeof node === "number" || typeof node === "boolean") return;
567
- if (Array.isArray(node)) {
568
- for (const item of node) {
569
- const hit = findEcrRepositoryRefInTree(item, resources);
570
- if (hit) return hit;
571
- }
572
- return;
573
- }
574
- if (typeof node !== "object") return void 0;
575
- const obj = node;
576
- if (typeof obj["Ref"] === "string") {
577
- const target = obj["Ref"];
578
- if (resources[target]?.Type === "AWS::ECR::Repository") return target;
579
- return;
580
- }
581
- const getAtt = obj["Fn::GetAtt"];
582
- if (getAtt !== void 0) {
583
- let lid;
584
- if (Array.isArray(getAtt) && typeof getAtt[0] === "string") lid = getAtt[0];
585
- else if (typeof getAtt === "string") lid = getAtt.split(".")[0];
586
- if (lid && resources[lid]?.Type === "AWS::ECR::Repository") return lid;
587
- return;
588
- }
589
- for (const value of Object.values(obj)) {
590
- const hit = findEcrRepositoryRefInTree(value, resources);
591
- if (hit) return hit;
592
- }
593
- }
594
- function resolveImageIntrinsic(node, resources, context) {
595
- const v = resolveImageIntrinsicAny(node, resources, context);
596
- if (typeof v === "string") return v;
597
- if (typeof v === "number" || typeof v === "boolean") return String(v);
598
- }
599
- function resolveImageIntrinsicAny(node, resources, context) {
600
- if (node === null || node === void 0) return void 0;
601
- if (typeof node === "string" || typeof node === "number" || typeof node === "boolean") return node;
602
- if (Array.isArray(node)) return;
603
- if (typeof node !== "object") return void 0;
604
- const obj = node;
605
- const keys = Object.keys(obj);
606
- if (keys.length !== 1) return void 0;
607
- const intrinsic = keys[0];
608
- const arg = obj[intrinsic];
609
- if (intrinsic === "Ref") {
610
- if (typeof arg !== "string") return void 0;
611
- if (arg.startsWith("AWS::")) {
612
- const p = context?.pseudoParameters;
613
- if (!p) return void 0;
614
- if (arg === "AWS::URLSuffix") return p.urlSuffix;
615
- if (arg === "AWS::Partition") return p.partition;
616
- if (arg === "AWS::Region") return p.region;
617
- if (arg === "AWS::AccountId") return p.accountId;
618
- return;
619
- }
620
- if (resources[arg]?.Type !== "AWS::ECR::Repository") return void 0;
621
- const stateEntry = context?.stateResources?.[arg];
622
- if (!stateEntry) return void 0;
623
- return stateEntry.physicalId;
624
- }
625
- if (intrinsic === "Fn::GetAtt") {
626
- let logicalId;
627
- let attr;
628
- if (Array.isArray(arg) && arg.length === 2 && typeof arg[0] === "string" && typeof arg[1] === "string") {
629
- logicalId = arg[0];
630
- attr = arg[1];
631
- } else if (typeof arg === "string") {
632
- const dot = arg.indexOf(".");
633
- if (dot > 0 && dot < arg.length - 1) {
634
- logicalId = arg.slice(0, dot);
635
- attr = arg.slice(dot + 1);
636
- }
637
- }
638
- if (!logicalId || !attr) return void 0;
639
- if (resources[logicalId]?.Type !== "AWS::ECR::Repository") return void 0;
640
- const cached = context?.stateResources?.[logicalId]?.attributes?.[attr];
641
- if (typeof cached === "string" && cached.length > 0) return cached;
642
- const physicalId = context?.stateResources?.[logicalId]?.physicalId;
643
- const p = context?.pseudoParameters;
644
- if (physicalId && p?.region && p.accountId) {
645
- if (attr === "Arn" && p.partition) return `arn:${p.partition}:ecr:${p.region}:${p.accountId}:repository/${physicalId}`;
646
- if (attr === "RepositoryUri" && p.urlSuffix) return `${p.accountId}.dkr.ecr.${p.region}.${p.urlSuffix}/${physicalId}`;
647
- }
648
- return;
649
- }
650
- if (intrinsic === "Fn::Split") {
651
- if (!Array.isArray(arg) || arg.length !== 2) return void 0;
652
- const argArr = arg;
653
- const delim = argArr[0];
654
- if (typeof delim !== "string") return void 0;
655
- const src = resolveImageIntrinsicAny(argArr[1], resources, context);
656
- if (typeof src !== "string") return void 0;
657
- return src.split(delim);
658
- }
659
- if (intrinsic === "Fn::Select") {
660
- if (!Array.isArray(arg) || arg.length !== 2) return void 0;
661
- const argArr = arg;
662
- const rawIndex = argArr[0];
663
- let index;
664
- if (typeof rawIndex === "number") index = rawIndex;
665
- else if (typeof rawIndex === "string" && /^-?\d+$/.test(rawIndex)) index = Number.parseInt(rawIndex, 10);
666
- if (index === void 0 || !Number.isFinite(index)) return void 0;
667
- const list = resolveImageIntrinsicAny(argArr[1], resources, context);
668
- if (Array.isArray(list)) {
669
- if (index < 0 || index >= list.length) return void 0;
670
- const picked = list[index];
671
- if (typeof picked === "string") return picked;
672
- return;
673
- }
674
- if (Array.isArray(argArr[1])) {
675
- const listLiteral = argArr[1];
676
- if (index < 0 || index >= listLiteral.length) return void 0;
677
- return resolveImageIntrinsic(listLiteral[index], resources, context);
678
- }
679
- return;
680
- }
681
- if (intrinsic === "Fn::Join") {
682
- if (!Array.isArray(arg) || arg.length !== 2) return void 0;
683
- const [delim, parts] = arg;
684
- if (typeof delim !== "string" || !Array.isArray(parts)) return void 0;
685
- const resolved = [];
686
- for (const part of parts) {
687
- const r = resolveImageIntrinsic(part, resources, context);
688
- if (r === void 0) return void 0;
689
- resolved.push(r);
690
- }
691
- return resolved.join(delim);
692
- }
693
- if (intrinsic === "Fn::Sub") {
694
- let template;
695
- if (typeof arg === "string") template = arg;
696
- else if (Array.isArray(arg) && typeof arg[0] === "string") template = arg[0];
697
- if (template === void 0) return void 0;
698
- const out = substituteImagePlaceholders(template, resources, context);
699
- if (out.includes("${")) return void 0;
700
- return out;
701
- }
702
- }
703
- function substituteImagePlaceholders(flat, resources, context) {
704
- if (!flat.includes("${")) return flat;
705
- return flat.replace(/\$\{([^}]+)\}/g, (full, key) => {
706
- if (context?.pseudoParameters) {
707
- if (key === "AWS::AccountId" && context.pseudoParameters.accountId) return context.pseudoParameters.accountId;
708
- if (key === "AWS::Region" && context.pseudoParameters.region) return context.pseudoParameters.region;
709
- if (key === "AWS::Partition" && context.pseudoParameters.partition) return context.pseudoParameters.partition;
710
- if (key === "AWS::URLSuffix" && context.pseudoParameters.urlSuffix) return context.pseudoParameters.urlSuffix;
711
- }
712
- if (context?.stateResources) {
713
- const dot = key.indexOf(".");
714
- const logicalId = dot === -1 ? key : key.slice(0, dot);
715
- const refResource = resources[logicalId];
716
- const stateEntry = context.stateResources[logicalId];
717
- if (refResource?.Type === "AWS::ECR::Repository" && stateEntry) {
718
- if (dot === -1) return stateEntry.physicalId;
719
- const attr = key.slice(dot + 1);
720
- const cached = stateEntry.attributes?.[attr];
721
- if (typeof cached === "string") return cached;
722
- }
723
- }
724
- return full;
725
- });
726
- }
727
-
728
- //#endregion
729
- //#region src/local/ecr-uri.ts
730
- const FIPS_AND_DUAL_STACK_PARTITIONS = ["aws", "aws-us-gov"];
731
- const ECR_REGISTRY_HOST_FORMS = [
732
- { labels: "dkr.ecr" },
733
- {
734
- labels: "dkr.ecr-fips",
735
- partitions: FIPS_AND_DUAL_STACK_PARTITIONS
736
- },
737
- {
738
- labels: "dkr-ecr",
739
- fixedUrlSuffix: "on.aws",
740
- partitions: FIPS_AND_DUAL_STACK_PARTITIONS
741
- },
742
- {
743
- labels: "dkr-ecr-fips",
744
- fixedUrlSuffix: "on.aws",
745
- partitions: FIPS_AND_DUAL_STACK_PARTITIONS
746
- }
747
- ];
748
- const ECR_HOST_FORM_BY_LABELS = new Map(ECR_REGISTRY_HOST_FORMS.map((f) => [f.labels, f]));
749
- function escapeRegExp(value) {
750
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
751
- }
752
- const ECR_URI_HOST_REGEX = new RegExp(`^(\\d{12})\\.(${[...ECR_REGISTRY_HOST_FORMS].sort((a, b) => b.labels.length - a.labels.length).map((f) => escapeRegExp(f.labels)).join("|")})\\.([^./]+)\\.([^/]+)/`, "i");
753
- const CANONICAL_REGION_SEGMENT = /^[A-Za-z0-9][A-Za-z0-9-]*$/;
754
- const REGION_SHAPE = /^(?:(?:us|eu|ap|sa|ca|me|af|il|mx)-[a-z0-9]+-[0-9]+|cn-[a-z0-9]+-[0-9]+|us-gov-[a-z0-9]+-[0-9]+|us-iso-[a-z0-9]+-[0-9]+|us-isob-[a-z0-9]+-[0-9]+|eu-isoe-[a-z0-9]+-[0-9]+|us-isof-[a-z0-9]+-[0-9]+|eusc-[a-z]+-[a-z0-9]+-[0-9]+)$/;
755
- const CANONICAL_URL_SUFFIX = /^[A-Za-z0-9][A-Za-z0-9.-]*$/;
756
- function foldAsciiUpperCase(value) {
757
- return value.replace(/[A-Z]+/g, (run) => run.toLowerCase());
758
- }
759
- function isRegistryHostComponent(component) {
760
- return component.includes(".") || component.includes(":") || component === "localhost";
761
- }
762
- function canonicalizeImageUriHost(imageUri) {
763
- const slash = imageUri.indexOf("/");
764
- if (slash < 0) return imageUri;
765
- const host = imageUri.slice(0, slash);
766
- if (!isRegistryHostComponent(host)) return imageUri;
767
- return foldAsciiUpperCase(host) + imageUri.slice(slash);
768
- }
769
- function parseEcrRegistryHost(imageUri) {
770
- const m = ECR_URI_HOST_REGEX.exec(imageUri);
771
- if (!m) return void 0;
772
- const [, accountId, rawLabels, rawRegion, rawSuffix] = m;
773
- if (!CANONICAL_REGION_SEGMENT.test(rawRegion)) return void 0;
774
- if (!CANONICAL_URL_SUFFIX.test(rawSuffix)) return void 0;
775
- const form = ECR_HOST_FORM_BY_LABELS.get(foldAsciiUpperCase(rawLabels));
776
- if (!form) return void 0;
777
- const region = foldAsciiUpperCase(rawRegion);
778
- if (!REGION_SHAPE.test(region)) return void 0;
779
- const suffix = foldAsciiUpperCase(rawSuffix);
780
- const partition = derivePartitionAndUrlSuffix(region);
781
- if (form.partitions && !form.partitions.includes(partition.partition)) return void 0;
782
- if (suffix !== (form.fixedUrlSuffix ?? partition.urlSuffix)) return void 0;
783
- return {
784
- accountId,
785
- region,
786
- registryHost: `${accountId}.${form.labels}.${region}.${suffix}`,
787
- form
788
- };
789
- }
790
-
791
- //#endregion
792
- //#region src/utils/docker-argv-redaction.ts
793
- const REDACTED_ARGV_VALUE = "***";
794
- const ARGV_VALUE_BEARING_FLAGS = /* @__PURE__ */ new Set([
795
- "-e",
796
- "--env",
797
- "--opt",
798
- "--label",
799
- "--build-arg"
800
- ]);
801
- const ARGV_PARAM_LIST_FLAGS = /* @__PURE__ */ new Set(["--cache-from", "--cache-to"]);
802
- const ARGV_PARAM_LIST_LOCATOR_PARAMS = /* @__PURE__ */ new Set([
803
- "type",
804
- "mode",
805
- "compression",
806
- "compression-level",
807
- "force-compression",
808
- "ignore-error",
809
- "image-manifest",
810
- "oci-mediatypes",
811
- "timeout",
812
- "ref",
813
- "dest",
814
- "src",
815
- "digest",
816
- "tag",
817
- "region",
818
- "bucket",
819
- "name",
820
- "prefix",
821
- "manifests_prefix",
822
- "blobs_prefix",
823
- "use_path_style",
824
- "upload_parallelism",
825
- "touch_refresh",
826
- "account_name",
827
- "scope"
828
- ]);
829
- const ARGV_PARAM_LIST_URL_PARAMS = /* @__PURE__ */ new Set([
830
- "account_url",
831
- "endpoint_url",
832
- "url",
833
- "url_v2"
834
- ]);
835
- function redactUrlLocator(value) {
836
- if (value === "") return value;
837
- const authority = /^(?:[a-zA-Z][a-zA-Z0-9+.-]*:)?\/\//.exec(value);
838
- if (authority === null) return REDACTED_ARGV_VALUE;
839
- const prefix = authority[0];
840
- let rest = value.slice(prefix.length);
841
- const lastAt = rest.lastIndexOf("@");
842
- if (lastAt >= 0) rest = `${REDACTED_ARGV_VALUE}${rest.slice(lastAt)}`;
843
- else {
844
- const host = rest.split(/[/?#]/)[0];
845
- const ipv6 = /^\[[0-9A-Fa-f:.]+\]/.exec(host);
846
- const bare = ipv6 ? host.slice(ipv6[0].length) : host;
847
- if (/:(?!\d{1,5}$)/.test(bare)) return REDACTED_ARGV_VALUE;
848
- }
849
- for (const delimiter of ["?", "#"]) {
850
- const at = rest.indexOf(delimiter);
851
- if (at >= 0) rest = `${rest.slice(0, at)}${delimiter}${REDACTED_ARGV_VALUE}`;
852
- }
853
- return `${prefix}${rest}`;
854
- }
855
- function maskArgvFlagValue(flag, value) {
856
- if (ARGV_VALUE_BEARING_FLAGS.has(flag)) {
857
- const eqIdx = value.indexOf("=");
858
- return eqIdx >= 0 ? `${value.substring(0, eqIdx)}=${REDACTED_ARGV_VALUE}` : void 0;
859
- }
860
- if (ARGV_PARAM_LIST_FLAGS.has(flag)) {
861
- let masked = false;
862
- const rawParts = value.split(",");
863
- const parts = rawParts.map((part, index) => {
864
- if (part === "") return part;
865
- if (masked) return REDACTED_ARGV_VALUE;
866
- const eqIdx = part.indexOf("=");
867
- if (eqIdx < 0) {
868
- if (index === 0) return part;
869
- masked = true;
870
- return REDACTED_ARGV_VALUE;
871
- }
872
- const name = part.substring(0, eqIdx);
873
- const normalized = name.trim().toLowerCase();
874
- if (ARGV_PARAM_LIST_URL_PARAMS.has(normalized)) {
875
- if (rawParts.slice(index + 1).some((later) => later.includes("@"))) {
876
- masked = true;
877
- return `${name}=${REDACTED_ARGV_VALUE}`;
878
- }
879
- const rawUrl = part.substring(eqIdx + 1);
880
- const safeUrl = redactUrlLocator(rawUrl);
881
- if (safeUrl === rawUrl) return part;
882
- masked = true;
883
- return `${name}=${safeUrl}`;
884
- }
885
- if (ARGV_PARAM_LIST_LOCATOR_PARAMS.has(normalized)) return part;
886
- masked = true;
887
- return `${name}=${REDACTED_ARGV_VALUE}`;
888
- });
889
- return masked ? parts.join(",") : void 0;
890
- }
891
- }
892
- function maskAttachedShortFlag(arg) {
893
- if (arg.startsWith("--") || !arg.startsWith("-")) return void 0;
894
- let chosen;
895
- for (const flag of ARGV_VALUE_BEARING_FLAGS) {
896
- if (flag.length !== 2 || !flag.startsWith("-") || flag[1] === "-") continue;
897
- const at = arg.indexOf(flag[1], 1);
898
- if (at < 0 || at === arg.length - 1) continue;
899
- if (chosen === void 0 || at < chosen.at) chosen = {
900
- at,
901
- flag
902
- };
903
- }
904
- if (chosen === void 0) return void 0;
905
- const masked = maskArgvFlagValue(chosen.flag, arg.slice(chosen.at + 1));
906
- return masked === void 0 ? void 0 : `${arg.slice(0, chosen.at + 1)}${masked}`;
907
- }
908
- function trailingShortFlagOfCluster(arg) {
909
- if (arg.startsWith("--") || !arg.startsWith("-")) return void 0;
910
- const last = arg[arg.length - 1];
911
- for (const flag of ARGV_VALUE_BEARING_FLAGS) if (flag.length === 2 && flag.startsWith("-") && flag[1] !== "-" && flag[1] === last) return flag;
912
- }
913
- function redactDockerArgvValues(args) {
914
- const out = [];
915
- for (let i = 0; i < args.length; i++) {
916
- const cur = args[i];
917
- const joinedEq = cur.indexOf("=");
918
- if (joinedEq > 0) {
919
- const maskedJoined = maskArgvFlagValue(cur.substring(0, joinedEq), cur.substring(joinedEq + 1));
920
- if (maskedJoined !== void 0) {
921
- out.push(`${cur.substring(0, joinedEq)}=${maskedJoined}`);
922
- continue;
923
- }
924
- }
925
- const maskedAttached = maskAttachedShortFlag(cur);
926
- if (maskedAttached !== void 0) {
927
- out.push(maskedAttached);
928
- continue;
929
- }
930
- const next = args[i + 1];
931
- if (typeof next === "string") {
932
- const maskedNext = maskArgvFlagValue(ARGV_VALUE_BEARING_FLAGS.has(cur) ? cur : trailingShortFlagOfCluster(cur) ?? cur, next);
933
- if (maskedNext !== void 0) {
934
- out.push(cur, maskedNext);
935
- i++;
936
- continue;
937
- }
938
- }
939
- out.push(cur);
940
- }
941
- return out;
942
- }
943
- function describeExitWithoutArgv(err) {
944
- const e = err;
945
- if (typeof e?.signal === "string" && e.signal !== "") return `killed by ${e.signal}`;
946
- if (typeof e?.code === "number") return `exited with code ${e.code}`;
947
- if (typeof e?.code === "string" && e.code !== "") return `failed (${e.code})`;
948
- return "failed with no output";
949
- }
950
-
951
- //#endregion
952
- //#region src/utils/shutdown-signal.ts
953
- function signalExitCode(signal) {
954
- return signal === "SIGINT" ? 130 : 143;
955
- }
956
- function createShutdownSignalHandler(options) {
957
- const exit = options.exit ?? ((code) => process.exit(code));
958
- const writeStderr = options.writeStderr ?? ((text) => void process.stderr.write(text));
959
- let count = 0;
960
- const handler = (signal, exitCode) => {
961
- const code = exitCode ?? signalExitCode(signal === "SIGINT" ? "SIGINT" : "SIGTERM");
962
- count += 1;
963
- if (count === 1) {
964
- options.info(options.stoppingMessage(signal));
965
- options.cleanup().then(() => exit(code));
966
- return;
967
- }
968
- if (count === 2) {
969
- options.warn(`Received ${signal} again; still shutting down — waiting for the cleanup to remove the containers and networks. Send it once more to force-exit (that skips the cleanup).`);
970
- return;
971
- }
972
- writeStderr(`Force-exit on repeated ${signal}; container cleanup skipped. ${options.forceExitHint}\n`);
973
- exit(code);
974
- };
975
- return handler;
976
- }
977
-
978
- //#endregion
979
- //#region src/assets/asset-source-path.ts
980
- function resolveAssetSourcePath(opts) {
981
- const { manifestDir, value, assetOutdir, field, subject, action, wrapError } = opts;
982
- const shownValue = displayUntrustedValue(value);
983
- if (isAbsolute(value) && opts.absolute !== "fold") {
984
- const absolute = resolve(value);
985
- const escape = absoluteAssemblyPathEscape(assetOutdir, absolute);
986
- if (escape !== void 0 && opts.absolute === "honour") throw wrapError(`${subject} has an absolute ${field}=${shownValue} which ` + renderAssemblyPathEscape(escape, assetOutdir, action));
987
- if (escape !== void 0) {
988
- const refusal = originScopeRefusal(absolute, assetOutdir);
989
- if (refusal !== void 0) throw wrapError(`${subject} has an absolute ${field}=${shownValue} which ${refusal}. An absolute ${field} is accepted only as a cdk synth --no-staging source folder: a directory inside your project (the git work tree holding the output directory, or the current directory when it contains the output directory or is a git work-tree root), not under a credential or version-control directory. Refusing to ${action}.`);
990
- warnAbsoluteOutsideAssembly(opts, absolute, escape);
991
- return absolute;
992
- }
993
- if (namesTheSameDirectory(assetOutdir, absolute)) warnWholeAssemblyAsSource(opts, absolute);
994
- return absolute;
995
- }
996
- const resolved = resolveAssemblyPath(manifestDir, value, { containWithin: assetOutdir });
997
- if (resolved.contained) return resolved.path;
998
- if (namesTheSameDirectory(assetOutdir, resolved.path)) {
999
- warnWholeAssemblyAsSource(opts, resolved.path);
1000
- return resolved.path;
1001
- }
1002
- throw wrapError(`${subject} has ${field}=${shownValue} which ` + renderAssemblyPathEscape(resolved, assetOutdir, action));
1003
- }
1004
- function broadRootReason(root, assetOutdir) {
1005
- if (root === resolve("/")) return "names the filesystem root";
1006
- if (containsOrIs(root, realPath(homedir()))) return "names your home directory or a directory containing it";
1007
- if (containsOrIs(root, realPath(assetOutdir))) return "names a directory containing the app's output directory";
1008
- }
1009
- function originScopeRefusal(absolute, assetOutdir) {
1010
- const root = realPath(absolute);
1011
- const broad = broadRootReason(root, assetOutdir);
1012
- if (broad !== void 0) return broad;
1013
- const projects = projectRoots(assetOutdir);
1014
- let hidden;
1015
- let isProjectRoot = false;
1016
- for (const project of projects) {
1017
- const rel = relative(project, root);
1018
- if (rel === "") isProjectRoot = true;
1019
- if (rel === "" || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) continue;
1020
- const dotted = rel.split(sep).find((c) => CREDENTIAL_DIR_NAMES.has(c.toLowerCase()));
1021
- if (dotted === void 0) return void 0;
1022
- hidden = dotted;
1023
- }
1024
- if (isProjectRoot) return "is your project root itself; name a folder inside it";
1025
- if (hidden !== void 0) return `passes through the credential / version-control directory ${displayUntrustedValue(hidden)} inside your project`;
1026
- const shown = projects.map((p) => displayUntrustedValue(p)).join(" or ");
1027
- return projects.length === 0 ? "is outside any usable project root (the current directory and the git work tree are too broad to scope it)" : `resolves to ${displayUntrustedValue(root)}, outside your project (${shown})`;
1028
- }
1029
- const CREDENTIAL_DIR_NAMES = /* @__PURE__ */ new Set([
1030
- ".git",
1031
- ".hg",
1032
- ".svn",
1033
- ".ssh",
1034
- ".aws",
1035
- ".gnupg",
1036
- ".docker",
1037
- ".kube",
1038
- ".config",
1039
- ".azure",
1040
- ".gcloud",
1041
- ".terraform",
1042
- ".npm",
1043
- ".pnpm-store",
1044
- ".yarn",
1045
- ".cache",
1046
- ".local"
1047
- ]);
1048
- function projectRoots(assetOutdir) {
1049
- const roots = [];
1050
- const usable = (p) => p !== resolve("/") && !containsOrIs(p, realPath(homedir()));
1051
- const cwd = realPath(process.cwd());
1052
- const outdir = realPath(assetOutdir);
1053
- if (usable(cwd) && (containsOrIs(cwd, outdir) || existsSync(join(cwd, ".git")))) roots.push(cwd);
1054
- let dir = outdir;
1055
- for (;;) {
1056
- if (existsSync(join(dir, ".git"))) {
1057
- if (usable(dir) && !roots.includes(dir)) roots.push(dir);
1058
- break;
1059
- }
1060
- const parent = dirname(dir);
1061
- if (parent === dir) break;
1062
- dir = parent;
1063
- }
1064
- return roots;
1065
- }
1066
- function containsOrIs(outer, inner) {
1067
- const rel = relative(outer, inner);
1068
- return rel === "" || rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
1069
- }
1070
- function realPath(p, hops = 0) {
1071
- const abs = resolve(p);
1072
- try {
1073
- return realpathSync.native(abs);
1074
- } catch {
1075
- const parent = dirname(abs);
1076
- if (parent === abs) return abs;
1077
- const realParent = realPath(parent, hops);
1078
- let link;
1079
- try {
1080
- link = readlinkSync(abs);
1081
- } catch {
1082
- link = void 0;
1083
- }
1084
- if (link !== void 0 && hops < 40) return realPath(resolve(realParent, link), hops + 1);
1085
- return join(realParent, basename(abs));
1086
- }
1087
- }
1088
- const warnedAbsoluteOutside = /* @__PURE__ */ new Set();
1089
- function warnAbsoluteOutsideAssembly(opts, absolute, escape) {
1090
- const logger = getLogger().child("assets");
1091
- const line = `${opts.subject} has an absolute ${opts.field} pointing outside the assembly: ` + displayUntrustedValue(absolute) + (escape.escape === "symlink" ? ` (through a symbolic link to ${displayUntrustedValue(escape.realPath)})` : "") + `. ${getEmbedConfig().productName} will ${opts.sink}. ` + (escape.escape === "lexical" ? "This is what cdk synth --no-staging emits, and is expected for it; if you did not synthesize with that flag, treat this assembly as untrusted." : "No CDK synth writes this, so treat this assembly as untrusted unless you made that link yourself.");
1092
- if (warnedAbsoluteOutside.has(line)) {
1093
- logger.debug(line);
1094
- return;
1095
- }
1096
- warnedAbsoluteOutside.add(line);
1097
- logger.warn(line);
1098
- }
1099
- const warnedWholeAssembly = /* @__PURE__ */ new Set();
1100
- function warnWholeAssemblyAsSource(opts, outdir) {
1101
- const logger = getLogger().child("assets");
1102
- const line = `${opts.subject} has ${opts.field} naming the assembly's output directory ITSELF: ${displayUntrustedValue(outdir)}. ${getEmbedConfig().productName} will ${opts.sink} — that is the WHOLE assembly, every template and every staged asset, not one asset directory. No CDK synth emits this, so treat this assembly as untrusted unless you wrote that path yourself.`;
1103
- if (warnedWholeAssembly.has(line)) {
1104
- logger.debug(line);
1105
- return;
1106
- }
1107
- warnedWholeAssembly.add(line);
1108
- logger.warn(line);
1109
- }
1110
-
1111
- //#endregion
1112
- //#region src/assets/docker-cache-option.ts
1113
- function cacheOptionToFlag(option) {
1114
- let flag = `type=${option.type}`;
1115
- if (option.params) for (const [k, v] of Object.entries(option.params)) flag += `,${k}=${v}`;
1116
- return flag;
1117
- }
1118
-
1119
- //#endregion
1120
- //#region src/assets/buildkit-passthrough-warnings.ts
1121
- function candidateHostPaths(value, opts) {
1122
- const out = [];
1123
- for (const part of value.split(",")) {
1124
- const eq = part.indexOf("=");
1125
- if (eq < 0) {
1126
- const bare = part.trim();
1127
- if (bare.length > 0) out.push({
1128
- path: bare,
1129
- write: opts.bareIsWrite
1130
- });
1131
- continue;
1132
- }
1133
- const key = part.slice(0, eq).trim().toLowerCase();
1134
- const v = part.slice(eq + 1).trim();
1135
- if (v.length === 0) continue;
1136
- if (key !== "src" && key !== "source" && key !== "dest") continue;
1137
- out.push({
1138
- path: v,
1139
- write: key === "dest"
1140
- });
1141
- }
1142
- return out;
1143
- }
1144
- function hostPathsOf(source) {
1145
- const refs = [];
1146
- if (source.dockerFile) refs.push({
1147
- field: "dockerFile",
1148
- where: "dockerFile",
1149
- path: source.dockerFile,
1150
- write: false
1151
- });
1152
- for (const [k, v] of Object.entries(source.dockerBuildContexts ?? {})) {
1153
- const rendered = `${k}=${v}`;
1154
- const contextPath = rendered.slice(rendered.indexOf("=") + 1).trim().replace(/^oci-layout:\/\//, "");
1155
- if (contextPath.length === 0) continue;
1156
- refs.push({
1157
- field: "dockerBuildContexts",
1158
- where: k,
1159
- path: contextPath,
1160
- write: false
1161
- });
1162
- }
1163
- const ssh = source.dockerBuildSsh ?? "";
1164
- const sshEq = ssh.indexOf("=");
1165
- (sshEq < 0 ? [] : ssh.slice(sshEq + 1).split(",")).forEach((entry, i) => {
1166
- const p = entry.trim();
1167
- if (p.length > 0) refs.push({
1168
- field: "dockerBuildSsh",
1169
- where: String(i),
1170
- path: p,
1171
- write: false
1172
- });
1173
- });
1174
- for (const [k, v] of Object.entries(source.dockerBuildSecrets ?? {})) for (const { path: p, write } of candidateHostPaths(`id=${k},${v}`, { bareIsWrite: false })) refs.push({
1175
- field: "dockerBuildSecrets",
1176
- where: k,
1177
- path: p,
1178
- write
1179
- });
1180
- (source.cacheFrom ?? []).forEach((c, i) => {
1181
- for (const { path: p, write } of candidateHostPaths(cacheOptionToFlag(c), { bareIsWrite: false })) refs.push({
1182
- field: "cacheFrom",
1183
- where: String(i),
1184
- path: p,
1185
- write
1186
- });
1187
- });
1188
- if (source.cacheTo) for (const { path: p, write } of candidateHostPaths(cacheOptionToFlag(source.cacheTo), { bareIsWrite: false })) refs.push({
1189
- field: "cacheTo",
1190
- where: "cacheTo",
1191
- path: p,
1192
- write
1193
- });
1194
- (source.dockerOutputs ?? []).forEach((o, i) => {
1195
- for (const { path: p, write } of candidateHostPaths(o, { bareIsWrite: true })) refs.push({
1196
- field: "dockerOutputs",
1197
- where: String(i),
1198
- path: p,
1199
- write
1200
- });
1201
- });
1202
- return refs;
1203
- }
1204
- const warnedPassthroughs = /* @__PURE__ */ new Set();
1205
- function warnEscapingBuildKitPaths(source, base, bound) {
1206
- const logger = getLogger().child("assets");
1207
- for (const ref of hostPathsOf(source)) {
1208
- const escape = assemblyPathEscape(base, bound, ref.path);
1209
- if (escape === void 0) continue;
1210
- const verb = ref.write ? "WRITE to" : "read";
1211
- const line = `Docker asset ${ref.field}` + (ref.where === ref.field ? "" : `[${displayUntrustedValue(ref.where)}]`) + ` names a host path outside the assembly, which ` + renderAssemblyPathEscape(escape, bound, "load", `${getEmbedConfig().productName} will ${verb} it during the image build, matching the CDK CLI — a pre-synthesized assembly is trusted input. If you did not produce this assembly, that is a host path it chose and the CloudFormation template does not show.`);
1212
- if (warnedPassthroughs.has(line)) {
1213
- logger.debug(line);
1214
- continue;
1215
- }
1216
- warnedPassthroughs.add(line);
1217
- logger.warn(line);
1218
- }
1219
- }
1220
-
1221
- //#endregion
1222
- //#region src/assets/docker-build.ts
1223
- const warnedExecutables = /* @__PURE__ */ new Set();
1224
- function warnManifestExecutable(cmd, executable, cwd) {
1225
- const logger = getLogger().child("docker-build");
1226
- const key = JSON.stringify([cwd, executable]);
1227
- if (warnedExecutables.has(key)) {
1228
- logger.debug(`source.executable already announced this process: ${displayUntrustedValue(cmd)} (cwd=${displayUntrustedValue(cwd)})`);
1229
- return;
1230
- }
1231
- warnedExecutables.add(key);
1232
- const argCount = executable.length - 1;
1233
- logger.warn(`Docker asset source.executable runs a command this asset manifest chose, on this machine: ${displayUntrustedValue(cmd)}` + (argCount > 0 ? ` (with ${argCount} argument(s); --verbose shows them)` : "") + `. ${getEmbedConfig().productName} runs it, matching the CDK CLI — a pre-synthesized assembly is trusted input. Note that this means running a local command against a pre-synthesized assembly DOES execute code from it, which the CloudFormation template does not show.`);
1234
- }
1235
- async function buildDockerImage(asset, cdkOutDir, options) {
1236
- const source = asset.source;
1237
- const logger = getLogger().child("docker-build");
1238
- const assetOutdir = options.assetOutdir ?? cdkOutDir;
1239
- const contextDirectory = (directory, sink) => resolveAssetSourcePath({
1240
- manifestDir: cdkOutDir,
1241
- value: directory,
1242
- assetOutdir,
1243
- absolute: "fold",
1244
- field: "source.directory",
1245
- subject: "Docker image asset",
1246
- action: "build it",
1247
- sink,
1248
- wrapError: (message) => new LocalInvokeBuildError(message)
1249
- });
1250
- if (source.executable && source.executable.length > 0) {
1251
- const [cmd, ...args] = source.executable;
1252
- if (!cmd) throw options.wrapError("asset source.executable[] is empty");
1253
- const cwd = source.directory ? contextDirectory(source.directory, "run this asset's source.executable with that directory as its working directory") : cdkOutDir;
1254
- logger.debug(`Building Docker image via executable: ${displayUntrustedValue(redactDockerArgvValues(source.executable).join(" "))} (cwd=${displayUntrustedValue(cwd)})`);
1255
- warnManifestExecutable(cmd, source.executable, cwd);
1256
- let result;
1257
- try {
1258
- result = await spawnStreaming(cmd, args, {
1259
- cwd,
1260
- env: dockerRegistryEnv(options.workload)
1261
- });
1262
- } catch (err) {
1263
- const e = err;
1264
- throw options.wrapError(displayToolOutput(e.stderr || e.message || String(err)));
1265
- }
1266
- const tag = result.stdout.trim();
1267
- if (!tag) throw options.wrapError(`docker build executable produced no output (expected the local image tag on stdout): ${displayUntrustedValue(cmd)}`);
1268
- return tag;
1269
- }
1270
- if (!source.directory) throw options.wrapError(`DockerImageAssetSource must set either 'directory' or 'executable' (fields present: ${displayUntrustedValue(Object.keys(source).join(", ") || "none")})`);
1271
- if (!options.tag) throw options.wrapError("buildDockerImage(directory mode) requires options.tag");
1272
- const buildArgs = buildDockerBuildCommand(source, options.tag, options.platform);
1273
- const contextDir = contextDirectory(source.directory, `send that directory to docker build as the context of an image ${getEmbedConfig().productName} then runs locally`);
1274
- buildArgs.push(".");
1275
- warnEscapingBuildKitPaths(source, contextDir, assetOutdir);
1276
- logger.debug(`${getDockerCmd()} ${redactDockerArgvValues(buildArgs).map((a) => displayUntrustedValue(a)).join(" ")} (cwd=${displayUntrustedValue(contextDir)})`);
1277
- try {
1278
- await runDockerStreaming(buildArgs, {
1279
- cwd: contextDir,
1280
- env: {
1281
- ...dockerRegistryEnv(options.workload),
1282
- BUILDX_NO_DEFAULT_ATTESTATIONS: "1"
1283
- },
1284
- ...options.progressLabel !== void 0 && { progressLabel: options.progressLabel }
1285
- });
1286
- } catch (err) {
1287
- const e = err;
1288
- throw options.wrapError(displayToolOutput(e.stderr || e.message || String(err)));
1289
- }
1290
- return options.tag;
1291
- }
1292
- function buildDockerBuildCommand(source, tag, platformOverride) {
1293
- const args = [
1294
- "build",
1295
- "--tag",
1296
- tag
1297
- ];
1298
- if (source.dockerBuildArgs) for (const [k, v] of Object.entries(source.dockerBuildArgs)) args.push("--build-arg", `${k}=${v}`);
1299
- if (source.dockerBuildContexts) for (const [k, v] of Object.entries(source.dockerBuildContexts)) args.push("--build-context", `${k}=${v}`);
1300
- if (source.dockerBuildSecrets) for (const [k, v] of Object.entries(source.dockerBuildSecrets)) args.push("--secret", `id=${k},${v}`);
1301
- if (source.dockerBuildSsh) args.push("--ssh", source.dockerBuildSsh);
1302
- if (source.dockerBuildTarget) args.push("--target", source.dockerBuildTarget);
1303
- if (source.dockerFile) args.push("--file", source.dockerFile);
1304
- if (source.networkMode) args.push("--network", source.networkMode);
1305
- const platform = platformOverride ?? source.platform;
1306
- if (platform) args.push("--platform", platform);
1307
- if (source.dockerOutputs) for (const output of source.dockerOutputs) args.push(`--output=${output}`);
1308
- if (source.cacheFrom) for (const c of source.cacheFrom) args.push("--cache-from", cacheOptionToFlag(c));
1309
- if (source.cacheTo) args.push("--cache-to", cacheOptionToFlag(source.cacheTo));
1310
- if (source.cacheDisabled) args.push("--no-cache");
1311
- return args;
1312
- }
1313
-
1314
- //#endregion
1315
- //#region src/local/ecr-puller.ts
1316
- function parseEcrUri(imageUri) {
1317
- const host = parseEcrRegistryHost(imageUri);
1318
- if (!host) return void 0;
1319
- const canonicalUri = canonicalizeImageUriHost(imageUri);
1320
- const m = /^([^:]+):(.+)$/.exec(canonicalUri.slice(canonicalUri.indexOf("/") + 1));
1321
- if (!m) return void 0;
1322
- return {
1323
- accountId: host.accountId,
1324
- region: host.region,
1325
- repository: m[1],
1326
- tag: m[2],
1327
- canonicalUri,
1328
- registryHost: host.registryHost
1329
- };
1330
- }
1331
- const ECR_SERVICE_ID = "ECR";
1332
- const ASSUMED_ROLE_CACHE = /* @__PURE__ */ new Map();
1333
- const CALLER_IDENTITY_CACHE = /* @__PURE__ */ new Map();
1334
- const STS_CREDENTIAL_SAFETY_MARGIN_MS = 300 * 1e3;
1335
- function isCredentialFresh(creds) {
1336
- if (!creds.expiration) return false;
1337
- return creds.expiration.getTime() - Date.now() > STS_CREDENTIAL_SAFETY_MARGIN_MS;
1338
- }
1339
- async function pullEcrImage(imageUri, options) {
1340
- const logger = getLogger().child("ecr-puller");
1341
- const parsed = parseEcrUri(imageUri);
1342
- if (!parsed) throw new LocalInvokeBuildError(`Image URI ${displayUntrustedValue(imageUri)} is not an ECR URI. ${getEmbedConfig().cliName} invoke v1 only authenticates against ECR for the deployed-image fallback path.`);
1343
- const canonicalUri = parsed.canonicalUri;
1344
- const callerRegion = options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"];
1345
- if (options.skipPull) {
1346
- logger.info(`Skipping ECR pull (--no-pull). Verifying ${canonicalUri} is in local cache...`);
1347
- await verifyImageInLocalCache(canonicalUri);
1348
- return canonicalUri;
1349
- }
1350
- if (deployArtifactsReadEmulator(ECR_SERVICE_ID, options.workload)) throw new LocalInvokeBuildError(`--aws-emulator: image ${displayUntrustedValue(canonicalUri)} is in a real ECR registry, and ECR is not excepted to real AWS for this workload, so it is not pulled (AWS Emulator serves no registry docker can pull from). To pull it from real ECR with your real identity, except ECR for the workload in the --env-vars file ("AWS_ENDPOINT_URL_ECR": "real-aws", under "Parameters" or the workload's own section); or pull the image yourself and pass --no-pull.`);
1351
- const callerIdentityKey = `${options.profile ?? "_noprofile"}|${callerRegion ?? "_unset"}`;
1352
- let callerAccount = CALLER_IDENTITY_CACHE.get(callerIdentityKey);
1353
- if (callerAccount === void 0) {
1354
- const sts = new STSClient(buildStsClientConfig({
1355
- region: callerRegion,
1356
- profile: options.profile
1357
- }));
1358
- try {
1359
- const identity = await sts.send(new GetCallerIdentityCommand({}));
1360
- if (!identity.Account) throw new LocalInvokeBuildError("STS GetCallerIdentity returned no Account. Verify your AWS credentials.");
1361
- callerAccount = identity.Account;
1362
- CALLER_IDENTITY_CACHE.set(callerIdentityKey, callerAccount);
1363
- } catch (err) {
1364
- if (err instanceof LocalInvokeBuildError) throw err;
1365
- throw new LocalInvokeBuildError(`STS GetCallerIdentity failed while preparing the ECR pull: ${describeAwsFailureForWarn(err, "STS GetCallerIdentity (ECR pull)")}. Verify your AWS credentials.`);
1366
- } finally {
1367
- sts.destroy();
1368
- }
1369
- }
1370
- const crossAccount = callerAccount !== parsed.accountId;
1371
- const crossRegion = callerRegion !== void 0 && callerRegion !== parsed.region;
1372
- let assumed;
1373
- if (awsEmulatorRealServices(options.workload) !== void 0) logger.info(`ECR is excepted to real AWS ("real-aws" on AWS_ENDPOINT_URL_ECR): pulling ${displayUntrustedValue(canonicalUri)} from real ECR with your real credentials.`);
1374
- if (options.ecrRoleArn) {
1375
- const cacheKey = `${options.profile ?? "_noprofile"}|${options.ecrRoleArn}|${callerRegion ?? "_unset"}`;
1376
- const cached = ASSUMED_ROLE_CACHE.get(cacheKey);
1377
- if (cached && isCredentialFresh(cached)) {
1378
- assumed = cached;
1379
- logger.debug(`Reusing cached AssumeRole credentials for ${options.ecrRoleArn}`);
1380
- } else {
1381
- assumed = await assumeRoleForEcr(options.ecrRoleArn, callerRegion, options.profile, logger);
1382
- ASSUMED_ROLE_CACHE.set(cacheKey, assumed);
1383
- logger.info(`Assumed role ${options.ecrRoleArn} for ECR pull (account=${parsed.accountId}, region=${parsed.region})`);
1384
- }
1385
- } else if (crossAccount) logger.info(`Cross-account ECR pull: image account ${parsed.accountId} != caller ${callerAccount}. Using the caller's credentials; pass --ecr-role-arn <arn> if AWS rejects with AccessDenied.`);
1386
- if (crossRegion) logger.info(`Cross-region ECR pull: image region ${parsed.region} != caller ${callerRegion ?? "(unset)"}. Authenticating against the image region directly.`);
1387
- const ecr = new ECRClient({
1388
- ...deployArtifactClientConfig({
1389
- service: "ECR",
1390
- profile: options.profile,
1391
- workload: options.workload
1392
- }),
1393
- region: parsed.region,
1394
- ...assumed ? { credentials: assumed } : options.profile ? { profile: options.profile } : {}
1395
- });
1396
- const dockerEnv = dockerRegistryEnv(options.workload);
1397
- try {
1398
- await ecrLogin(ecr, dockerEnv, {
1399
- accountId: parsed.accountId,
1400
- region: parsed.region,
1401
- registryHost: parsed.registryHost
1402
- });
1403
- } finally {
1404
- ecr.destroy();
1405
- }
1406
- logger.info(`Pulling ${canonicalUri}...`);
1407
- try {
1408
- await runDockerForeground(["pull", canonicalUri], { env: dockerEnv });
1409
- } catch (err) {
1410
- throw new LocalInvokeBuildError(`docker pull ${canonicalUri} failed: ${err.message}`);
1411
- }
1412
- return canonicalUri;
1413
- }
1414
- async function assumeRoleForEcr(roleArn, callerRegion, profile, logger) {
1415
- if (!isIamRoleArn(roleArn)) throw new LocalInvokeBuildError(refusedRoleArnMessage(roleArn));
1416
- logger.debug(`Assuming role ${roleArn} for ECR pull...`);
1417
- const sts = new STSClient(workloadStsClientConfig({
1418
- region: callerRegion,
1419
- profile
1420
- }));
1421
- try {
1422
- const creds = (await sts.send(new AssumeRoleCommand({
1423
- RoleArn: roleArn,
1424
- RoleSessionName: `${getEmbedConfig().resourceNamePrefix}-ecr-${Date.now()}`,
1425
- DurationSeconds: 3600
1426
- }))).Credentials;
1427
- if (!creds || !creds.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken) throw new LocalInvokeBuildError(`AssumeRole(${roleArn}) returned no usable credentials. Verify the role's trust policy allows your identity to assume it.`);
1428
- return {
1429
- accessKeyId: creds.AccessKeyId,
1430
- secretAccessKey: creds.SecretAccessKey,
1431
- sessionToken: creds.SessionToken,
1432
- ...creds.Expiration && { expiration: creds.Expiration }
1433
- };
1434
- } catch (err) {
1435
- if (err instanceof LocalInvokeBuildError) throw err;
1436
- const reason = describeAwsFailureForWarn(err, "STS AssumeRole (ECR pull)");
1437
- throw new LocalInvokeBuildError(`Failed to assume role ${flattenToOneLine(roleArn)} for ECR pull: ${reason}. Verify the role exists and its trust policy permits the caller's identity to assume it.`);
1438
- } finally {
1439
- sts.destroy();
1440
- }
1441
- }
1442
- async function ecrLogin(client, dockerEnv, target) {
1443
- const { accountId, region, registryHost } = target;
1444
- getLogger().child("ecr-puller").debug(`ECR login (account=${accountId}, region=${region}, host=${registryHost})`);
1445
- const authData = (await client.send(new GetAuthorizationTokenCommand({})).catch((err) => {
1446
- throw new LocalInvokeBuildError(`ECR GetAuthorizationToken failed (account=${accountId}, region=${region}): ${describeAwsFailureForWarn(err, "ECR GetAuthorizationToken")}. Verify the credentials can call ecr:GetAuthorizationToken.`);
1447
- })).authorizationData?.[0];
1448
- if (!authData?.authorizationToken) throw new LocalInvokeBuildError("Failed to get ECR authorization token");
1449
- const [username, password] = Buffer.from(authData.authorizationToken, "base64").toString().split(":");
1450
- if (!username || password === void 0) throw new LocalInvokeBuildError("ECR authorization token has unexpected shape (missing username/password)");
1451
- const endpoint = `https://${registryHost}`;
1452
- try {
1453
- await runDockerStreaming([
1454
- "login",
1455
- "--username",
1456
- username,
1457
- "--password-stdin",
1458
- endpoint
1459
- ], {
1460
- input: password,
1461
- env: dockerEnv
1462
- });
1463
- } catch (err) {
1464
- const e = err;
1465
- throw new LocalInvokeBuildError(`ECR login failed: ${formatDockerLoginError(e.stderr || e.message || String(err), endpoint)}`);
1466
- }
1467
- }
1468
- async function verifyImageInLocalCache(imageUri) {
1469
- try {
1470
- await runDockerStreaming([
1471
- "image",
1472
- "inspect",
1473
- imageUri
1474
- ]);
1475
- } catch {
1476
- throw new LocalInvokeBuildError(`Image ${displayUntrustedValue(imageUri)} is not in the local docker cache and --no-pull was set. Either remove --no-pull (${getEmbedConfig().productName} will pull from ECR) or pre-pull the image manually with \`docker pull\`.`);
1477
- }
1478
- }
1479
- async function isImageInLocalCache(imageRef) {
1480
- try {
1481
- await runDockerStreaming([
1482
- "image",
1483
- "inspect",
1484
- imageRef
1485
- ]);
1486
- return true;
1487
- } catch {
1488
- return false;
1489
- }
1490
- }
1491
-
1492
- //#endregion
1493
- //#region src/local/docker-image-builder.ts
1494
- async function buildContainerImage(asset, cdkOutDir, options) {
1495
- const tag = computeLocalTag(asset.source);
1496
- const platform = architectureToPlatform(options.architecture);
1497
- const logger = getLogger().child("local-invoke-build");
1498
- if (options.noBuild === true) {
1499
- logger.info(`Skipping docker build (--no-build). Verifying ${tag} is in local registry...`);
1500
- if (!await isImageInLocalCache(tag)) throw new LocalInvokeBuildError(`image ${displayIdent(tag)} not in local registry and --no-build is set; remove --no-build or run \`docker build\` manually first.`);
1501
- logger.debug(`Local tag ${tag} is cached; skipping build.`);
1502
- return tag;
1503
- }
1504
- logger.info(`Building container image (platform=${platform})...`);
1505
- logger.debug(`Local tag: ${tag}`);
1506
- const actualTag = await buildDockerImage(asset, cdkOutDir, {
1507
- tag,
1508
- platform,
1509
- wrapError: (stderr) => new LocalInvokeBuildError(`docker build failed for container Lambda asset (${displayUntrustedValue(asset.source.directory ?? asset.source.executable?.[0] ?? "")}): ${stderr}`),
1510
- progressLabel: `Building container image (platform=${platform})`,
1511
- ...options.assetOutdir !== void 0 && { assetOutdir: options.assetOutdir },
1512
- ...options.workload !== void 0 && { workload: options.workload }
1513
- });
1514
- if (actualTag !== tag) {
1515
- logger.debug(`Re-tagging executable-built image ${displayIdent(actualTag)} → ${displayIdent(tag)}`);
1516
- try {
1517
- await runDockerStreaming([
1518
- "tag",
1519
- actualTag,
1520
- tag
1521
- ]);
1522
- } catch (err) {
1523
- const e = err;
1524
- throw new LocalInvokeBuildError(`docker tag failed re-tagging ${displayIdent(actualTag)} → ${displayIdent(tag)}: ${displayToolOutput(e.stderr?.trim() || e.message || String(err))}`);
1525
- }
1526
- }
1527
- return tag;
1528
- }
1529
- function architectureToPlatform(architecture) {
1530
- return architecture === "arm64" ? "linux/arm64" : "linux/amd64";
1531
- }
1532
- function hostPlatform() {
1533
- return process.arch === "arm64" ? "linux/arm64" : "linux/amd64";
1534
- }
1535
- const warnedEmulatedPlatforms = /* @__PURE__ */ new Set();
1536
- function warnIfEmulatedPlatform(platform, opts = {}) {
1537
- if (!platform) return;
1538
- const host = hostPlatform();
1539
- if (platform === host) return;
1540
- if (warnedEmulatedPlatforms.has(platform)) return;
1541
- warnedEmulatedPlatforms.add(platform);
1542
- const logger = opts.logger ?? getLogger();
1543
- const what = opts.label ? `${displayIdent(opts.label)} ` : "";
1544
- logger.warn(`Running ${what}under ${platform} emulation on a ${host} host (the declared architecture differs from the host). Emulation is slower and some compiled binaries can fail under it. If the container does not run correctly, enable Rosetta for x86/amd64 emulation in your Docker engine settings, or build the function for ${host}.`);
1545
- }
1546
- function computeLocalTag(source) {
1547
- const hash = createHash("sha256");
1548
- pushField(hash, "directory", source.directory ?? "");
1549
- pushField(hash, "executable", (source.executable ?? []).join(" "));
1550
- pushField(hash, "dockerFile", source.dockerFile ?? "");
1551
- pushField(hash, "dockerBuildTarget", source.dockerBuildTarget ?? "");
1552
- pushField(hash, "networkMode", source.networkMode ?? "");
1553
- pushField(hash, "platform", source.platform ?? "");
1554
- pushField(hash, "dockerBuildSsh", source.dockerBuildSsh ?? "");
1555
- pushField(hash, "cacheDisabled", source.cacheDisabled ? "1" : "0");
1556
- pushMap(hash, "dockerBuildArgs", source.dockerBuildArgs);
1557
- pushMap(hash, "dockerBuildContexts", source.dockerBuildContexts);
1558
- pushMap(hash, "dockerBuildSecrets", source.dockerBuildSecrets);
1559
- pushField(hash, "dockerOutputs", (source.dockerOutputs ?? []).join(""));
1560
- pushField(hash, "cacheFrom", (source.cacheFrom ?? []).map((o) => JSON.stringify(o)).join(""));
1561
- pushField(hash, "cacheTo", source.cacheTo ? JSON.stringify(source.cacheTo) : "");
1562
- return `${getEmbedConfig().resourceNamePrefix}-invoke-${hash.digest("hex").slice(0, 16)}`;
1563
- }
1564
- function pushField(hash, name, value) {
1565
- hash.update(name);
1566
- hash.update("=");
1567
- hash.update(value);
1568
- hash.update("\0");
1569
- }
1570
- function pushMap(hash, name, value) {
1571
- hash.update(name);
1572
- hash.update("={");
1573
- if (value) for (const [k, v] of Object.entries(value)) {
1574
- hash.update(k);
1575
- hash.update("=");
1576
- hash.update(v);
1577
- hash.update(";");
1578
- }
1579
- hash.update("}\0");
1580
- }
1581
-
1582
- //#endregion
1583
- //#region src/local/loopback-forwarder.ts
1584
- const execFileAsync$1 = promisify(execFile);
1585
- const LOOPBACK_FORWARDER_IMAGE = "public.ecr.aws/docker/library/node:22-alpine@sha256:1314fb30ec37592d5845e5a160d522c0e680972aa534549940b8d73bde88481a";
1586
- const FORWARDER_CA_PATH = "/cdkl-forwarder/ca.pem";
1587
- const FORWARDER_RESIGN_CONFIG_PATH = "/cdkl-forwarder/resign.json";
1588
- const STATUS_PREFIX = "CDKL-FWD";
1589
- const LOOPBACK_FORWARDER_SCRIPT = `
1590
- const net=require('net'),dns=require('dns'),fs=require('fs'),https=require('https');
1591
- const ports=process.argv[1]==='-'?[]:process.argv[1].split(',').map(Number),mode=process.argv[2],ca=process.argv[3]==='-'?undefined:process.argv[3],resign=process.argv[4];
1592
- const out=(s)=>process.stdout.write('${STATUS_PREFIX} '+s+'\\n');
1593
- function gateway(){try{for(const l of fs.readFileSync('/proc/net/route','utf8').split('\\n').slice(1)){const f=l.trim().split(/\\s+/);if(f[1]==='00000000'&&f[2]&&f[2]!=='00000000'){return [3,2,1,0].map(i=>parseInt(f[2].substr(i*2,2),16)).join('.');}}}catch(e){}return undefined;}
1594
- function listen(port,host,up){return new Promise((res)=>{const s=net.createServer((c)=>{const u=net.connect(port,up);c.pipe(u);u.pipe(c);u.on('error',()=>c.destroy());c.on('error',()=>u.destroy());});s.once('error',(e)=>res(e.code||'ERROR'));s.listen(port,host,()=>res(undefined));});}
1595
- function tcpProbe(port,up){return new Promise((res)=>{const c=net.connect(port,up);c.setTimeout(5000,()=>{c.destroy();res('ETIMEDOUT');});c.once('connect',()=>{c.destroy();res(undefined);});c.once('error',(e)=>res(e.code||'ERROR'));});}
1596
- function healthProbe(port){return new Promise((res)=>{const r=https.get({host:'127.0.0.1',port,path:'/__admin/health',ca:fs.readFileSync(ca),timeout:5000},(m)=>{m.resume();res(m.statusCode===200?undefined:'HTTP_'+m.statusCode);});r.on('timeout',()=>{r.destroy();res('ETIMEDOUT');});r.on('error',(e)=>res(e.code||'ERROR'));});}
1597
- dns.lookup('host.docker.internal',{family:4},async(err,addr)=>{
1598
- const up=err?gateway():addr;
1599
- if(!up&&ports.length>0){out('FAIL '+ports[0]+' NO_UPSTREAM none');return;}
1600
- for(const p of ports){const e=await listen(p,'127.0.0.1',up);if(e){out('FAIL '+p+' LISTEN_'+e+' '+up);return;}await listen(p,'::1',up);}
1601
- for(const p of ports){const e=await tcpProbe(p,up);if(e){out('FAIL '+p+' '+e+' '+up);return;}}
1602
- let probePort=ports[0];
1603
- if(resign){const r=await startResigner(resign);if(r.error){out('FAIL '+(r.port||0)+' RESIGN_'+r.error+' '+(up||'none'));return;}probePort=r.port;}
1604
- if(mode==='health'&&ca&&probePort){const e=await healthProbe(probePort);if(e){out('FAIL '+probePort+' '+(resign?'RESIGN_UPSTREAM_'+e:e)+' '+(up||'none'));return;}}
1605
- out('READY '+(up||'none'));
1606
- });
1607
- `;
1608
- function parseForwarderStatus(logs) {
1609
- for (const raw of logs.split("\n")) {
1610
- const line = raw.trim();
1611
- if (!line.startsWith(`${STATUS_PREFIX} `)) continue;
1612
- const parts = line.slice(9).split(" ");
1613
- if (parts[0] === "READY") return {
1614
- ok: true,
1615
- ...parts[1] && parts[1] !== "none" ? { upstream: parts[1] } : {}
1616
- };
1617
- if (parts[0] === "FAIL") {
1618
- const port = Number(parts[1]);
1619
- return {
1620
- ok: false,
1621
- ...Number.isInteger(port) ? { failedPort: port } : {},
1622
- ...parts[2] ? { reason: parts[2] } : {},
1623
- ...parts[3] && parts[3] !== "none" ? { upstream: parts[3] } : {}
1624
- };
1625
- }
1626
- }
1627
- }
1628
- function buildLoopbackForwarderDockerArgs(opts) {
1629
- const ns = `container:${opts.workloadContainer}`;
1630
- const args = [
1631
- "run",
1632
- "-d",
1633
- "--rm",
1634
- "--name",
1635
- opts.name,
1636
- "--network",
1637
- ns,
1638
- "--pid",
1639
- ns
1640
- ];
1641
- if (opts.skipPull) args.push("--pull", "never");
1642
- args.push(...forwarderMounts(opts), ...forwarderCommand(opts));
1643
- return args;
1644
- }
1645
- function buildNamespaceOwnerForwarderDockerArgs(opts) {
1646
- const ns = opts.namespace;
1647
- const args = [
1648
- "run",
1649
- "-d",
1650
- "--rm",
1651
- "--name",
1652
- opts.name,
1653
- "--network",
1654
- ns.network
1655
- ];
1656
- for (const a of ns.networkAliases ?? []) args.push("--network-alias", a);
1657
- for (const p of ns.publish ?? []) args.push("-p", p);
1658
- for (const h of ns.addHosts ?? []) args.push("--add-host", h);
1659
- for (const l of ns.links ?? []) args.push("--link", l);
1660
- if (opts.skipPull) args.push("--pull", "never");
1661
- args.push(...forwarderMounts(opts), ...forwarderCommand(opts));
1662
- return args;
1663
- }
1664
- function forwarderMounts(opts) {
1665
- const args = [];
1666
- if (opts.caHostPath) args.push("-v", `${opts.caHostPath}:${FORWARDER_CA_PATH}:ro`);
1667
- if (opts.resignConfigHostPath) args.push("-v", `${opts.resignConfigHostPath}:${FORWARDER_RESIGN_CONFIG_PATH}:ro`);
1668
- return args;
1669
- }
1670
- function forwarderCommand(opts) {
1671
- const resign = opts.resignConfigHostPath !== void 0;
1672
- return [
1673
- "--entrypoint",
1674
- "node",
1675
- LOOPBACK_FORWARDER_IMAGE,
1676
- "-e",
1677
- resign ? RESIGN_SIDECAR_SCRIPT + LOOPBACK_FORWARDER_SCRIPT : LOOPBACK_FORWARDER_SCRIPT,
1678
- opts.ports.length > 0 ? opts.ports.join(",") : "-",
1679
- opts.probe === "health" && opts.caHostPath ? "health" : "tcp",
1680
- ...opts.caHostPath ? [FORWARDER_CA_PATH] : resign ? ["-"] : [],
1681
- ...resign ? [FORWARDER_RESIGN_CONFIG_PATH] : []
1682
- ];
1683
- }
1684
- let imageReady;
1685
- let pullFailure;
1686
- const PULL_FAILURE_TTL_MS = 6e4;
1687
- async function ensureForwarderImage(skipPull = false) {
1688
- if (skipPull) return;
1689
- if (pullFailure && Date.now() - pullFailure.at < PULL_FAILURE_TTL_MS) throw pullFailure.error;
1690
- imageReady ??= (async () => {
1691
- try {
1692
- await execFileAsync$1(getDockerCmd(), [
1693
- "image",
1694
- "inspect",
1695
- LOOPBACK_FORWARDER_IMAGE
1696
- ]);
1697
- } catch {
1698
- await pullImage(LOOPBACK_FORWARDER_IMAGE, false);
1699
- }
1700
- })().catch((err) => {
1701
- imageReady = void 0;
1702
- pullFailure = {
1703
- at: Date.now(),
1704
- error: err
1705
- };
1706
- throw err;
1707
- });
1708
- return imageReady;
1709
- }
1710
- function newLoopbackForwarderName() {
1711
- return `${getEmbedConfig().resourceNamePrefix}-awsfwd-${process.pid}-${randomBytes(4).toString("hex")}`;
1712
- }
1713
- async function startLoopbackForwarder(opts) {
1714
- const logger = getLogger().child("forwarder");
1715
- await ensureForwarderImage(opts.skipPull === true);
1716
- const name = opts.name ?? newLoopbackForwarderName();
1717
- const args = buildLoopbackForwarderDockerArgs({
1718
- ...opts,
1719
- name
1720
- });
1721
- logger.debug(`Starting loopback forwarder ${name} for ports ${opts.ports.join(",")}`);
1722
- try {
1723
- await execFileAsync$1(getDockerCmd(), args);
1724
- } catch (error) {
1725
- throw new Error(`Could not start the loopback forwarder container: ${displayToolOutput(error.stderr?.trim() || describeExitWithoutArgv(error))}`);
1726
- }
1727
- return awaitLoopbackForwarderStatus(name, opts.timeoutMs);
1728
- }
1729
- async function awaitLoopbackForwarderStatus(name, timeoutMs = 2e4) {
1730
- const logger = getLogger().child("forwarder");
1731
- const deadline = Date.now() + timeoutMs;
1732
- for (;;) {
1733
- let logs = "";
1734
- try {
1735
- const r = await execFileAsync$1(getDockerCmd(), ["logs", name]);
1736
- logs = `${r.stdout}\n${r.stderr}`;
1737
- } catch (error) {
1738
- throw new Error(`The loopback forwarder container exited before reporting: ${displayToolOutput(error.stderr?.trim() || describeExitWithoutArgv(error))}`);
1739
- }
1740
- const status = parseForwarderStatus(logs);
1741
- if (status) {
1742
- logger.debug(status.ok ? `Loopback forwarder ${name} ready (upstream ${status.upstream ?? "?"})` : `Loopback forwarder ${name} failed: port ${status.failedPort} ${status.reason}`);
1743
- return {
1744
- name,
1745
- ...status
1746
- };
1747
- }
1748
- if (Date.now() > deadline) {
1749
- await execFileAsync$1(getDockerCmd(), [
1750
- "rm",
1751
- "-f",
1752
- name
1753
- ]).catch(() => void 0);
1754
- throw new Error("The loopback forwarder container did not report readiness in time.");
1755
- }
1756
- await new Promise((r) => setTimeout(r, 100));
1757
- }
1758
- }
1759
- function describeForwarderFailure(result, endpointLabel) {
1760
- const port = result.failedPort ?? 0;
1761
- const via = result.upstream ? ` via ${result.upstream}:${port}` : "";
1762
- const reason = result.reason ?? "unknown";
1763
- if (reason.startsWith("RESIGN_UPSTREAM_")) return `${endpointLabel}: the AWS Emulator sidecar started, but a health check through it did not reach the emulator (${reason.slice(16)}). Check that the emulator is still running, and that its certificate comes from the CA cdk-local trusts (a restart issues a new one).`;
1764
- if (reason.startsWith("RESIGN_")) {
1765
- const inUse = reason === "RESIGN_EADDRINUSE";
1766
- return `${endpointLabel}: the AWS Emulator sidecar could not start${port > 0 ? ` on 127.0.0.1:${port}` : ""} inside the workload container (${reason.slice(7)}). ` + (inUse ? `The workload already listens on port ${port}; under --aws-emulator it is reserved for the AWS Emulator sidecar.` : reason.startsWith("RESIGN_CONFIG_") ? "Its config file was not readable in the container; check that the temp directory is shared with Docker." : "It could not listen on that address inside the container.");
1767
- }
1768
- if (reason.startsWith("LISTEN_")) return `${endpointLabel}: the workload container already uses port ${port} on its loopback, so cdk-local cannot forward 127.0.0.1:${port} to the host. Run the endpoint on another port.`;
1769
- if (reason.startsWith("HTTP_") || reason.includes("CERT") || reason === "DEPTH_ZERO_SELF_SIGNED_CERT") return `${endpointLabel}: containers reach port ${port}${via}, but its health check failed (${reason}). Check that the endpoint on port ${port} is the one whose CA cdk-local mounted (a restart issues a new certificate).`;
1770
- return `${endpointLabel}: containers cannot reach port ${port} on the host${via} (${reason}). cdk-local forwards 127.0.0.1:${port} inside each workload container to the host, so the endpoint must also accept connections from the Docker network. On Docker Desktop, publish it on the host's 127.0.0.1 (\`-p 127.0.0.1:${port}:${port}\`). On Linux, publish it on 127.0.0.1 AND on the Docker bridge gateway: run the emulator's Docker image with \`-p 127.0.0.1:${port}:${port} -p ${result.upstream && /^\d+\.\d+\.\d+\.\d+$/.test(result.upstream) && !result.upstream.startsWith("192.168.65.") ? result.upstream : "<docker bridge gateway, e.g. 172.17.0.1>"}:${port}:${port}\` (a native install listens on one address only, so it cannot serve this machine and containers at once). Do not bind 0.0.0.0 on a host: that opens it to your network.`;
1771
- }
1772
-
1773
- //#endregion
1774
- //#region src/local/docker-runner.ts
1775
- var docker_runner_exports = /* @__PURE__ */ __exportAll({
1776
- DockerRunnerError: () => DockerRunnerError,
1777
- MAX_HELD_LINE_CHARS: () => MAX_HELD_LINE_CHARS,
1778
- PREFIX_CONTAINER_OUTPUT_ENV: () => PREFIX_CONTAINER_OUTPUT_ENV,
1779
- SENSITIVE_ENV_KEYS: () => SENSITIVE_ENV_KEYS,
1780
- appendEnvFlags: () => appendEnvFlags,
1781
- attachLoopbackForwarder: () => attachLoopbackForwarder,
1782
- createPrefixedLineRelay: () => createPrefixedLineRelay,
1783
- describeRefusedSensitiveEnvKeys: () => describeRefusedSensitiveEnvKeys,
1784
- ensureDockerAvailable: () => ensureDockerAvailable,
1785
- execEnvForSecrets: () => execEnvForSecrets,
1786
- pickFreePort: () => pickFreePort,
1787
- prepareLoopbackForwarder: () => prepareLoopbackForwarder,
1788
- pullImage: () => pullImage,
1789
- rememberForwardedPorts: () => rememberForwardedPorts,
1790
- removeContainer: () => removeContainer,
1791
- runDetached: () => runDetached,
1792
- startNamespaceOwnerForwarder: () => startNamespaceOwnerForwarder,
1793
- streamLogs: () => streamLogs,
1794
- withWorkloadAwsWiring: () => withWorkloadAwsWiring
1795
- });
1796
- const execFileAsync = promisify(execFile);
1797
- var DockerRunnerError = class DockerRunnerError extends Error {
1798
- constructor(message) {
1799
- super(message);
1800
- this.name = "DockerRunnerError";
1801
- Object.setPrototypeOf(this, DockerRunnerError.prototype);
1802
- }
1803
- };
1804
- async function pullImage(image, skipPull, platform) {
1805
- const logger = getLogger().child("docker");
1806
- if (skipPull) {
1807
- logger.debug(`Skipping docker pull for ${displayIdent(image)} (--no-pull)`);
1808
- return;
1809
- }
1810
- const platformArgs = platform ? ["--platform", platform] : [];
1811
- if (getLogger().getLevel() === "debug") {
1812
- logger.info(`Pulling ${displayIdent(image)}${platform ? ` (${displayIdent(platform)})` : ""}...`);
1813
- try {
1814
- await runDockerForeground([
1815
- "pull",
1816
- ...platformArgs,
1817
- image
1818
- ]);
1819
- } catch (err) {
1820
- const e = err;
1821
- throw new DockerRunnerError(`docker pull ${displayIdent(image)} failed: ${e.message}`);
1822
- }
1823
- return;
1824
- }
1825
- logger.debug(`Pulling ${displayIdent(image)} (silent — pass --verbose to stream progress)`);
1826
- try {
1827
- await runDockerStreaming([
1828
- "pull",
1829
- ...platformArgs,
1830
- image
1831
- ], {
1832
- streamLive: false,
1833
- progressLabel: `Pulling ${displayIdent(image)}`
1834
- });
1835
- } catch (err) {
1836
- const e = err;
1837
- if (e.exitCode === void 0 || e.exitCode === null) throw new DockerRunnerError(`docker pull ${displayIdent(image)} failed: ${displayToolOutput(e.message ?? String(err))}`);
1838
- const detail = e.stderr?.trim() || e.stdout?.trim() || "(no output)";
1839
- throw new DockerRunnerError(`docker pull ${displayIdent(image)} exited with code ${e.exitCode}: ${displayToolOutput(detail)}`);
1840
- }
1841
- }
1842
- async function runDetached(opts) {
1843
- const wiring = getActiveWorkloadAwsWiring();
1844
- if (!wiring) return runDetachedContainer(opts);
1845
- const wired = await withWorkloadAwsWiring(opts, wiring);
1846
- if (opts.network === "host" && wiring.mixed && effectiveLoopbackPorts(wired.env, wiring).includes(wiring.mixed.resignPort)) throw new DockerRunnerError("AWS Emulator mixed mode (a \"real-aws\" exception in --env-vars) needs the re-signing sidecar, which cannot run on the host network. Drop the host network for this workload, or use --aws-emulator without a \"real-aws\" exception.");
1847
- const ports = opts.network === "host" ? [] : effectiveLoopbackPorts(wired.env, wiring);
1848
- if (ports.length > 0) await prepareLoopbackForwarder(wiring);
1849
- const id = await runDetachedContainer(wired);
1850
- await attachLoopbackForwarder(id, wiring, ports);
1851
- return id;
1852
- }
1853
- async function withWorkloadAwsWiring(opts, wiring) {
1854
- const defaults = wiring.mode === "forward" && !opts.env["AWS_ACCESS_KEY_ID"] ? await resolveHostDefaultCredentials() : void 0;
1855
- const env = applyWorkloadAwsWiringToEnv(opts.env, wiring, defaults);
1856
- if (opts.network === "host" && wiring.sidecar?.passthrough && env["AWS_ENDPOINT_URL_SQS"] === wiring.sidecar.endpoint) delete env["AWS_ENDPOINT_URL_SQS"];
1857
- const credsPath = `${getEmbedConfig().awsBindMountPath}/credentials`;
1858
- const emulatorOnly = workloadGetsEmulatorKeysOnly(opts.env, wiring);
1859
- const extra = (opts.extraMounts ?? []).filter((m) => !emulatorOnly || m.containerPath !== credsPath);
1860
- const taken = new Set([...opts.mounts, ...extra].map((m) => m.containerPath));
1861
- for (const m of wiring.mounts) if (!taken.has(m.containerPath)) extra.push({
1862
- ...m,
1863
- readOnly: true
1864
- });
1865
- return {
1866
- ...opts,
1867
- env,
1868
- extraMounts: extra
1869
- };
1870
- }
1871
- async function prepareLoopbackForwarder(wiring) {
1872
- try {
1873
- await ensureForwarderImage(wiring.skipPull === true);
1874
- } catch (err) {
1875
- if (wiring.mode === "emulator" || wiring.mixed) throw new DockerRunnerError(`--aws-emulator: the loopback forwarder image could not be pulled: ${err.message}`);
1876
- }
1877
- }
1878
- const warnedForwarderPorts = /* @__PURE__ */ new Set();
1879
- function warnForwarderOnce(key, message) {
1880
- if (warnedForwarderPorts.has(key)) return;
1881
- warnedForwarderPorts.add(key);
1882
- getLogger().warn(message);
1883
- }
1884
- async function isContainerRunning(containerId) {
1885
- try {
1886
- const { stdout } = await execFileAsync(getDockerCmd(), [
1887
- "inspect",
1888
- "-f",
1889
- "{{.State.Running}}",
1890
- containerId
1891
- ]);
1892
- return stdout.trim() === "true";
1893
- } catch (err) {
1894
- return !/no such (object|container)/i.test(err.stderr ?? "");
1895
- }
1896
- }
1897
- const signalGuarded = /* @__PURE__ */ new Map();
1898
- let signalListeners = [];
1899
- async function removeOnSignalWhile(containers, fn) {
1900
- if (signalListeners.length === 0) signalListeners = ["SIGINT", "SIGTERM"].map((signal) => {
1901
- const listener = () => {
1902
- const names = [...signalGuarded.keys()];
1903
- if (names.length > 0) try {
1904
- execFileSync(getDockerCmd(), [
1905
- "rm",
1906
- "-f",
1907
- ...names
1908
- ], {
1909
- stdio: "ignore",
1910
- timeout: 15e3
1911
- });
1912
- } catch {}
1913
- if (process.listenerCount(signal) === 1) process.exit(signalExitCode(signal));
1914
- };
1915
- process.on(signal, listener);
1916
- return {
1917
- signal,
1918
- listener
1919
- };
1920
- });
1921
- for (const c of containers) signalGuarded.set(c, (signalGuarded.get(c) ?? 0) + 1);
1922
- try {
1923
- return await fn();
1924
- } finally {
1925
- for (const c of containers) {
1926
- const n = (signalGuarded.get(c) ?? 1) - 1;
1927
- if (n > 0) signalGuarded.set(c, n);
1928
- else signalGuarded.delete(c);
1929
- }
1930
- if (signalGuarded.size === 0) {
1931
- for (const { signal, listener } of signalListeners) process.off(signal, listener);
1932
- signalListeners = [];
1933
- }
1934
- }
1935
- }
1936
- const forwardedPortsByContainer = /* @__PURE__ */ new Map();
1937
- async function attachLoopbackForwarder(containerId, wiring, ports = forwardedPortsByContainer.get(containerId) ?? wiring.loopbackPorts) {
1938
- if (samePorts(ports, wiring.loopbackPorts)) forwardedPortsByContainer.delete(containerId);
1939
- else forwardedPortsByContainer.set(containerId, ports);
1940
- if (ports.length === 0) return;
1941
- const plan = loopbackForwarderPlan(wiring, ports);
1942
- const fatal = forwarderFailureIsFatal(wiring, plan);
1943
- const label = forwarderEndpointLabel(wiring);
1944
- const forwarderName = newLoopbackForwarderName();
1945
- let result;
1946
- try {
1947
- result = await removeOnSignalWhile([containerId, forwarderName], () => startLoopbackForwarder({
1948
- workloadContainer: containerId,
1949
- ports: plan.ports,
1950
- probe: wiring.probeHealth ? "health" : "tcp",
1951
- name: forwarderName,
1952
- ...wiring.skipPull ? { skipPull: true } : {},
1953
- ...wiring.caHostPath ? { caHostPath: wiring.caHostPath } : {},
1954
- ...plan.resignConfigHostPath ? { resignConfigHostPath: plan.resignConfigHostPath } : {}
1955
- }));
1956
- } catch (err) {
1957
- if (!await isContainerRunning(containerId)) return;
1958
- const message = `${label}: ${err.message}`;
1959
- if (fatal) {
1960
- await removeContainer(containerId);
1961
- throw new DockerRunnerError(message);
1962
- }
1963
- warnForwarderOnce("start", message);
1964
- return;
1965
- }
1966
- if (result.ok) return;
1967
- const message = describeForwarderFailure(result, label);
1968
- if (fatal) {
1969
- await removeContainer(containerId);
1970
- throw new DockerRunnerError(message);
1971
- }
1972
- warnForwarderOnce(String(result.failedPort ?? 0), message);
1973
- }
1974
- async function startNamespaceOwnerForwarder(opts) {
1975
- const { name, wiring } = opts;
1976
- const label = forwarderEndpointLabel(wiring);
1977
- const plan = loopbackForwarderPlan(wiring, opts.ports);
1978
- const fatal = forwarderFailureIsFatal(wiring, plan);
1979
- await prepareLoopbackForwarder(wiring);
1980
- const args = buildNamespaceOwnerForwarderDockerArgs({
1981
- name,
1982
- namespace: opts.namespace,
1983
- ports: plan.ports,
1984
- probe: wiring.probeHealth ? "health" : "tcp",
1985
- ...wiring.skipPull ? { skipPull: true } : {},
1986
- ...wiring.caHostPath ? { caHostPath: wiring.caHostPath } : {},
1987
- ...plan.resignConfigHostPath ? { resignConfigHostPath: plan.resignConfigHostPath } : {}
1988
- });
1989
- getLogger().child("docker").debug(`Starting loopback forwarder ${name} for ports ${opts.ports.join(",")}`);
1990
- const fail = async (key, message, remove = true) => {
1991
- if (remove) await removeContainer(name);
1992
- if (fatal) throw new DockerRunnerError(message);
1993
- warnForwarderOnce(key, message);
1994
- };
1995
- return removeOnSignalWhile([name], async () => {
1996
- let id;
1997
- try {
1998
- id = (await execFileAsync(getDockerCmd(), args)).stdout.trim();
1999
- } catch (error) {
2000
- const err = error;
2001
- const conflict = /is already (?:in use|used) by|Conflict\. The container name/.test(err.stderr ?? "");
2002
- return fail("start", `${label}: could not start the loopback forwarder for ${opts.workloadLabel} (it holds the ${opts.workloadLabel}'s network aliases and published ports): ` + displayToolOutput(err.stderr?.trim() || describeExitWithoutArgv(error)), !conflict);
2003
- }
2004
- let result;
2005
- try {
2006
- result = await awaitLoopbackForwarderStatus(name);
2007
- } catch (err) {
2008
- return fail("start", `${label}: ${err.message}`);
2009
- }
2010
- if (result.ok) return id || name;
2011
- return fail(String(result.failedPort ?? 0), describeForwarderFailure(result, label));
2012
- });
2013
- }
2014
- function rememberForwardedPorts(containerId, wiring, ports) {
2015
- if (samePorts(ports, wiring.loopbackPorts)) forwardedPortsByContainer.delete(containerId);
2016
- else forwardedPortsByContainer.set(containerId, ports);
2017
- }
2018
- function forwarderEndpointLabel(wiring) {
2019
- if (wiring.mode === "emulator") return `--aws-emulator ${wiring.emulatorEndpoint?.url ?? ""}`;
2020
- if (wiring.mixed) return `--aws-emulator ${wiring.mixed.emulatorEndpoint.url} (mixed mode)`;
2021
- return "The AWS endpoint override (AWS_ENDPOINT_URL*)";
2022
- }
2023
- function forwarderFailureIsFatal(wiring, plan) {
2024
- return wiring.mode === "emulator" || plan.resignConfigHostPath !== void 0;
2025
- }
2026
- function samePorts(a, b) {
2027
- return a.length === b.length && a.every((p) => b.includes(p));
2028
- }
2029
- async function runDetachedContainer(opts) {
2030
- const args = opts.autoRemove === false ? ["run", "-d"] : [
2031
- "run",
2032
- "-d",
2033
- "--rm"
2034
- ];
2035
- if (opts.name) args.push("--name", opts.name);
2036
- if (opts.network) args.push("--network", opts.network);
2037
- if (opts.platform) {
2038
- args.push("--platform", opts.platform);
2039
- warnIfEmulatedPlatform(opts.platform, opts.name ? { label: opts.name } : {});
2040
- }
2041
- if (opts.extraHosts) for (const entry of opts.extraHosts) args.push("--add-host", `${entry.host}:${entry.ip}`);
2042
- const host = opts.host ?? "127.0.0.1";
2043
- args.push("-p", `${host}:${opts.hostPort}:${opts.containerPort ?? 8080}`);
2044
- if (opts.debugPort !== void 0) args.push("-p", `${host}:${opts.debugPort}:${opts.debugPort}`);
2045
- for (const extra of opts.extraPorts ?? []) args.push("-p", `${host}:${extra.hostPort}:${extra.containerPort}`);
2046
- for (const mount of opts.mounts) {
2047
- const ro = mount.readOnly ? ":ro" : "";
2048
- args.push("-v", `${mount.hostPath}:${mount.containerPath}${ro}`);
2049
- }
2050
- if (opts.extraMounts) for (const mount of opts.extraMounts) {
2051
- const ro = mount.readOnly ? ":ro" : "";
2052
- args.push("-v", `${mount.hostPath}:${mount.containerPath}${ro}`);
2053
- }
2054
- const sensitiveKeys = opts.sensitiveEnvKeys && opts.sensitiveEnvKeys.size > 0 ? /* @__PURE__ */ new Set([...SENSITIVE_ENV_KEYS, ...opts.sensitiveEnvKeys]) : SENSITIVE_ENV_KEYS;
2055
- const { passthrough: passthroughEnv, collisions } = appendEnvFlags(args, opts.env, sensitiveKeys);
2056
- if (opts.tmpfs) args.push("--tmpfs", `${opts.tmpfs.target}:rw,size=${opts.tmpfs.sizeMb}m`);
2057
- if (opts.workingDir) args.push("--workdir", opts.workingDir);
2058
- let entryPointTail = [];
2059
- if (opts.entryPoint && opts.entryPoint.length > 0) {
2060
- args.push("--entrypoint", opts.entryPoint[0]);
2061
- entryPointTail = opts.entryPoint.slice(1);
2062
- }
2063
- args.push(...noImplicitEcrPullArgs(opts.image), opts.image, ...entryPointTail, ...opts.cmd);
2064
- const logger = getLogger().child("docker");
2065
- logger.debug(`${getDockerCmd()} ${redactDockerArgvValues(args).map((a) => displayUntrustedValue(a)).join(" ")}`);
2066
- if (collisions.length > 0) logger.warn(describeRefusedSensitiveEnvKeys(`Container for image ${displayUntrustedValue(opts.image)}`, collisions));
2067
- const forwardedKeys = Object.keys(passthroughEnv);
2068
- const refusal = finchSecretArgvRefusal(forwardedKeys.filter((k) => opts.sensitiveEnvKeys?.has(k)), {
2069
- label: "Container for image",
2070
- name: opts.image
2071
- });
2072
- if (refusal !== void 0) throw new DockerRunnerError(refusal);
2073
- warnFinchArgvExposure(forwardedKeys);
2074
- try {
2075
- const { stdout } = await execFileAsync(getDockerCmd(), args, {
2076
- maxBuffer: 10 * 1024 * 1024,
2077
- ...execEnvForSecrets(passthroughEnv)
2078
- });
2079
- return stdout.trim();
2080
- } catch (error) {
2081
- throw new DockerRunnerError(`docker run failed: ${displayToolOutput(error.stderr?.trim() || describeExitWithoutArgv(error))}`);
2082
- }
2083
- }
2084
- const PREFIX_CONTAINER_OUTPUT_ENV = "CDKL_PREFIX_CONTAINER_OUTPUT";
2085
- const MAX_HELD_LINE_CHARS = 64 * 1024;
2086
- function createPrefixedLineRelay(prefix, out) {
2087
- const decoder = new StringDecoder("utf8");
2088
- let pending = "";
2089
- const emit = (text) => {
2090
- let nl = text.indexOf("\n");
2091
- if (nl !== -1) nl += pending.length;
2092
- pending += text;
2093
- while (nl !== -1) {
2094
- out.write(`${prefix}${pending.slice(0, nl + 1)}`);
2095
- pending = pending.slice(nl + 1);
2096
- nl = pending.indexOf("\n");
2097
- }
2098
- while (pending.length >= MAX_HELD_LINE_CHARS) {
2099
- const high = pending.charCodeAt(MAX_HELD_LINE_CHARS - 1);
2100
- const cut = high >= 55296 && high <= 56319 ? MAX_HELD_LINE_CHARS - 1 : MAX_HELD_LINE_CHARS;
2101
- out.write(`${prefix}${pending.slice(0, cut)}\n`);
2102
- pending = pending.slice(cut);
2103
- }
2104
- };
2105
- return {
2106
- push: (chunk) => emit(decoder.write(chunk)),
2107
- flush: () => {
2108
- emit(decoder.end());
2109
- if (pending.length > 0) {
2110
- out.write(`${prefix}${pending}\n`);
2111
- pending = "";
2112
- }
2113
- }
2114
- };
2115
- }
2116
- function streamLogs(containerId, opts = {}) {
2117
- const proc = spawn(getDockerCmd(), [
2118
- "logs",
2119
- "-f",
2120
- containerId
2121
- ], { stdio: [
2122
- "ignore",
2123
- "pipe",
2124
- "pipe"
2125
- ] });
2126
- if (opts.onOutput) {
2127
- const tap = opts.onOutput;
2128
- proc.stdout?.on("data", (chunk) => tap(chunk));
2129
- proc.stderr?.on("data", (chunk) => tap(chunk));
2130
- }
2131
- let flush = () => void 0;
2132
- if (opts.prefix !== void 0 || process.env["CDKL_PREFIX_CONTAINER_OUTPUT"] === "1") {
2133
- const tag = opts.prefix ?? `[${flattenToOneLine(containerId.slice(0, 12)).replace(/[\]\s]/g, "_")}] `;
2134
- const stdoutRelay = createPrefixedLineRelay(tag, process.stdout);
2135
- const stderrRelay = createPrefixedLineRelay(tag, process.stderr);
2136
- proc.stdout?.on("data", (chunk) => stdoutRelay.push(chunk));
2137
- proc.stderr?.on("data", (chunk) => stderrRelay.push(chunk));
2138
- flush = () => {
2139
- stdoutRelay.flush();
2140
- stderrRelay.flush();
2141
- };
2142
- proc.on("close", () => flush());
2143
- } else {
2144
- proc.stdout?.on("data", (chunk) => process.stdout.write(chunk));
2145
- proc.stderr?.on("data", (chunk) => process.stderr.write(chunk));
2146
- }
2147
- proc.on("error", () => {});
2148
- return () => {
2149
- flush();
2150
- if (!proc.killed) proc.kill("SIGTERM");
2151
- };
2152
- }
2153
- async function removeContainer(containerId) {
2154
- if (!containerId) return;
2155
- forwardedPortsByContainer.delete(containerId);
2156
- const logger = getLogger().child("docker");
2157
- try {
2158
- await execFileAsync(getDockerCmd(), [
2159
- "rm",
2160
- "-f",
2161
- containerId
2162
- ]);
2163
- logger.debug(`Removed container ${containerId}`);
2164
- } catch (error) {
2165
- const err = error;
2166
- logger.debug(`docker rm -f ${containerId} failed: ${err.stderr || err.message || String(error)}`);
2167
- }
2168
- }
2169
- async function ensureDockerAvailable() {
2170
- const cmd = getDockerCmd();
2171
- try {
2172
- await execFileAsync(cmd, [
2173
- "version",
2174
- "--format",
2175
- "{{.Server.Version}}"
2176
- ]);
2177
- } catch (error) {
2178
- const err = error;
2179
- if (err.code === "ENOENT") throw new DockerRunnerError(`${cmd} is not installed or not on PATH. ${getEmbedConfig().cliName} invoke needs Docker (or a compatible CLI specified via CDK_DOCKER) — install it and retry.`);
2180
- throw new DockerRunnerError(`${cmd} daemon is not reachable: ${err.stderr?.trim() || err.message || String(error)}. Start Docker Desktop / the docker daemon and retry.`);
2181
- }
2182
- }
2183
- function pickFreePort() {
2184
- return new Promise((resolvePort, rejectPort) => {
2185
- const server = createServer();
2186
- server.unref();
2187
- server.on("error", rejectPort);
2188
- server.listen(0, "127.0.0.1", () => {
2189
- const address = server.address();
2190
- if (!address || typeof address === "string") {
2191
- server.close();
2192
- rejectPort(/* @__PURE__ */ new Error("Could not allocate a host port"));
2193
- return;
2194
- }
2195
- const port = address.port;
2196
- server.close(() => resolvePort(port));
2197
- });
2198
- });
2199
- }
2200
- const SENSITIVE_ENV_KEYS = /* @__PURE__ */ new Set([
2201
- "AWS_ACCESS_KEY_ID",
2202
- "AWS_SECRET_ACCESS_KEY",
2203
- "AWS_SESSION_TOKEN"
2204
- ]);
2205
- function appendEnvFlags(args, env, sensitiveKeys) {
2206
- const passthrough = {};
2207
- const collisions = [];
2208
- for (const [k, v] of Object.entries(env)) if (sensitiveKeys.has(k)) {
2209
- if (isMalformedEnvKey(k) || isDockerClientEnvKey(k)) {
2210
- collisions.push(k);
2211
- continue;
2212
- }
2213
- args.push("-e", k);
2214
- defineOwnKey(passthrough, k, v);
2215
- } else args.push("-e", `${k}=${v}`);
2216
- return {
2217
- passthrough,
2218
- collisions
2219
- };
2220
- }
2221
- function describeRefusedSensitiveEnvKeys(subject, keys) {
2222
- return `${subject}: sensitive env var(s) ${keys.map(displayUntrustedValue).join(", ")} share a name with a variable the container client reads (docker, or the CDK_DOCKER binary) or have a malformed name (empty, or containing '=' / NUL), and were NOT passed to the container at all (a colliding name would hijack that client; a malformed name cannot form a valid environment variable). Rename the variable if the container needs it.`;
2223
- }
2224
- function execEnvForSecrets(passthrough) {
2225
- const keys = Object.keys(passthrough).filter((k) => !isMalformedEnvKey(k) && !isDockerClientEnvKey(k));
2226
- if (keys.length === 0) return {};
2227
- const env = { ...process.env };
2228
- for (const k of keys) defineOwnKey(env, k, passthrough[k]);
2229
- return { env };
2230
- }
2231
-
2232
- //#endregion
2233
- export { isIamRoleArn as $, buildDockerImage as A, substituteImagePlaceholders as B, startLoopbackForwarder as C, isImageInLocalCache as D, warnIfEmulatedPlatform as E, parseEcrRegistryHost as F, LocalStartServiceError as G, CdkLocalError as H, contextForContainer as I, withErrorHandling as J, RouteDiscoveryError as K, derivePartitionAndUrlSuffix as L, createShutdownSignalHandler as M, signalExitCode as N, parseEcrUri as O, describeExitWithoutArgv as P, describeRejectedRoleArn as Q, derivePseudoParametersFromRegion as R, ensureForwarderImage as S, buildContainerImage as T, InteractiveTtyRequiredError as U, tryResolveImageFnJoin as V, LocalInvokeBuildError as W, applyRoleArnIfSet as X, AssumeRoleFailure as Y, assumeRoleCredentials as Z, LOOPBACK_FORWARDER_IMAGE as _, describeRefusedSensitiveEnvKeys as a, buildNamespaceOwnerForwarderDockerArgs as b, execEnvForSecrets as c, rememberForwardedPorts as d, refusedRoleArnMessage as et, removeContainer as f, withWorkloadAwsWiring as g, streamLogs as h, attachLoopbackForwarder as i, resolveAssetSourcePath as j, pullEcrImage as k, pickFreePort as l, startNamespaceOwnerForwarder as m, SENSITIVE_ENV_KEYS as n, buildStsClientConfig as nt, docker_runner_exports as o, runDetached as p, TargetSelectionCancelledError as q, appendEnvFlags as r, resolveProfileCredentials as rt, ensureDockerAvailable as s, DockerRunnerError as t, emulateRoleAfterDeniedAssume as tt, pullImage as u, awaitLoopbackForwarderStatus as v, architectureToPlatform as w, describeForwarderFailure as x, buildLoopbackForwarderDockerArgs as y, formatStateRemedy as z };
2234
- //# sourceMappingURL=docker-runner-DjcP2syq.js.map