cdk-local 0.44.0 → 0.46.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 +3 -3
- package/dist/cli.js +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/{local-list-EweOsy3A.js → local-list-DL6DlYSN.js} +832 -127
- package/dist/local-list-DL6DlYSN.js.map +1 -0
- package/package.json +1 -1
- package/dist/local-list-EweOsy3A.js.map +0 -1
|
@@ -1235,6 +1235,22 @@ function resolveAgentCoreTarget(target, stacks, imageContext) {
|
|
|
1235
1235
|
return extractRuntimeProperties(stack, logicalId, resource, resources, imageContext);
|
|
1236
1236
|
}
|
|
1237
1237
|
/**
|
|
1238
|
+
* Best-effort pick of the candidate stack a target lives in, BEFORE full
|
|
1239
|
+
* resolution — so the command can build a `--from-cfn-stack` image-resolution
|
|
1240
|
+
* context (state load + pseudo parameters) and thread it into
|
|
1241
|
+
* {@link resolveAgentCoreTarget} so a same-stack `AWS::ECR::Repository`
|
|
1242
|
+
* `Fn::Join` ContainerUri resolves to the deployed URI. Returns undefined when
|
|
1243
|
+
* the stack is ambiguous (multi-stack app, no prefix) — the caller proceeds
|
|
1244
|
+
* without a context and the resolver surfaces its own error if one is needed.
|
|
1245
|
+
* Mirrors `run-task`'s `pickCandidateStack`.
|
|
1246
|
+
*/
|
|
1247
|
+
function pickAgentCoreCandidateStack(target, stacks) {
|
|
1248
|
+
const parsed = parseTarget(target);
|
|
1249
|
+
if (parsed.stackPattern === null) return stacks.length === 1 ? stacks[0] : void 0;
|
|
1250
|
+
const matched = matchStacks(stacks, [parsed.stackPattern]);
|
|
1251
|
+
return matched.length === 1 ? matched[0] : void 0;
|
|
1252
|
+
}
|
|
1253
|
+
/**
|
|
1238
1254
|
* Single-stack auto-detect: if the app has exactly one stack the user may
|
|
1239
1255
|
* omit the stack prefix; otherwise an explicit stack pattern is required.
|
|
1240
1256
|
* Mirrors the Lambda / ECS resolvers' behavior via the shared
|
|
@@ -7794,7 +7810,7 @@ async function localInvokeCommand(target, options, extraStateProviders) {
|
|
|
7794
7810
|
}
|
|
7795
7811
|
}
|
|
7796
7812
|
if (!assumeSucceeded) {
|
|
7797
|
-
forwardAwsEnv$
|
|
7813
|
+
forwardAwsEnv$3(dockerEnv);
|
|
7798
7814
|
applyProfileCredentialsOverlay(dockerEnv, profileCredentials, false);
|
|
7799
7815
|
if (profileCredsFile) {
|
|
7800
7816
|
dockerEnv["AWS_SHARED_CREDENTIALS_FILE"] = profileCredsFile.containerPath;
|
|
@@ -7849,14 +7865,14 @@ async function localInvokeCommand(target, options, extraStateProviders) {
|
|
|
7849
7865
|
}
|
|
7850
7866
|
}
|
|
7851
7867
|
async function resolveImagePlan(lambda, options) {
|
|
7852
|
-
if (lambda.kind === "zip") return resolveZipImagePlan(lambda, options);
|
|
7853
|
-
return resolveContainerImagePlan(lambda, options);
|
|
7868
|
+
if (lambda.kind === "zip") return resolveZipImagePlan$1(lambda, options);
|
|
7869
|
+
return resolveContainerImagePlan$1(lambda, options);
|
|
7854
7870
|
}
|
|
7855
|
-
async function resolveZipImagePlan(lambda, options) {
|
|
7871
|
+
async function resolveZipImagePlan$1(lambda, options) {
|
|
7856
7872
|
let inlineTmpDir;
|
|
7857
7873
|
let codeDir = lambda.codePath;
|
|
7858
7874
|
if (codeDir === null) {
|
|
7859
|
-
inlineTmpDir = materializeInlineCode$
|
|
7875
|
+
inlineTmpDir = materializeInlineCode$2(lambda.handler, lambda.inlineCode ?? "", resolveRuntimeFileExtension(lambda.runtime));
|
|
7860
7876
|
codeDir = inlineTmpDir;
|
|
7861
7877
|
}
|
|
7862
7878
|
const image = resolveRuntimeImage(lambda.runtime);
|
|
@@ -7933,7 +7949,7 @@ function materializeLambdaLayers$1(layers) {
|
|
|
7933
7949
|
tmpDir
|
|
7934
7950
|
};
|
|
7935
7951
|
}
|
|
7936
|
-
async function resolveContainerImagePlan(lambda, options) {
|
|
7952
|
+
async function resolveContainerImagePlan$1(lambda, options) {
|
|
7937
7953
|
const logger = getLogger();
|
|
7938
7954
|
const platform = architectureToPlatform(lambda.architecture);
|
|
7939
7955
|
const localBuild = await resolveLocalBuildPlan$1(lambda);
|
|
@@ -8083,7 +8099,7 @@ async function assumeLambdaExecutionRole$1(roleArn, region) {
|
|
|
8083
8099
|
sts.destroy();
|
|
8084
8100
|
}
|
|
8085
8101
|
}
|
|
8086
|
-
function forwardAwsEnv$
|
|
8102
|
+
function forwardAwsEnv$3(env) {
|
|
8087
8103
|
for (const key of [
|
|
8088
8104
|
"AWS_ACCESS_KEY_ID",
|
|
8089
8105
|
"AWS_SECRET_ACCESS_KEY",
|
|
@@ -8123,7 +8139,7 @@ function applyProfileCredentialsOverlay(env, profileCreds, assumeRoleActive) {
|
|
|
8123
8139
|
if (profileCreds.sessionToken) env["AWS_SESSION_TOKEN"] = profileCreds.sessionToken;
|
|
8124
8140
|
else delete env["AWS_SESSION_TOKEN"];
|
|
8125
8141
|
}
|
|
8126
|
-
function materializeInlineCode$
|
|
8142
|
+
function materializeInlineCode$2(handler, source, fileExtension) {
|
|
8127
8143
|
const lastDot = handler.lastIndexOf(".");
|
|
8128
8144
|
if (lastDot <= 0) throw new Error(`Handler '${handler}' is malformed: expected '<modulePath>.<exportName>'.`);
|
|
8129
8145
|
const modulePath = handler.substring(0, lastDot);
|
|
@@ -8138,10 +8154,10 @@ function suggestAssumeRoleFromState(state, logicalId) {
|
|
|
8138
8154
|
const roleArn = resolveExecutionRoleArnFromState(state, logicalId);
|
|
8139
8155
|
if (roleArn) logger.info(`Hint: the deployed function uses execution role ${roleArn}. Re-run with --assume-role to invoke under the deployed function's narrow permissions.`);
|
|
8140
8156
|
}
|
|
8141
|
-
function resolveExecutionRoleArnFromState(state, logicalId) {
|
|
8157
|
+
function resolveExecutionRoleArnFromState(state, logicalId, roleProperty = "Role") {
|
|
8142
8158
|
const lambda = state.resources[logicalId];
|
|
8143
8159
|
if (!lambda) return void 0;
|
|
8144
|
-
const roleRef = lambda.properties?.[
|
|
8160
|
+
const roleRef = lambda.properties?.[roleProperty] ?? lambda.observedProperties?.[roleProperty];
|
|
8145
8161
|
if (typeof roleRef === "string" && roleRef.startsWith("arn:")) return roleRef;
|
|
8146
8162
|
if (typeof roleRef === "object" && roleRef !== null) {
|
|
8147
8163
|
const refLogicalId = pickReferencedLogicalId(roleRef);
|
|
@@ -12749,7 +12765,7 @@ const MOCK_API_ID = "local";
|
|
|
12749
12765
|
* UTF-8; otherwise base64. Mirrors what API Gateway emits.
|
|
12750
12766
|
*/
|
|
12751
12767
|
function buildHttpApiV2Event(req, ctx, opts = {}) {
|
|
12752
|
-
const { rawPath, rawQueryString } = splitRawUrl$
|
|
12768
|
+
const { rawPath, rawQueryString } = splitRawUrl$2(req.rawUrl);
|
|
12753
12769
|
const { headers, cookies } = normalizeHeadersV2(req.headers);
|
|
12754
12770
|
const queryStringParameters = parseQueryStringV2(rawQueryString);
|
|
12755
12771
|
const userAgent = headers["user-agent"] ?? "";
|
|
@@ -12809,7 +12825,7 @@ function buildHttpApiV2Event(req, ctx, opts = {}) {
|
|
|
12809
12825
|
* - `pathParameters` may be `null` when there are none (matches AWS).
|
|
12810
12826
|
*/
|
|
12811
12827
|
function buildRestV1Event(req, ctx, opts = {}) {
|
|
12812
|
-
const { rawPath, rawQueryString } = splitRawUrl$
|
|
12828
|
+
const { rawPath, rawQueryString } = splitRawUrl$2(req.rawUrl);
|
|
12813
12829
|
const { singular: headers, multi: multiValueHeaders } = normalizeHeadersV1(req.headers);
|
|
12814
12830
|
const { singular: queryStringParameters, multi: multiValueQueryStringParameters } = parseQueryStringV1(rawQueryString);
|
|
12815
12831
|
const contentType = headers["content-type"] ?? "";
|
|
@@ -12899,7 +12915,7 @@ function applyAuthorizerOverlay(event, overlay) {
|
|
|
12899
12915
|
* `rawQueryString` (everything after, or `''`). Neither component is
|
|
12900
12916
|
* decoded — that's the whole point of "raw" per the AWS spec.
|
|
12901
12917
|
*/
|
|
12902
|
-
function splitRawUrl$
|
|
12918
|
+
function splitRawUrl$2(rawUrl) {
|
|
12903
12919
|
const q = rawUrl.indexOf("?");
|
|
12904
12920
|
if (q === -1) return {
|
|
12905
12921
|
rawPath: rawUrl,
|
|
@@ -13030,7 +13046,7 @@ function encodeBody(body, contentType) {
|
|
|
13030
13046
|
body: "",
|
|
13031
13047
|
isBase64Encoded: false
|
|
13032
13048
|
};
|
|
13033
|
-
if (isTextualContentType(contentType)) return {
|
|
13049
|
+
if (isTextualContentType$1(contentType)) return {
|
|
13034
13050
|
body: body.toString("utf-8"),
|
|
13035
13051
|
isBase64Encoded: false
|
|
13036
13052
|
};
|
|
@@ -13051,7 +13067,7 @@ const TEXT_PREFIXES = [
|
|
|
13051
13067
|
* Whether the given `Content-Type` value indicates textual data (so the
|
|
13052
13068
|
* event body should be UTF-8 instead of base64).
|
|
13053
13069
|
*/
|
|
13054
|
-
function isTextualContentType(contentType) {
|
|
13070
|
+
function isTextualContentType$1(contentType) {
|
|
13055
13071
|
if (!contentType) return false;
|
|
13056
13072
|
const lower = contentType.toLowerCase();
|
|
13057
13073
|
return TEXT_PREFIXES.some((p) => lower.startsWith(p));
|
|
@@ -13116,7 +13132,7 @@ function formatRequestTime(d) {
|
|
|
13116
13132
|
* v2 separates them.
|
|
13117
13133
|
*/
|
|
13118
13134
|
function translateLambdaResponse(payload, version) {
|
|
13119
|
-
if (isErrorEnvelope(payload)) return errorEnvelopeResponse();
|
|
13135
|
+
if (isErrorEnvelope$1(payload)) return errorEnvelopeResponse();
|
|
13120
13136
|
if (isShapedResponse(payload)) return translateShapedResponse(payload, version);
|
|
13121
13137
|
return autoFormatResponse(payload);
|
|
13122
13138
|
}
|
|
@@ -13127,7 +13143,7 @@ function translateLambdaResponse(payload, version) {
|
|
|
13127
13143
|
* matters because user code MAY return a Lambda Proxy response that
|
|
13128
13144
|
* happens to have `errorMessage` as a payload field.
|
|
13129
13145
|
*/
|
|
13130
|
-
function isErrorEnvelope(payload) {
|
|
13146
|
+
function isErrorEnvelope$1(payload) {
|
|
13131
13147
|
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
|
|
13132
13148
|
const obj = payload;
|
|
13133
13149
|
if ("statusCode" in obj) return false;
|
|
@@ -13185,7 +13201,7 @@ function translateShapedResponse(payload, version) {
|
|
|
13185
13201
|
const headersIn = payload["headers"];
|
|
13186
13202
|
if (headersIn && typeof headersIn === "object" && !Array.isArray(headersIn)) for (const [name, value] of Object.entries(headersIn)) {
|
|
13187
13203
|
const lower = name.toLowerCase();
|
|
13188
|
-
const stringValue = stringifyHeaderValue(value);
|
|
13204
|
+
const stringValue = stringifyHeaderValue$1(value);
|
|
13189
13205
|
if (lower === "set-cookie") {
|
|
13190
13206
|
cookies.push(stringValue);
|
|
13191
13207
|
continue;
|
|
@@ -13197,7 +13213,7 @@ function translateShapedResponse(payload, version) {
|
|
|
13197
13213
|
if (mvh && typeof mvh === "object" && !Array.isArray(mvh)) for (const [name, values] of Object.entries(mvh)) {
|
|
13198
13214
|
if (!Array.isArray(values)) continue;
|
|
13199
13215
|
const lower = name.toLowerCase();
|
|
13200
|
-
const stringified = values.map((v) => stringifyHeaderValue(v));
|
|
13216
|
+
const stringified = values.map((v) => stringifyHeaderValue$1(v));
|
|
13201
13217
|
if (lower === "set-cookie") {
|
|
13202
13218
|
for (const c of stringified) cookies.push(c);
|
|
13203
13219
|
continue;
|
|
@@ -13242,7 +13258,7 @@ function autoFormatResponse(payload) {
|
|
|
13242
13258
|
* comma-joined to match the same dup-coalesce rule used on the request
|
|
13243
13259
|
* side.
|
|
13244
13260
|
*/
|
|
13245
|
-
function stringifyHeaderValue(value) {
|
|
13261
|
+
function stringifyHeaderValue$1(value) {
|
|
13246
13262
|
if (value === null || value === void 0) return "";
|
|
13247
13263
|
if (Array.isArray(value)) return value.map((v) => stringifyValue(v)).join(",");
|
|
13248
13264
|
return stringifyValue(value);
|
|
@@ -14040,7 +14056,7 @@ function parseAuthorizationHeader(value) {
|
|
|
14040
14056
|
* Signature = HexEncode(HMAC(SigningKey, StringToSign))
|
|
14041
14057
|
*/
|
|
14042
14058
|
function computeSignature(req, parsed, secretAccessKey, amzDate) {
|
|
14043
|
-
const { path, query } = splitRawUrl(req.rawUrl);
|
|
14059
|
+
const { path, query } = splitRawUrl$1(req.rawUrl);
|
|
14044
14060
|
const canonicalUri = canonicalizePath(path);
|
|
14045
14061
|
const canonicalQuery = canonicalizeQueryString(query);
|
|
14046
14062
|
const headerLines = [];
|
|
@@ -14081,7 +14097,7 @@ function sha256Hex(buf) {
|
|
|
14081
14097
|
* Important: keep the path RAW for canonicalization — the canonicalizer
|
|
14082
14098
|
* does its own URI-encoding so we do NOT decode here.
|
|
14083
14099
|
*/
|
|
14084
|
-
function splitRawUrl(rawUrl) {
|
|
14100
|
+
function splitRawUrl$1(rawUrl) {
|
|
14085
14101
|
const q = rawUrl.indexOf("?");
|
|
14086
14102
|
if (q < 0) return {
|
|
14087
14103
|
path: rawUrl,
|
|
@@ -16669,7 +16685,7 @@ async function buildContainerSpec(args) {
|
|
|
16669
16685
|
let imageRef;
|
|
16670
16686
|
let platform;
|
|
16671
16687
|
if (lambda.kind === "zip") {
|
|
16672
|
-
codeDir = lambda.codePath ?? materializeInlineCode(lambda.handler, lambda.inlineCode ?? "", resolveRuntimeFileExtension(lambda.runtime), inlineTmpDirs);
|
|
16688
|
+
codeDir = lambda.codePath ?? materializeInlineCode$1(lambda.handler, lambda.inlineCode ?? "", resolveRuntimeFileExtension(lambda.runtime), inlineTmpDirs);
|
|
16673
16689
|
optDir = await materializeLambdaLayers(lambda.layers, layerTmpDirs, layerRoleArn);
|
|
16674
16690
|
} else {
|
|
16675
16691
|
imageRef = (await resolveContainerImageForStartApi(lambda, skipPull, args.profile)).imageRef;
|
|
@@ -16729,7 +16745,7 @@ async function buildContainerSpec(args) {
|
|
|
16729
16745
|
dockerEnv["AWS_SESSION_TOKEN"] = creds.sessionToken;
|
|
16730
16746
|
if (stsRegion) dockerEnv["AWS_REGION"] = stsRegion;
|
|
16731
16747
|
} else {
|
|
16732
|
-
forwardAwsEnv$
|
|
16748
|
+
forwardAwsEnv$2(dockerEnv);
|
|
16733
16749
|
if (profileCredentials) {
|
|
16734
16750
|
dockerEnv["AWS_ACCESS_KEY_ID"] = profileCredentials.accessKeyId;
|
|
16735
16751
|
dockerEnv["AWS_SECRET_ACCESS_KEY"] = profileCredentials.secretAccessKey;
|
|
@@ -17071,7 +17087,7 @@ function formatRestV1IntegrationLabel(integration) {
|
|
|
17071
17087
|
* them. (`cdkl invoke` runs once and `--rm` is the right model;
|
|
17072
17088
|
* `cdkl start-api` lives across requests, so leaks compound.)
|
|
17073
17089
|
*/
|
|
17074
|
-
function materializeInlineCode(handler, source, fileExtension, tmpDirsOut) {
|
|
17090
|
+
function materializeInlineCode$1(handler, source, fileExtension, tmpDirsOut) {
|
|
17075
17091
|
const lastDot = handler.lastIndexOf(".");
|
|
17076
17092
|
if (lastDot <= 0) throw new Error(`Handler '${handler}' is malformed: expected '<modulePath>.<exportName>'.`);
|
|
17077
17093
|
const modulePath = handler.substring(0, lastDot);
|
|
@@ -17113,7 +17129,7 @@ function readEnvOverridesFile$3(filePath) {
|
|
|
17113
17129
|
* handler's AWS SDK calls can authenticate. Used when --assume-role is
|
|
17114
17130
|
* NOT set for that Lambda — SAM-compatible default.
|
|
17115
17131
|
*/
|
|
17116
|
-
function forwardAwsEnv$
|
|
17132
|
+
function forwardAwsEnv$2(env) {
|
|
17117
17133
|
for (const key of [
|
|
17118
17134
|
"AWS_ACCESS_KEY_ID",
|
|
17119
17135
|
"AWS_SECRET_ACCESS_KEY",
|
|
@@ -17615,7 +17631,13 @@ async function localInvokeAgentCoreCommand(target, options, extraStateProviders)
|
|
|
17615
17631
|
let stopLogs;
|
|
17616
17632
|
let sigintHandler;
|
|
17617
17633
|
let profileCredsFile;
|
|
17634
|
+
let stateProvider;
|
|
17618
17635
|
const cleanup = singleFlight(async () => {
|
|
17636
|
+
if (stateProvider) try {
|
|
17637
|
+
stateProvider.dispose();
|
|
17638
|
+
} catch (err) {
|
|
17639
|
+
getLogger().debug(`state provider dispose failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
17640
|
+
}
|
|
17619
17641
|
if (stopLogs) try {
|
|
17620
17642
|
stopLogs();
|
|
17621
17643
|
} catch (err) {
|
|
@@ -17655,12 +17677,19 @@ async function localInvokeAgentCoreCommand(target, options, extraStateProviders)
|
|
|
17655
17677
|
...Object.keys(context).length > 0 && { context }
|
|
17656
17678
|
};
|
|
17657
17679
|
const { stacks } = await synthesizer.synthesize(synthOpts);
|
|
17658
|
-
const
|
|
17680
|
+
const resolvedTarget = await resolveSingleTarget(target, {
|
|
17659
17681
|
entries: listTargets(stacks).agentCoreRuntimes,
|
|
17660
17682
|
message: "Select an AgentCore Runtime to invoke",
|
|
17661
17683
|
noun: "AgentCore Runtimes",
|
|
17662
17684
|
onMissing: () => new CdkLocalError(`${getEmbedConfig().cliName} invoke-agentcore requires a <target> (an AgentCore Runtime display path or logical ID). Run \`${getEmbedConfig().cliName} list\` to see them, or run it in a TTY to pick interactively.`, "LOCAL_INVOKE_AGENTCORE_TARGET_REQUIRED")
|
|
17663
|
-
})
|
|
17685
|
+
});
|
|
17686
|
+
const candidate = pickAgentCoreCandidateStack(resolvedTarget, stacks);
|
|
17687
|
+
stateProvider = createLocalStateProvider(options, candidate?.stackName ?? "", await resolveCfnFallbackRegion(options, candidate?.region), extraStateProviders);
|
|
17688
|
+
const { context: imageContext, loaded: loadedState } = stateProvider && candidate ? await buildAgentCoreImageContext(candidate, stateProvider, options) : {
|
|
17689
|
+
context: void 0,
|
|
17690
|
+
loaded: void 0
|
|
17691
|
+
};
|
|
17692
|
+
const resolved = resolveAgentCoreTarget(resolvedTarget, stacks, imageContext);
|
|
17664
17693
|
logger.info(`Target: ${resolved.stack.stackName}/${resolved.logicalId} (${resolved.protocol})`);
|
|
17665
17694
|
const isMcp = resolved.protocol === "MCP";
|
|
17666
17695
|
const sessionId = options.sessionId ?? randomUUID();
|
|
@@ -17671,7 +17700,7 @@ async function localInvokeAgentCoreCommand(target, options, extraStateProviders)
|
|
|
17671
17700
|
if (resolved.jwtAuthorizer || options.bearerToken) logger.info(`MCP runtime: invoking the local container's ${MCP_PATH} directly (vanilla MCP). An inbound JWT / --bearer-token is an AgentCore managed-plane concern and is not applied locally.`);
|
|
17672
17701
|
} else authorization = await resolveInboundAuthorization(resolved, options);
|
|
17673
17702
|
const image = await resolveAgentCoreImage(resolved, options);
|
|
17674
|
-
const dockerEnv = await buildContainerEnv(resolved, options, profileCredentials, profileCredsFile,
|
|
17703
|
+
const { env: dockerEnv, sensitiveEnvKeys } = await buildContainerEnv(resolved, options, profileCredentials, profileCredsFile, stateProvider, loadedState, imageContext);
|
|
17675
17704
|
const hostPort = await pickFreePort();
|
|
17676
17705
|
const containerHost = options.containerHost;
|
|
17677
17706
|
const containerName = `${getEmbedConfig().resourceNamePrefix}-agentcore-${process.pid}-${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -17686,7 +17715,8 @@ async function localInvokeAgentCoreCommand(target, options, extraStateProviders)
|
|
|
17686
17715
|
host: containerHost,
|
|
17687
17716
|
platform: options.platform,
|
|
17688
17717
|
name: containerName,
|
|
17689
|
-
...isMcp && { containerPort: 8e3 }
|
|
17718
|
+
...isMcp && { containerPort: 8e3 },
|
|
17719
|
+
...sensitiveEnvKeys.size > 0 && { sensitiveEnvKeys }
|
|
17690
17720
|
});
|
|
17691
17721
|
stopLogs = streamLogs(containerId);
|
|
17692
17722
|
sigintHandler = () => {
|
|
@@ -17818,32 +17848,37 @@ function findFileAssetByObjectKey(fileAssets, hash) {
|
|
|
17818
17848
|
for (const asset of fileAssets.values()) if (Object.values(asset.destinations).some((d) => d.objectKey === zip || d.objectKey.endsWith(`/${zip}`))) return asset;
|
|
17819
17849
|
}
|
|
17820
17850
|
/**
|
|
17821
|
-
* Build the container env
|
|
17822
|
-
*
|
|
17823
|
-
*
|
|
17851
|
+
* Build the container env + the set of env keys to keep off the `docker run`
|
|
17852
|
+
* argv. Substitutes `--from-cfn-stack` state into the template env (reusing the
|
|
17853
|
+
* shared state load + image-resolution context — Ref / Fn::Sub / Fn::Join +
|
|
17854
|
+
* SSM parameters, with decrypted SecureString values flagged sensitive),
|
|
17855
|
+
* applies `--env-vars` overrides, then injects AWS credentials (`--assume-role`
|
|
17856
|
+
* STS temp creds — resolving an intrinsic RoleArn from state for bare
|
|
17857
|
+
* `--assume-role` — else `--profile` / dev creds).
|
|
17858
|
+
*
|
|
17859
|
+
* The state provider + loaded record + image context are built once by the
|
|
17860
|
+
* caller and shared here, so this does not re-load state.
|
|
17824
17861
|
*/
|
|
17825
|
-
async function buildContainerEnv(resolved, options, profileCredentials, profileCredsFile,
|
|
17862
|
+
async function buildContainerEnv(resolved, options, profileCredentials, profileCredsFile, stateProvider, loaded, imageContext) {
|
|
17826
17863
|
const logger = getLogger();
|
|
17827
17864
|
let templateEnv = resolved.environmentVariables;
|
|
17828
|
-
const
|
|
17829
|
-
if (stateProvider)
|
|
17830
|
-
const
|
|
17831
|
-
|
|
17832
|
-
|
|
17833
|
-
|
|
17834
|
-
|
|
17835
|
-
|
|
17836
|
-
|
|
17837
|
-
|
|
17838
|
-
|
|
17839
|
-
|
|
17840
|
-
|
|
17841
|
-
|
|
17842
|
-
|
|
17843
|
-
|
|
17844
|
-
}
|
|
17845
|
-
} finally {
|
|
17846
|
-
stateProvider.dispose();
|
|
17865
|
+
const sensitiveEnvKeys = /* @__PURE__ */ new Set();
|
|
17866
|
+
if (stateProvider && loaded) {
|
|
17867
|
+
const subContext = {
|
|
17868
|
+
resources: imageContext?.stateResources ?? loaded.resources,
|
|
17869
|
+
consumerRegion: loaded.region
|
|
17870
|
+
};
|
|
17871
|
+
const pseudo = imageContext?.pseudoParameters ?? derivePseudoParametersFromRegion(loaded.region);
|
|
17872
|
+
if (pseudo) subContext.pseudoParameters = pseudo;
|
|
17873
|
+
if (imageContext?.stateParameters) subContext.parameters = imageContext.stateParameters;
|
|
17874
|
+
if (imageContext?.stateSensitiveParameters?.length) subContext.sensitiveParameters = new Set(imageContext.stateSensitiveParameters);
|
|
17875
|
+
const resolver = await stateProvider.buildCrossStackResolver(loaded.region);
|
|
17876
|
+
if (resolver) subContext.crossStackResolver = resolver;
|
|
17877
|
+
const { env, audit } = await substituteEnvVarsFromStateAsync(templateEnv, subContext);
|
|
17878
|
+
templateEnv = env;
|
|
17879
|
+
for (const key of audit.resolvedKeys) logger.debug(`${stateProvider.label}: substituted env var ${key}`);
|
|
17880
|
+
for (const key of audit.sensitiveKeys) sensitiveEnvKeys.add(key);
|
|
17881
|
+
for (const { key, reason } of audit.unresolved) logger.warn(`${stateProvider.label}: could not substitute env var ${key} (${reason}). Override it via --env-vars or it will be dropped.`);
|
|
17847
17882
|
}
|
|
17848
17883
|
const overrides = readEnvOverridesFile$2(options.envVars);
|
|
17849
17884
|
const cdkPath = readCdkPathOrUndefined(resolved.resource);
|
|
@@ -17853,7 +17888,7 @@ async function buildContainerEnv(resolved, options, profileCredentials, profileC
|
|
|
17853
17888
|
logger.warn(`Environment variable ${key} contains a CloudFormation intrinsic and was dropped. Override it with --env-vars (e.g. {"${overrideKeyExample}":{"${key}":"<literal>"}}), or pass a state-source flag (e.g. --from-cfn-stack) to recover deployed values.`);
|
|
17854
17889
|
}
|
|
17855
17890
|
const dockerEnv = { ...envResult.resolved };
|
|
17856
|
-
const assumeRoleArn = resolveAssumeRoleArn(options, resolved);
|
|
17891
|
+
const assumeRoleArn = resolveAssumeRoleArn(options, resolved, loaded);
|
|
17857
17892
|
await applyAgentCoreCredentialEnv(dockerEnv, {
|
|
17858
17893
|
...assumeRoleArn !== void 0 && { assumeRoleArn },
|
|
17859
17894
|
...options.region !== void 0 && { region: options.region },
|
|
@@ -17863,7 +17898,56 @@ async function buildContainerEnv(resolved, options, profileCredentials, profileC
|
|
|
17863
17898
|
profileName: profileCredsFile.profileName
|
|
17864
17899
|
} }
|
|
17865
17900
|
});
|
|
17866
|
-
return
|
|
17901
|
+
return {
|
|
17902
|
+
env: dockerEnv,
|
|
17903
|
+
sensitiveEnvKeys
|
|
17904
|
+
};
|
|
17905
|
+
}
|
|
17906
|
+
/**
|
|
17907
|
+
* Build the `--from-cfn-stack` image-resolution context + return the loaded
|
|
17908
|
+
* state record (loaded once, reused by env substitution + role resolution).
|
|
17909
|
+
* Mirrors `run-task`'s `buildEcsImageResolutionContext`: pseudo parameters
|
|
17910
|
+
* (region + STS account id), the deployed resources, and SSM template
|
|
17911
|
+
* parameters (decrypted SecureString logical ids flagged sensitive).
|
|
17912
|
+
*/
|
|
17913
|
+
async function buildAgentCoreImageContext(candidate, stateProvider, options) {
|
|
17914
|
+
const logger = getLogger();
|
|
17915
|
+
const region = options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? candidate.region;
|
|
17916
|
+
let accountId;
|
|
17917
|
+
try {
|
|
17918
|
+
accountId = await resolveCallerAccountId$2(region, options.profile);
|
|
17919
|
+
} catch (err) {
|
|
17920
|
+
logger.warn(`--from-cfn-stack: STS GetCallerIdentity failed: ${err instanceof Error ? err.message : String(err)}. A same-stack ECR image URI referencing \${AWS::AccountId} may not resolve.`);
|
|
17921
|
+
}
|
|
17922
|
+
const context = {};
|
|
17923
|
+
const pseudo = derivePseudoParametersFromRegion(region, accountId);
|
|
17924
|
+
if (pseudo) context.pseudoParameters = pseudo;
|
|
17925
|
+
const loaded = await stateProvider.load(candidate.stackName, candidate.region);
|
|
17926
|
+
if (loaded) {
|
|
17927
|
+
context.stateResources = loaded.resources;
|
|
17928
|
+
if (stateProvider.resolveTemplateSsmParameters) {
|
|
17929
|
+
const ssm = await stateProvider.resolveTemplateSsmParameters(candidate.template);
|
|
17930
|
+
if (Object.keys(ssm.values).length > 0) context.stateParameters = ssm.values;
|
|
17931
|
+
if (ssm.secureStringLogicalIds.length > 0) context.stateSensitiveParameters = ssm.secureStringLogicalIds;
|
|
17932
|
+
}
|
|
17933
|
+
}
|
|
17934
|
+
return {
|
|
17935
|
+
context,
|
|
17936
|
+
loaded: loaded ?? void 0
|
|
17937
|
+
};
|
|
17938
|
+
}
|
|
17939
|
+
/** STS `GetCallerIdentity` for the `${AWS::AccountId}` pseudo parameter (threads `--profile`). */
|
|
17940
|
+
async function resolveCallerAccountId$2(region, profile) {
|
|
17941
|
+
const { STSClient, GetCallerIdentityCommand } = await import("@aws-sdk/client-sts");
|
|
17942
|
+
const sts = new STSClient({
|
|
17943
|
+
...region && { region },
|
|
17944
|
+
...profile && { profile }
|
|
17945
|
+
});
|
|
17946
|
+
try {
|
|
17947
|
+
return (await sts.send(new GetCallerIdentityCommand({}))).Account;
|
|
17948
|
+
} finally {
|
|
17949
|
+
sts.destroy();
|
|
17950
|
+
}
|
|
17867
17951
|
}
|
|
17868
17952
|
/**
|
|
17869
17953
|
* Inject AWS credentials into the container env. Precedence:
|
|
@@ -17893,7 +17977,7 @@ async function applyAgentCoreCredentialEnv(dockerEnv, args) {
|
|
|
17893
17977
|
}
|
|
17894
17978
|
}
|
|
17895
17979
|
if (!assumeSucceeded) {
|
|
17896
|
-
forwardAwsEnv(dockerEnv);
|
|
17980
|
+
forwardAwsEnv$1(dockerEnv);
|
|
17897
17981
|
applyProfileCredentialsOverlay(dockerEnv, args.profileCredentials, false);
|
|
17898
17982
|
if (args.profileCredsFile) {
|
|
17899
17983
|
dockerEnv["AWS_SHARED_CREDENTIALS_FILE"] = args.profileCredsFile.containerPath;
|
|
@@ -17903,14 +17987,23 @@ async function applyAgentCoreCredentialEnv(dockerEnv, args) {
|
|
|
17903
17987
|
}
|
|
17904
17988
|
/**
|
|
17905
17989
|
* Resolve the role ARN to assume, honoring the three `--assume-role` forms.
|
|
17906
|
-
* Bare `--assume-role` uses the runtime's literal `RoleArn`;
|
|
17907
|
-
*
|
|
17990
|
+
* Bare `--assume-role` uses the runtime's literal `RoleArn`; when that is an
|
|
17991
|
+
* intrinsic (the common L2 case — `Fn::GetAtt` to an auto-created role) it
|
|
17992
|
+
* resolves the execution-role ARN from `--from-cfn-stack` state, and only
|
|
17993
|
+
* warns + falls back to dev creds when neither is available.
|
|
17908
17994
|
*/
|
|
17909
|
-
function resolveAssumeRoleArn(options, resolved) {
|
|
17995
|
+
function resolveAssumeRoleArn(options, resolved, loaded) {
|
|
17910
17996
|
if (typeof options.assumeRole === "string") return options.assumeRole;
|
|
17911
17997
|
if (options.assumeRole === true) {
|
|
17912
17998
|
if (resolved.roleArn) return resolved.roleArn;
|
|
17913
|
-
|
|
17999
|
+
if (loaded) {
|
|
18000
|
+
const fromState = resolveExecutionRoleArnFromState(loaded, resolved.logicalId, "RoleArn");
|
|
18001
|
+
if (fromState) {
|
|
18002
|
+
getLogger().debug(`--assume-role: resolved RoleArn from state: ${fromState}`);
|
|
18003
|
+
return fromState;
|
|
18004
|
+
}
|
|
18005
|
+
}
|
|
18006
|
+
getLogger().warn("--assume-role passed without an ARN, but the runtime's RoleArn is not a literal ARN in the template " + (loaded ? "and could not be resolved from the deployed stack state. " : "and no --from-cfn-stack state is available to resolve it. ") + "Pass the ARN explicitly: --assume-role <arn>. Falling back to the developer's shell credentials.");
|
|
17914
18007
|
}
|
|
17915
18008
|
}
|
|
17916
18009
|
function emitResult(result) {
|
|
@@ -17962,7 +18055,7 @@ function emitMcpResult(result) {
|
|
|
17962
18055
|
function platformToArchitecture(platform) {
|
|
17963
18056
|
return platform === "linux/amd64" ? "x86_64" : "arm64";
|
|
17964
18057
|
}
|
|
17965
|
-
function forwardAwsEnv(env) {
|
|
18058
|
+
function forwardAwsEnv$1(env) {
|
|
17966
18059
|
for (const key of [
|
|
17967
18060
|
"AWS_ACCESS_KEY_ID",
|
|
17968
18061
|
"AWS_SECRET_ACCESS_KEY",
|
|
@@ -20606,6 +20699,203 @@ var FrontDoorEndpointPool = class {
|
|
|
20606
20699
|
}
|
|
20607
20700
|
};
|
|
20608
20701
|
|
|
20702
|
+
//#endregion
|
|
20703
|
+
//#region src/local/alb-lambda-event.ts
|
|
20704
|
+
/** Content types ALB treats as text (body sent verbatim, `isBase64Encoded: false`). */
|
|
20705
|
+
function isTextualContentType(contentType) {
|
|
20706
|
+
const ct = contentType.toLowerCase();
|
|
20707
|
+
return ct.startsWith("text/") || ct.startsWith("application/json") || ct.startsWith("application/javascript") || ct.startsWith("application/xml");
|
|
20708
|
+
}
|
|
20709
|
+
/**
|
|
20710
|
+
* Build a `AlbHttpRequestSnapshot` from a live `node:http` request plus the
|
|
20711
|
+
* already-buffered body. Header values arrive as `string | string[]`; this
|
|
20712
|
+
* normalizes them to `string[]` (preserving multi-value) the builder expects.
|
|
20713
|
+
*/
|
|
20714
|
+
function snapshotFromIncoming(req, body) {
|
|
20715
|
+
const headers = {};
|
|
20716
|
+
for (const [name, value] of Object.entries(req.headers)) {
|
|
20717
|
+
if (value === void 0) continue;
|
|
20718
|
+
headers[name] = Array.isArray(value) ? value : [value];
|
|
20719
|
+
}
|
|
20720
|
+
return {
|
|
20721
|
+
method: (req.method ?? "GET").toUpperCase(),
|
|
20722
|
+
rawUrl: req.url ?? "/",
|
|
20723
|
+
headers,
|
|
20724
|
+
body
|
|
20725
|
+
};
|
|
20726
|
+
}
|
|
20727
|
+
/** Split a raw URL into its path (query-stripped, not decoded) and raw query string. */
|
|
20728
|
+
function splitRawUrl(rawUrl) {
|
|
20729
|
+
const hashIdx = rawUrl.indexOf("#");
|
|
20730
|
+
const noHash = hashIdx === -1 ? rawUrl : rawUrl.slice(0, hashIdx);
|
|
20731
|
+
const qIdx = noHash.indexOf("?");
|
|
20732
|
+
if (qIdx === -1) return {
|
|
20733
|
+
path: noHash,
|
|
20734
|
+
rawQuery: ""
|
|
20735
|
+
};
|
|
20736
|
+
return {
|
|
20737
|
+
path: noHash.slice(0, qIdx),
|
|
20738
|
+
rawQuery: noHash.slice(qIdx + 1)
|
|
20739
|
+
};
|
|
20740
|
+
}
|
|
20741
|
+
/**
|
|
20742
|
+
* Parse a raw query string into single + multi-value maps. ALB does NOT decode
|
|
20743
|
+
* query parameters (the docs explicitly say "If the query parameters are
|
|
20744
|
+
* URL-encoded, the load balancer does not decode them"), so values are kept
|
|
20745
|
+
* verbatim. A bare `?flag` key yields the empty string value.
|
|
20746
|
+
*/
|
|
20747
|
+
function parseQuery(rawQuery) {
|
|
20748
|
+
const single = {};
|
|
20749
|
+
const multi = {};
|
|
20750
|
+
if (rawQuery.length === 0) return {
|
|
20751
|
+
single,
|
|
20752
|
+
multi
|
|
20753
|
+
};
|
|
20754
|
+
for (const pair of rawQuery.split("&")) {
|
|
20755
|
+
if (pair.length === 0) continue;
|
|
20756
|
+
const eq = pair.indexOf("=");
|
|
20757
|
+
const key = eq === -1 ? pair : pair.slice(0, eq);
|
|
20758
|
+
const value = eq === -1 ? "" : pair.slice(eq + 1);
|
|
20759
|
+
single[key] = value;
|
|
20760
|
+
(multi[key] ??= []).push(value);
|
|
20761
|
+
}
|
|
20762
|
+
return {
|
|
20763
|
+
single,
|
|
20764
|
+
multi
|
|
20765
|
+
};
|
|
20766
|
+
}
|
|
20767
|
+
/**
|
|
20768
|
+
* Build the single + multi-value header maps. Names are lowercased; the single
|
|
20769
|
+
* map keeps the LAST value (ALB default-format), the multi map keeps every
|
|
20770
|
+
* value in arrival order.
|
|
20771
|
+
*/
|
|
20772
|
+
function buildHeaderMaps(headers) {
|
|
20773
|
+
const single = {};
|
|
20774
|
+
const multi = {};
|
|
20775
|
+
for (const [name, values] of Object.entries(headers)) {
|
|
20776
|
+
const lower = name.toLowerCase();
|
|
20777
|
+
const list = values.slice();
|
|
20778
|
+
multi[lower] = list;
|
|
20779
|
+
if (list.length > 0) single[lower] = list[list.length - 1];
|
|
20780
|
+
}
|
|
20781
|
+
return {
|
|
20782
|
+
single,
|
|
20783
|
+
multi
|
|
20784
|
+
};
|
|
20785
|
+
}
|
|
20786
|
+
/** Header lookup that tolerates the case-folded multi-value request map. */
|
|
20787
|
+
function firstHeader(headers, name) {
|
|
20788
|
+
const lower = name.toLowerCase();
|
|
20789
|
+
for (const [k, v] of Object.entries(headers)) if (k.toLowerCase() === lower) return v[0];
|
|
20790
|
+
}
|
|
20791
|
+
/**
|
|
20792
|
+
* Build the ALB Lambda-target invocation event from an HTTP request snapshot.
|
|
20793
|
+
* Emits exactly the single-value OR multi-value variant per
|
|
20794
|
+
* `opts.multiValueHeaders`.
|
|
20795
|
+
*/
|
|
20796
|
+
function buildAlbLambdaEvent(req, opts) {
|
|
20797
|
+
const { path, rawQuery } = splitRawUrl(req.rawUrl);
|
|
20798
|
+
const query = parseQuery(rawQuery);
|
|
20799
|
+
const headerMaps = buildHeaderMaps(req.headers);
|
|
20800
|
+
const contentEncoding = firstHeader(req.headers, "content-encoding");
|
|
20801
|
+
const contentType = firstHeader(req.headers, "content-type") ?? "";
|
|
20802
|
+
const isBase64Encoded = req.body.length > 0 && (contentEncoding !== void 0 ? true : !isTextualContentType(contentType));
|
|
20803
|
+
const body = isBase64Encoded ? req.body.toString("base64") : req.body.toString("utf-8");
|
|
20804
|
+
const event = {
|
|
20805
|
+
requestContext: { elb: { targetGroupArn: opts.targetGroupArn } },
|
|
20806
|
+
httpMethod: req.method,
|
|
20807
|
+
path,
|
|
20808
|
+
isBase64Encoded,
|
|
20809
|
+
body
|
|
20810
|
+
};
|
|
20811
|
+
if (opts.multiValueHeaders) {
|
|
20812
|
+
event["multiValueHeaders"] = headerMaps.multi;
|
|
20813
|
+
event["multiValueQueryStringParameters"] = query.multi;
|
|
20814
|
+
} else {
|
|
20815
|
+
event["headers"] = headerMaps.single;
|
|
20816
|
+
event["queryStringParameters"] = query.single;
|
|
20817
|
+
}
|
|
20818
|
+
return event;
|
|
20819
|
+
}
|
|
20820
|
+
/** A Lambda RIE error envelope (`{errorMessage, errorType, ...}`) with no statusCode. */
|
|
20821
|
+
function isErrorEnvelope(payload) {
|
|
20822
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
|
|
20823
|
+
const obj = payload;
|
|
20824
|
+
if ("statusCode" in obj) return false;
|
|
20825
|
+
return typeof obj["errorMessage"] === "string";
|
|
20826
|
+
}
|
|
20827
|
+
/**
|
|
20828
|
+
* The canonical 502 a real ALB returns when a Lambda target's response is
|
|
20829
|
+
* malformed (missing/invalid `statusCode`, non-object payload, or a runtime
|
|
20830
|
+
* error envelope). Plain-text body, mirroring ALB's own 502 page shape closely
|
|
20831
|
+
* enough for local dev.
|
|
20832
|
+
*/
|
|
20833
|
+
function badGatewayResponse() {
|
|
20834
|
+
const body = Buffer.from("<html><body><h1>502 Bad Gateway</h1></body></html>", "utf-8");
|
|
20835
|
+
return {
|
|
20836
|
+
statusCode: 502,
|
|
20837
|
+
statusDescription: "502 Bad Gateway",
|
|
20838
|
+
headers: {
|
|
20839
|
+
"content-type": ["text/html"],
|
|
20840
|
+
"content-length": [String(body.length)]
|
|
20841
|
+
},
|
|
20842
|
+
body
|
|
20843
|
+
};
|
|
20844
|
+
}
|
|
20845
|
+
function stringifyHeaderValue(value) {
|
|
20846
|
+
if (value === null || value === void 0) return "";
|
|
20847
|
+
if (typeof value === "string") return value;
|
|
20848
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
|
|
20849
|
+
return JSON.stringify(value) ?? "";
|
|
20850
|
+
}
|
|
20851
|
+
/**
|
|
20852
|
+
* Translate a Lambda ALB-target response payload into HTTP components.
|
|
20853
|
+
*
|
|
20854
|
+
* A well-formed response is an object with a numeric `statusCode`. Headers come
|
|
20855
|
+
* from `headers` (single-value) and/or `multiValueHeaders` (array-valued); ALB
|
|
20856
|
+
* accepts either regardless of the request-side multi-value setting, so both
|
|
20857
|
+
* are honored here (multiValueHeaders extend / append to the single map).
|
|
20858
|
+
* `body` is optional; `isBase64Encoded: true` means the body is base64 and is
|
|
20859
|
+
* decoded to raw bytes.
|
|
20860
|
+
*
|
|
20861
|
+
* Anything else -> 502 (matching a real ALB), incl. a runtime error envelope.
|
|
20862
|
+
*/
|
|
20863
|
+
function translateAlbLambdaResponse(payload) {
|
|
20864
|
+
if (isErrorEnvelope(payload)) return badGatewayResponse();
|
|
20865
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return badGatewayResponse();
|
|
20866
|
+
const obj = payload;
|
|
20867
|
+
const statusRaw = obj["statusCode"];
|
|
20868
|
+
if (typeof statusRaw !== "number" || !Number.isFinite(statusRaw)) return badGatewayResponse();
|
|
20869
|
+
const statusCode = Math.trunc(statusRaw);
|
|
20870
|
+
const isBase64 = obj["isBase64Encoded"] === true;
|
|
20871
|
+
const rawBody = obj["body"];
|
|
20872
|
+
let body;
|
|
20873
|
+
if (rawBody === void 0 || rawBody === null) body = Buffer.alloc(0);
|
|
20874
|
+
else if (typeof rawBody === "string") body = isBase64 ? Buffer.from(rawBody, "base64") : Buffer.from(rawBody, "utf-8");
|
|
20875
|
+
else body = Buffer.from(JSON.stringify(rawBody), "utf-8");
|
|
20876
|
+
const headers = {};
|
|
20877
|
+
const addHeader = (name, value) => {
|
|
20878
|
+
const lower = name.toLowerCase();
|
|
20879
|
+
(headers[lower] ??= []).push(value);
|
|
20880
|
+
};
|
|
20881
|
+
const singleHeaders = obj["headers"];
|
|
20882
|
+
if (singleHeaders && typeof singleHeaders === "object" && !Array.isArray(singleHeaders)) for (const [name, value] of Object.entries(singleHeaders)) addHeader(name, stringifyHeaderValue(value));
|
|
20883
|
+
const multiHeaders = obj["multiValueHeaders"];
|
|
20884
|
+
if (multiHeaders && typeof multiHeaders === "object" && !Array.isArray(multiHeaders)) for (const [name, values] of Object.entries(multiHeaders)) {
|
|
20885
|
+
if (!Array.isArray(values)) continue;
|
|
20886
|
+
for (const v of values) addHeader(name, stringifyHeaderValue(v));
|
|
20887
|
+
}
|
|
20888
|
+
headers["content-length"] = [String(body.length)];
|
|
20889
|
+
const result = {
|
|
20890
|
+
statusCode,
|
|
20891
|
+
headers,
|
|
20892
|
+
body
|
|
20893
|
+
};
|
|
20894
|
+
const statusDescription = obj["statusDescription"];
|
|
20895
|
+
if (typeof statusDescription === "string" && statusDescription.length > 0) result.statusDescription = statusDescription;
|
|
20896
|
+
return result;
|
|
20897
|
+
}
|
|
20898
|
+
|
|
20609
20899
|
//#endregion
|
|
20610
20900
|
//#region src/local/front-door-server.ts
|
|
20611
20901
|
/** Default per-request upstream timeout — a hung replica yields a 504, not a hang. */
|
|
@@ -20646,31 +20936,56 @@ async function startFrontDoorServer(opts) {
|
|
|
20646
20936
|
}
|
|
20647
20937
|
};
|
|
20648
20938
|
}
|
|
20939
|
+
/**
|
|
20940
|
+
* Resolve the dispatch target for a request path from whichever selector the
|
|
20941
|
+
* caller supplied. `selectTarget` (#123, ECS-or-Lambda) wins over the
|
|
20942
|
+
* pool-only `selectPool`; a `selectPool` hit is adapted to a `kind: 'pool'`
|
|
20943
|
+
* target so the request handler has a single code path.
|
|
20944
|
+
*/
|
|
20945
|
+
function resolveDispatchTarget(opts, requestPath) {
|
|
20946
|
+
if (opts.selectTarget) return opts.selectTarget(requestPath);
|
|
20947
|
+
if (opts.selectPool) {
|
|
20948
|
+
const pool = opts.selectPool(requestPath);
|
|
20949
|
+
return pool ? {
|
|
20950
|
+
kind: "pool",
|
|
20951
|
+
pool
|
|
20952
|
+
} : void 0;
|
|
20953
|
+
}
|
|
20954
|
+
}
|
|
20649
20955
|
function handleProxyRequest(req, res, opts) {
|
|
20650
|
-
|
|
20651
|
-
|
|
20956
|
+
const url = req.url ?? "/";
|
|
20957
|
+
if (opts.route) {
|
|
20652
20958
|
const action = opts.route({
|
|
20653
20959
|
path: url,
|
|
20654
20960
|
...hostHeader(req)
|
|
20655
20961
|
});
|
|
20656
|
-
if (!action)
|
|
20657
|
-
writeError(res, 404, `No listener rule matched '${url}' on ${opts.label}, and the listener has no default action forwarding to a local target.`);
|
|
20658
|
-
resolve();
|
|
20659
|
-
return;
|
|
20660
|
-
}
|
|
20962
|
+
if (!action) return reply404(req, res, opts);
|
|
20661
20963
|
if (action.kind === "redirect" || action.kind === "fixed-response") {
|
|
20662
20964
|
req.resume();
|
|
20663
20965
|
if (action.kind === "redirect") writeRedirect(res, action, req, opts.listenerPort);
|
|
20664
20966
|
else writeFixedResponse(res, action);
|
|
20665
|
-
resolve();
|
|
20666
|
-
return;
|
|
20967
|
+
return Promise.resolve();
|
|
20667
20968
|
}
|
|
20668
|
-
const
|
|
20669
|
-
if (!
|
|
20969
|
+
const picked = pickWeightedTarget(action.pools);
|
|
20970
|
+
if (!picked) {
|
|
20670
20971
|
writeError(res, 502, `No forward target selected behind ${opts.label} (every weighted target has weight 0).`);
|
|
20671
|
-
resolve();
|
|
20672
|
-
return;
|
|
20972
|
+
return Promise.resolve();
|
|
20673
20973
|
}
|
|
20974
|
+
if ("lambda" in picked) return handleLambdaRequest(req, res, picked.lambda, opts);
|
|
20975
|
+
return handlePoolRequest(req, res, picked.pool, opts);
|
|
20976
|
+
}
|
|
20977
|
+
const target = resolveDispatchTarget(opts, url);
|
|
20978
|
+
if (!target) return reply404(req, res, opts);
|
|
20979
|
+
if (target.kind === "lambda") return handleLambdaRequest(req, res, target.lambda, opts);
|
|
20980
|
+
return handlePoolRequest(req, res, target.pool, opts);
|
|
20981
|
+
}
|
|
20982
|
+
/** Reply 404 — an ALB listener with no matching rule and no default action. */
|
|
20983
|
+
function reply404(req, res, opts) {
|
|
20984
|
+
writeError(res, 404, `No listener rule matched '${req.url ?? "/"}' on ${opts.label}, and the listener has no default action forwarding to a local target.`);
|
|
20985
|
+
return Promise.resolve();
|
|
20986
|
+
}
|
|
20987
|
+
function handlePoolRequest(req, res, pool, opts) {
|
|
20988
|
+
return new Promise((resolve) => {
|
|
20674
20989
|
const endpoint = pool.next();
|
|
20675
20990
|
if (!endpoint) {
|
|
20676
20991
|
writeError(res, 503, `No running replicas behind ${opts.label} for the matched target. The front-door has no healthy target to forward to.`);
|
|
@@ -20727,23 +21042,24 @@ function hostHeader(req) {
|
|
|
20727
21042
|
return host ? { host } : {};
|
|
20728
21043
|
}
|
|
20729
21044
|
/**
|
|
20730
|
-
* Pick one
|
|
20731
|
-
* A single-entry set short-circuits to that
|
|
20732
|
-
* every weight is 0 (an ALB-valid but un-routable forward).
|
|
21045
|
+
* Pick one weighted target from a forward set: weighted random over the
|
|
21046
|
+
* non-zero weights. A single-entry set short-circuits to that entry. Returns
|
|
21047
|
+
* `undefined` when every weight is 0 (an ALB-valid but un-routable forward).
|
|
21048
|
+
* Used for forwards that may mix ECS pools and Lambda invokers.
|
|
20733
21049
|
*/
|
|
20734
|
-
function
|
|
20735
|
-
if (
|
|
20736
|
-
if (
|
|
20737
|
-
const total =
|
|
21050
|
+
function pickWeightedTarget(targets) {
|
|
21051
|
+
if (targets.length === 0) return void 0;
|
|
21052
|
+
if (targets.length === 1) return targets[0].weight > 0 ? targets[0] : void 0;
|
|
21053
|
+
const total = targets.reduce((sum, t) => sum + Math.max(0, t.weight), 0);
|
|
20738
21054
|
if (total <= 0) return void 0;
|
|
20739
21055
|
let roll = Math.random() * total;
|
|
20740
|
-
for (const
|
|
20741
|
-
const w = Math.max(0,
|
|
21056
|
+
for (const t of targets) {
|
|
21057
|
+
const w = Math.max(0, t.weight);
|
|
20742
21058
|
if (w === 0) continue;
|
|
20743
21059
|
roll -= w;
|
|
20744
|
-
if (roll < 0) return
|
|
21060
|
+
if (roll < 0) return t;
|
|
20745
21061
|
}
|
|
20746
|
-
for (let i =
|
|
21062
|
+
for (let i = targets.length - 1; i >= 0; i--) if (Math.max(0, targets[i].weight) > 0) return targets[i];
|
|
20747
21063
|
}
|
|
20748
21064
|
/**
|
|
20749
21065
|
* Synthesize an ALB-style redirect (301 / 302). ALB builds the `Location` from
|
|
@@ -20791,6 +21107,82 @@ function writeFixedResponse(res, action) {
|
|
|
20791
21107
|
});
|
|
20792
21108
|
res.end(body);
|
|
20793
21109
|
}
|
|
21110
|
+
/** Maximum request body the ALB Lambda-target path buffers (ALB's own limit is 1 MB). */
|
|
21111
|
+
const ALB_LAMBDA_MAX_BODY_BYTES = 1024 * 1024;
|
|
21112
|
+
/**
|
|
21113
|
+
* Serve a request that resolved to a Lambda forward target (#123). Buffers the
|
|
21114
|
+
* request body (ALB caps the Lambda-target request body at 1 MB), translates
|
|
21115
|
+
* the request into the ALB Lambda-target event, invokes the function locally,
|
|
21116
|
+
* and writes the translated response. A malformed handler response or an
|
|
21117
|
+
* invoke failure surfaces as 502 — mirroring a real ALB.
|
|
21118
|
+
*/
|
|
21119
|
+
function handleLambdaRequest(req, res, lambda, opts) {
|
|
21120
|
+
const logger = getLogger().child("front-door");
|
|
21121
|
+
return new Promise((resolve) => {
|
|
21122
|
+
let settled = false;
|
|
21123
|
+
const done = () => {
|
|
21124
|
+
if (settled) return;
|
|
21125
|
+
settled = true;
|
|
21126
|
+
resolve();
|
|
21127
|
+
};
|
|
21128
|
+
const chunks = [];
|
|
21129
|
+
let total = 0;
|
|
21130
|
+
let aborted = false;
|
|
21131
|
+
req.on("data", (chunk) => {
|
|
21132
|
+
if (aborted) return;
|
|
21133
|
+
total += chunk.length;
|
|
21134
|
+
if (total > ALB_LAMBDA_MAX_BODY_BYTES) {
|
|
21135
|
+
aborted = true;
|
|
21136
|
+
writeError(res, 413, `Request body exceeds the ${ALB_LAMBDA_MAX_BODY_BYTES}-byte Lambda-target limit on ${opts.label}.`);
|
|
21137
|
+
req.destroy();
|
|
21138
|
+
done();
|
|
21139
|
+
return;
|
|
21140
|
+
}
|
|
21141
|
+
chunks.push(chunk);
|
|
21142
|
+
});
|
|
21143
|
+
req.on("error", () => {
|
|
21144
|
+
if (!res.headersSent) writeError(res, 400, `Failed to read request body on ${opts.label}.`);
|
|
21145
|
+
done();
|
|
21146
|
+
});
|
|
21147
|
+
req.on("end", () => {
|
|
21148
|
+
if (aborted) return;
|
|
21149
|
+
const serveLambda = async () => {
|
|
21150
|
+
try {
|
|
21151
|
+
const body = Buffer.concat(chunks);
|
|
21152
|
+
const forwardHeaders = { ...req.headers };
|
|
21153
|
+
stripHopByHopHeaders(forwardHeaders);
|
|
21154
|
+
appendForwardedHeaders(forwardHeaders, req, opts.listenerPort);
|
|
21155
|
+
const snapshot = snapshotFromIncoming(req, body);
|
|
21156
|
+
for (const [name, value] of Object.entries(forwardHeaders)) {
|
|
21157
|
+
if (value === void 0) continue;
|
|
21158
|
+
snapshot.headers[name] = Array.isArray(value) ? value : [value];
|
|
21159
|
+
}
|
|
21160
|
+
const event = buildAlbLambdaEvent(snapshot, {
|
|
21161
|
+
targetGroupArn: lambda.targetGroupArn,
|
|
21162
|
+
multiValueHeaders: lambda.multiValueHeaders
|
|
21163
|
+
});
|
|
21164
|
+
const translated = translateAlbLambdaResponse(await lambda.invoke(event));
|
|
21165
|
+
if (res.headersSent || res.writableEnded) {
|
|
21166
|
+
done();
|
|
21167
|
+
return;
|
|
21168
|
+
}
|
|
21169
|
+
const outHeaders = {};
|
|
21170
|
+
for (const [name, values] of Object.entries(translated.headers)) outHeaders[name] = values.length === 1 ? values[0] : values;
|
|
21171
|
+
if (translated.statusDescription) res.writeHead(translated.statusCode, translated.statusDescription, outHeaders);
|
|
21172
|
+
else res.writeHead(translated.statusCode, outHeaders);
|
|
21173
|
+
res.end(translated.body);
|
|
21174
|
+
done();
|
|
21175
|
+
} catch (err) {
|
|
21176
|
+
logger.debug(`Lambda target '${lambda.label}' request failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
21177
|
+
if (!res.headersSent) writeError(res, 502, `Lambda target '${lambda.label}' behind ${opts.label} failed.`);
|
|
21178
|
+
else if (!res.writableEnded) res.destroy();
|
|
21179
|
+
done();
|
|
21180
|
+
}
|
|
21181
|
+
};
|
|
21182
|
+
serveLambda();
|
|
21183
|
+
});
|
|
21184
|
+
});
|
|
21185
|
+
}
|
|
20794
21186
|
/** Standard hop-by-hop headers (RFC 7230 §6.1) — a proxy must not forward these. */
|
|
20795
21187
|
const HOP_BY_HOP_HEADERS = [
|
|
20796
21188
|
"connection",
|
|
@@ -20915,6 +21307,186 @@ function globToRegExp(pattern, caseInsensitive) {
|
|
|
20915
21307
|
return new RegExp(`^${body}$`);
|
|
20916
21308
|
}
|
|
20917
21309
|
|
|
21310
|
+
//#endregion
|
|
21311
|
+
//#region src/local/front-door-lambda-runner.ts
|
|
21312
|
+
/** Forward the dev shell's AWS credential / region env into the Lambda container. */
|
|
21313
|
+
function forwardAwsEnv() {
|
|
21314
|
+
const env = {};
|
|
21315
|
+
for (const key of [
|
|
21316
|
+
"AWS_ACCESS_KEY_ID",
|
|
21317
|
+
"AWS_SECRET_ACCESS_KEY",
|
|
21318
|
+
"AWS_SESSION_TOKEN",
|
|
21319
|
+
"AWS_REGION",
|
|
21320
|
+
"AWS_DEFAULT_REGION"
|
|
21321
|
+
]) {
|
|
21322
|
+
const value = process.env[key];
|
|
21323
|
+
if (value !== void 0) env[key] = value;
|
|
21324
|
+
}
|
|
21325
|
+
return env;
|
|
21326
|
+
}
|
|
21327
|
+
/**
|
|
21328
|
+
* Materialize an inline (`Code.ZipFile`) ZIP Lambda's source into a temp dir at
|
|
21329
|
+
* the path implied by `handler`, returning the dir to bind-mount. Mirrors
|
|
21330
|
+
* `local-invoke.ts:materializeInlineCode`.
|
|
21331
|
+
*/
|
|
21332
|
+
function materializeInlineCode(handler, source, fileExtension) {
|
|
21333
|
+
const lastDot = handler.lastIndexOf(".");
|
|
21334
|
+
if (lastDot <= 0) throw new Error(`Handler '${handler}' is malformed: expected '<modulePath>.<exportName>'.`);
|
|
21335
|
+
const modulePath = handler.substring(0, lastDot);
|
|
21336
|
+
const dir = mkdtempSync(path.join(tmpdir(), `${getEmbedConfig().resourceNamePrefix}-alb-lambda-`));
|
|
21337
|
+
const filePath = path.join(dir, `${modulePath}${fileExtension}`);
|
|
21338
|
+
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
21339
|
+
writeFileSync(filePath, source, "utf-8");
|
|
21340
|
+
return dir;
|
|
21341
|
+
}
|
|
21342
|
+
async function resolveZipImagePlan(lambda, opts) {
|
|
21343
|
+
let inlineTmpDir;
|
|
21344
|
+
let codeDir = lambda.codePath;
|
|
21345
|
+
if (codeDir === null) {
|
|
21346
|
+
inlineTmpDir = materializeInlineCode(lambda.handler, lambda.inlineCode ?? "", resolveRuntimeFileExtension(lambda.runtime));
|
|
21347
|
+
codeDir = inlineTmpDir;
|
|
21348
|
+
}
|
|
21349
|
+
const image = resolveRuntimeImage(lambda.runtime);
|
|
21350
|
+
await pullImage(image, opts.skipPull === true);
|
|
21351
|
+
const containerCodePath = resolveRuntimeCodeMountPath(lambda.runtime);
|
|
21352
|
+
return {
|
|
21353
|
+
image,
|
|
21354
|
+
mounts: [{
|
|
21355
|
+
hostPath: codeDir,
|
|
21356
|
+
containerPath: containerCodePath,
|
|
21357
|
+
readOnly: true
|
|
21358
|
+
}],
|
|
21359
|
+
cmd: [lambda.handler],
|
|
21360
|
+
...inlineTmpDir !== void 0 && { inlineTmpDir }
|
|
21361
|
+
};
|
|
21362
|
+
}
|
|
21363
|
+
async function resolveContainerImagePlan(lambda, opts) {
|
|
21364
|
+
const logger = getLogger().child("front-door-lambda");
|
|
21365
|
+
const platform = opts.platformOverride ?? architectureToPlatform(lambda.architecture);
|
|
21366
|
+
const manifestPath = lambda.stack.assetManifestPath;
|
|
21367
|
+
let imageRef;
|
|
21368
|
+
let localBuilt = false;
|
|
21369
|
+
if (manifestPath) {
|
|
21370
|
+
const cdkOutDir = path.dirname(manifestPath);
|
|
21371
|
+
const manifest = await new AssetManifestLoader().loadManifest(cdkOutDir, lambda.stack.stackName);
|
|
21372
|
+
const entry = manifest ? getDockerImageBySourceHash(manifest, lambda.imageUri) : void 0;
|
|
21373
|
+
if (entry) {
|
|
21374
|
+
imageRef = await buildContainerImage(entry.asset, cdkOutDir, { architecture: lambda.architecture });
|
|
21375
|
+
localBuilt = true;
|
|
21376
|
+
}
|
|
21377
|
+
}
|
|
21378
|
+
if (!localBuilt) {
|
|
21379
|
+
if (!parseEcrUri(lambda.imageUri)) throw new Error(`Container Lambda '${lambda.logicalId}' has no matching asset in cdk.out, and Code.ImageUri '${lambda.imageUri}' is not an ECR URI ${getEmbedConfig().binaryName} can authenticate against. Re-synthesize the CDK app or deploy the image to ECR first.`);
|
|
21380
|
+
logger.info(`No matching cdk.out asset for ${lambda.imageUri}; falling back to ECR pull...`);
|
|
21381
|
+
imageRef = await pullEcrImage(lambda.imageUri, {
|
|
21382
|
+
skipPull: opts.skipPull === true,
|
|
21383
|
+
...opts.region !== void 0 && { region: opts.region },
|
|
21384
|
+
...opts.ecrRoleArn !== void 0 && { ecrRoleArn: opts.ecrRoleArn }
|
|
21385
|
+
});
|
|
21386
|
+
}
|
|
21387
|
+
return {
|
|
21388
|
+
image: imageRef,
|
|
21389
|
+
mounts: [],
|
|
21390
|
+
cmd: lambda.imageConfig.command ?? [],
|
|
21391
|
+
platform,
|
|
21392
|
+
...lambda.imageConfig.entryPoint && lambda.imageConfig.entryPoint.length > 0 && { entryPoint: lambda.imageConfig.entryPoint },
|
|
21393
|
+
...lambda.imageConfig.workingDirectory !== void 0 && { workingDir: lambda.imageConfig.workingDirectory }
|
|
21394
|
+
};
|
|
21395
|
+
}
|
|
21396
|
+
/**
|
|
21397
|
+
* Build a {@link FrontDoorLambdaRunner} for a resolved Lambda. Construction is
|
|
21398
|
+
* pure (no docker work); `start()` does the boot. The invoke timeout defaults
|
|
21399
|
+
* to `max(30s, timeoutSec * 2 * 1000)` — same formula as `cdkl invoke`.
|
|
21400
|
+
*/
|
|
21401
|
+
function createFrontDoorLambdaRunner(lambda, opts) {
|
|
21402
|
+
const logger = getLogger().child("front-door-lambda");
|
|
21403
|
+
const defaultTimeoutMs = Math.max(3e4, lambda.timeoutSec * 2 * 1e3);
|
|
21404
|
+
let plan;
|
|
21405
|
+
let containerId;
|
|
21406
|
+
let hostPort;
|
|
21407
|
+
let stopLogStream;
|
|
21408
|
+
let starting;
|
|
21409
|
+
let stopped = false;
|
|
21410
|
+
async function doStart() {
|
|
21411
|
+
plan = lambda.kind === "zip" ? await resolveZipImagePlan(lambda, opts) : await resolveContainerImagePlan(lambda, opts);
|
|
21412
|
+
const port = await pickFreePort();
|
|
21413
|
+
hostPort = port;
|
|
21414
|
+
const name = `${getEmbedConfig().resourceNamePrefix}-alblambda-${lambda.logicalId}-${process.pid}-${Math.floor(Math.random() * 1e6)}`;
|
|
21415
|
+
const env = {
|
|
21416
|
+
AWS_LAMBDA_FUNCTION_NAME: lambda.logicalId,
|
|
21417
|
+
AWS_LAMBDA_FUNCTION_MEMORY_SIZE: String(lambda.memoryMb),
|
|
21418
|
+
AWS_LAMBDA_FUNCTION_TIMEOUT: String(lambda.timeoutSec),
|
|
21419
|
+
AWS_LAMBDA_FUNCTION_VERSION: "$LATEST",
|
|
21420
|
+
AWS_LAMBDA_LOG_GROUP_NAME: `/aws/lambda/${lambda.logicalId}`,
|
|
21421
|
+
AWS_LAMBDA_LOG_STREAM_NAME: "local",
|
|
21422
|
+
...forwardAwsEnv()
|
|
21423
|
+
};
|
|
21424
|
+
logger.info(`Starting Lambda target container for ${lambda.logicalId} (image=${plan.image}, port=${port})...`);
|
|
21425
|
+
const id = await runDetached({
|
|
21426
|
+
image: plan.image,
|
|
21427
|
+
mounts: plan.mounts,
|
|
21428
|
+
env,
|
|
21429
|
+
cmd: plan.cmd,
|
|
21430
|
+
hostPort: port,
|
|
21431
|
+
host: opts.containerHost,
|
|
21432
|
+
name,
|
|
21433
|
+
...plan.platform !== void 0 && { platform: plan.platform },
|
|
21434
|
+
...plan.entryPoint !== void 0 && { entryPoint: plan.entryPoint },
|
|
21435
|
+
...plan.workingDir !== void 0 && { workingDir: plan.workingDir }
|
|
21436
|
+
});
|
|
21437
|
+
containerId = id;
|
|
21438
|
+
stopLogStream = opts.streamLogs === false ? void 0 : streamLogs(id);
|
|
21439
|
+
try {
|
|
21440
|
+
await waitForRieReady(opts.containerHost, port, 3e4);
|
|
21441
|
+
} catch (err) {
|
|
21442
|
+
try {
|
|
21443
|
+
stopLogStream?.();
|
|
21444
|
+
} catch {}
|
|
21445
|
+
await removeContainer(id).catch(() => void 0);
|
|
21446
|
+
containerId = void 0;
|
|
21447
|
+
throw err;
|
|
21448
|
+
}
|
|
21449
|
+
}
|
|
21450
|
+
return {
|
|
21451
|
+
logicalId: lambda.logicalId,
|
|
21452
|
+
async start() {
|
|
21453
|
+
if (stopped) throw new Error("FrontDoorLambdaRunner.start called after stop");
|
|
21454
|
+
if (containerId) return;
|
|
21455
|
+
if (!starting) starting = doStart();
|
|
21456
|
+
await starting;
|
|
21457
|
+
},
|
|
21458
|
+
async invoke(event, timeoutMs) {
|
|
21459
|
+
if (!containerId || hostPort === void 0) throw new Error(`FrontDoorLambdaRunner('${lambda.logicalId}').invoke called before start() completed.`);
|
|
21460
|
+
return (await invokeRie(opts.containerHost, hostPort, event, timeoutMs ?? defaultTimeoutMs)).payload;
|
|
21461
|
+
},
|
|
21462
|
+
async stop() {
|
|
21463
|
+
if (stopped) return;
|
|
21464
|
+
stopped = true;
|
|
21465
|
+
try {
|
|
21466
|
+
stopLogStream?.();
|
|
21467
|
+
} catch (err) {
|
|
21468
|
+
logger.debug(`stopLogStream(${lambda.logicalId}) failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
21469
|
+
}
|
|
21470
|
+
if (containerId) {
|
|
21471
|
+
try {
|
|
21472
|
+
await removeContainer(containerId);
|
|
21473
|
+
} catch (err) {
|
|
21474
|
+
logger.warn(`Failed to remove Lambda target container for ${lambda.logicalId}: ${err instanceof Error ? err.message : String(err)}. Continuing cleanup.`);
|
|
21475
|
+
}
|
|
21476
|
+
containerId = void 0;
|
|
21477
|
+
}
|
|
21478
|
+
if (plan?.inlineTmpDir) try {
|
|
21479
|
+
rmSync(plan.inlineTmpDir, {
|
|
21480
|
+
recursive: true,
|
|
21481
|
+
force: true
|
|
21482
|
+
});
|
|
21483
|
+
} catch (err) {
|
|
21484
|
+
logger.debug(`Failed to remove inline-code tmpdir ${plan.inlineTmpDir}: ${err instanceof Error ? err.message : String(err)}`);
|
|
21485
|
+
}
|
|
21486
|
+
}
|
|
21487
|
+
};
|
|
21488
|
+
}
|
|
21489
|
+
|
|
20918
21490
|
//#endregion
|
|
20919
21491
|
//#region src/cli/commands/ecs-service-emulator.ts
|
|
20920
21492
|
/**
|
|
@@ -20935,6 +21507,7 @@ async function runEcsServiceEmulator(targets, options, strategy, extraStateProvi
|
|
|
20935
21507
|
let profileCredsFile;
|
|
20936
21508
|
let frontDoorServers = [];
|
|
20937
21509
|
let frontDoorByService = /* @__PURE__ */ new Map();
|
|
21510
|
+
let frontDoorLambdaRunners = [];
|
|
20938
21511
|
const cleanup = singleFlight(async () => {
|
|
20939
21512
|
await Promise.allSettled(perTarget.map(async (pt) => {
|
|
20940
21513
|
if (pt.controller) await pt.controller.shutdown();
|
|
@@ -20945,6 +21518,8 @@ async function runEcsServiceEmulator(targets, options, strategy, extraStateProvi
|
|
|
20945
21518
|
}));
|
|
20946
21519
|
await Promise.allSettled(frontDoorServers.map((s) => s.close().catch((err) => getLogger().warn(`front-door server teardown failed: ${err instanceof Error ? err.message : String(err)}`))));
|
|
20947
21520
|
frontDoorServers = [];
|
|
21521
|
+
await Promise.allSettled(frontDoorLambdaRunners.map((r) => r.stop().catch((err) => getLogger().warn(`front-door Lambda target teardown failed: ${err instanceof Error ? err.message : String(err)}`))));
|
|
21522
|
+
frontDoorLambdaRunners = [];
|
|
20948
21523
|
if (profileCredsFile) {
|
|
20949
21524
|
try {
|
|
20950
21525
|
await profileCredsFile.dispose();
|
|
@@ -20989,7 +21564,8 @@ async function runEcsServiceEmulator(targets, options, strategy, extraStateProvi
|
|
|
20989
21564
|
});
|
|
20990
21565
|
const { boots, frontDoor, warnings } = strategy.resolveBoots(stacks, resolvedTargets);
|
|
20991
21566
|
for (const w of warnings) logger.warn(w);
|
|
20992
|
-
|
|
21567
|
+
const hasFrontDoorListeners = !!frontDoor && frontDoor.listeners.length > 0;
|
|
21568
|
+
if (boots.length === 0 && !hasFrontDoorListeners) throw new LocalStartServiceError(`No runnable target resolved from ${resolvedTargets.join(", ")}.`);
|
|
20993
21569
|
rejectExplicitCfnStackWithMultipleStacks(options, boots.length);
|
|
20994
21570
|
perTarget = boots.map((boot) => ({
|
|
20995
21571
|
boot,
|
|
@@ -21020,9 +21596,10 @@ async function runEcsServiceEmulator(targets, options, strategy, extraStateProvi
|
|
|
21020
21596
|
sharedNetwork
|
|
21021
21597
|
};
|
|
21022
21598
|
if (frontDoor && frontDoor.listeners.length > 0) {
|
|
21023
|
-
const built = await buildFrontDoor(frontDoor, options
|
|
21599
|
+
const built = await buildFrontDoor(frontDoor, options, logger);
|
|
21024
21600
|
frontDoorServers = built.servers;
|
|
21025
21601
|
frontDoorByService = built.frontDoorByService;
|
|
21602
|
+
frontDoorLambdaRunners = built.lambdaRunners;
|
|
21026
21603
|
}
|
|
21027
21604
|
sigintHandler = () => {
|
|
21028
21605
|
sigintCount += 1;
|
|
@@ -21036,10 +21613,13 @@ async function runEcsServiceEmulator(targets, options, strategy, extraStateProvi
|
|
|
21036
21613
|
process.on("SIGINT", sigintHandler);
|
|
21037
21614
|
process.on("SIGTERM", sigintHandler);
|
|
21038
21615
|
for (const pt of perTarget) pt.controller = await bootOneTarget(pt.boot, pt.runState, stacks, options, discovery, skipPull, extraStateProviders, profileCredsFile, frontDoorByService.get(pt.boot.target));
|
|
21039
|
-
|
|
21040
|
-
|
|
21616
|
+
if (perTarget.length > 0) {
|
|
21617
|
+
const summary = perTarget.map((pt) => `${pt.controller.service.serviceName} (${pt.controller.activeReplicaCount()} replica(s))`).join(", ");
|
|
21618
|
+
logger.info(`Service(s) running: ${summary}.`);
|
|
21619
|
+
} else logger.info(`Service(s) running: ${frontDoorLambdaRunners.length} Lambda target(s) behind the ALB front-door.`);
|
|
21041
21620
|
logger.info("Press ^C to shut down.");
|
|
21042
|
-
await Promise.all(perTarget.map((pt) => pt.controller.waitForShutdown()));
|
|
21621
|
+
if (perTarget.length > 0) await Promise.all(perTarget.map((pt) => pt.controller.waitForShutdown()));
|
|
21622
|
+
else await new Promise(() => {});
|
|
21043
21623
|
} finally {
|
|
21044
21624
|
if (sigintHandler) {
|
|
21045
21625
|
process.off("SIGINT", sigintHandler);
|
|
@@ -21137,28 +21717,63 @@ async function runOneTarget(boot, runState, stacks, options, discovery, skipPull
|
|
|
21137
21717
|
* already in use) every server started so far is closed and the error is
|
|
21138
21718
|
* re-thrown with a `--lb-port` hint.
|
|
21139
21719
|
*/
|
|
21140
|
-
async function buildFrontDoor(plan,
|
|
21720
|
+
async function buildFrontDoor(plan, options, logger) {
|
|
21721
|
+
const containerHost = options.containerHost;
|
|
21141
21722
|
const servers = [];
|
|
21142
|
-
const
|
|
21143
|
-
const
|
|
21723
|
+
const poolRegistry = /* @__PURE__ */ new Map();
|
|
21724
|
+
const lambdaRegistry = /* @__PURE__ */ new Map();
|
|
21725
|
+
const dispatchFor = (t) => {
|
|
21726
|
+
if (t.kind === "lambda") {
|
|
21727
|
+
let runner = lambdaRegistry.get(t.lambda.logicalId);
|
|
21728
|
+
if (!runner) {
|
|
21729
|
+
runner = createFrontDoorLambdaRunner(t.lambda, {
|
|
21730
|
+
containerHost,
|
|
21731
|
+
skipPull: options.pull === false,
|
|
21732
|
+
...options.platform !== void 0 && { platformOverride: options.platform },
|
|
21733
|
+
...options.ecrRoleArn !== void 0 && { ecrRoleArn: options.ecrRoleArn },
|
|
21734
|
+
...options.region !== void 0 && { region: options.region }
|
|
21735
|
+
});
|
|
21736
|
+
lambdaRegistry.set(t.lambda.logicalId, runner);
|
|
21737
|
+
}
|
|
21738
|
+
const boundRunner = runner;
|
|
21739
|
+
return {
|
|
21740
|
+
kind: "lambda",
|
|
21741
|
+
lambda: {
|
|
21742
|
+
targetGroupArn: t.targetGroupArn,
|
|
21743
|
+
multiValueHeaders: t.multiValueHeaders,
|
|
21744
|
+
label: t.lambda.logicalId,
|
|
21745
|
+
invoke: (event) => boundRunner.invoke(event)
|
|
21746
|
+
}
|
|
21747
|
+
};
|
|
21748
|
+
}
|
|
21144
21749
|
const key = `${t.serviceTarget} ${t.targetContainerName} ${t.targetContainerPort}`;
|
|
21145
|
-
let entry =
|
|
21750
|
+
let entry = poolRegistry.get(key);
|
|
21146
21751
|
if (!entry) {
|
|
21147
21752
|
entry = {
|
|
21148
21753
|
pool: new FrontDoorEndpointPool(),
|
|
21149
21754
|
target: t
|
|
21150
21755
|
};
|
|
21151
|
-
|
|
21756
|
+
poolRegistry.set(key, entry);
|
|
21152
21757
|
}
|
|
21153
|
-
return
|
|
21758
|
+
return {
|
|
21759
|
+
kind: "pool",
|
|
21760
|
+
pool: entry.pool
|
|
21761
|
+
};
|
|
21762
|
+
};
|
|
21763
|
+
const weightedTargetFor = (t) => {
|
|
21764
|
+
const dispatch = dispatchFor(t);
|
|
21765
|
+
return dispatch.kind === "lambda" ? {
|
|
21766
|
+
lambda: dispatch.lambda,
|
|
21767
|
+
weight: t.weight
|
|
21768
|
+
} : {
|
|
21769
|
+
pool: dispatch.pool,
|
|
21770
|
+
weight: t.weight
|
|
21771
|
+
};
|
|
21154
21772
|
};
|
|
21155
21773
|
const toRouteAction = (action) => {
|
|
21156
21774
|
if (action.kind === "forward") return {
|
|
21157
21775
|
kind: "forward",
|
|
21158
|
-
pools: action.targets.map(
|
|
21159
|
-
pool: poolFor(t),
|
|
21160
|
-
weight: t.weight
|
|
21161
|
-
}))
|
|
21776
|
+
pools: action.targets.map(weightedTargetFor)
|
|
21162
21777
|
};
|
|
21163
21778
|
if (action.kind === "redirect") return {
|
|
21164
21779
|
kind: "redirect",
|
|
@@ -21199,12 +21814,17 @@ async function buildFrontDoor(plan, containerHost, logger) {
|
|
|
21199
21814
|
for (const r of [...listener.rules].sort((a, b) => a.priority - b.priority)) logger.info(` ${describeConditions(r)} (priority ${r.priority}) -> ${describeAction(r.action)}`);
|
|
21200
21815
|
if (!listener.defaultAction) logger.info(" (no default action: unmatched requests return 404)");
|
|
21201
21816
|
}
|
|
21817
|
+
for (const runner of lambdaRegistry.values()) {
|
|
21818
|
+
logger.info(`Booting Lambda target '${runner.logicalId}' behind the ALB front-door...`);
|
|
21819
|
+
await runner.start();
|
|
21820
|
+
}
|
|
21202
21821
|
} catch (err) {
|
|
21203
21822
|
await Promise.allSettled(servers.map((s) => s.close()));
|
|
21823
|
+
await Promise.allSettled([...lambdaRegistry.values()].map((r) => r.stop()));
|
|
21204
21824
|
throw new LocalStartServiceError(`Failed to start ALB front-door: ${err instanceof Error ? err.message : String(err)}. If a listener port is privileged (< 1024), remap it to a non-privileged host port with --lb-port <listenerPort>=<hostPort> (e.g. --lb-port 80=8080).`);
|
|
21205
21825
|
}
|
|
21206
21826
|
const frontDoorByService = /* @__PURE__ */ new Map();
|
|
21207
|
-
for (const { pool, target } of
|
|
21827
|
+
for (const { pool, target } of poolRegistry.values()) {
|
|
21208
21828
|
const list = frontDoorByService.get(target.serviceTarget) ?? [];
|
|
21209
21829
|
list.push({
|
|
21210
21830
|
pool,
|
|
@@ -21215,7 +21835,8 @@ async function buildFrontDoor(plan, containerHost, logger) {
|
|
|
21215
21835
|
}
|
|
21216
21836
|
return {
|
|
21217
21837
|
servers,
|
|
21218
|
-
frontDoorByService
|
|
21838
|
+
frontDoorByService,
|
|
21839
|
+
lambdaRunners: [...lambdaRegistry.values()]
|
|
21219
21840
|
};
|
|
21220
21841
|
}
|
|
21221
21842
|
/** Human-readable summary of a planned rule's path / host conditions (for the boot banner). */
|
|
@@ -21229,11 +21850,17 @@ function describeConditions(rule) {
|
|
|
21229
21850
|
function describeAction(action) {
|
|
21230
21851
|
if (action.kind === "redirect") return `redirect ${action.statusCode}`;
|
|
21231
21852
|
if (action.kind === "fixed-response") return `fixed-response ${action.statusCode}`;
|
|
21232
|
-
if (action.targets.length === 1)
|
|
21233
|
-
|
|
21234
|
-
|
|
21235
|
-
|
|
21236
|
-
|
|
21853
|
+
if (action.targets.length === 1) return describeTarget(action.targets[0]);
|
|
21854
|
+
return `weighted forward [${action.targets.map((t) => `${describeTargetShort(t)}@${t.weight}`).join(", ")}]`;
|
|
21855
|
+
}
|
|
21856
|
+
/** One forward target, described in full (for a single-target forward banner). */
|
|
21857
|
+
function describeTarget(t) {
|
|
21858
|
+
if (t.kind === "lambda") return `Lambda ${t.lambda.logicalId} (invoke)`;
|
|
21859
|
+
return `${t.serviceTarget} (container ${t.targetContainerName}:${t.targetContainerPort}) (round-robin)`;
|
|
21860
|
+
}
|
|
21861
|
+
/** One forward target, described compactly (for a weighted-forward banner). */
|
|
21862
|
+
function describeTargetShort(t) {
|
|
21863
|
+
return t.kind === "lambda" ? `Lambda ${t.lambda.logicalId}` : t.serviceTarget;
|
|
21237
21864
|
}
|
|
21238
21865
|
async function resolvePlaceholderAccount(arn, region) {
|
|
21239
21866
|
if (!arn.includes("${AWS::AccountId}")) return arn;
|
|
@@ -21468,17 +22095,20 @@ function createLocalStartServiceCommand(opts = {}) {
|
|
|
21468
22095
|
* its path / host conditions.
|
|
21469
22096
|
*
|
|
21470
22097
|
* Scope: HTTP listeners; `path-pattern` + `host-header` conditions; `forward`
|
|
21471
|
-
* (single or weighted) to ECS services
|
|
21472
|
-
*
|
|
21473
|
-
*
|
|
21474
|
-
*
|
|
21475
|
-
*
|
|
22098
|
+
* (single or weighted) to ECS services AND/OR Lambda functions
|
|
22099
|
+
* (`TargetType:"lambda"` target groups — #123: the TG -> backing
|
|
22100
|
+
* `AWS::Lambda::Function` is resolved and the front-door invokes it locally per
|
|
22101
|
+
* request); `redirect` / `fixed-response` actions. A single weighted forward may
|
|
22102
|
+
* mix ECS and Lambda targets. Skipped with a warning: HTTPS/TLS listeners, the
|
|
22103
|
+
* other condition fields (http-header / http-request-method / query-string /
|
|
22104
|
+
* source-ip), and `authenticate-cognito` / `authenticate-oidc` actions.
|
|
21476
22105
|
*/
|
|
21477
22106
|
const ALB_TYPE = "AWS::ElasticLoadBalancingV2::LoadBalancer";
|
|
21478
22107
|
const LISTENER_TYPE = "AWS::ElasticLoadBalancingV2::Listener";
|
|
21479
22108
|
const LISTENER_RULE_TYPE = "AWS::ElasticLoadBalancingV2::ListenerRule";
|
|
21480
22109
|
const TARGET_GROUP_TYPE = "AWS::ElasticLoadBalancingV2::TargetGroup";
|
|
21481
22110
|
const SERVICE_TYPE = "AWS::ECS::Service";
|
|
22111
|
+
const LAMBDA_FUNCTION_TYPE = "AWS::Lambda::Function";
|
|
21482
22112
|
/**
|
|
21483
22113
|
* Resolve an ALB into its front-door listeners + routing tables. Pure — reads
|
|
21484
22114
|
* only the supplied stack template. Returns an empty `listeners` array (with
|
|
@@ -21572,7 +22202,12 @@ function resolveAction(actions, resources, tgToService, stackName, label, warnin
|
|
|
21572
22202
|
}
|
|
21573
22203
|
if (sawAuthenticate) warnings.push(`${label} is an authenticate-* action with no local-servable terminal action. The local ALB front-door does not enforce authenticate-cognito / authenticate-oidc; skipping it.`);
|
|
21574
22204
|
}
|
|
21575
|
-
/**
|
|
22205
|
+
/**
|
|
22206
|
+
* Resolve a `forward` action into one or more weighted targets. Each target
|
|
22207
|
+
* group is either an ECS service (the original `start-alb` path) or a
|
|
22208
|
+
* `TargetType: lambda` group backed by an in-stack Lambda (#123); a single
|
|
22209
|
+
* weighted forward may mix both.
|
|
22210
|
+
*/
|
|
21576
22211
|
function resolveForwardAction(action, resources, tgToService, stackName, label, warnings) {
|
|
21577
22212
|
const refs = collectForwardTargetGroupRefs(action);
|
|
21578
22213
|
if (refs.length === 0) {
|
|
@@ -21586,8 +22221,13 @@ function resolveForwardAction(action, resources, tgToService, stackName, label,
|
|
|
21586
22221
|
warnings.push(`${label} forwards to target group '${tgRef}', but no ${TARGET_GROUP_TYPE} with that logical id exists in ${stackName}. Skipping that target group.`);
|
|
21587
22222
|
continue;
|
|
21588
22223
|
}
|
|
21589
|
-
|
|
21590
|
-
|
|
22224
|
+
const tgProps = tg.Properties ?? {};
|
|
22225
|
+
if (tgProps["TargetType"] === "lambda") {
|
|
22226
|
+
const lambdaTarget = resolveLambdaForwardTarget(tgProps, tgRef, resources, stackName, label, warnings);
|
|
22227
|
+
if (lambdaTarget) targets.push({
|
|
22228
|
+
...lambdaTarget,
|
|
22229
|
+
weight
|
|
22230
|
+
});
|
|
21591
22231
|
continue;
|
|
21592
22232
|
}
|
|
21593
22233
|
const backing = tgToService.get(tgRef);
|
|
@@ -21596,6 +22236,7 @@ function resolveForwardAction(action, resources, tgToService, stackName, label,
|
|
|
21596
22236
|
continue;
|
|
21597
22237
|
}
|
|
21598
22238
|
targets.push({
|
|
22239
|
+
kind: "ecs",
|
|
21599
22240
|
serviceLogicalId: backing.serviceLogicalId,
|
|
21600
22241
|
targetContainerName: backing.containerName,
|
|
21601
22242
|
targetContainerPort: backing.containerPort,
|
|
@@ -21609,6 +22250,29 @@ function resolveForwardAction(action, resources, tgToService, stackName, label,
|
|
|
21609
22250
|
targets
|
|
21610
22251
|
};
|
|
21611
22252
|
}
|
|
22253
|
+
/**
|
|
22254
|
+
* Resolve a `TargetType: lambda` target group into its `FrontDoorLambdaTarget`
|
|
22255
|
+
* (weight applied by the caller), or `undefined` (with a warning) when the
|
|
22256
|
+
* backing function is not an in-stack `AWS::Lambda::Function` reference.
|
|
22257
|
+
*/
|
|
22258
|
+
function resolveLambdaForwardTarget(tgProps, tgRef, resources, stackName, label, warnings) {
|
|
22259
|
+
const lambdaLogicalId = resolveLambdaTargetLogicalId(tgProps["Targets"]);
|
|
22260
|
+
if (!lambdaLogicalId) {
|
|
22261
|
+
warnings.push(`${label} forwards to a Lambda target group '${tgRef}', but its Targets[].Id is not an in-stack { "Fn::GetAtt": [<FnLogicalId>, "Arn"] } reference; the local ALB front-door supports an in-stack Lambda target only (literal / imported ARNs deferred). Skipping that target group.`);
|
|
22262
|
+
return;
|
|
22263
|
+
}
|
|
22264
|
+
const lambda = resources[lambdaLogicalId];
|
|
22265
|
+
if (!lambda || lambda.Type !== LAMBDA_FUNCTION_TYPE) {
|
|
22266
|
+
warnings.push(`${label} forwards to Lambda target group '${tgRef}', whose target resolves to '${lambdaLogicalId}', but no ${LAMBDA_FUNCTION_TYPE} with that logical id exists in ${stackName}. Skipping that target group.`);
|
|
22267
|
+
return;
|
|
22268
|
+
}
|
|
22269
|
+
return {
|
|
22270
|
+
kind: "lambda",
|
|
22271
|
+
lambdaLogicalId,
|
|
22272
|
+
targetGroupLogicalId: tgRef,
|
|
22273
|
+
multiValueHeaders: readMultiValueHeadersAttribute(tgProps["TargetGroupAttributes"])
|
|
22274
|
+
};
|
|
22275
|
+
}
|
|
21612
22276
|
/** Resolve a `redirect` action into its `Location`-template fields + status code. */
|
|
21613
22277
|
function resolveRedirectAction(action, label, warnings) {
|
|
21614
22278
|
const cfg = action["RedirectConfig"];
|
|
@@ -21641,6 +22305,38 @@ function resolveFixedResponseAction(action) {
|
|
|
21641
22305
|
return out;
|
|
21642
22306
|
}
|
|
21643
22307
|
/**
|
|
22308
|
+
* Resolve a `TargetType: lambda` target group's backing Lambda logical id from
|
|
22309
|
+
* its `Targets[].Id`. CDK synthesizes the registration as
|
|
22310
|
+
* `Targets: [{ Id: { "Fn::GetAtt": [<FnLogicalId>, "Arn"] } }]`; a `Ref` to the
|
|
22311
|
+
* function (its name) is also accepted. Returns the logical id, or `undefined`
|
|
22312
|
+
* when the target is a literal / imported ARN (not an in-stack reference).
|
|
22313
|
+
*/
|
|
22314
|
+
function resolveLambdaTargetLogicalId(targets) {
|
|
22315
|
+
if (!Array.isArray(targets) || targets.length === 0) return void 0;
|
|
22316
|
+
const first = targets[0];
|
|
22317
|
+
if (!first || typeof first !== "object") return void 0;
|
|
22318
|
+
const id = first["Id"];
|
|
22319
|
+
if (!id || typeof id !== "object" || Array.isArray(id)) return void 0;
|
|
22320
|
+
const idObj = id;
|
|
22321
|
+
const getAtt = idObj["Fn::GetAtt"];
|
|
22322
|
+
if (Array.isArray(getAtt) && typeof getAtt[0] === "string" && getAtt[0].length > 0) return getAtt[0];
|
|
22323
|
+
const ref = idObj["Ref"];
|
|
22324
|
+
if (typeof ref === "string" && ref.length > 0) return ref;
|
|
22325
|
+
}
|
|
22326
|
+
/**
|
|
22327
|
+
* Read the `lambda.multi_value_headers.enabled` target-group attribute (a
|
|
22328
|
+
* string `"true"` / `"false"` in CFn). Defaults to `false` when absent.
|
|
22329
|
+
*/
|
|
22330
|
+
function readMultiValueHeadersAttribute(attributes) {
|
|
22331
|
+
if (!Array.isArray(attributes)) return false;
|
|
22332
|
+
for (const attr of attributes) {
|
|
22333
|
+
if (!attr || typeof attr !== "object") continue;
|
|
22334
|
+
const a = attr;
|
|
22335
|
+
if (a["Key"] === "lambda.multi_value_headers.enabled") return String(a["Value"]).toLowerCase() === "true";
|
|
22336
|
+
}
|
|
22337
|
+
return false;
|
|
22338
|
+
}
|
|
22339
|
+
/**
|
|
21644
22340
|
* Build a `targetGroupLogicalId -> backing ECS service` index by scanning every
|
|
21645
22341
|
* `AWS::ECS::Service.LoadBalancers[]`. First service wins on a shared target
|
|
21646
22342
|
* group (unusual; would only happen with a hand-rolled template).
|
|
@@ -21900,19 +22596,28 @@ function albStrategy(options) {
|
|
|
21900
22596
|
const { stack, albLogicalId } = resolveAlbTarget(albTarget, stacks);
|
|
21901
22597
|
const resolution = resolveAlbFrontDoor(stack, albLogicalId);
|
|
21902
22598
|
warnings.push(...resolution.warnings);
|
|
22599
|
+
const qualifyTarget = (t) => {
|
|
22600
|
+
if (t.kind === "lambda") return {
|
|
22601
|
+
kind: "lambda",
|
|
22602
|
+
lambda: resolveLambdaTarget(`${stack.stackName}:${t.lambdaLogicalId}`, stacks),
|
|
22603
|
+
targetGroupArn: `${stack.stackName}:${t.targetGroupLogicalId}`,
|
|
22604
|
+
multiValueHeaders: t.multiValueHeaders,
|
|
22605
|
+
weight: t.weight
|
|
22606
|
+
};
|
|
22607
|
+
const serviceTarget = `${stack.stackName}:${t.serviceLogicalId}`;
|
|
22608
|
+
serviceTargets.add(serviceTarget);
|
|
22609
|
+
return {
|
|
22610
|
+
kind: "ecs",
|
|
22611
|
+
serviceTarget,
|
|
22612
|
+
targetContainerName: t.targetContainerName,
|
|
22613
|
+
targetContainerPort: t.targetContainerPort,
|
|
22614
|
+
weight: t.weight
|
|
22615
|
+
};
|
|
22616
|
+
};
|
|
21903
22617
|
const qualify = (action) => {
|
|
21904
22618
|
if (action.kind === "forward") return {
|
|
21905
22619
|
kind: "forward",
|
|
21906
|
-
targets: action.targets.map(
|
|
21907
|
-
const serviceTarget = `${stack.stackName}:${t.serviceLogicalId}`;
|
|
21908
|
-
serviceTargets.add(serviceTarget);
|
|
21909
|
-
return {
|
|
21910
|
-
serviceTarget,
|
|
21911
|
-
targetContainerName: t.targetContainerName,
|
|
21912
|
-
targetContainerPort: t.targetContainerPort,
|
|
21913
|
-
weight: t.weight
|
|
21914
|
-
};
|
|
21915
|
-
})
|
|
22620
|
+
targets: action.targets.map(qualifyTarget)
|
|
21916
22621
|
};
|
|
21917
22622
|
if (action.kind === "redirect") return {
|
|
21918
22623
|
kind: "redirect",
|
|
@@ -21975,7 +22680,7 @@ function albStrategy(options) {
|
|
|
21975
22680
|
*/
|
|
21976
22681
|
function createLocalStartAlbCommand(opts = {}) {
|
|
21977
22682
|
setEmbedConfig(opts.embedConfig);
|
|
21978
|
-
return addCommonEcsServiceOptions(new Command("start-alb").description("Run an Application Load Balancer locally: name the ALB, and cdk-local boots the ECS service(s) behind its HTTP listeners and stands up a local front-door on each listener port that round-robins across the running replicas and routes its listener rules across the backing services — a stable host endpoint, like behind a real load balancer. The symmetric ALB counterpart of `start-api`. Each <target> accepts a CDK display path (MyStack/MyAlb) or stack-qualified logical ID; single-stack apps may omit the stack prefix. Supports HTTP listeners; path-pattern and host-header rule conditions; forward (single and weighted), redirect, and fixed-response actions
|
|
22683
|
+
return addCommonEcsServiceOptions(new Command("start-alb").description("Run an Application Load Balancer locally: name the ALB, and cdk-local boots the ECS service(s) behind its HTTP listeners and stands up a local front-door on each listener port that round-robins across the running replicas and routes its listener rules across the backing services — a stable host endpoint, like behind a real load balancer. The symmetric ALB counterpart of `start-api`. Each <target> accepts a CDK display path (MyStack/MyAlb) or stack-qualified logical ID; single-stack apps may omit the stack prefix. Supports HTTP listeners; path-pattern and host-header rule conditions; forward (single and weighted), redirect, and fixed-response actions; and ECS or Lambda targets (a Lambda target group is invoked locally via the Lambda RIE). HTTPS listeners, the other rule conditions (http-header / query-string / source-ip / http-request-method), and authenticate-* actions are skipped with a warning. Omit <targets> in an interactive terminal to multi-select the load balancers from a list.").argument("[targets...]", "One or more CDK display paths or stack-qualified logical IDs of the AWS::ElasticLoadBalancingV2::LoadBalancer resources to run (omit to multi-select interactively in a TTY)").addOption(new Option("--lb-port <listenerPort=hostPort...>", "Bind the local front-door on a specific host port (e.g. 80=8080); repeatable. Default: host port == ALB listener port. Use this on macOS to remap a privileged listener port (< 1024) to a non-privileged host port.")).action(withErrorHandling(async (targets, options) => {
|
|
21979
22684
|
await runEcsServiceEmulator(targets, options, albStrategy(options), opts.extraStateProviders);
|
|
21980
22685
|
})));
|
|
21981
22686
|
}
|
|
@@ -22052,4 +22757,4 @@ function createLocalListCommand(opts = {}) {
|
|
|
22052
22757
|
|
|
22053
22758
|
//#endregion
|
|
22054
22759
|
export { createJwksCache as $, resolveLambdaArnIntrinsic as $t, invokeTokenAuthorizer as A, substituteAgainstStateAsync as At, VtlEvaluationError as B, resolveCfnRegion as Bt, resolveSelectionExpression as C, architectureToPlatform as Ct, computeRequestIdentityHash as D, resolveRuntimeImage as Dt, buildMethodArn as E, resolveRuntimeFileExtension as Et, buildRestV1Event as F, LocalStateSourceError as Ft, buildMgmtEndpointEnvUrl as G, resolveWatchConfig as Gt, probeHostGatewaySupport as H, CfnLocalStateProvider as Ht, evaluateResponseParameters as I, createLocalStateProvider as It, buildConnectEvent as J, discoverWebSocketApis as Jt, handleConnectionsRequest as K, countTargets as Kt, pickResponseTemplate as L, isCfnFlagPresent as Lt, translateLambdaResponse as M, substituteEnvVarsFromStateAsync as Mt, applyAuthorizerOverlay as N, resolveEnvVars as Nt, evaluateCachedLambdaPolicy as O, EcsTaskResolutionError as Ot, buildHttpApiV2Event as P, materializeLayerFromArn as Pt, buildJwksUrlFromIssuer as Q, pickRefLogicalId as Qt, selectIntegrationResponse as R, rejectExplicitCfnStackWithMultipleStacks as Rt, startApiServer as S, createLocalInvokeCommand as St, defaultCredentialsLoader as T, resolveRuntimeCodeMountPath as Tt, bufferToBody as U, collectSsmParameterRefs as Ut, HOST_GATEWAY_MIN_VERSION as V, resolveCfnStackName as Vt, ConnectionRegistry as W, resolveSsmParameters as Wt, buildMessageEvent as X, parseSelectionExpressionPath as Xt, buildDisconnectEvent as Y, discoverWebSocketApisOrThrow as Yt, buildCognitoJwksUrl as Z, discoverRoutes as Zt, availableApiIdentifiers as _, SUPPORTED_CODE_RUNTIMES as _t, buildCloudMapIndex as a, derivePseudoParametersFromRegion as an, buildCorsConfigByApiId as at, groupRoutesByServer as b, renderCodeDockerfile as bt, createLocalRunTaskCommand as c, LocalInvokeBuildError as cn, matchPreflight as ct, createWatchPredicates as d, MCP_PROTOCOL_VERSION as dt, AGENTCORE_HTTP_PROTOCOL as en, verifyCognitoJwt as et, resolveApiTargetSubset as f, mcpInvokeOnce as ft, buildStageMap as g, waitForAgentCorePing as gt, attachStageContext as h, invokeAgentCore as ht, createLocalStartServiceCommand as i, resolveAgentCoreTarget as in, applyCorsResponseHeaders as it, matchRoute as j, substituteEnvVarsFromState as jt, invokeRequestAuthorizer as k, substituteAgainstState as kt, createLocalInvokeAgentCoreCommand as l, MCP_CONTAINER_PORT as lt, createFileWatcher as m, AGENTCORE_SESSION_ID_HEADER as mt, formatTargetListing as n, AGENTCORE_RUNTIME_TYPE as nn, verifyJwtViaDiscovery as nt, CloudMapRegistry as o, substituteImagePlaceholders as on, buildCorsConfigFromCloudFrontChain as ot, createAuthorizerCache as p, parseSseForJsonRpc as pt, parseConnectionsPath as q, listTargets as qt, createLocalStartAlbCommand as r, AgentCoreResolutionError as rn, attachAuthorizers as rt, getContainerNetworkIp as s, tryResolveImageFnJoin as sn, isFunctionUrlOacFronted as st, createLocalListCommand as t, AGENTCORE_MCP_PROTOCOL as tn, verifyJwtAuthorizer as tt, createLocalStartApiCommand as u, MCP_PATH as ut, filterRoutesByApiIdentifier as v, buildAgentCoreCodeImage as vt, resolveServiceIntegrationParameters as w, buildContainerImage as wt, readMtlsMaterialsFromDisk as x, toCmdArgv as xt, filterRoutesByApiIdentifiers as y, computeCodeImageTag as yt, tryParseStatus as z, resolveCfnFallbackRegion as zt };
|
|
22055
|
-
//# sourceMappingURL=local-list-
|
|
22760
|
+
//# sourceMappingURL=local-list-DL6DlYSN.js.map
|