cdk-local 0.160.1 → 0.161.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/assembly-path-R96uhMm1.js +208 -0
- package/dist/assembly-path-R96uhMm1.js.map +1 -0
- package/dist/cli.js +2 -2
- package/dist/{docker-cmd-CoZdbeos.js → docker-cmd-Cjg3jGYF.js} +5 -223
- package/dist/docker-cmd-Cjg3jGYF.js.map +1 -0
- package/dist/docker-runner-CnXQIUjd.js +1810 -0
- package/dist/docker-runner-CnXQIUjd.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/internal.d.ts +141 -2
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +5 -3
- package/dist/{local-studio-Ur6enHho.js → local-studio-DNWiPLme.js} +310 -2273
- package/dist/local-studio-DNWiPLme.js.map +1 -0
- package/dist/{local-studio-DgWD3_iK.d.ts → local-studio-DYKegdhd.d.ts} +18 -2
- package/dist/local-studio-DYKegdhd.d.ts.map +1 -0
- package/dist/rolldown-runtime-w6R9maHv.js +18 -0
- package/dist/workload-aws-wiring-BIxf0jtl.js +916 -0
- package/dist/workload-aws-wiring-BIxf0jtl.js.map +1 -0
- package/package.json +1 -1
- package/dist/docker-cmd-CoZdbeos.js.map +0 -1
- package/dist/local-studio-DgWD3_iK.d.ts.map +0 -1
- package/dist/local-studio-Ur6enHho.js.map +0 -1
|
@@ -0,0 +1,1810 @@
|
|
|
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-Co5XVwco.js";
|
|
3
|
+
import { D as readEnvCallerCredentials, M as workloadStsClientConfig, N as buildProxyClientConfig, O as recordAssumedRole, S as SECOND_ASSUME_MESSAGE, T as assumedRoleRecorded, _ as resolveHostDefaultCredentials, l as effectiveLoopbackPorts, o as applyWorkloadAwsWiringToEnv, u as getActiveWorkloadAwsWiring } from "./workload-aws-wiring-BIxf0jtl.js";
|
|
4
|
+
import { a as namesTheSameDirectory, d as flattenToOneLine, i as isPasteableCommandArg, l as describeAwsFailureForWarn, n as assemblyPathEscape, o as renderAssemblyPathEscape, r as displayUntrustedValue, s as resolveAssemblyPath, t as absoluteAssemblyPathEscape } from "./assembly-path-R96uhMm1.js";
|
|
5
|
+
import { a as isDockerClientEnvKey, c as runDockerStreaming, i as getDockerCmd, l as spawnStreaming, n as finchSecretArgvRefusal, o as isMalformedEnvKey, r as formatDockerLoginError, s as runDockerForeground, u as warnFinchArgvExposure } from "./docker-cmd-Cjg3jGYF.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, 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-arn.ts
|
|
57
|
+
const IAM_ROLE_ARN_MAX_LENGTH = 2048;
|
|
58
|
+
const IAM_ROLE_ARN_PATTERN = /^arn:[A-Za-z0-9-]+:iam::[0-9]+:role\/[!-~]+$/;
|
|
59
|
+
function isIamRoleArn(value) {
|
|
60
|
+
if (typeof value !== "string") return false;
|
|
61
|
+
if (value.length > 2048) return false;
|
|
62
|
+
return IAM_ROLE_ARN_PATTERN.test(value);
|
|
63
|
+
}
|
|
64
|
+
const REJECTED_ARN_PREVIEW_MAX = 128;
|
|
65
|
+
function describeRejectedRoleArn(value) {
|
|
66
|
+
if (typeof value !== "string") return `a non-string ${typeof value} value`;
|
|
67
|
+
const points = [...flattenToOneLine(value)];
|
|
68
|
+
return `"${points.length <= REJECTED_ARN_PREVIEW_MAX ? points.join("") : `${points.slice(0, REJECTED_ARN_PREVIEW_MAX).join("")}...`}" (${points.length} characters)`;
|
|
69
|
+
}
|
|
70
|
+
function refusedRoleArnMessage(value) {
|
|
71
|
+
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.`;
|
|
72
|
+
}
|
|
73
|
+
var AssumeRoleFailure = class AssumeRoleFailure extends Error {
|
|
74
|
+
detail;
|
|
75
|
+
constructor(message, detail) {
|
|
76
|
+
super(message);
|
|
77
|
+
this.name = "AssumeRoleFailure";
|
|
78
|
+
this.detail = detail;
|
|
79
|
+
Object.setPrototypeOf(this, AssumeRoleFailure.prototype);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
async function assumeRoleCredentials(opts) {
|
|
83
|
+
if (!isIamRoleArn(opts.roleArn)) {
|
|
84
|
+
const detail = `the role ARN is not well-formed: ${describeRejectedRoleArn(opts.roleArn)}`;
|
|
85
|
+
const message = refusedRoleArnMessage(opts.roleArn);
|
|
86
|
+
throw opts.makeError ? opts.makeError(message) : new AssumeRoleFailure(message, detail);
|
|
87
|
+
}
|
|
88
|
+
const sts = new STSClient(workloadStsClientConfig({
|
|
89
|
+
region: opts.region,
|
|
90
|
+
profile: opts.profile
|
|
91
|
+
}));
|
|
92
|
+
const shownArn = flattenToOneLine(opts.roleArn);
|
|
93
|
+
let response;
|
|
94
|
+
try {
|
|
95
|
+
response = await sts.send(new AssumeRoleCommand({
|
|
96
|
+
RoleArn: opts.roleArn,
|
|
97
|
+
RoleSessionName: `${getEmbedConfig().resourceNamePrefix}-${opts.sessionNameSuffix}-${Date.now()}`,
|
|
98
|
+
DurationSeconds: 3600
|
|
99
|
+
}));
|
|
100
|
+
} catch (err) {
|
|
101
|
+
const detail = describeAwsFailureForWarn(err, "STS AssumeRole");
|
|
102
|
+
const message = `AssumeRole(${shownArn}) failed: ${detail}`;
|
|
103
|
+
throw opts.makeError ? opts.makeError(message) : new AssumeRoleFailure(message, detail);
|
|
104
|
+
} finally {
|
|
105
|
+
sts.destroy();
|
|
106
|
+
}
|
|
107
|
+
const creds = response.Credentials;
|
|
108
|
+
if (!creds?.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken) {
|
|
109
|
+
const message = `AssumeRole(${shownArn}) returned no usable credentials.`;
|
|
110
|
+
throw opts.makeError ? opts.makeError(message) : new AssumeRoleFailure(message, "the response carried no usable credentials");
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
accessKeyId: creds.AccessKeyId,
|
|
114
|
+
secretAccessKey: creds.SecretAccessKey,
|
|
115
|
+
sessionToken: creds.SessionToken
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
async function applyRoleArnIfSet(opts) {
|
|
119
|
+
const roleArn = opts.roleArn || process.env[`${getEmbedConfig().envPrefix}_ROLE_ARN`];
|
|
120
|
+
if (!roleArn) return;
|
|
121
|
+
if (!isIamRoleArn(roleArn)) throw new Error(refusedRoleArnMessage(roleArn));
|
|
122
|
+
if (assumedRoleRecorded() && getEmbedConfig().workloadIdentity === "caller") throw new Error(SECOND_ASSUME_MESSAGE);
|
|
123
|
+
const logger = getLogger().child("role-arn");
|
|
124
|
+
const shownArn = flattenToOneLine(roleArn);
|
|
125
|
+
logger.debug(`Assuming role ${shownArn}...`);
|
|
126
|
+
const sts = new STSClient(buildStsClientConfig({
|
|
127
|
+
region: opts.region,
|
|
128
|
+
profile: opts.profile
|
|
129
|
+
}));
|
|
130
|
+
let response;
|
|
131
|
+
try {
|
|
132
|
+
response = await sts.send(new AssumeRoleCommand({
|
|
133
|
+
RoleArn: roleArn,
|
|
134
|
+
RoleSessionName: `${getEmbedConfig().binaryName}-${Date.now()}`,
|
|
135
|
+
DurationSeconds: 3600
|
|
136
|
+
}));
|
|
137
|
+
} catch (err) {
|
|
138
|
+
throw new Error(`AssumeRole(${shownArn}) failed: ${describeAwsFailureForWarn(err, "STS AssumeRole")}`);
|
|
139
|
+
} finally {
|
|
140
|
+
sts.destroy();
|
|
141
|
+
}
|
|
142
|
+
if (!response.Credentials) throw new Error(`AssumeRole returned no credentials for role ${shownArn}`);
|
|
143
|
+
const { AccessKeyId, SecretAccessKey, SessionToken, Expiration } = response.Credentials;
|
|
144
|
+
if (!AccessKeyId || !SecretAccessKey || !SessionToken) throw new Error(`AssumeRole response missing credentials fields for role ${shownArn}`);
|
|
145
|
+
if (!assumedRoleRecorded()) recordAssumedRole(readEnvCallerCredentials(), {
|
|
146
|
+
accessKeyId: AccessKeyId,
|
|
147
|
+
secretAccessKey: SecretAccessKey,
|
|
148
|
+
sessionToken: SessionToken
|
|
149
|
+
});
|
|
150
|
+
process.env["AWS_ACCESS_KEY_ID"] = AccessKeyId;
|
|
151
|
+
process.env["AWS_SECRET_ACCESS_KEY"] = SecretAccessKey;
|
|
152
|
+
process.env["AWS_SESSION_TOKEN"] = SessionToken;
|
|
153
|
+
logger.info(`Assumed role ${shownArn} (session expires ${Expiration?.toISOString() ?? "unknown"})`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
//#endregion
|
|
157
|
+
//#region src/utils/own-keys.ts
|
|
158
|
+
function defineOwnKey(target, key, value) {
|
|
159
|
+
Object.defineProperty(target, key, {
|
|
160
|
+
value,
|
|
161
|
+
writable: true,
|
|
162
|
+
enumerable: true,
|
|
163
|
+
configurable: true
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
//#endregion
|
|
168
|
+
//#region src/utils/error-handler.ts
|
|
169
|
+
var CdkLocalError = class CdkLocalError extends Error {
|
|
170
|
+
code;
|
|
171
|
+
cause;
|
|
172
|
+
constructor(message, code, cause) {
|
|
173
|
+
super(message);
|
|
174
|
+
this.code = code;
|
|
175
|
+
this.cause = cause;
|
|
176
|
+
this.name = "CdkLocalError";
|
|
177
|
+
Object.setPrototypeOf(this, CdkLocalError.prototype);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
var LocalInvokeBuildError = class LocalInvokeBuildError extends CdkLocalError {
|
|
181
|
+
constructor(message, cause) {
|
|
182
|
+
super(message, "LOCAL_INVOKE_BUILD_ERROR", cause);
|
|
183
|
+
this.name = "LocalInvokeBuildError";
|
|
184
|
+
Object.setPrototypeOf(this, LocalInvokeBuildError.prototype);
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
var RouteDiscoveryError = class RouteDiscoveryError extends CdkLocalError {
|
|
188
|
+
constructor(message, cause) {
|
|
189
|
+
super(message, "ROUTE_DISCOVERY_ERROR", cause);
|
|
190
|
+
this.name = "RouteDiscoveryError";
|
|
191
|
+
Object.setPrototypeOf(this, RouteDiscoveryError.prototype);
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
var LocalStartServiceError = class LocalStartServiceError extends CdkLocalError {
|
|
195
|
+
constructor(message, cause) {
|
|
196
|
+
super(message, "LOCAL_START_SERVICE_ERROR", cause);
|
|
197
|
+
this.name = "LocalStartServiceError";
|
|
198
|
+
Object.setPrototypeOf(this, LocalStartServiceError.prototype);
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
var TargetSelectionCancelledError = class TargetSelectionCancelledError extends CdkLocalError {
|
|
202
|
+
silent = true;
|
|
203
|
+
exitCode = 130;
|
|
204
|
+
constructor() {
|
|
205
|
+
super("Target selection cancelled.", "TARGET_SELECTION_CANCELLED");
|
|
206
|
+
this.name = "TargetSelectionCancelledError";
|
|
207
|
+
Object.setPrototypeOf(this, TargetSelectionCancelledError.prototype);
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
var InteractiveTtyRequiredError = class InteractiveTtyRequiredError extends CdkLocalError {
|
|
211
|
+
constructor(message) {
|
|
212
|
+
super(message, "INTERACTIVE_TTY_REQUIRED");
|
|
213
|
+
this.name = "InteractiveTtyRequiredError";
|
|
214
|
+
Object.setPrototypeOf(this, InteractiveTtyRequiredError.prototype);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
function isCdkLocalError(error) {
|
|
218
|
+
return error instanceof CdkLocalError;
|
|
219
|
+
}
|
|
220
|
+
function formatError(error) {
|
|
221
|
+
if (isCdkLocalError(error)) {
|
|
222
|
+
let message = `${error.name}: ${error.message}`;
|
|
223
|
+
if (error.cause) message += `\nCaused by: ${error.cause.message}`;
|
|
224
|
+
return message;
|
|
225
|
+
}
|
|
226
|
+
if (error instanceof Error) return `${error.name}: ${error.message}`;
|
|
227
|
+
return String(error);
|
|
228
|
+
}
|
|
229
|
+
function handleError(error) {
|
|
230
|
+
const logger = getLogger();
|
|
231
|
+
if (!(error instanceof CdkLocalError && error.silent)) logger.error(formatError(error));
|
|
232
|
+
if (error instanceof Error && error.stack) logger.debug("Stack trace:", error.stack);
|
|
233
|
+
const customExitCode = error instanceof CdkLocalError ? error.exitCode : void 0;
|
|
234
|
+
const exitCode = typeof customExitCode === "number" ? customExitCode : 1;
|
|
235
|
+
process.exit(exitCode);
|
|
236
|
+
}
|
|
237
|
+
function withErrorHandling(fn) {
|
|
238
|
+
return async (...args) => {
|
|
239
|
+
try {
|
|
240
|
+
await fn(...args);
|
|
241
|
+
} catch (error) {
|
|
242
|
+
handleError(error);
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
//#endregion
|
|
248
|
+
//#region src/local/intrinsic-image.ts
|
|
249
|
+
function formatStateRemedy(context) {
|
|
250
|
+
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`;
|
|
251
|
+
return "pass --from-cfn-stack to load the deployed stack state";
|
|
252
|
+
}
|
|
253
|
+
const PARTITION_TABLE = [
|
|
254
|
+
{
|
|
255
|
+
regionPrefix: "cn-",
|
|
256
|
+
partition: "aws-cn",
|
|
257
|
+
urlSuffix: "amazonaws.com.cn"
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
regionPrefix: "us-gov-",
|
|
261
|
+
partition: "aws-us-gov",
|
|
262
|
+
urlSuffix: "amazonaws.com"
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
regionPrefix: "us-isob-",
|
|
266
|
+
partition: "aws-iso-b",
|
|
267
|
+
urlSuffix: "sc2s.sgov.gov"
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
regionPrefix: "us-isof-",
|
|
271
|
+
partition: "aws-iso-f",
|
|
272
|
+
urlSuffix: "csp.hci.ic.gov"
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
regionPrefix: "us-iso-",
|
|
276
|
+
partition: "aws-iso",
|
|
277
|
+
urlSuffix: "c2s.ic.gov"
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
regionPrefix: "eu-isoe-",
|
|
281
|
+
partition: "aws-iso-e",
|
|
282
|
+
urlSuffix: "cloud.adc-e.uk"
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
regionPrefix: "eusc-",
|
|
286
|
+
partition: "aws-eusc",
|
|
287
|
+
urlSuffix: "amazonaws.eu"
|
|
288
|
+
}
|
|
289
|
+
];
|
|
290
|
+
function derivePartitionAndUrlSuffix(region) {
|
|
291
|
+
const normalized = region.toLowerCase();
|
|
292
|
+
for (const row of PARTITION_TABLE) if (normalized.startsWith(row.regionPrefix)) return {
|
|
293
|
+
partition: row.partition,
|
|
294
|
+
urlSuffix: row.urlSuffix
|
|
295
|
+
};
|
|
296
|
+
return {
|
|
297
|
+
partition: "aws",
|
|
298
|
+
urlSuffix: "amazonaws.com"
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
function derivePseudoParametersFromRegion(region, accountId) {
|
|
302
|
+
if (!region || typeof region !== "string" || region.length === 0) return void 0;
|
|
303
|
+
const { partition, urlSuffix } = derivePartitionAndUrlSuffix(region);
|
|
304
|
+
return {
|
|
305
|
+
...accountId !== void 0 && { accountId },
|
|
306
|
+
region,
|
|
307
|
+
partition,
|
|
308
|
+
urlSuffix
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
function tryResolveImageFnJoin(raw, resources, context) {
|
|
312
|
+
if (!raw || typeof raw !== "object") return { kind: "not-applicable" };
|
|
313
|
+
const arg = raw["Fn::Join"];
|
|
314
|
+
if (arg === void 0) return { kind: "not-applicable" };
|
|
315
|
+
if (!Array.isArray(arg) || arg.length !== 2 || !Array.isArray(arg[1])) return {
|
|
316
|
+
kind: "unsupported-join",
|
|
317
|
+
reason: "Fn::Join must be [delimiter, [elements]]"
|
|
318
|
+
};
|
|
319
|
+
const [delimiter, elements] = arg;
|
|
320
|
+
if (typeof delimiter !== "string") return {
|
|
321
|
+
kind: "unsupported-join",
|
|
322
|
+
reason: `Fn::Join delimiter must be a string, got ${typeof delimiter}`
|
|
323
|
+
};
|
|
324
|
+
const repoLogicalId = findEcrRepositoryRefInTree(elements, resources);
|
|
325
|
+
const stateResources = context?.stateResources;
|
|
326
|
+
if (repoLogicalId && !stateResources) return {
|
|
327
|
+
kind: "needs-state",
|
|
328
|
+
repoLogicalId
|
|
329
|
+
};
|
|
330
|
+
const parts = [];
|
|
331
|
+
for (const element of elements) {
|
|
332
|
+
const r = resolveImageIntrinsic(element, resources, context);
|
|
333
|
+
if (r === void 0) {
|
|
334
|
+
if (!repoLogicalId) return { kind: "not-applicable" };
|
|
335
|
+
return {
|
|
336
|
+
kind: "unsupported-join",
|
|
337
|
+
reason: "one or more Fn::Join elements could not be resolved"
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
parts.push(r);
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
kind: "resolved",
|
|
344
|
+
uri: parts.join(delimiter)
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
function findEcrRepositoryRefInTree(node, resources) {
|
|
348
|
+
if (node === null || node === void 0) return void 0;
|
|
349
|
+
if (typeof node === "string" || typeof node === "number" || typeof node === "boolean") return;
|
|
350
|
+
if (Array.isArray(node)) {
|
|
351
|
+
for (const item of node) {
|
|
352
|
+
const hit = findEcrRepositoryRefInTree(item, resources);
|
|
353
|
+
if (hit) return hit;
|
|
354
|
+
}
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
if (typeof node !== "object") return void 0;
|
|
358
|
+
const obj = node;
|
|
359
|
+
if (typeof obj["Ref"] === "string") {
|
|
360
|
+
const target = obj["Ref"];
|
|
361
|
+
if (resources[target]?.Type === "AWS::ECR::Repository") return target;
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
const getAtt = obj["Fn::GetAtt"];
|
|
365
|
+
if (getAtt !== void 0) {
|
|
366
|
+
let lid;
|
|
367
|
+
if (Array.isArray(getAtt) && typeof getAtt[0] === "string") lid = getAtt[0];
|
|
368
|
+
else if (typeof getAtt === "string") lid = getAtt.split(".")[0];
|
|
369
|
+
if (lid && resources[lid]?.Type === "AWS::ECR::Repository") return lid;
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
for (const value of Object.values(obj)) {
|
|
373
|
+
const hit = findEcrRepositoryRefInTree(value, resources);
|
|
374
|
+
if (hit) return hit;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
function resolveImageIntrinsic(node, resources, context) {
|
|
378
|
+
const v = resolveImageIntrinsicAny(node, resources, context);
|
|
379
|
+
if (typeof v === "string") return v;
|
|
380
|
+
if (typeof v === "number" || typeof v === "boolean") return String(v);
|
|
381
|
+
}
|
|
382
|
+
function resolveImageIntrinsicAny(node, resources, context) {
|
|
383
|
+
if (node === null || node === void 0) return void 0;
|
|
384
|
+
if (typeof node === "string" || typeof node === "number" || typeof node === "boolean") return node;
|
|
385
|
+
if (Array.isArray(node)) return;
|
|
386
|
+
if (typeof node !== "object") return void 0;
|
|
387
|
+
const obj = node;
|
|
388
|
+
const keys = Object.keys(obj);
|
|
389
|
+
if (keys.length !== 1) return void 0;
|
|
390
|
+
const intrinsic = keys[0];
|
|
391
|
+
const arg = obj[intrinsic];
|
|
392
|
+
if (intrinsic === "Ref") {
|
|
393
|
+
if (typeof arg !== "string") return void 0;
|
|
394
|
+
if (arg.startsWith("AWS::")) {
|
|
395
|
+
const p = context?.pseudoParameters;
|
|
396
|
+
if (!p) return void 0;
|
|
397
|
+
if (arg === "AWS::URLSuffix") return p.urlSuffix;
|
|
398
|
+
if (arg === "AWS::Partition") return p.partition;
|
|
399
|
+
if (arg === "AWS::Region") return p.region;
|
|
400
|
+
if (arg === "AWS::AccountId") return p.accountId;
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
if (resources[arg]?.Type !== "AWS::ECR::Repository") return void 0;
|
|
404
|
+
const stateEntry = context?.stateResources?.[arg];
|
|
405
|
+
if (!stateEntry) return void 0;
|
|
406
|
+
return stateEntry.physicalId;
|
|
407
|
+
}
|
|
408
|
+
if (intrinsic === "Fn::GetAtt") {
|
|
409
|
+
let logicalId;
|
|
410
|
+
let attr;
|
|
411
|
+
if (Array.isArray(arg) && arg.length === 2 && typeof arg[0] === "string" && typeof arg[1] === "string") {
|
|
412
|
+
logicalId = arg[0];
|
|
413
|
+
attr = arg[1];
|
|
414
|
+
} else if (typeof arg === "string") {
|
|
415
|
+
const dot = arg.indexOf(".");
|
|
416
|
+
if (dot > 0 && dot < arg.length - 1) {
|
|
417
|
+
logicalId = arg.slice(0, dot);
|
|
418
|
+
attr = arg.slice(dot + 1);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (!logicalId || !attr) return void 0;
|
|
422
|
+
if (resources[logicalId]?.Type !== "AWS::ECR::Repository") return void 0;
|
|
423
|
+
const cached = context?.stateResources?.[logicalId]?.attributes?.[attr];
|
|
424
|
+
if (typeof cached === "string" && cached.length > 0) return cached;
|
|
425
|
+
const physicalId = context?.stateResources?.[logicalId]?.physicalId;
|
|
426
|
+
const p = context?.pseudoParameters;
|
|
427
|
+
if (physicalId && p?.region && p.accountId) {
|
|
428
|
+
if (attr === "Arn" && p.partition) return `arn:${p.partition}:ecr:${p.region}:${p.accountId}:repository/${physicalId}`;
|
|
429
|
+
if (attr === "RepositoryUri" && p.urlSuffix) return `${p.accountId}.dkr.ecr.${p.region}.${p.urlSuffix}/${physicalId}`;
|
|
430
|
+
}
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
if (intrinsic === "Fn::Split") {
|
|
434
|
+
if (!Array.isArray(arg) || arg.length !== 2) return void 0;
|
|
435
|
+
const argArr = arg;
|
|
436
|
+
const delim = argArr[0];
|
|
437
|
+
if (typeof delim !== "string") return void 0;
|
|
438
|
+
const src = resolveImageIntrinsicAny(argArr[1], resources, context);
|
|
439
|
+
if (typeof src !== "string") return void 0;
|
|
440
|
+
return src.split(delim);
|
|
441
|
+
}
|
|
442
|
+
if (intrinsic === "Fn::Select") {
|
|
443
|
+
if (!Array.isArray(arg) || arg.length !== 2) return void 0;
|
|
444
|
+
const argArr = arg;
|
|
445
|
+
const rawIndex = argArr[0];
|
|
446
|
+
let index;
|
|
447
|
+
if (typeof rawIndex === "number") index = rawIndex;
|
|
448
|
+
else if (typeof rawIndex === "string" && /^-?\d+$/.test(rawIndex)) index = Number.parseInt(rawIndex, 10);
|
|
449
|
+
if (index === void 0 || !Number.isFinite(index)) return void 0;
|
|
450
|
+
const list = resolveImageIntrinsicAny(argArr[1], resources, context);
|
|
451
|
+
if (Array.isArray(list)) {
|
|
452
|
+
if (index < 0 || index >= list.length) return void 0;
|
|
453
|
+
const picked = list[index];
|
|
454
|
+
if (typeof picked === "string") return picked;
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
if (Array.isArray(argArr[1])) {
|
|
458
|
+
const listLiteral = argArr[1];
|
|
459
|
+
if (index < 0 || index >= listLiteral.length) return void 0;
|
|
460
|
+
return resolveImageIntrinsic(listLiteral[index], resources, context);
|
|
461
|
+
}
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
if (intrinsic === "Fn::Join") {
|
|
465
|
+
if (!Array.isArray(arg) || arg.length !== 2) return void 0;
|
|
466
|
+
const [delim, parts] = arg;
|
|
467
|
+
if (typeof delim !== "string" || !Array.isArray(parts)) return void 0;
|
|
468
|
+
const resolved = [];
|
|
469
|
+
for (const part of parts) {
|
|
470
|
+
const r = resolveImageIntrinsic(part, resources, context);
|
|
471
|
+
if (r === void 0) return void 0;
|
|
472
|
+
resolved.push(r);
|
|
473
|
+
}
|
|
474
|
+
return resolved.join(delim);
|
|
475
|
+
}
|
|
476
|
+
if (intrinsic === "Fn::Sub") {
|
|
477
|
+
let template;
|
|
478
|
+
if (typeof arg === "string") template = arg;
|
|
479
|
+
else if (Array.isArray(arg) && typeof arg[0] === "string") template = arg[0];
|
|
480
|
+
if (template === void 0) return void 0;
|
|
481
|
+
const out = substituteImagePlaceholders(template, resources, context);
|
|
482
|
+
if (out.includes("${")) return void 0;
|
|
483
|
+
return out;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
function substituteImagePlaceholders(flat, resources, context) {
|
|
487
|
+
if (!flat.includes("${")) return flat;
|
|
488
|
+
return flat.replace(/\$\{([^}]+)\}/g, (full, key) => {
|
|
489
|
+
if (context?.pseudoParameters) {
|
|
490
|
+
if (key === "AWS::AccountId" && context.pseudoParameters.accountId) return context.pseudoParameters.accountId;
|
|
491
|
+
if (key === "AWS::Region" && context.pseudoParameters.region) return context.pseudoParameters.region;
|
|
492
|
+
if (key === "AWS::Partition" && context.pseudoParameters.partition) return context.pseudoParameters.partition;
|
|
493
|
+
if (key === "AWS::URLSuffix" && context.pseudoParameters.urlSuffix) return context.pseudoParameters.urlSuffix;
|
|
494
|
+
}
|
|
495
|
+
if (context?.stateResources) {
|
|
496
|
+
const dot = key.indexOf(".");
|
|
497
|
+
const logicalId = dot === -1 ? key : key.slice(0, dot);
|
|
498
|
+
const refResource = resources[logicalId];
|
|
499
|
+
const stateEntry = context.stateResources[logicalId];
|
|
500
|
+
if (refResource?.Type === "AWS::ECR::Repository" && stateEntry) {
|
|
501
|
+
if (dot === -1) return stateEntry.physicalId;
|
|
502
|
+
const attr = key.slice(dot + 1);
|
|
503
|
+
const cached = stateEntry.attributes?.[attr];
|
|
504
|
+
if (typeof cached === "string") return cached;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return full;
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
//#endregion
|
|
512
|
+
//#region src/local/ecr-uri.ts
|
|
513
|
+
const FIPS_AND_DUAL_STACK_PARTITIONS = ["aws", "aws-us-gov"];
|
|
514
|
+
const ECR_REGISTRY_HOST_FORMS = [
|
|
515
|
+
{ labels: "dkr.ecr" },
|
|
516
|
+
{
|
|
517
|
+
labels: "dkr.ecr-fips",
|
|
518
|
+
partitions: FIPS_AND_DUAL_STACK_PARTITIONS
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
labels: "dkr-ecr",
|
|
522
|
+
fixedUrlSuffix: "on.aws",
|
|
523
|
+
partitions: FIPS_AND_DUAL_STACK_PARTITIONS
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
labels: "dkr-ecr-fips",
|
|
527
|
+
fixedUrlSuffix: "on.aws",
|
|
528
|
+
partitions: FIPS_AND_DUAL_STACK_PARTITIONS
|
|
529
|
+
}
|
|
530
|
+
];
|
|
531
|
+
const ECR_HOST_FORM_BY_LABELS = new Map(ECR_REGISTRY_HOST_FORMS.map((f) => [f.labels, f]));
|
|
532
|
+
function escapeRegExp(value) {
|
|
533
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
534
|
+
}
|
|
535
|
+
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");
|
|
536
|
+
const CANONICAL_REGION_SEGMENT = /^[A-Za-z0-9][A-Za-z0-9-]*$/;
|
|
537
|
+
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]+)$/;
|
|
538
|
+
const CANONICAL_URL_SUFFIX = /^[A-Za-z0-9][A-Za-z0-9.-]*$/;
|
|
539
|
+
function foldAsciiUpperCase(value) {
|
|
540
|
+
return value.replace(/[A-Z]+/g, (run) => run.toLowerCase());
|
|
541
|
+
}
|
|
542
|
+
function isRegistryHostComponent(component) {
|
|
543
|
+
return component.includes(".") || component.includes(":") || component === "localhost";
|
|
544
|
+
}
|
|
545
|
+
function canonicalizeImageUriHost(imageUri) {
|
|
546
|
+
const slash = imageUri.indexOf("/");
|
|
547
|
+
if (slash < 0) return imageUri;
|
|
548
|
+
const host = imageUri.slice(0, slash);
|
|
549
|
+
if (!isRegistryHostComponent(host)) return imageUri;
|
|
550
|
+
return foldAsciiUpperCase(host) + imageUri.slice(slash);
|
|
551
|
+
}
|
|
552
|
+
function parseEcrRegistryHost(imageUri) {
|
|
553
|
+
const m = ECR_URI_HOST_REGEX.exec(imageUri);
|
|
554
|
+
if (!m) return void 0;
|
|
555
|
+
const [, accountId, rawLabels, rawRegion, rawSuffix] = m;
|
|
556
|
+
if (!CANONICAL_REGION_SEGMENT.test(rawRegion)) return void 0;
|
|
557
|
+
if (!CANONICAL_URL_SUFFIX.test(rawSuffix)) return void 0;
|
|
558
|
+
const form = ECR_HOST_FORM_BY_LABELS.get(foldAsciiUpperCase(rawLabels));
|
|
559
|
+
if (!form) return void 0;
|
|
560
|
+
const region = foldAsciiUpperCase(rawRegion);
|
|
561
|
+
if (!REGION_SHAPE.test(region)) return void 0;
|
|
562
|
+
const suffix = foldAsciiUpperCase(rawSuffix);
|
|
563
|
+
const partition = derivePartitionAndUrlSuffix(region);
|
|
564
|
+
if (form.partitions && !form.partitions.includes(partition.partition)) return void 0;
|
|
565
|
+
if (suffix !== (form.fixedUrlSuffix ?? partition.urlSuffix)) return void 0;
|
|
566
|
+
return {
|
|
567
|
+
accountId,
|
|
568
|
+
region,
|
|
569
|
+
registryHost: `${accountId}.${form.labels}.${region}.${suffix}`,
|
|
570
|
+
form
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
//#endregion
|
|
575
|
+
//#region src/utils/docker-argv-redaction.ts
|
|
576
|
+
const REDACTED_ARGV_VALUE = "***";
|
|
577
|
+
const ARGV_VALUE_BEARING_FLAGS = /* @__PURE__ */ new Set([
|
|
578
|
+
"-e",
|
|
579
|
+
"--env",
|
|
580
|
+
"--opt",
|
|
581
|
+
"--label",
|
|
582
|
+
"--build-arg"
|
|
583
|
+
]);
|
|
584
|
+
const ARGV_PARAM_LIST_FLAGS = /* @__PURE__ */ new Set(["--cache-from", "--cache-to"]);
|
|
585
|
+
const ARGV_PARAM_LIST_LOCATOR_PARAMS = /* @__PURE__ */ new Set([
|
|
586
|
+
"type",
|
|
587
|
+
"mode",
|
|
588
|
+
"compression",
|
|
589
|
+
"compression-level",
|
|
590
|
+
"force-compression",
|
|
591
|
+
"ignore-error",
|
|
592
|
+
"image-manifest",
|
|
593
|
+
"oci-mediatypes",
|
|
594
|
+
"timeout",
|
|
595
|
+
"ref",
|
|
596
|
+
"dest",
|
|
597
|
+
"src",
|
|
598
|
+
"digest",
|
|
599
|
+
"tag",
|
|
600
|
+
"region",
|
|
601
|
+
"bucket",
|
|
602
|
+
"name",
|
|
603
|
+
"prefix",
|
|
604
|
+
"manifests_prefix",
|
|
605
|
+
"blobs_prefix",
|
|
606
|
+
"use_path_style",
|
|
607
|
+
"upload_parallelism",
|
|
608
|
+
"touch_refresh",
|
|
609
|
+
"account_name",
|
|
610
|
+
"scope"
|
|
611
|
+
]);
|
|
612
|
+
const ARGV_PARAM_LIST_URL_PARAMS = /* @__PURE__ */ new Set([
|
|
613
|
+
"account_url",
|
|
614
|
+
"endpoint_url",
|
|
615
|
+
"url",
|
|
616
|
+
"url_v2"
|
|
617
|
+
]);
|
|
618
|
+
function redactUrlLocator(value) {
|
|
619
|
+
if (value === "") return value;
|
|
620
|
+
const authority = /^(?:[a-zA-Z][a-zA-Z0-9+.-]*:)?\/\//.exec(value);
|
|
621
|
+
if (authority === null) return REDACTED_ARGV_VALUE;
|
|
622
|
+
const prefix = authority[0];
|
|
623
|
+
let rest = value.slice(prefix.length);
|
|
624
|
+
const lastAt = rest.lastIndexOf("@");
|
|
625
|
+
if (lastAt >= 0) rest = `${REDACTED_ARGV_VALUE}${rest.slice(lastAt)}`;
|
|
626
|
+
else {
|
|
627
|
+
const host = rest.split(/[/?#]/)[0];
|
|
628
|
+
const ipv6 = /^\[[0-9A-Fa-f:.]+\]/.exec(host);
|
|
629
|
+
const bare = ipv6 ? host.slice(ipv6[0].length) : host;
|
|
630
|
+
if (/:(?!\d{1,5}$)/.test(bare)) return REDACTED_ARGV_VALUE;
|
|
631
|
+
}
|
|
632
|
+
for (const delimiter of ["?", "#"]) {
|
|
633
|
+
const at = rest.indexOf(delimiter);
|
|
634
|
+
if (at >= 0) rest = `${rest.slice(0, at)}${delimiter}${REDACTED_ARGV_VALUE}`;
|
|
635
|
+
}
|
|
636
|
+
return `${prefix}${rest}`;
|
|
637
|
+
}
|
|
638
|
+
function maskArgvFlagValue(flag, value) {
|
|
639
|
+
if (ARGV_VALUE_BEARING_FLAGS.has(flag)) {
|
|
640
|
+
const eqIdx = value.indexOf("=");
|
|
641
|
+
return eqIdx >= 0 ? `${value.substring(0, eqIdx)}=${REDACTED_ARGV_VALUE}` : void 0;
|
|
642
|
+
}
|
|
643
|
+
if (ARGV_PARAM_LIST_FLAGS.has(flag)) {
|
|
644
|
+
let masked = false;
|
|
645
|
+
const rawParts = value.split(",");
|
|
646
|
+
const parts = rawParts.map((part, index) => {
|
|
647
|
+
if (part === "") return part;
|
|
648
|
+
if (masked) return REDACTED_ARGV_VALUE;
|
|
649
|
+
const eqIdx = part.indexOf("=");
|
|
650
|
+
if (eqIdx < 0) {
|
|
651
|
+
if (index === 0) return part;
|
|
652
|
+
masked = true;
|
|
653
|
+
return REDACTED_ARGV_VALUE;
|
|
654
|
+
}
|
|
655
|
+
const name = part.substring(0, eqIdx);
|
|
656
|
+
const normalized = name.trim().toLowerCase();
|
|
657
|
+
if (ARGV_PARAM_LIST_URL_PARAMS.has(normalized)) {
|
|
658
|
+
if (rawParts.slice(index + 1).some((later) => later.includes("@"))) {
|
|
659
|
+
masked = true;
|
|
660
|
+
return `${name}=${REDACTED_ARGV_VALUE}`;
|
|
661
|
+
}
|
|
662
|
+
const rawUrl = part.substring(eqIdx + 1);
|
|
663
|
+
const safeUrl = redactUrlLocator(rawUrl);
|
|
664
|
+
if (safeUrl === rawUrl) return part;
|
|
665
|
+
masked = true;
|
|
666
|
+
return `${name}=${safeUrl}`;
|
|
667
|
+
}
|
|
668
|
+
if (ARGV_PARAM_LIST_LOCATOR_PARAMS.has(normalized)) return part;
|
|
669
|
+
masked = true;
|
|
670
|
+
return `${name}=${REDACTED_ARGV_VALUE}`;
|
|
671
|
+
});
|
|
672
|
+
return masked ? parts.join(",") : void 0;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
function maskAttachedShortFlag(arg) {
|
|
676
|
+
if (arg.startsWith("--") || !arg.startsWith("-")) return void 0;
|
|
677
|
+
let chosen;
|
|
678
|
+
for (const flag of ARGV_VALUE_BEARING_FLAGS) {
|
|
679
|
+
if (flag.length !== 2 || !flag.startsWith("-") || flag[1] === "-") continue;
|
|
680
|
+
const at = arg.indexOf(flag[1], 1);
|
|
681
|
+
if (at < 0 || at === arg.length - 1) continue;
|
|
682
|
+
if (chosen === void 0 || at < chosen.at) chosen = {
|
|
683
|
+
at,
|
|
684
|
+
flag
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
if (chosen === void 0) return void 0;
|
|
688
|
+
const masked = maskArgvFlagValue(chosen.flag, arg.slice(chosen.at + 1));
|
|
689
|
+
return masked === void 0 ? void 0 : `${arg.slice(0, chosen.at + 1)}${masked}`;
|
|
690
|
+
}
|
|
691
|
+
function trailingShortFlagOfCluster(arg) {
|
|
692
|
+
if (arg.startsWith("--") || !arg.startsWith("-")) return void 0;
|
|
693
|
+
const last = arg[arg.length - 1];
|
|
694
|
+
for (const flag of ARGV_VALUE_BEARING_FLAGS) if (flag.length === 2 && flag.startsWith("-") && flag[1] !== "-" && flag[1] === last) return flag;
|
|
695
|
+
}
|
|
696
|
+
function redactDockerArgvValues(args) {
|
|
697
|
+
const out = [];
|
|
698
|
+
for (let i = 0; i < args.length; i++) {
|
|
699
|
+
const cur = args[i];
|
|
700
|
+
const joinedEq = cur.indexOf("=");
|
|
701
|
+
if (joinedEq > 0) {
|
|
702
|
+
const maskedJoined = maskArgvFlagValue(cur.substring(0, joinedEq), cur.substring(joinedEq + 1));
|
|
703
|
+
if (maskedJoined !== void 0) {
|
|
704
|
+
out.push(`${cur.substring(0, joinedEq)}=${maskedJoined}`);
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
const maskedAttached = maskAttachedShortFlag(cur);
|
|
709
|
+
if (maskedAttached !== void 0) {
|
|
710
|
+
out.push(maskedAttached);
|
|
711
|
+
continue;
|
|
712
|
+
}
|
|
713
|
+
const next = args[i + 1];
|
|
714
|
+
if (typeof next === "string") {
|
|
715
|
+
const maskedNext = maskArgvFlagValue(ARGV_VALUE_BEARING_FLAGS.has(cur) ? cur : trailingShortFlagOfCluster(cur) ?? cur, next);
|
|
716
|
+
if (maskedNext !== void 0) {
|
|
717
|
+
out.push(cur, maskedNext);
|
|
718
|
+
i++;
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
out.push(cur);
|
|
723
|
+
}
|
|
724
|
+
return out;
|
|
725
|
+
}
|
|
726
|
+
function describeExitWithoutArgv(err) {
|
|
727
|
+
const e = err;
|
|
728
|
+
if (typeof e?.signal === "string" && e.signal !== "") return `killed by ${e.signal}`;
|
|
729
|
+
if (typeof e?.code === "number") return `exited with code ${e.code}`;
|
|
730
|
+
if (typeof e?.code === "string" && e.code !== "") return `failed (${e.code})`;
|
|
731
|
+
return "failed with no output";
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
//#endregion
|
|
735
|
+
//#region src/assets/asset-source-path.ts
|
|
736
|
+
function resolveAssetSourcePath(opts) {
|
|
737
|
+
const { manifestDir, value, assetOutdir, field, subject, action, wrapError } = opts;
|
|
738
|
+
const shownValue = displayUntrustedValue(value);
|
|
739
|
+
if (isAbsolute(value) && opts.absolute !== "fold") {
|
|
740
|
+
const absolute = resolve(value);
|
|
741
|
+
const escape = absoluteAssemblyPathEscape(assetOutdir, absolute);
|
|
742
|
+
if (escape !== void 0 && opts.absolute === "honour") throw wrapError(`${subject} has an absolute ${field}=${shownValue} which ` + renderAssemblyPathEscape(escape, assetOutdir, action));
|
|
743
|
+
if (escape !== void 0) {
|
|
744
|
+
const refusal = originScopeRefusal(absolute, assetOutdir);
|
|
745
|
+
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}.`);
|
|
746
|
+
warnAbsoluteOutsideAssembly(opts, absolute, escape);
|
|
747
|
+
return absolute;
|
|
748
|
+
}
|
|
749
|
+
if (namesTheSameDirectory(assetOutdir, absolute)) warnWholeAssemblyAsSource(opts, absolute);
|
|
750
|
+
return absolute;
|
|
751
|
+
}
|
|
752
|
+
const resolved = resolveAssemblyPath(manifestDir, value, { containWithin: assetOutdir });
|
|
753
|
+
if (resolved.contained) return resolved.path;
|
|
754
|
+
if (namesTheSameDirectory(assetOutdir, resolved.path)) {
|
|
755
|
+
warnWholeAssemblyAsSource(opts, resolved.path);
|
|
756
|
+
return resolved.path;
|
|
757
|
+
}
|
|
758
|
+
throw wrapError(`${subject} has ${field}=${shownValue} which ` + renderAssemblyPathEscape(resolved, assetOutdir, action));
|
|
759
|
+
}
|
|
760
|
+
function broadRootReason(root, assetOutdir) {
|
|
761
|
+
if (root === resolve("/")) return "names the filesystem root";
|
|
762
|
+
if (containsOrIs(root, realPath(homedir()))) return "names your home directory or a directory containing it";
|
|
763
|
+
if (containsOrIs(root, realPath(assetOutdir))) return "names a directory containing the app's output directory";
|
|
764
|
+
}
|
|
765
|
+
function originScopeRefusal(absolute, assetOutdir) {
|
|
766
|
+
const root = realPath(absolute);
|
|
767
|
+
const broad = broadRootReason(root, assetOutdir);
|
|
768
|
+
if (broad !== void 0) return broad;
|
|
769
|
+
const projects = projectRoots(assetOutdir);
|
|
770
|
+
let hidden;
|
|
771
|
+
let isProjectRoot = false;
|
|
772
|
+
for (const project of projects) {
|
|
773
|
+
const rel = relative(project, root);
|
|
774
|
+
if (rel === "") isProjectRoot = true;
|
|
775
|
+
if (rel === "" || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) continue;
|
|
776
|
+
const dotted = rel.split(sep).find((c) => CREDENTIAL_DIR_NAMES.has(c.toLowerCase()));
|
|
777
|
+
if (dotted === void 0) return void 0;
|
|
778
|
+
hidden = dotted;
|
|
779
|
+
}
|
|
780
|
+
if (isProjectRoot) return "is your project root itself; name a folder inside it";
|
|
781
|
+
if (hidden !== void 0) return `passes through the credential / version-control directory ${displayUntrustedValue(hidden)} inside your project`;
|
|
782
|
+
const shown = projects.map((p) => displayUntrustedValue(p)).join(" or ");
|
|
783
|
+
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})`;
|
|
784
|
+
}
|
|
785
|
+
const CREDENTIAL_DIR_NAMES = /* @__PURE__ */ new Set([
|
|
786
|
+
".git",
|
|
787
|
+
".hg",
|
|
788
|
+
".svn",
|
|
789
|
+
".ssh",
|
|
790
|
+
".aws",
|
|
791
|
+
".gnupg",
|
|
792
|
+
".docker",
|
|
793
|
+
".kube",
|
|
794
|
+
".config",
|
|
795
|
+
".azure",
|
|
796
|
+
".gcloud",
|
|
797
|
+
".terraform",
|
|
798
|
+
".npm",
|
|
799
|
+
".pnpm-store",
|
|
800
|
+
".yarn",
|
|
801
|
+
".cache",
|
|
802
|
+
".local"
|
|
803
|
+
]);
|
|
804
|
+
function projectRoots(assetOutdir) {
|
|
805
|
+
const roots = [];
|
|
806
|
+
const usable = (p) => p !== resolve("/") && !containsOrIs(p, realPath(homedir()));
|
|
807
|
+
const cwd = realPath(process.cwd());
|
|
808
|
+
const outdir = realPath(assetOutdir);
|
|
809
|
+
if (usable(cwd) && (containsOrIs(cwd, outdir) || existsSync(join(cwd, ".git")))) roots.push(cwd);
|
|
810
|
+
let dir = outdir;
|
|
811
|
+
for (;;) {
|
|
812
|
+
if (existsSync(join(dir, ".git"))) {
|
|
813
|
+
if (usable(dir) && !roots.includes(dir)) roots.push(dir);
|
|
814
|
+
break;
|
|
815
|
+
}
|
|
816
|
+
const parent = dirname(dir);
|
|
817
|
+
if (parent === dir) break;
|
|
818
|
+
dir = parent;
|
|
819
|
+
}
|
|
820
|
+
return roots;
|
|
821
|
+
}
|
|
822
|
+
function containsOrIs(outer, inner) {
|
|
823
|
+
const rel = relative(outer, inner);
|
|
824
|
+
return rel === "" || rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
|
|
825
|
+
}
|
|
826
|
+
function realPath(p, hops = 0) {
|
|
827
|
+
const abs = resolve(p);
|
|
828
|
+
try {
|
|
829
|
+
return realpathSync.native(abs);
|
|
830
|
+
} catch {
|
|
831
|
+
const parent = dirname(abs);
|
|
832
|
+
if (parent === abs) return abs;
|
|
833
|
+
const realParent = realPath(parent, hops);
|
|
834
|
+
let link;
|
|
835
|
+
try {
|
|
836
|
+
link = readlinkSync(abs);
|
|
837
|
+
} catch {
|
|
838
|
+
link = void 0;
|
|
839
|
+
}
|
|
840
|
+
if (link !== void 0 && hops < 40) return realPath(resolve(realParent, link), hops + 1);
|
|
841
|
+
return join(realParent, basename(abs));
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
const warnedAbsoluteOutside = /* @__PURE__ */ new Set();
|
|
845
|
+
function warnAbsoluteOutsideAssembly(opts, absolute, escape) {
|
|
846
|
+
const logger = getLogger().child("assets");
|
|
847
|
+
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.");
|
|
848
|
+
if (warnedAbsoluteOutside.has(line)) {
|
|
849
|
+
logger.debug(line);
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
warnedAbsoluteOutside.add(line);
|
|
853
|
+
logger.warn(line);
|
|
854
|
+
}
|
|
855
|
+
const warnedWholeAssembly = /* @__PURE__ */ new Set();
|
|
856
|
+
function warnWholeAssemblyAsSource(opts, outdir) {
|
|
857
|
+
const logger = getLogger().child("assets");
|
|
858
|
+
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.`;
|
|
859
|
+
if (warnedWholeAssembly.has(line)) {
|
|
860
|
+
logger.debug(line);
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
warnedWholeAssembly.add(line);
|
|
864
|
+
logger.warn(line);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
//#endregion
|
|
868
|
+
//#region src/assets/docker-cache-option.ts
|
|
869
|
+
function cacheOptionToFlag(option) {
|
|
870
|
+
let flag = `type=${option.type}`;
|
|
871
|
+
if (option.params) for (const [k, v] of Object.entries(option.params)) flag += `,${k}=${v}`;
|
|
872
|
+
return flag;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
//#endregion
|
|
876
|
+
//#region src/assets/buildkit-passthrough-warnings.ts
|
|
877
|
+
function candidateHostPaths(value, opts) {
|
|
878
|
+
const out = [];
|
|
879
|
+
for (const part of value.split(",")) {
|
|
880
|
+
const eq = part.indexOf("=");
|
|
881
|
+
if (eq < 0) {
|
|
882
|
+
const bare = part.trim();
|
|
883
|
+
if (bare.length > 0) out.push({
|
|
884
|
+
path: bare,
|
|
885
|
+
write: opts.bareIsWrite
|
|
886
|
+
});
|
|
887
|
+
continue;
|
|
888
|
+
}
|
|
889
|
+
const key = part.slice(0, eq).trim().toLowerCase();
|
|
890
|
+
const v = part.slice(eq + 1).trim();
|
|
891
|
+
if (v.length === 0) continue;
|
|
892
|
+
if (key !== "src" && key !== "source" && key !== "dest") continue;
|
|
893
|
+
out.push({
|
|
894
|
+
path: v,
|
|
895
|
+
write: key === "dest"
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
return out;
|
|
899
|
+
}
|
|
900
|
+
function hostPathsOf(source) {
|
|
901
|
+
const refs = [];
|
|
902
|
+
if (source.dockerFile) refs.push({
|
|
903
|
+
field: "dockerFile",
|
|
904
|
+
where: "dockerFile",
|
|
905
|
+
path: source.dockerFile,
|
|
906
|
+
write: false
|
|
907
|
+
});
|
|
908
|
+
for (const [k, v] of Object.entries(source.dockerBuildContexts ?? {})) {
|
|
909
|
+
const rendered = `${k}=${v}`;
|
|
910
|
+
const contextPath = rendered.slice(rendered.indexOf("=") + 1).trim().replace(/^oci-layout:\/\//, "");
|
|
911
|
+
if (contextPath.length === 0) continue;
|
|
912
|
+
refs.push({
|
|
913
|
+
field: "dockerBuildContexts",
|
|
914
|
+
where: k,
|
|
915
|
+
path: contextPath,
|
|
916
|
+
write: false
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
const ssh = source.dockerBuildSsh ?? "";
|
|
920
|
+
const sshEq = ssh.indexOf("=");
|
|
921
|
+
(sshEq < 0 ? [] : ssh.slice(sshEq + 1).split(",")).forEach((entry, i) => {
|
|
922
|
+
const p = entry.trim();
|
|
923
|
+
if (p.length > 0) refs.push({
|
|
924
|
+
field: "dockerBuildSsh",
|
|
925
|
+
where: String(i),
|
|
926
|
+
path: p,
|
|
927
|
+
write: false
|
|
928
|
+
});
|
|
929
|
+
});
|
|
930
|
+
for (const [k, v] of Object.entries(source.dockerBuildSecrets ?? {})) for (const { path: p, write } of candidateHostPaths(`id=${k},${v}`, { bareIsWrite: false })) refs.push({
|
|
931
|
+
field: "dockerBuildSecrets",
|
|
932
|
+
where: k,
|
|
933
|
+
path: p,
|
|
934
|
+
write
|
|
935
|
+
});
|
|
936
|
+
(source.cacheFrom ?? []).forEach((c, i) => {
|
|
937
|
+
for (const { path: p, write } of candidateHostPaths(cacheOptionToFlag(c), { bareIsWrite: false })) refs.push({
|
|
938
|
+
field: "cacheFrom",
|
|
939
|
+
where: String(i),
|
|
940
|
+
path: p,
|
|
941
|
+
write
|
|
942
|
+
});
|
|
943
|
+
});
|
|
944
|
+
if (source.cacheTo) for (const { path: p, write } of candidateHostPaths(cacheOptionToFlag(source.cacheTo), { bareIsWrite: false })) refs.push({
|
|
945
|
+
field: "cacheTo",
|
|
946
|
+
where: "cacheTo",
|
|
947
|
+
path: p,
|
|
948
|
+
write
|
|
949
|
+
});
|
|
950
|
+
(source.dockerOutputs ?? []).forEach((o, i) => {
|
|
951
|
+
for (const { path: p, write } of candidateHostPaths(o, { bareIsWrite: true })) refs.push({
|
|
952
|
+
field: "dockerOutputs",
|
|
953
|
+
where: String(i),
|
|
954
|
+
path: p,
|
|
955
|
+
write
|
|
956
|
+
});
|
|
957
|
+
});
|
|
958
|
+
return refs;
|
|
959
|
+
}
|
|
960
|
+
const warnedPassthroughs = /* @__PURE__ */ new Set();
|
|
961
|
+
function warnEscapingBuildKitPaths(source, base, bound) {
|
|
962
|
+
const logger = getLogger().child("assets");
|
|
963
|
+
for (const ref of hostPathsOf(source)) {
|
|
964
|
+
const escape = assemblyPathEscape(base, bound, ref.path);
|
|
965
|
+
if (escape === void 0) continue;
|
|
966
|
+
const verb = ref.write ? "WRITE to" : "read";
|
|
967
|
+
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.`);
|
|
968
|
+
if (warnedPassthroughs.has(line)) {
|
|
969
|
+
logger.debug(line);
|
|
970
|
+
continue;
|
|
971
|
+
}
|
|
972
|
+
warnedPassthroughs.add(line);
|
|
973
|
+
logger.warn(line);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
//#endregion
|
|
978
|
+
//#region src/assets/docker-build.ts
|
|
979
|
+
const warnedExecutables = /* @__PURE__ */ new Set();
|
|
980
|
+
function warnManifestExecutable(cmd, executable, cwd) {
|
|
981
|
+
const logger = getLogger().child("docker-build");
|
|
982
|
+
const key = JSON.stringify([cwd, executable]);
|
|
983
|
+
if (warnedExecutables.has(key)) {
|
|
984
|
+
logger.debug(`source.executable already announced this process: ${displayUntrustedValue(cmd)} (cwd=${displayUntrustedValue(cwd)})`);
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
warnedExecutables.add(key);
|
|
988
|
+
const argCount = executable.length - 1;
|
|
989
|
+
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.`);
|
|
990
|
+
}
|
|
991
|
+
async function buildDockerImage(asset, cdkOutDir, options) {
|
|
992
|
+
const source = asset.source;
|
|
993
|
+
const logger = getLogger().child("docker-build");
|
|
994
|
+
const assetOutdir = options.assetOutdir ?? cdkOutDir;
|
|
995
|
+
const contextDirectory = (directory, sink) => resolveAssetSourcePath({
|
|
996
|
+
manifestDir: cdkOutDir,
|
|
997
|
+
value: directory,
|
|
998
|
+
assetOutdir,
|
|
999
|
+
absolute: "fold",
|
|
1000
|
+
field: "source.directory",
|
|
1001
|
+
subject: "Docker image asset",
|
|
1002
|
+
action: "build it",
|
|
1003
|
+
sink,
|
|
1004
|
+
wrapError: (message) => new LocalInvokeBuildError(message)
|
|
1005
|
+
});
|
|
1006
|
+
if (source.executable && source.executable.length > 0) {
|
|
1007
|
+
const [cmd, ...args] = source.executable;
|
|
1008
|
+
if (!cmd) throw options.wrapError("asset source.executable[] is empty");
|
|
1009
|
+
const cwd = source.directory ? contextDirectory(source.directory, "run this asset's source.executable with that directory as its working directory") : cdkOutDir;
|
|
1010
|
+
logger.debug(`Building Docker image via executable: ${displayUntrustedValue(redactDockerArgvValues(source.executable).join(" "))} (cwd=${displayUntrustedValue(cwd)})`);
|
|
1011
|
+
warnManifestExecutable(cmd, source.executable, cwd);
|
|
1012
|
+
let result;
|
|
1013
|
+
try {
|
|
1014
|
+
result = await spawnStreaming(cmd, args, { cwd });
|
|
1015
|
+
} catch (err) {
|
|
1016
|
+
const e = err;
|
|
1017
|
+
throw options.wrapError(displayToolOutput(e.stderr || e.message || String(err)));
|
|
1018
|
+
}
|
|
1019
|
+
const tag = result.stdout.trim();
|
|
1020
|
+
if (!tag) throw options.wrapError(`docker build executable produced no output (expected the local image tag on stdout): ${displayUntrustedValue(cmd)}`);
|
|
1021
|
+
return tag;
|
|
1022
|
+
}
|
|
1023
|
+
if (!source.directory) throw options.wrapError(`DockerImageAssetSource must set either 'directory' or 'executable' (fields present: ${displayUntrustedValue(Object.keys(source).join(", ") || "none")})`);
|
|
1024
|
+
if (!options.tag) throw options.wrapError("buildDockerImage(directory mode) requires options.tag");
|
|
1025
|
+
const buildArgs = buildDockerBuildCommand(source, options.tag, options.platform);
|
|
1026
|
+
const contextDir = contextDirectory(source.directory, `send that directory to docker build as the context of an image ${getEmbedConfig().productName} then runs locally`);
|
|
1027
|
+
buildArgs.push(".");
|
|
1028
|
+
warnEscapingBuildKitPaths(source, contextDir, assetOutdir);
|
|
1029
|
+
logger.debug(`${getDockerCmd()} ${redactDockerArgvValues(buildArgs).map((a) => displayUntrustedValue(a)).join(" ")} (cwd=${displayUntrustedValue(contextDir)})`);
|
|
1030
|
+
try {
|
|
1031
|
+
await runDockerStreaming(buildArgs, {
|
|
1032
|
+
cwd: contextDir,
|
|
1033
|
+
env: { BUILDX_NO_DEFAULT_ATTESTATIONS: "1" },
|
|
1034
|
+
...options.progressLabel !== void 0 && { progressLabel: options.progressLabel }
|
|
1035
|
+
});
|
|
1036
|
+
} catch (err) {
|
|
1037
|
+
const e = err;
|
|
1038
|
+
throw options.wrapError(displayToolOutput(e.stderr || e.message || String(err)));
|
|
1039
|
+
}
|
|
1040
|
+
return options.tag;
|
|
1041
|
+
}
|
|
1042
|
+
function buildDockerBuildCommand(source, tag, platformOverride) {
|
|
1043
|
+
const args = [
|
|
1044
|
+
"build",
|
|
1045
|
+
"--tag",
|
|
1046
|
+
tag
|
|
1047
|
+
];
|
|
1048
|
+
if (source.dockerBuildArgs) for (const [k, v] of Object.entries(source.dockerBuildArgs)) args.push("--build-arg", `${k}=${v}`);
|
|
1049
|
+
if (source.dockerBuildContexts) for (const [k, v] of Object.entries(source.dockerBuildContexts)) args.push("--build-context", `${k}=${v}`);
|
|
1050
|
+
if (source.dockerBuildSecrets) for (const [k, v] of Object.entries(source.dockerBuildSecrets)) args.push("--secret", `id=${k},${v}`);
|
|
1051
|
+
if (source.dockerBuildSsh) args.push("--ssh", source.dockerBuildSsh);
|
|
1052
|
+
if (source.dockerBuildTarget) args.push("--target", source.dockerBuildTarget);
|
|
1053
|
+
if (source.dockerFile) args.push("--file", source.dockerFile);
|
|
1054
|
+
if (source.networkMode) args.push("--network", source.networkMode);
|
|
1055
|
+
const platform = platformOverride ?? source.platform;
|
|
1056
|
+
if (platform) args.push("--platform", platform);
|
|
1057
|
+
if (source.dockerOutputs) for (const output of source.dockerOutputs) args.push(`--output=${output}`);
|
|
1058
|
+
if (source.cacheFrom) for (const c of source.cacheFrom) args.push("--cache-from", cacheOptionToFlag(c));
|
|
1059
|
+
if (source.cacheTo) args.push("--cache-to", cacheOptionToFlag(source.cacheTo));
|
|
1060
|
+
if (source.cacheDisabled) args.push("--no-cache");
|
|
1061
|
+
return args;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
//#endregion
|
|
1065
|
+
//#region src/local/ecr-puller.ts
|
|
1066
|
+
function parseEcrUri(imageUri) {
|
|
1067
|
+
const host = parseEcrRegistryHost(imageUri);
|
|
1068
|
+
if (!host) return void 0;
|
|
1069
|
+
const canonicalUri = canonicalizeImageUriHost(imageUri);
|
|
1070
|
+
const m = /^([^:]+):(.+)$/.exec(canonicalUri.slice(canonicalUri.indexOf("/") + 1));
|
|
1071
|
+
if (!m) return void 0;
|
|
1072
|
+
return {
|
|
1073
|
+
accountId: host.accountId,
|
|
1074
|
+
region: host.region,
|
|
1075
|
+
repository: m[1],
|
|
1076
|
+
tag: m[2],
|
|
1077
|
+
canonicalUri,
|
|
1078
|
+
registryHost: host.registryHost
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
const ASSUMED_ROLE_CACHE = /* @__PURE__ */ new Map();
|
|
1082
|
+
const CALLER_IDENTITY_CACHE = /* @__PURE__ */ new Map();
|
|
1083
|
+
const STS_CREDENTIAL_SAFETY_MARGIN_MS = 300 * 1e3;
|
|
1084
|
+
function isCredentialFresh(creds) {
|
|
1085
|
+
if (!creds.expiration) return false;
|
|
1086
|
+
return creds.expiration.getTime() - Date.now() > STS_CREDENTIAL_SAFETY_MARGIN_MS;
|
|
1087
|
+
}
|
|
1088
|
+
async function pullEcrImage(imageUri, options) {
|
|
1089
|
+
const logger = getLogger().child("ecr-puller");
|
|
1090
|
+
const parsed = parseEcrUri(imageUri);
|
|
1091
|
+
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.`);
|
|
1092
|
+
const canonicalUri = parsed.canonicalUri;
|
|
1093
|
+
const callerRegion = options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"];
|
|
1094
|
+
if (options.skipPull) {
|
|
1095
|
+
logger.info(`Skipping ECR pull (--no-pull). Verifying ${canonicalUri} is in local cache...`);
|
|
1096
|
+
await verifyImageInLocalCache(canonicalUri);
|
|
1097
|
+
return canonicalUri;
|
|
1098
|
+
}
|
|
1099
|
+
const callerIdentityKey = `${options.profile ?? "_noprofile"}|${callerRegion ?? "_unset"}`;
|
|
1100
|
+
let callerAccount = CALLER_IDENTITY_CACHE.get(callerIdentityKey);
|
|
1101
|
+
if (callerAccount === void 0) {
|
|
1102
|
+
const sts = new STSClient(buildStsClientConfig({
|
|
1103
|
+
region: callerRegion,
|
|
1104
|
+
profile: options.profile
|
|
1105
|
+
}));
|
|
1106
|
+
try {
|
|
1107
|
+
const identity = await sts.send(new GetCallerIdentityCommand({}));
|
|
1108
|
+
if (!identity.Account) throw new LocalInvokeBuildError("STS GetCallerIdentity returned no Account. Verify your AWS credentials.");
|
|
1109
|
+
callerAccount = identity.Account;
|
|
1110
|
+
CALLER_IDENTITY_CACHE.set(callerIdentityKey, callerAccount);
|
|
1111
|
+
} catch (err) {
|
|
1112
|
+
if (err instanceof LocalInvokeBuildError) throw err;
|
|
1113
|
+
throw new LocalInvokeBuildError(`STS GetCallerIdentity failed while preparing the ECR pull: ${describeAwsFailureForWarn(err, "STS GetCallerIdentity (ECR pull)")}. Verify your AWS credentials.`);
|
|
1114
|
+
} finally {
|
|
1115
|
+
sts.destroy();
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
const crossAccount = callerAccount !== parsed.accountId;
|
|
1119
|
+
const crossRegion = callerRegion !== void 0 && callerRegion !== parsed.region;
|
|
1120
|
+
let assumed;
|
|
1121
|
+
if (options.ecrRoleArn) {
|
|
1122
|
+
const cacheKey = `${options.profile ?? "_noprofile"}|${options.ecrRoleArn}|${callerRegion ?? "_unset"}`;
|
|
1123
|
+
const cached = ASSUMED_ROLE_CACHE.get(cacheKey);
|
|
1124
|
+
if (cached && isCredentialFresh(cached)) {
|
|
1125
|
+
assumed = cached;
|
|
1126
|
+
logger.debug(`Reusing cached AssumeRole credentials for ${options.ecrRoleArn}`);
|
|
1127
|
+
} else {
|
|
1128
|
+
assumed = await assumeRoleForEcr(options.ecrRoleArn, callerRegion, options.profile, logger);
|
|
1129
|
+
ASSUMED_ROLE_CACHE.set(cacheKey, assumed);
|
|
1130
|
+
logger.info(`Assumed role ${options.ecrRoleArn} for ECR pull (account=${parsed.accountId}, region=${parsed.region})`);
|
|
1131
|
+
}
|
|
1132
|
+
} 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.`);
|
|
1133
|
+
if (crossRegion) logger.info(`Cross-region ECR pull: image region ${parsed.region} != caller ${callerRegion ?? "(unset)"}. Authenticating against the image region directly.`);
|
|
1134
|
+
const ecr = new ECRClient({
|
|
1135
|
+
...buildProxyClientConfig({ profile: options.profile }),
|
|
1136
|
+
region: parsed.region,
|
|
1137
|
+
...assumed ? { credentials: assumed } : options.profile ? { profile: options.profile } : {}
|
|
1138
|
+
});
|
|
1139
|
+
try {
|
|
1140
|
+
await ecrLogin(ecr, {
|
|
1141
|
+
accountId: parsed.accountId,
|
|
1142
|
+
region: parsed.region,
|
|
1143
|
+
registryHost: parsed.registryHost
|
|
1144
|
+
});
|
|
1145
|
+
} finally {
|
|
1146
|
+
ecr.destroy();
|
|
1147
|
+
}
|
|
1148
|
+
logger.info(`Pulling ${canonicalUri}...`);
|
|
1149
|
+
try {
|
|
1150
|
+
await runDockerForeground(["pull", canonicalUri]);
|
|
1151
|
+
} catch (err) {
|
|
1152
|
+
throw new LocalInvokeBuildError(`docker pull ${canonicalUri} failed: ${err.message}`);
|
|
1153
|
+
}
|
|
1154
|
+
return canonicalUri;
|
|
1155
|
+
}
|
|
1156
|
+
async function assumeRoleForEcr(roleArn, callerRegion, profile, logger) {
|
|
1157
|
+
if (!isIamRoleArn(roleArn)) throw new LocalInvokeBuildError(refusedRoleArnMessage(roleArn));
|
|
1158
|
+
logger.debug(`Assuming role ${roleArn} for ECR pull...`);
|
|
1159
|
+
const sts = new STSClient(workloadStsClientConfig({
|
|
1160
|
+
region: callerRegion,
|
|
1161
|
+
profile
|
|
1162
|
+
}));
|
|
1163
|
+
try {
|
|
1164
|
+
const creds = (await sts.send(new AssumeRoleCommand({
|
|
1165
|
+
RoleArn: roleArn,
|
|
1166
|
+
RoleSessionName: `${getEmbedConfig().resourceNamePrefix}-ecr-${Date.now()}`,
|
|
1167
|
+
DurationSeconds: 3600
|
|
1168
|
+
}))).Credentials;
|
|
1169
|
+
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.`);
|
|
1170
|
+
return {
|
|
1171
|
+
accessKeyId: creds.AccessKeyId,
|
|
1172
|
+
secretAccessKey: creds.SecretAccessKey,
|
|
1173
|
+
sessionToken: creds.SessionToken,
|
|
1174
|
+
...creds.Expiration && { expiration: creds.Expiration }
|
|
1175
|
+
};
|
|
1176
|
+
} catch (err) {
|
|
1177
|
+
if (err instanceof LocalInvokeBuildError) throw err;
|
|
1178
|
+
const reason = describeAwsFailureForWarn(err, "STS AssumeRole (ECR pull)");
|
|
1179
|
+
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.`);
|
|
1180
|
+
} finally {
|
|
1181
|
+
sts.destroy();
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
async function ecrLogin(client, target) {
|
|
1185
|
+
const { accountId, region, registryHost } = target;
|
|
1186
|
+
getLogger().child("ecr-puller").debug(`ECR login (account=${accountId}, region=${region}, host=${registryHost})`);
|
|
1187
|
+
const authData = (await client.send(new GetAuthorizationTokenCommand({})).catch((err) => {
|
|
1188
|
+
throw new LocalInvokeBuildError(`ECR GetAuthorizationToken failed (account=${accountId}, region=${region}): ${describeAwsFailureForWarn(err, "ECR GetAuthorizationToken")}. Verify the credentials can call ecr:GetAuthorizationToken.`);
|
|
1189
|
+
})).authorizationData?.[0];
|
|
1190
|
+
if (!authData?.authorizationToken) throw new LocalInvokeBuildError("Failed to get ECR authorization token");
|
|
1191
|
+
const [username, password] = Buffer.from(authData.authorizationToken, "base64").toString().split(":");
|
|
1192
|
+
if (!username || password === void 0) throw new LocalInvokeBuildError("ECR authorization token has unexpected shape (missing username/password)");
|
|
1193
|
+
const endpoint = `https://${registryHost}`;
|
|
1194
|
+
try {
|
|
1195
|
+
await runDockerStreaming([
|
|
1196
|
+
"login",
|
|
1197
|
+
"--username",
|
|
1198
|
+
username,
|
|
1199
|
+
"--password-stdin",
|
|
1200
|
+
endpoint
|
|
1201
|
+
], { input: password });
|
|
1202
|
+
} catch (err) {
|
|
1203
|
+
const e = err;
|
|
1204
|
+
throw new LocalInvokeBuildError(`ECR login failed: ${formatDockerLoginError(e.stderr || e.message || String(err), endpoint)}`);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
async function verifyImageInLocalCache(imageUri) {
|
|
1208
|
+
try {
|
|
1209
|
+
await runDockerStreaming([
|
|
1210
|
+
"image",
|
|
1211
|
+
"inspect",
|
|
1212
|
+
imageUri
|
|
1213
|
+
]);
|
|
1214
|
+
} catch {
|
|
1215
|
+
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\`.`);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
async function isImageInLocalCache(imageRef) {
|
|
1219
|
+
try {
|
|
1220
|
+
await runDockerStreaming([
|
|
1221
|
+
"image",
|
|
1222
|
+
"inspect",
|
|
1223
|
+
imageRef
|
|
1224
|
+
]);
|
|
1225
|
+
return true;
|
|
1226
|
+
} catch {
|
|
1227
|
+
return false;
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
//#endregion
|
|
1232
|
+
//#region src/local/docker-image-builder.ts
|
|
1233
|
+
async function buildContainerImage(asset, cdkOutDir, options) {
|
|
1234
|
+
const tag = computeLocalTag(asset.source);
|
|
1235
|
+
const platform = architectureToPlatform(options.architecture);
|
|
1236
|
+
const logger = getLogger().child("local-invoke-build");
|
|
1237
|
+
if (options.noBuild === true) {
|
|
1238
|
+
logger.info(`Skipping docker build (--no-build). Verifying ${tag} is in local registry...`);
|
|
1239
|
+
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.`);
|
|
1240
|
+
logger.debug(`Local tag ${tag} is cached; skipping build.`);
|
|
1241
|
+
return tag;
|
|
1242
|
+
}
|
|
1243
|
+
logger.info(`Building container image (platform=${platform})...`);
|
|
1244
|
+
logger.debug(`Local tag: ${tag}`);
|
|
1245
|
+
const actualTag = await buildDockerImage(asset, cdkOutDir, {
|
|
1246
|
+
tag,
|
|
1247
|
+
platform,
|
|
1248
|
+
wrapError: (stderr) => new LocalInvokeBuildError(`docker build failed for container Lambda asset (${displayUntrustedValue(asset.source.directory ?? asset.source.executable?.[0] ?? "")}): ${stderr}`),
|
|
1249
|
+
progressLabel: `Building container image (platform=${platform})`,
|
|
1250
|
+
...options.assetOutdir !== void 0 && { assetOutdir: options.assetOutdir }
|
|
1251
|
+
});
|
|
1252
|
+
if (actualTag !== tag) {
|
|
1253
|
+
logger.debug(`Re-tagging executable-built image ${displayIdent(actualTag)} → ${displayIdent(tag)}`);
|
|
1254
|
+
try {
|
|
1255
|
+
await runDockerStreaming([
|
|
1256
|
+
"tag",
|
|
1257
|
+
actualTag,
|
|
1258
|
+
tag
|
|
1259
|
+
]);
|
|
1260
|
+
} catch (err) {
|
|
1261
|
+
const e = err;
|
|
1262
|
+
throw new LocalInvokeBuildError(`docker tag failed re-tagging ${displayIdent(actualTag)} → ${displayIdent(tag)}: ${displayToolOutput(e.stderr?.trim() || e.message || String(err))}`);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
return tag;
|
|
1266
|
+
}
|
|
1267
|
+
function architectureToPlatform(architecture) {
|
|
1268
|
+
return architecture === "arm64" ? "linux/arm64" : "linux/amd64";
|
|
1269
|
+
}
|
|
1270
|
+
function hostPlatform() {
|
|
1271
|
+
return process.arch === "arm64" ? "linux/arm64" : "linux/amd64";
|
|
1272
|
+
}
|
|
1273
|
+
const warnedEmulatedPlatforms = /* @__PURE__ */ new Set();
|
|
1274
|
+
function warnIfEmulatedPlatform(platform, opts = {}) {
|
|
1275
|
+
if (!platform) return;
|
|
1276
|
+
const host = hostPlatform();
|
|
1277
|
+
if (platform === host) return;
|
|
1278
|
+
if (warnedEmulatedPlatforms.has(platform)) return;
|
|
1279
|
+
warnedEmulatedPlatforms.add(platform);
|
|
1280
|
+
const logger = opts.logger ?? getLogger();
|
|
1281
|
+
const what = opts.label ? `${displayIdent(opts.label)} ` : "";
|
|
1282
|
+
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}.`);
|
|
1283
|
+
}
|
|
1284
|
+
function computeLocalTag(source) {
|
|
1285
|
+
const hash = createHash("sha256");
|
|
1286
|
+
pushField(hash, "directory", source.directory ?? "");
|
|
1287
|
+
pushField(hash, "executable", (source.executable ?? []).join(" "));
|
|
1288
|
+
pushField(hash, "dockerFile", source.dockerFile ?? "");
|
|
1289
|
+
pushField(hash, "dockerBuildTarget", source.dockerBuildTarget ?? "");
|
|
1290
|
+
pushField(hash, "networkMode", source.networkMode ?? "");
|
|
1291
|
+
pushField(hash, "platform", source.platform ?? "");
|
|
1292
|
+
pushField(hash, "dockerBuildSsh", source.dockerBuildSsh ?? "");
|
|
1293
|
+
pushField(hash, "cacheDisabled", source.cacheDisabled ? "1" : "0");
|
|
1294
|
+
pushMap(hash, "dockerBuildArgs", source.dockerBuildArgs);
|
|
1295
|
+
pushMap(hash, "dockerBuildContexts", source.dockerBuildContexts);
|
|
1296
|
+
pushMap(hash, "dockerBuildSecrets", source.dockerBuildSecrets);
|
|
1297
|
+
pushField(hash, "dockerOutputs", (source.dockerOutputs ?? []).join(""));
|
|
1298
|
+
pushField(hash, "cacheFrom", (source.cacheFrom ?? []).map((o) => JSON.stringify(o)).join(""));
|
|
1299
|
+
pushField(hash, "cacheTo", source.cacheTo ? JSON.stringify(source.cacheTo) : "");
|
|
1300
|
+
return `${getEmbedConfig().resourceNamePrefix}-invoke-${hash.digest("hex").slice(0, 16)}`;
|
|
1301
|
+
}
|
|
1302
|
+
function pushField(hash, name, value) {
|
|
1303
|
+
hash.update(name);
|
|
1304
|
+
hash.update("=");
|
|
1305
|
+
hash.update(value);
|
|
1306
|
+
hash.update("\0");
|
|
1307
|
+
}
|
|
1308
|
+
function pushMap(hash, name, value) {
|
|
1309
|
+
hash.update(name);
|
|
1310
|
+
hash.update("={");
|
|
1311
|
+
if (value) for (const [k, v] of Object.entries(value)) {
|
|
1312
|
+
hash.update(k);
|
|
1313
|
+
hash.update("=");
|
|
1314
|
+
hash.update(v);
|
|
1315
|
+
hash.update(";");
|
|
1316
|
+
}
|
|
1317
|
+
hash.update("}\0");
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
//#endregion
|
|
1321
|
+
//#region src/local/loopback-forwarder.ts
|
|
1322
|
+
const execFileAsync$1 = promisify(execFile);
|
|
1323
|
+
const LOOPBACK_FORWARDER_IMAGE = "public.ecr.aws/docker/library/node:22-alpine@sha256:1314fb30ec37592d5845e5a160d522c0e680972aa534549940b8d73bde88481a";
|
|
1324
|
+
const FORWARDER_CA_PATH = "/cdkl-forwarder/ca.pem";
|
|
1325
|
+
const STATUS_PREFIX = "CDKL-FWD";
|
|
1326
|
+
const LOOPBACK_FORWARDER_SCRIPT = `
|
|
1327
|
+
const net=require('net'),dns=require('dns'),fs=require('fs'),https=require('https');
|
|
1328
|
+
const ports=process.argv[1].split(',').map(Number),mode=process.argv[2],ca=process.argv[3];
|
|
1329
|
+
const out=(s)=>process.stdout.write('${STATUS_PREFIX} '+s+'\\n');
|
|
1330
|
+
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;}
|
|
1331
|
+
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));});}
|
|
1332
|
+
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'));});}
|
|
1333
|
+
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'));});}
|
|
1334
|
+
dns.lookup('host.docker.internal',{family:4},async(err,addr)=>{
|
|
1335
|
+
const up=err?gateway():addr;
|
|
1336
|
+
if(!up){out('FAIL '+ports[0]+' NO_UPSTREAM none');return;}
|
|
1337
|
+
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);}
|
|
1338
|
+
for(const p of ports){const e=await tcpProbe(p,up);if(e){out('FAIL '+p+' '+e+' '+up);return;}}
|
|
1339
|
+
if(mode==='health'&&ca){const e=await healthProbe(ports[0]);if(e){out('FAIL '+ports[0]+' '+e+' '+up);return;}}
|
|
1340
|
+
out('READY '+up);
|
|
1341
|
+
});
|
|
1342
|
+
`;
|
|
1343
|
+
function parseForwarderStatus(logs) {
|
|
1344
|
+
for (const raw of logs.split("\n")) {
|
|
1345
|
+
const line = raw.trim();
|
|
1346
|
+
if (!line.startsWith(`${STATUS_PREFIX} `)) continue;
|
|
1347
|
+
const parts = line.slice(9).split(" ");
|
|
1348
|
+
if (parts[0] === "READY") return {
|
|
1349
|
+
ok: true,
|
|
1350
|
+
...parts[1] ? { upstream: parts[1] } : {}
|
|
1351
|
+
};
|
|
1352
|
+
if (parts[0] === "FAIL") {
|
|
1353
|
+
const port = Number(parts[1]);
|
|
1354
|
+
return {
|
|
1355
|
+
ok: false,
|
|
1356
|
+
...Number.isInteger(port) ? { failedPort: port } : {},
|
|
1357
|
+
...parts[2] ? { reason: parts[2] } : {},
|
|
1358
|
+
...parts[3] && parts[3] !== "none" ? { upstream: parts[3] } : {}
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
function buildLoopbackForwarderDockerArgs(opts) {
|
|
1364
|
+
const ns = `container:${opts.workloadContainer}`;
|
|
1365
|
+
const args = [
|
|
1366
|
+
"run",
|
|
1367
|
+
"-d",
|
|
1368
|
+
"--rm",
|
|
1369
|
+
"--name",
|
|
1370
|
+
opts.name,
|
|
1371
|
+
"--network",
|
|
1372
|
+
ns,
|
|
1373
|
+
"--pid",
|
|
1374
|
+
ns
|
|
1375
|
+
];
|
|
1376
|
+
if (opts.caHostPath) args.push("-v", `${opts.caHostPath}:${FORWARDER_CA_PATH}:ro`);
|
|
1377
|
+
args.push("--entrypoint", "node", LOOPBACK_FORWARDER_IMAGE, "-e", LOOPBACK_FORWARDER_SCRIPT, opts.ports.join(","), opts.probe === "health" && opts.caHostPath ? "health" : "tcp", ...opts.caHostPath ? [FORWARDER_CA_PATH] : []);
|
|
1378
|
+
return args;
|
|
1379
|
+
}
|
|
1380
|
+
let imageReady;
|
|
1381
|
+
let pullFailure;
|
|
1382
|
+
const PULL_FAILURE_TTL_MS = 6e4;
|
|
1383
|
+
async function ensureForwarderImage() {
|
|
1384
|
+
if (pullFailure && Date.now() - pullFailure.at < PULL_FAILURE_TTL_MS) throw pullFailure.error;
|
|
1385
|
+
imageReady ??= (async () => {
|
|
1386
|
+
try {
|
|
1387
|
+
await execFileAsync$1(getDockerCmd(), [
|
|
1388
|
+
"image",
|
|
1389
|
+
"inspect",
|
|
1390
|
+
LOOPBACK_FORWARDER_IMAGE
|
|
1391
|
+
]);
|
|
1392
|
+
} catch {
|
|
1393
|
+
await pullImage(LOOPBACK_FORWARDER_IMAGE, false);
|
|
1394
|
+
}
|
|
1395
|
+
})().catch((err) => {
|
|
1396
|
+
imageReady = void 0;
|
|
1397
|
+
pullFailure = {
|
|
1398
|
+
at: Date.now(),
|
|
1399
|
+
error: err
|
|
1400
|
+
};
|
|
1401
|
+
throw err;
|
|
1402
|
+
});
|
|
1403
|
+
return imageReady;
|
|
1404
|
+
}
|
|
1405
|
+
async function startLoopbackForwarder(opts) {
|
|
1406
|
+
const logger = getLogger().child("forwarder");
|
|
1407
|
+
await ensureForwarderImage();
|
|
1408
|
+
const name = `${getEmbedConfig().resourceNamePrefix}-awsfwd-${process.pid}-${randomBytes(4).toString("hex")}`;
|
|
1409
|
+
const args = buildLoopbackForwarderDockerArgs({
|
|
1410
|
+
...opts,
|
|
1411
|
+
name
|
|
1412
|
+
});
|
|
1413
|
+
logger.debug(`Starting loopback forwarder ${name} for ports ${opts.ports.join(",")}`);
|
|
1414
|
+
try {
|
|
1415
|
+
await execFileAsync$1(getDockerCmd(), args);
|
|
1416
|
+
} catch (error) {
|
|
1417
|
+
throw new Error(`Could not start the loopback forwarder container: ${displayToolOutput(error.stderr?.trim() || describeExitWithoutArgv(error))}`);
|
|
1418
|
+
}
|
|
1419
|
+
const deadline = Date.now() + (opts.timeoutMs ?? 2e4);
|
|
1420
|
+
for (;;) {
|
|
1421
|
+
let logs = "";
|
|
1422
|
+
try {
|
|
1423
|
+
const r = await execFileAsync$1(getDockerCmd(), ["logs", name]);
|
|
1424
|
+
logs = `${r.stdout}\n${r.stderr}`;
|
|
1425
|
+
} catch (error) {
|
|
1426
|
+
throw new Error(`The loopback forwarder container exited before reporting: ${displayToolOutput(error.stderr?.trim() || describeExitWithoutArgv(error))}`);
|
|
1427
|
+
}
|
|
1428
|
+
const status = parseForwarderStatus(logs);
|
|
1429
|
+
if (status) {
|
|
1430
|
+
logger.debug(status.ok ? `Loopback forwarder ${name} ready (upstream ${status.upstream ?? "?"})` : `Loopback forwarder ${name} failed: port ${status.failedPort} ${status.reason}`);
|
|
1431
|
+
return {
|
|
1432
|
+
name,
|
|
1433
|
+
...status
|
|
1434
|
+
};
|
|
1435
|
+
}
|
|
1436
|
+
if (Date.now() > deadline) {
|
|
1437
|
+
await execFileAsync$1(getDockerCmd(), [
|
|
1438
|
+
"rm",
|
|
1439
|
+
"-f",
|
|
1440
|
+
name
|
|
1441
|
+
]).catch(() => void 0);
|
|
1442
|
+
throw new Error("The loopback forwarder container did not report readiness in time.");
|
|
1443
|
+
}
|
|
1444
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
function describeForwarderFailure(result, endpointLabel) {
|
|
1448
|
+
const port = result.failedPort ?? 0;
|
|
1449
|
+
const via = result.upstream ? ` via ${result.upstream}:${port}` : "";
|
|
1450
|
+
const reason = result.reason ?? "unknown";
|
|
1451
|
+
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.`;
|
|
1452
|
+
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).`;
|
|
1453
|
+
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.`;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
//#endregion
|
|
1457
|
+
//#region src/local/docker-runner.ts
|
|
1458
|
+
var docker_runner_exports = /* @__PURE__ */ __exportAll({
|
|
1459
|
+
DockerRunnerError: () => DockerRunnerError,
|
|
1460
|
+
MAX_HELD_LINE_CHARS: () => MAX_HELD_LINE_CHARS,
|
|
1461
|
+
PREFIX_CONTAINER_OUTPUT_ENV: () => PREFIX_CONTAINER_OUTPUT_ENV,
|
|
1462
|
+
SENSITIVE_ENV_KEYS: () => SENSITIVE_ENV_KEYS,
|
|
1463
|
+
appendEnvFlags: () => appendEnvFlags,
|
|
1464
|
+
attachLoopbackForwarder: () => attachLoopbackForwarder,
|
|
1465
|
+
createPrefixedLineRelay: () => createPrefixedLineRelay,
|
|
1466
|
+
describeRefusedSensitiveEnvKeys: () => describeRefusedSensitiveEnvKeys,
|
|
1467
|
+
ensureDockerAvailable: () => ensureDockerAvailable,
|
|
1468
|
+
execEnvForSecrets: () => execEnvForSecrets,
|
|
1469
|
+
pickFreePort: () => pickFreePort,
|
|
1470
|
+
prepareLoopbackForwarder: () => prepareLoopbackForwarder,
|
|
1471
|
+
pullImage: () => pullImage,
|
|
1472
|
+
removeContainer: () => removeContainer,
|
|
1473
|
+
runDetached: () => runDetached,
|
|
1474
|
+
streamLogs: () => streamLogs,
|
|
1475
|
+
withWorkloadAwsWiring: () => withWorkloadAwsWiring
|
|
1476
|
+
});
|
|
1477
|
+
const execFileAsync = promisify(execFile);
|
|
1478
|
+
var DockerRunnerError = class DockerRunnerError extends Error {
|
|
1479
|
+
constructor(message) {
|
|
1480
|
+
super(message);
|
|
1481
|
+
this.name = "DockerRunnerError";
|
|
1482
|
+
Object.setPrototypeOf(this, DockerRunnerError.prototype);
|
|
1483
|
+
}
|
|
1484
|
+
};
|
|
1485
|
+
async function pullImage(image, skipPull, platform) {
|
|
1486
|
+
const logger = getLogger().child("docker");
|
|
1487
|
+
if (skipPull) {
|
|
1488
|
+
logger.debug(`Skipping docker pull for ${displayIdent(image)} (--no-pull)`);
|
|
1489
|
+
return;
|
|
1490
|
+
}
|
|
1491
|
+
const platformArgs = platform ? ["--platform", platform] : [];
|
|
1492
|
+
if (getLogger().getLevel() === "debug") {
|
|
1493
|
+
logger.info(`Pulling ${displayIdent(image)}${platform ? ` (${displayIdent(platform)})` : ""}...`);
|
|
1494
|
+
try {
|
|
1495
|
+
await runDockerForeground([
|
|
1496
|
+
"pull",
|
|
1497
|
+
...platformArgs,
|
|
1498
|
+
image
|
|
1499
|
+
]);
|
|
1500
|
+
} catch (err) {
|
|
1501
|
+
const e = err;
|
|
1502
|
+
throw new DockerRunnerError(`docker pull ${displayIdent(image)} failed: ${e.message}`);
|
|
1503
|
+
}
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1506
|
+
logger.debug(`Pulling ${displayIdent(image)} (silent — pass --verbose to stream progress)`);
|
|
1507
|
+
try {
|
|
1508
|
+
await runDockerStreaming([
|
|
1509
|
+
"pull",
|
|
1510
|
+
...platformArgs,
|
|
1511
|
+
image
|
|
1512
|
+
], {
|
|
1513
|
+
streamLive: false,
|
|
1514
|
+
progressLabel: `Pulling ${displayIdent(image)}`
|
|
1515
|
+
});
|
|
1516
|
+
} catch (err) {
|
|
1517
|
+
const e = err;
|
|
1518
|
+
if (e.exitCode === void 0 || e.exitCode === null) throw new DockerRunnerError(`docker pull ${displayIdent(image)} failed: ${displayToolOutput(e.message ?? String(err))}`);
|
|
1519
|
+
const detail = e.stderr?.trim() || e.stdout?.trim() || "(no output)";
|
|
1520
|
+
throw new DockerRunnerError(`docker pull ${displayIdent(image)} exited with code ${e.exitCode}: ${displayToolOutput(detail)}`);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
async function runDetached(opts) {
|
|
1524
|
+
const wiring = getActiveWorkloadAwsWiring();
|
|
1525
|
+
if (!wiring) return runDetachedContainer(opts);
|
|
1526
|
+
const wired = await withWorkloadAwsWiring(opts, wiring);
|
|
1527
|
+
const ports = opts.network === "host" ? [] : effectiveLoopbackPorts(wired.env, wiring);
|
|
1528
|
+
if (ports.length > 0) await prepareLoopbackForwarder(wiring);
|
|
1529
|
+
const id = await runDetachedContainer(wired);
|
|
1530
|
+
await attachLoopbackForwarder(id, wiring, ports);
|
|
1531
|
+
return id;
|
|
1532
|
+
}
|
|
1533
|
+
async function withWorkloadAwsWiring(opts, wiring) {
|
|
1534
|
+
const defaults = wiring.mode === "forward" && !opts.env["AWS_ACCESS_KEY_ID"] ? await resolveHostDefaultCredentials() : void 0;
|
|
1535
|
+
const env = applyWorkloadAwsWiringToEnv(opts.env, wiring, defaults);
|
|
1536
|
+
const credsPath = `${getEmbedConfig().awsBindMountPath}/credentials`;
|
|
1537
|
+
const extra = (opts.extraMounts ?? []).filter((m) => wiring.mode !== "emulator" || m.containerPath !== credsPath);
|
|
1538
|
+
const taken = new Set([...opts.mounts, ...extra].map((m) => m.containerPath));
|
|
1539
|
+
for (const m of wiring.mounts) if (!taken.has(m.containerPath)) extra.push({
|
|
1540
|
+
...m,
|
|
1541
|
+
readOnly: true
|
|
1542
|
+
});
|
|
1543
|
+
return {
|
|
1544
|
+
...opts,
|
|
1545
|
+
env,
|
|
1546
|
+
extraMounts: extra
|
|
1547
|
+
};
|
|
1548
|
+
}
|
|
1549
|
+
async function prepareLoopbackForwarder(wiring) {
|
|
1550
|
+
try {
|
|
1551
|
+
await ensureForwarderImage();
|
|
1552
|
+
} catch (err) {
|
|
1553
|
+
if (wiring.mode === "emulator") throw new DockerRunnerError(`--aws-emulator: the loopback forwarder image could not be pulled: ${err.message}`);
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
const warnedForwarderPorts = /* @__PURE__ */ new Set();
|
|
1557
|
+
function warnForwarderOnce(key, message) {
|
|
1558
|
+
if (warnedForwarderPorts.has(key)) return;
|
|
1559
|
+
warnedForwarderPorts.add(key);
|
|
1560
|
+
getLogger().warn(message);
|
|
1561
|
+
}
|
|
1562
|
+
async function isContainerRunning(containerId) {
|
|
1563
|
+
try {
|
|
1564
|
+
const { stdout } = await execFileAsync(getDockerCmd(), [
|
|
1565
|
+
"inspect",
|
|
1566
|
+
"-f",
|
|
1567
|
+
"{{.State.Running}}",
|
|
1568
|
+
containerId
|
|
1569
|
+
]);
|
|
1570
|
+
return stdout.trim() === "true";
|
|
1571
|
+
} catch (err) {
|
|
1572
|
+
return !/no such (object|container)/i.test(err.stderr ?? "");
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
const forwardedPortsByContainer = /* @__PURE__ */ new Map();
|
|
1576
|
+
async function attachLoopbackForwarder(containerId, wiring, ports = forwardedPortsByContainer.get(containerId) ?? wiring.loopbackPorts) {
|
|
1577
|
+
forwardedPortsByContainer.set(containerId, ports);
|
|
1578
|
+
if (ports.length === 0) return;
|
|
1579
|
+
const label = wiring.mode === "emulator" ? `--aws-emulator ${wiring.emulatorEndpoint?.url ?? ""}` : "The AWS endpoint override (AWS_ENDPOINT_URL*)";
|
|
1580
|
+
let result;
|
|
1581
|
+
try {
|
|
1582
|
+
result = await startLoopbackForwarder({
|
|
1583
|
+
workloadContainer: containerId,
|
|
1584
|
+
ports,
|
|
1585
|
+
probe: wiring.probeHealth ? "health" : "tcp",
|
|
1586
|
+
...wiring.caHostPath ? { caHostPath: wiring.caHostPath } : {}
|
|
1587
|
+
});
|
|
1588
|
+
} catch (err) {
|
|
1589
|
+
if (!await isContainerRunning(containerId)) return;
|
|
1590
|
+
const message = `${label}: ${err.message}`;
|
|
1591
|
+
if (wiring.mode === "emulator") {
|
|
1592
|
+
await removeContainer(containerId);
|
|
1593
|
+
throw new DockerRunnerError(message);
|
|
1594
|
+
}
|
|
1595
|
+
warnForwarderOnce("start", message);
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1598
|
+
if (result.ok) return;
|
|
1599
|
+
const message = describeForwarderFailure(result, label);
|
|
1600
|
+
if (wiring.mode === "emulator") {
|
|
1601
|
+
await removeContainer(containerId);
|
|
1602
|
+
throw new DockerRunnerError(message);
|
|
1603
|
+
}
|
|
1604
|
+
warnForwarderOnce(String(result.failedPort ?? 0), message);
|
|
1605
|
+
}
|
|
1606
|
+
async function runDetachedContainer(opts) {
|
|
1607
|
+
const args = opts.autoRemove === false ? ["run", "-d"] : [
|
|
1608
|
+
"run",
|
|
1609
|
+
"-d",
|
|
1610
|
+
"--rm"
|
|
1611
|
+
];
|
|
1612
|
+
if (opts.name) args.push("--name", opts.name);
|
|
1613
|
+
if (opts.network) args.push("--network", opts.network);
|
|
1614
|
+
if (opts.platform) {
|
|
1615
|
+
args.push("--platform", opts.platform);
|
|
1616
|
+
warnIfEmulatedPlatform(opts.platform, opts.name ? { label: opts.name } : {});
|
|
1617
|
+
}
|
|
1618
|
+
if (opts.extraHosts) for (const entry of opts.extraHosts) args.push("--add-host", `${entry.host}:${entry.ip}`);
|
|
1619
|
+
const host = opts.host ?? "127.0.0.1";
|
|
1620
|
+
args.push("-p", `${host}:${opts.hostPort}:${opts.containerPort ?? 8080}`);
|
|
1621
|
+
if (opts.debugPort !== void 0) args.push("-p", `${host}:${opts.debugPort}:${opts.debugPort}`);
|
|
1622
|
+
for (const extra of opts.extraPorts ?? []) args.push("-p", `${host}:${extra.hostPort}:${extra.containerPort}`);
|
|
1623
|
+
for (const mount of opts.mounts) {
|
|
1624
|
+
const ro = mount.readOnly ? ":ro" : "";
|
|
1625
|
+
args.push("-v", `${mount.hostPath}:${mount.containerPath}${ro}`);
|
|
1626
|
+
}
|
|
1627
|
+
if (opts.extraMounts) for (const mount of opts.extraMounts) {
|
|
1628
|
+
const ro = mount.readOnly ? ":ro" : "";
|
|
1629
|
+
args.push("-v", `${mount.hostPath}:${mount.containerPath}${ro}`);
|
|
1630
|
+
}
|
|
1631
|
+
const sensitiveKeys = opts.sensitiveEnvKeys && opts.sensitiveEnvKeys.size > 0 ? /* @__PURE__ */ new Set([...SENSITIVE_ENV_KEYS, ...opts.sensitiveEnvKeys]) : SENSITIVE_ENV_KEYS;
|
|
1632
|
+
const { passthrough: passthroughEnv, collisions } = appendEnvFlags(args, opts.env, sensitiveKeys);
|
|
1633
|
+
if (opts.tmpfs) args.push("--tmpfs", `${opts.tmpfs.target}:rw,size=${opts.tmpfs.sizeMb}m`);
|
|
1634
|
+
if (opts.workingDir) args.push("--workdir", opts.workingDir);
|
|
1635
|
+
let entryPointTail = [];
|
|
1636
|
+
if (opts.entryPoint && opts.entryPoint.length > 0) {
|
|
1637
|
+
args.push("--entrypoint", opts.entryPoint[0]);
|
|
1638
|
+
entryPointTail = opts.entryPoint.slice(1);
|
|
1639
|
+
}
|
|
1640
|
+
args.push(opts.image, ...entryPointTail, ...opts.cmd);
|
|
1641
|
+
const logger = getLogger().child("docker");
|
|
1642
|
+
logger.debug(`${getDockerCmd()} ${redactDockerArgvValues(args).map((a) => displayUntrustedValue(a)).join(" ")}`);
|
|
1643
|
+
if (collisions.length > 0) logger.warn(describeRefusedSensitiveEnvKeys(`Container for image ${displayUntrustedValue(opts.image)}`, collisions));
|
|
1644
|
+
const forwardedKeys = Object.keys(passthroughEnv);
|
|
1645
|
+
const refusal = finchSecretArgvRefusal(forwardedKeys.filter((k) => opts.sensitiveEnvKeys?.has(k)), {
|
|
1646
|
+
label: "Container for image",
|
|
1647
|
+
name: opts.image
|
|
1648
|
+
});
|
|
1649
|
+
if (refusal !== void 0) throw new DockerRunnerError(refusal);
|
|
1650
|
+
warnFinchArgvExposure(forwardedKeys);
|
|
1651
|
+
try {
|
|
1652
|
+
const { stdout } = await execFileAsync(getDockerCmd(), args, {
|
|
1653
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
1654
|
+
...execEnvForSecrets(passthroughEnv)
|
|
1655
|
+
});
|
|
1656
|
+
return stdout.trim();
|
|
1657
|
+
} catch (error) {
|
|
1658
|
+
throw new DockerRunnerError(`docker run failed: ${displayToolOutput(error.stderr?.trim() || describeExitWithoutArgv(error))}`);
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
const PREFIX_CONTAINER_OUTPUT_ENV = "CDKL_PREFIX_CONTAINER_OUTPUT";
|
|
1662
|
+
const MAX_HELD_LINE_CHARS = 64 * 1024;
|
|
1663
|
+
function createPrefixedLineRelay(prefix, out) {
|
|
1664
|
+
const decoder = new StringDecoder("utf8");
|
|
1665
|
+
let pending = "";
|
|
1666
|
+
const emit = (text) => {
|
|
1667
|
+
let nl = text.indexOf("\n");
|
|
1668
|
+
if (nl !== -1) nl += pending.length;
|
|
1669
|
+
pending += text;
|
|
1670
|
+
while (nl !== -1) {
|
|
1671
|
+
out.write(`${prefix}${pending.slice(0, nl + 1)}`);
|
|
1672
|
+
pending = pending.slice(nl + 1);
|
|
1673
|
+
nl = pending.indexOf("\n");
|
|
1674
|
+
}
|
|
1675
|
+
while (pending.length >= MAX_HELD_LINE_CHARS) {
|
|
1676
|
+
const high = pending.charCodeAt(MAX_HELD_LINE_CHARS - 1);
|
|
1677
|
+
const cut = high >= 55296 && high <= 56319 ? MAX_HELD_LINE_CHARS - 1 : MAX_HELD_LINE_CHARS;
|
|
1678
|
+
out.write(`${prefix}${pending.slice(0, cut)}\n`);
|
|
1679
|
+
pending = pending.slice(cut);
|
|
1680
|
+
}
|
|
1681
|
+
};
|
|
1682
|
+
return {
|
|
1683
|
+
push: (chunk) => emit(decoder.write(chunk)),
|
|
1684
|
+
flush: () => {
|
|
1685
|
+
emit(decoder.end());
|
|
1686
|
+
if (pending.length > 0) {
|
|
1687
|
+
out.write(`${prefix}${pending}\n`);
|
|
1688
|
+
pending = "";
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
};
|
|
1692
|
+
}
|
|
1693
|
+
function streamLogs(containerId, opts = {}) {
|
|
1694
|
+
const proc = spawn(getDockerCmd(), [
|
|
1695
|
+
"logs",
|
|
1696
|
+
"-f",
|
|
1697
|
+
containerId
|
|
1698
|
+
], { stdio: [
|
|
1699
|
+
"ignore",
|
|
1700
|
+
"pipe",
|
|
1701
|
+
"pipe"
|
|
1702
|
+
] });
|
|
1703
|
+
if (opts.onOutput) {
|
|
1704
|
+
const tap = opts.onOutput;
|
|
1705
|
+
proc.stdout?.on("data", (chunk) => tap(chunk));
|
|
1706
|
+
proc.stderr?.on("data", (chunk) => tap(chunk));
|
|
1707
|
+
}
|
|
1708
|
+
let flush = () => void 0;
|
|
1709
|
+
if (opts.prefix !== void 0 || process.env["CDKL_PREFIX_CONTAINER_OUTPUT"] === "1") {
|
|
1710
|
+
const tag = opts.prefix ?? `[${flattenToOneLine(containerId.slice(0, 12)).replace(/[\]\s]/g, "_")}] `;
|
|
1711
|
+
const stdoutRelay = createPrefixedLineRelay(tag, process.stdout);
|
|
1712
|
+
const stderrRelay = createPrefixedLineRelay(tag, process.stderr);
|
|
1713
|
+
proc.stdout?.on("data", (chunk) => stdoutRelay.push(chunk));
|
|
1714
|
+
proc.stderr?.on("data", (chunk) => stderrRelay.push(chunk));
|
|
1715
|
+
flush = () => {
|
|
1716
|
+
stdoutRelay.flush();
|
|
1717
|
+
stderrRelay.flush();
|
|
1718
|
+
};
|
|
1719
|
+
proc.on("close", () => flush());
|
|
1720
|
+
} else {
|
|
1721
|
+
proc.stdout?.on("data", (chunk) => process.stdout.write(chunk));
|
|
1722
|
+
proc.stderr?.on("data", (chunk) => process.stderr.write(chunk));
|
|
1723
|
+
}
|
|
1724
|
+
proc.on("error", () => {});
|
|
1725
|
+
return () => {
|
|
1726
|
+
flush();
|
|
1727
|
+
if (!proc.killed) proc.kill("SIGTERM");
|
|
1728
|
+
};
|
|
1729
|
+
}
|
|
1730
|
+
async function removeContainer(containerId) {
|
|
1731
|
+
if (!containerId) return;
|
|
1732
|
+
const logger = getLogger().child("docker");
|
|
1733
|
+
try {
|
|
1734
|
+
await execFileAsync(getDockerCmd(), [
|
|
1735
|
+
"rm",
|
|
1736
|
+
"-f",
|
|
1737
|
+
containerId
|
|
1738
|
+
]);
|
|
1739
|
+
logger.debug(`Removed container ${containerId}`);
|
|
1740
|
+
} catch (error) {
|
|
1741
|
+
const err = error;
|
|
1742
|
+
logger.debug(`docker rm -f ${containerId} failed: ${err.stderr || err.message || String(error)}`);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
async function ensureDockerAvailable() {
|
|
1746
|
+
const cmd = getDockerCmd();
|
|
1747
|
+
try {
|
|
1748
|
+
await execFileAsync(cmd, [
|
|
1749
|
+
"version",
|
|
1750
|
+
"--format",
|
|
1751
|
+
"{{.Server.Version}}"
|
|
1752
|
+
]);
|
|
1753
|
+
} catch (error) {
|
|
1754
|
+
const err = error;
|
|
1755
|
+
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.`);
|
|
1756
|
+
throw new DockerRunnerError(`${cmd} daemon is not reachable: ${err.stderr?.trim() || err.message || String(error)}. Start Docker Desktop / the docker daemon and retry.`);
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
function pickFreePort() {
|
|
1760
|
+
return new Promise((resolvePort, rejectPort) => {
|
|
1761
|
+
const server = createServer();
|
|
1762
|
+
server.unref();
|
|
1763
|
+
server.on("error", rejectPort);
|
|
1764
|
+
server.listen(0, "127.0.0.1", () => {
|
|
1765
|
+
const address = server.address();
|
|
1766
|
+
if (!address || typeof address === "string") {
|
|
1767
|
+
server.close();
|
|
1768
|
+
rejectPort(/* @__PURE__ */ new Error("Could not allocate a host port"));
|
|
1769
|
+
return;
|
|
1770
|
+
}
|
|
1771
|
+
const port = address.port;
|
|
1772
|
+
server.close(() => resolvePort(port));
|
|
1773
|
+
});
|
|
1774
|
+
});
|
|
1775
|
+
}
|
|
1776
|
+
const SENSITIVE_ENV_KEYS = /* @__PURE__ */ new Set([
|
|
1777
|
+
"AWS_ACCESS_KEY_ID",
|
|
1778
|
+
"AWS_SECRET_ACCESS_KEY",
|
|
1779
|
+
"AWS_SESSION_TOKEN"
|
|
1780
|
+
]);
|
|
1781
|
+
function appendEnvFlags(args, env, sensitiveKeys) {
|
|
1782
|
+
const passthrough = {};
|
|
1783
|
+
const collisions = [];
|
|
1784
|
+
for (const [k, v] of Object.entries(env)) if (sensitiveKeys.has(k)) {
|
|
1785
|
+
if (isMalformedEnvKey(k) || isDockerClientEnvKey(k)) {
|
|
1786
|
+
collisions.push(k);
|
|
1787
|
+
continue;
|
|
1788
|
+
}
|
|
1789
|
+
args.push("-e", k);
|
|
1790
|
+
defineOwnKey(passthrough, k, v);
|
|
1791
|
+
} else args.push("-e", `${k}=${v}`);
|
|
1792
|
+
return {
|
|
1793
|
+
passthrough,
|
|
1794
|
+
collisions
|
|
1795
|
+
};
|
|
1796
|
+
}
|
|
1797
|
+
function describeRefusedSensitiveEnvKeys(subject, keys) {
|
|
1798
|
+
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.`;
|
|
1799
|
+
}
|
|
1800
|
+
function execEnvForSecrets(passthrough) {
|
|
1801
|
+
const keys = Object.keys(passthrough).filter((k) => !isMalformedEnvKey(k) && !isDockerClientEnvKey(k));
|
|
1802
|
+
if (keys.length === 0) return {};
|
|
1803
|
+
const env = { ...process.env };
|
|
1804
|
+
for (const k of keys) defineOwnKey(env, k, passthrough[k]);
|
|
1805
|
+
return { env };
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
//#endregion
|
|
1809
|
+
export { parseEcrRegistryHost as A, RouteDiscoveryError as B, warnIfEmulatedPlatform as C, buildDockerImage as D, pullEcrImage as E, tryResolveImageFnJoin as F, applyRoleArnIfSet as G, withErrorHandling as H, CdkLocalError as I, isIamRoleArn as J, assumeRoleCredentials as K, InteractiveTtyRequiredError as L, derivePseudoParametersFromRegion as M, formatStateRemedy as N, resolveAssetSourcePath as O, substituteImagePlaceholders as P, LocalInvokeBuildError as R, buildContainerImage as S, parseEcrUri as T, defineOwnKey as U, TargetSelectionCancelledError as V, AssumeRoleFailure as W, buildStsClientConfig as X, refusedRoleArnMessage as Y, resolveProfileCredentials as Z, buildLoopbackForwarderDockerArgs as _, describeRefusedSensitiveEnvKeys as a, startLoopbackForwarder as b, execEnvForSecrets as c, pullImage as d, removeContainer as f, LOOPBACK_FORWARDER_IMAGE as g, withWorkloadAwsWiring as h, attachLoopbackForwarder as i, derivePartitionAndUrlSuffix as j, describeExitWithoutArgv as k, pickFreePort as l, streamLogs as m, SENSITIVE_ENV_KEYS as n, docker_runner_exports as o, runDetached as p, describeRejectedRoleArn as q, appendEnvFlags as r, ensureDockerAvailable as s, DockerRunnerError as t, prepareLoopbackForwarder as u, describeForwarderFailure as v, isImageInLocalCache as w, architectureToPlatform as x, ensureForwarderImage as y, LocalStartServiceError as z };
|
|
1810
|
+
//# sourceMappingURL=docker-runner-CnXQIUjd.js.map
|