cdk-local 0.43.0 → 0.45.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-bwZnI2pV.js → local-list-jk2HzwoN.js} +1067 -176
- package/dist/local-list-jk2HzwoN.js.map +1 -0
- package/package.json +1 -1
- package/dist/local-list-bwZnI2pV.js.map +0 -1
|
@@ -7794,7 +7794,7 @@ async function localInvokeCommand(target, options, extraStateProviders) {
|
|
|
7794
7794
|
}
|
|
7795
7795
|
}
|
|
7796
7796
|
if (!assumeSucceeded) {
|
|
7797
|
-
forwardAwsEnv$
|
|
7797
|
+
forwardAwsEnv$3(dockerEnv);
|
|
7798
7798
|
applyProfileCredentialsOverlay(dockerEnv, profileCredentials, false);
|
|
7799
7799
|
if (profileCredsFile) {
|
|
7800
7800
|
dockerEnv["AWS_SHARED_CREDENTIALS_FILE"] = profileCredsFile.containerPath;
|
|
@@ -7849,14 +7849,14 @@ async function localInvokeCommand(target, options, extraStateProviders) {
|
|
|
7849
7849
|
}
|
|
7850
7850
|
}
|
|
7851
7851
|
async function resolveImagePlan(lambda, options) {
|
|
7852
|
-
if (lambda.kind === "zip") return resolveZipImagePlan(lambda, options);
|
|
7853
|
-
return resolveContainerImagePlan(lambda, options);
|
|
7852
|
+
if (lambda.kind === "zip") return resolveZipImagePlan$1(lambda, options);
|
|
7853
|
+
return resolveContainerImagePlan$1(lambda, options);
|
|
7854
7854
|
}
|
|
7855
|
-
async function resolveZipImagePlan(lambda, options) {
|
|
7855
|
+
async function resolveZipImagePlan$1(lambda, options) {
|
|
7856
7856
|
let inlineTmpDir;
|
|
7857
7857
|
let codeDir = lambda.codePath;
|
|
7858
7858
|
if (codeDir === null) {
|
|
7859
|
-
inlineTmpDir = materializeInlineCode$
|
|
7859
|
+
inlineTmpDir = materializeInlineCode$2(lambda.handler, lambda.inlineCode ?? "", resolveRuntimeFileExtension(lambda.runtime));
|
|
7860
7860
|
codeDir = inlineTmpDir;
|
|
7861
7861
|
}
|
|
7862
7862
|
const image = resolveRuntimeImage(lambda.runtime);
|
|
@@ -7933,7 +7933,7 @@ function materializeLambdaLayers$1(layers) {
|
|
|
7933
7933
|
tmpDir
|
|
7934
7934
|
};
|
|
7935
7935
|
}
|
|
7936
|
-
async function resolveContainerImagePlan(lambda, options) {
|
|
7936
|
+
async function resolveContainerImagePlan$1(lambda, options) {
|
|
7937
7937
|
const logger = getLogger();
|
|
7938
7938
|
const platform = architectureToPlatform(lambda.architecture);
|
|
7939
7939
|
const localBuild = await resolveLocalBuildPlan$1(lambda);
|
|
@@ -8083,7 +8083,7 @@ async function assumeLambdaExecutionRole$1(roleArn, region) {
|
|
|
8083
8083
|
sts.destroy();
|
|
8084
8084
|
}
|
|
8085
8085
|
}
|
|
8086
|
-
function forwardAwsEnv$
|
|
8086
|
+
function forwardAwsEnv$3(env) {
|
|
8087
8087
|
for (const key of [
|
|
8088
8088
|
"AWS_ACCESS_KEY_ID",
|
|
8089
8089
|
"AWS_SECRET_ACCESS_KEY",
|
|
@@ -8123,7 +8123,7 @@ function applyProfileCredentialsOverlay(env, profileCreds, assumeRoleActive) {
|
|
|
8123
8123
|
if (profileCreds.sessionToken) env["AWS_SESSION_TOKEN"] = profileCreds.sessionToken;
|
|
8124
8124
|
else delete env["AWS_SESSION_TOKEN"];
|
|
8125
8125
|
}
|
|
8126
|
-
function materializeInlineCode$
|
|
8126
|
+
function materializeInlineCode$2(handler, source, fileExtension) {
|
|
8127
8127
|
const lastDot = handler.lastIndexOf(".");
|
|
8128
8128
|
if (lastDot <= 0) throw new Error(`Handler '${handler}' is malformed: expected '<modulePath>.<exportName>'.`);
|
|
8129
8129
|
const modulePath = handler.substring(0, lastDot);
|
|
@@ -12749,7 +12749,7 @@ const MOCK_API_ID = "local";
|
|
|
12749
12749
|
* UTF-8; otherwise base64. Mirrors what API Gateway emits.
|
|
12750
12750
|
*/
|
|
12751
12751
|
function buildHttpApiV2Event(req, ctx, opts = {}) {
|
|
12752
|
-
const { rawPath, rawQueryString } = splitRawUrl$
|
|
12752
|
+
const { rawPath, rawQueryString } = splitRawUrl$2(req.rawUrl);
|
|
12753
12753
|
const { headers, cookies } = normalizeHeadersV2(req.headers);
|
|
12754
12754
|
const queryStringParameters = parseQueryStringV2(rawQueryString);
|
|
12755
12755
|
const userAgent = headers["user-agent"] ?? "";
|
|
@@ -12809,7 +12809,7 @@ function buildHttpApiV2Event(req, ctx, opts = {}) {
|
|
|
12809
12809
|
* - `pathParameters` may be `null` when there are none (matches AWS).
|
|
12810
12810
|
*/
|
|
12811
12811
|
function buildRestV1Event(req, ctx, opts = {}) {
|
|
12812
|
-
const { rawPath, rawQueryString } = splitRawUrl$
|
|
12812
|
+
const { rawPath, rawQueryString } = splitRawUrl$2(req.rawUrl);
|
|
12813
12813
|
const { singular: headers, multi: multiValueHeaders } = normalizeHeadersV1(req.headers);
|
|
12814
12814
|
const { singular: queryStringParameters, multi: multiValueQueryStringParameters } = parseQueryStringV1(rawQueryString);
|
|
12815
12815
|
const contentType = headers["content-type"] ?? "";
|
|
@@ -12899,7 +12899,7 @@ function applyAuthorizerOverlay(event, overlay) {
|
|
|
12899
12899
|
* `rawQueryString` (everything after, or `''`). Neither component is
|
|
12900
12900
|
* decoded — that's the whole point of "raw" per the AWS spec.
|
|
12901
12901
|
*/
|
|
12902
|
-
function splitRawUrl$
|
|
12902
|
+
function splitRawUrl$2(rawUrl) {
|
|
12903
12903
|
const q = rawUrl.indexOf("?");
|
|
12904
12904
|
if (q === -1) return {
|
|
12905
12905
|
rawPath: rawUrl,
|
|
@@ -13030,7 +13030,7 @@ function encodeBody(body, contentType) {
|
|
|
13030
13030
|
body: "",
|
|
13031
13031
|
isBase64Encoded: false
|
|
13032
13032
|
};
|
|
13033
|
-
if (isTextualContentType(contentType)) return {
|
|
13033
|
+
if (isTextualContentType$1(contentType)) return {
|
|
13034
13034
|
body: body.toString("utf-8"),
|
|
13035
13035
|
isBase64Encoded: false
|
|
13036
13036
|
};
|
|
@@ -13051,7 +13051,7 @@ const TEXT_PREFIXES = [
|
|
|
13051
13051
|
* Whether the given `Content-Type` value indicates textual data (so the
|
|
13052
13052
|
* event body should be UTF-8 instead of base64).
|
|
13053
13053
|
*/
|
|
13054
|
-
function isTextualContentType(contentType) {
|
|
13054
|
+
function isTextualContentType$1(contentType) {
|
|
13055
13055
|
if (!contentType) return false;
|
|
13056
13056
|
const lower = contentType.toLowerCase();
|
|
13057
13057
|
return TEXT_PREFIXES.some((p) => lower.startsWith(p));
|
|
@@ -13116,7 +13116,7 @@ function formatRequestTime(d) {
|
|
|
13116
13116
|
* v2 separates them.
|
|
13117
13117
|
*/
|
|
13118
13118
|
function translateLambdaResponse(payload, version) {
|
|
13119
|
-
if (isErrorEnvelope(payload)) return errorEnvelopeResponse();
|
|
13119
|
+
if (isErrorEnvelope$1(payload)) return errorEnvelopeResponse();
|
|
13120
13120
|
if (isShapedResponse(payload)) return translateShapedResponse(payload, version);
|
|
13121
13121
|
return autoFormatResponse(payload);
|
|
13122
13122
|
}
|
|
@@ -13127,7 +13127,7 @@ function translateLambdaResponse(payload, version) {
|
|
|
13127
13127
|
* matters because user code MAY return a Lambda Proxy response that
|
|
13128
13128
|
* happens to have `errorMessage` as a payload field.
|
|
13129
13129
|
*/
|
|
13130
|
-
function isErrorEnvelope(payload) {
|
|
13130
|
+
function isErrorEnvelope$1(payload) {
|
|
13131
13131
|
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
|
|
13132
13132
|
const obj = payload;
|
|
13133
13133
|
if ("statusCode" in obj) return false;
|
|
@@ -13185,7 +13185,7 @@ function translateShapedResponse(payload, version) {
|
|
|
13185
13185
|
const headersIn = payload["headers"];
|
|
13186
13186
|
if (headersIn && typeof headersIn === "object" && !Array.isArray(headersIn)) for (const [name, value] of Object.entries(headersIn)) {
|
|
13187
13187
|
const lower = name.toLowerCase();
|
|
13188
|
-
const stringValue = stringifyHeaderValue(value);
|
|
13188
|
+
const stringValue = stringifyHeaderValue$1(value);
|
|
13189
13189
|
if (lower === "set-cookie") {
|
|
13190
13190
|
cookies.push(stringValue);
|
|
13191
13191
|
continue;
|
|
@@ -13197,7 +13197,7 @@ function translateShapedResponse(payload, version) {
|
|
|
13197
13197
|
if (mvh && typeof mvh === "object" && !Array.isArray(mvh)) for (const [name, values] of Object.entries(mvh)) {
|
|
13198
13198
|
if (!Array.isArray(values)) continue;
|
|
13199
13199
|
const lower = name.toLowerCase();
|
|
13200
|
-
const stringified = values.map((v) => stringifyHeaderValue(v));
|
|
13200
|
+
const stringified = values.map((v) => stringifyHeaderValue$1(v));
|
|
13201
13201
|
if (lower === "set-cookie") {
|
|
13202
13202
|
for (const c of stringified) cookies.push(c);
|
|
13203
13203
|
continue;
|
|
@@ -13242,7 +13242,7 @@ function autoFormatResponse(payload) {
|
|
|
13242
13242
|
* comma-joined to match the same dup-coalesce rule used on the request
|
|
13243
13243
|
* side.
|
|
13244
13244
|
*/
|
|
13245
|
-
function stringifyHeaderValue(value) {
|
|
13245
|
+
function stringifyHeaderValue$1(value) {
|
|
13246
13246
|
if (value === null || value === void 0) return "";
|
|
13247
13247
|
if (Array.isArray(value)) return value.map((v) => stringifyValue(v)).join(",");
|
|
13248
13248
|
return stringifyValue(value);
|
|
@@ -14040,7 +14040,7 @@ function parseAuthorizationHeader(value) {
|
|
|
14040
14040
|
* Signature = HexEncode(HMAC(SigningKey, StringToSign))
|
|
14041
14041
|
*/
|
|
14042
14042
|
function computeSignature(req, parsed, secretAccessKey, amzDate) {
|
|
14043
|
-
const { path, query } = splitRawUrl(req.rawUrl);
|
|
14043
|
+
const { path, query } = splitRawUrl$1(req.rawUrl);
|
|
14044
14044
|
const canonicalUri = canonicalizePath(path);
|
|
14045
14045
|
const canonicalQuery = canonicalizeQueryString(query);
|
|
14046
14046
|
const headerLines = [];
|
|
@@ -14081,7 +14081,7 @@ function sha256Hex(buf) {
|
|
|
14081
14081
|
* Important: keep the path RAW for canonicalization — the canonicalizer
|
|
14082
14082
|
* does its own URI-encoding so we do NOT decode here.
|
|
14083
14083
|
*/
|
|
14084
|
-
function splitRawUrl(rawUrl) {
|
|
14084
|
+
function splitRawUrl$1(rawUrl) {
|
|
14085
14085
|
const q = rawUrl.indexOf("?");
|
|
14086
14086
|
if (q < 0) return {
|
|
14087
14087
|
path: rawUrl,
|
|
@@ -16669,7 +16669,7 @@ async function buildContainerSpec(args) {
|
|
|
16669
16669
|
let imageRef;
|
|
16670
16670
|
let platform;
|
|
16671
16671
|
if (lambda.kind === "zip") {
|
|
16672
|
-
codeDir = lambda.codePath ?? materializeInlineCode(lambda.handler, lambda.inlineCode ?? "", resolveRuntimeFileExtension(lambda.runtime), inlineTmpDirs);
|
|
16672
|
+
codeDir = lambda.codePath ?? materializeInlineCode$1(lambda.handler, lambda.inlineCode ?? "", resolveRuntimeFileExtension(lambda.runtime), inlineTmpDirs);
|
|
16673
16673
|
optDir = await materializeLambdaLayers(lambda.layers, layerTmpDirs, layerRoleArn);
|
|
16674
16674
|
} else {
|
|
16675
16675
|
imageRef = (await resolveContainerImageForStartApi(lambda, skipPull, args.profile)).imageRef;
|
|
@@ -16729,7 +16729,7 @@ async function buildContainerSpec(args) {
|
|
|
16729
16729
|
dockerEnv["AWS_SESSION_TOKEN"] = creds.sessionToken;
|
|
16730
16730
|
if (stsRegion) dockerEnv["AWS_REGION"] = stsRegion;
|
|
16731
16731
|
} else {
|
|
16732
|
-
forwardAwsEnv$
|
|
16732
|
+
forwardAwsEnv$2(dockerEnv);
|
|
16733
16733
|
if (profileCredentials) {
|
|
16734
16734
|
dockerEnv["AWS_ACCESS_KEY_ID"] = profileCredentials.accessKeyId;
|
|
16735
16735
|
dockerEnv["AWS_SECRET_ACCESS_KEY"] = profileCredentials.secretAccessKey;
|
|
@@ -17071,7 +17071,7 @@ function formatRestV1IntegrationLabel(integration) {
|
|
|
17071
17071
|
* them. (`cdkl invoke` runs once and `--rm` is the right model;
|
|
17072
17072
|
* `cdkl start-api` lives across requests, so leaks compound.)
|
|
17073
17073
|
*/
|
|
17074
|
-
function materializeInlineCode(handler, source, fileExtension, tmpDirsOut) {
|
|
17074
|
+
function materializeInlineCode$1(handler, source, fileExtension, tmpDirsOut) {
|
|
17075
17075
|
const lastDot = handler.lastIndexOf(".");
|
|
17076
17076
|
if (lastDot <= 0) throw new Error(`Handler '${handler}' is malformed: expected '<modulePath>.<exportName>'.`);
|
|
17077
17077
|
const modulePath = handler.substring(0, lastDot);
|
|
@@ -17113,7 +17113,7 @@ function readEnvOverridesFile$3(filePath) {
|
|
|
17113
17113
|
* handler's AWS SDK calls can authenticate. Used when --assume-role is
|
|
17114
17114
|
* NOT set for that Lambda — SAM-compatible default.
|
|
17115
17115
|
*/
|
|
17116
|
-
function forwardAwsEnv$
|
|
17116
|
+
function forwardAwsEnv$2(env) {
|
|
17117
17117
|
for (const key of [
|
|
17118
17118
|
"AWS_ACCESS_KEY_ID",
|
|
17119
17119
|
"AWS_SECRET_ACCESS_KEY",
|
|
@@ -17893,7 +17893,7 @@ async function applyAgentCoreCredentialEnv(dockerEnv, args) {
|
|
|
17893
17893
|
}
|
|
17894
17894
|
}
|
|
17895
17895
|
if (!assumeSucceeded) {
|
|
17896
|
-
forwardAwsEnv(dockerEnv);
|
|
17896
|
+
forwardAwsEnv$1(dockerEnv);
|
|
17897
17897
|
applyProfileCredentialsOverlay(dockerEnv, args.profileCredentials, false);
|
|
17898
17898
|
if (args.profileCredsFile) {
|
|
17899
17899
|
dockerEnv["AWS_SHARED_CREDENTIALS_FILE"] = args.profileCredsFile.containerPath;
|
|
@@ -17962,7 +17962,7 @@ function emitMcpResult(result) {
|
|
|
17962
17962
|
function platformToArchitecture(platform) {
|
|
17963
17963
|
return platform === "linux/amd64" ? "x86_64" : "arm64";
|
|
17964
17964
|
}
|
|
17965
|
-
function forwardAwsEnv(env) {
|
|
17965
|
+
function forwardAwsEnv$1(env) {
|
|
17966
17966
|
for (const key of [
|
|
17967
17967
|
"AWS_ACCESS_KEY_ID",
|
|
17968
17968
|
"AWS_SECRET_ACCESS_KEY",
|
|
@@ -20606,6 +20606,203 @@ var FrontDoorEndpointPool = class {
|
|
|
20606
20606
|
}
|
|
20607
20607
|
};
|
|
20608
20608
|
|
|
20609
|
+
//#endregion
|
|
20610
|
+
//#region src/local/alb-lambda-event.ts
|
|
20611
|
+
/** Content types ALB treats as text (body sent verbatim, `isBase64Encoded: false`). */
|
|
20612
|
+
function isTextualContentType(contentType) {
|
|
20613
|
+
const ct = contentType.toLowerCase();
|
|
20614
|
+
return ct.startsWith("text/") || ct.startsWith("application/json") || ct.startsWith("application/javascript") || ct.startsWith("application/xml");
|
|
20615
|
+
}
|
|
20616
|
+
/**
|
|
20617
|
+
* Build a `AlbHttpRequestSnapshot` from a live `node:http` request plus the
|
|
20618
|
+
* already-buffered body. Header values arrive as `string | string[]`; this
|
|
20619
|
+
* normalizes them to `string[]` (preserving multi-value) the builder expects.
|
|
20620
|
+
*/
|
|
20621
|
+
function snapshotFromIncoming(req, body) {
|
|
20622
|
+
const headers = {};
|
|
20623
|
+
for (const [name, value] of Object.entries(req.headers)) {
|
|
20624
|
+
if (value === void 0) continue;
|
|
20625
|
+
headers[name] = Array.isArray(value) ? value : [value];
|
|
20626
|
+
}
|
|
20627
|
+
return {
|
|
20628
|
+
method: (req.method ?? "GET").toUpperCase(),
|
|
20629
|
+
rawUrl: req.url ?? "/",
|
|
20630
|
+
headers,
|
|
20631
|
+
body
|
|
20632
|
+
};
|
|
20633
|
+
}
|
|
20634
|
+
/** Split a raw URL into its path (query-stripped, not decoded) and raw query string. */
|
|
20635
|
+
function splitRawUrl(rawUrl) {
|
|
20636
|
+
const hashIdx = rawUrl.indexOf("#");
|
|
20637
|
+
const noHash = hashIdx === -1 ? rawUrl : rawUrl.slice(0, hashIdx);
|
|
20638
|
+
const qIdx = noHash.indexOf("?");
|
|
20639
|
+
if (qIdx === -1) return {
|
|
20640
|
+
path: noHash,
|
|
20641
|
+
rawQuery: ""
|
|
20642
|
+
};
|
|
20643
|
+
return {
|
|
20644
|
+
path: noHash.slice(0, qIdx),
|
|
20645
|
+
rawQuery: noHash.slice(qIdx + 1)
|
|
20646
|
+
};
|
|
20647
|
+
}
|
|
20648
|
+
/**
|
|
20649
|
+
* Parse a raw query string into single + multi-value maps. ALB does NOT decode
|
|
20650
|
+
* query parameters (the docs explicitly say "If the query parameters are
|
|
20651
|
+
* URL-encoded, the load balancer does not decode them"), so values are kept
|
|
20652
|
+
* verbatim. A bare `?flag` key yields the empty string value.
|
|
20653
|
+
*/
|
|
20654
|
+
function parseQuery(rawQuery) {
|
|
20655
|
+
const single = {};
|
|
20656
|
+
const multi = {};
|
|
20657
|
+
if (rawQuery.length === 0) return {
|
|
20658
|
+
single,
|
|
20659
|
+
multi
|
|
20660
|
+
};
|
|
20661
|
+
for (const pair of rawQuery.split("&")) {
|
|
20662
|
+
if (pair.length === 0) continue;
|
|
20663
|
+
const eq = pair.indexOf("=");
|
|
20664
|
+
const key = eq === -1 ? pair : pair.slice(0, eq);
|
|
20665
|
+
const value = eq === -1 ? "" : pair.slice(eq + 1);
|
|
20666
|
+
single[key] = value;
|
|
20667
|
+
(multi[key] ??= []).push(value);
|
|
20668
|
+
}
|
|
20669
|
+
return {
|
|
20670
|
+
single,
|
|
20671
|
+
multi
|
|
20672
|
+
};
|
|
20673
|
+
}
|
|
20674
|
+
/**
|
|
20675
|
+
* Build the single + multi-value header maps. Names are lowercased; the single
|
|
20676
|
+
* map keeps the LAST value (ALB default-format), the multi map keeps every
|
|
20677
|
+
* value in arrival order.
|
|
20678
|
+
*/
|
|
20679
|
+
function buildHeaderMaps(headers) {
|
|
20680
|
+
const single = {};
|
|
20681
|
+
const multi = {};
|
|
20682
|
+
for (const [name, values] of Object.entries(headers)) {
|
|
20683
|
+
const lower = name.toLowerCase();
|
|
20684
|
+
const list = values.slice();
|
|
20685
|
+
multi[lower] = list;
|
|
20686
|
+
if (list.length > 0) single[lower] = list[list.length - 1];
|
|
20687
|
+
}
|
|
20688
|
+
return {
|
|
20689
|
+
single,
|
|
20690
|
+
multi
|
|
20691
|
+
};
|
|
20692
|
+
}
|
|
20693
|
+
/** Header lookup that tolerates the case-folded multi-value request map. */
|
|
20694
|
+
function firstHeader(headers, name) {
|
|
20695
|
+
const lower = name.toLowerCase();
|
|
20696
|
+
for (const [k, v] of Object.entries(headers)) if (k.toLowerCase() === lower) return v[0];
|
|
20697
|
+
}
|
|
20698
|
+
/**
|
|
20699
|
+
* Build the ALB Lambda-target invocation event from an HTTP request snapshot.
|
|
20700
|
+
* Emits exactly the single-value OR multi-value variant per
|
|
20701
|
+
* `opts.multiValueHeaders`.
|
|
20702
|
+
*/
|
|
20703
|
+
function buildAlbLambdaEvent(req, opts) {
|
|
20704
|
+
const { path, rawQuery } = splitRawUrl(req.rawUrl);
|
|
20705
|
+
const query = parseQuery(rawQuery);
|
|
20706
|
+
const headerMaps = buildHeaderMaps(req.headers);
|
|
20707
|
+
const contentEncoding = firstHeader(req.headers, "content-encoding");
|
|
20708
|
+
const contentType = firstHeader(req.headers, "content-type") ?? "";
|
|
20709
|
+
const isBase64Encoded = req.body.length > 0 && (contentEncoding !== void 0 ? true : !isTextualContentType(contentType));
|
|
20710
|
+
const body = isBase64Encoded ? req.body.toString("base64") : req.body.toString("utf-8");
|
|
20711
|
+
const event = {
|
|
20712
|
+
requestContext: { elb: { targetGroupArn: opts.targetGroupArn } },
|
|
20713
|
+
httpMethod: req.method,
|
|
20714
|
+
path,
|
|
20715
|
+
isBase64Encoded,
|
|
20716
|
+
body
|
|
20717
|
+
};
|
|
20718
|
+
if (opts.multiValueHeaders) {
|
|
20719
|
+
event["multiValueHeaders"] = headerMaps.multi;
|
|
20720
|
+
event["multiValueQueryStringParameters"] = query.multi;
|
|
20721
|
+
} else {
|
|
20722
|
+
event["headers"] = headerMaps.single;
|
|
20723
|
+
event["queryStringParameters"] = query.single;
|
|
20724
|
+
}
|
|
20725
|
+
return event;
|
|
20726
|
+
}
|
|
20727
|
+
/** A Lambda RIE error envelope (`{errorMessage, errorType, ...}`) with no statusCode. */
|
|
20728
|
+
function isErrorEnvelope(payload) {
|
|
20729
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
|
|
20730
|
+
const obj = payload;
|
|
20731
|
+
if ("statusCode" in obj) return false;
|
|
20732
|
+
return typeof obj["errorMessage"] === "string";
|
|
20733
|
+
}
|
|
20734
|
+
/**
|
|
20735
|
+
* The canonical 502 a real ALB returns when a Lambda target's response is
|
|
20736
|
+
* malformed (missing/invalid `statusCode`, non-object payload, or a runtime
|
|
20737
|
+
* error envelope). Plain-text body, mirroring ALB's own 502 page shape closely
|
|
20738
|
+
* enough for local dev.
|
|
20739
|
+
*/
|
|
20740
|
+
function badGatewayResponse() {
|
|
20741
|
+
const body = Buffer.from("<html><body><h1>502 Bad Gateway</h1></body></html>", "utf-8");
|
|
20742
|
+
return {
|
|
20743
|
+
statusCode: 502,
|
|
20744
|
+
statusDescription: "502 Bad Gateway",
|
|
20745
|
+
headers: {
|
|
20746
|
+
"content-type": ["text/html"],
|
|
20747
|
+
"content-length": [String(body.length)]
|
|
20748
|
+
},
|
|
20749
|
+
body
|
|
20750
|
+
};
|
|
20751
|
+
}
|
|
20752
|
+
function stringifyHeaderValue(value) {
|
|
20753
|
+
if (value === null || value === void 0) return "";
|
|
20754
|
+
if (typeof value === "string") return value;
|
|
20755
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
|
|
20756
|
+
return JSON.stringify(value) ?? "";
|
|
20757
|
+
}
|
|
20758
|
+
/**
|
|
20759
|
+
* Translate a Lambda ALB-target response payload into HTTP components.
|
|
20760
|
+
*
|
|
20761
|
+
* A well-formed response is an object with a numeric `statusCode`. Headers come
|
|
20762
|
+
* from `headers` (single-value) and/or `multiValueHeaders` (array-valued); ALB
|
|
20763
|
+
* accepts either regardless of the request-side multi-value setting, so both
|
|
20764
|
+
* are honored here (multiValueHeaders extend / append to the single map).
|
|
20765
|
+
* `body` is optional; `isBase64Encoded: true` means the body is base64 and is
|
|
20766
|
+
* decoded to raw bytes.
|
|
20767
|
+
*
|
|
20768
|
+
* Anything else -> 502 (matching a real ALB), incl. a runtime error envelope.
|
|
20769
|
+
*/
|
|
20770
|
+
function translateAlbLambdaResponse(payload) {
|
|
20771
|
+
if (isErrorEnvelope(payload)) return badGatewayResponse();
|
|
20772
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return badGatewayResponse();
|
|
20773
|
+
const obj = payload;
|
|
20774
|
+
const statusRaw = obj["statusCode"];
|
|
20775
|
+
if (typeof statusRaw !== "number" || !Number.isFinite(statusRaw)) return badGatewayResponse();
|
|
20776
|
+
const statusCode = Math.trunc(statusRaw);
|
|
20777
|
+
const isBase64 = obj["isBase64Encoded"] === true;
|
|
20778
|
+
const rawBody = obj["body"];
|
|
20779
|
+
let body;
|
|
20780
|
+
if (rawBody === void 0 || rawBody === null) body = Buffer.alloc(0);
|
|
20781
|
+
else if (typeof rawBody === "string") body = isBase64 ? Buffer.from(rawBody, "base64") : Buffer.from(rawBody, "utf-8");
|
|
20782
|
+
else body = Buffer.from(JSON.stringify(rawBody), "utf-8");
|
|
20783
|
+
const headers = {};
|
|
20784
|
+
const addHeader = (name, value) => {
|
|
20785
|
+
const lower = name.toLowerCase();
|
|
20786
|
+
(headers[lower] ??= []).push(value);
|
|
20787
|
+
};
|
|
20788
|
+
const singleHeaders = obj["headers"];
|
|
20789
|
+
if (singleHeaders && typeof singleHeaders === "object" && !Array.isArray(singleHeaders)) for (const [name, value] of Object.entries(singleHeaders)) addHeader(name, stringifyHeaderValue(value));
|
|
20790
|
+
const multiHeaders = obj["multiValueHeaders"];
|
|
20791
|
+
if (multiHeaders && typeof multiHeaders === "object" && !Array.isArray(multiHeaders)) for (const [name, values] of Object.entries(multiHeaders)) {
|
|
20792
|
+
if (!Array.isArray(values)) continue;
|
|
20793
|
+
for (const v of values) addHeader(name, stringifyHeaderValue(v));
|
|
20794
|
+
}
|
|
20795
|
+
headers["content-length"] = [String(body.length)];
|
|
20796
|
+
const result = {
|
|
20797
|
+
statusCode,
|
|
20798
|
+
headers,
|
|
20799
|
+
body
|
|
20800
|
+
};
|
|
20801
|
+
const statusDescription = obj["statusDescription"];
|
|
20802
|
+
if (typeof statusDescription === "string" && statusDescription.length > 0) result.statusDescription = statusDescription;
|
|
20803
|
+
return result;
|
|
20804
|
+
}
|
|
20805
|
+
|
|
20609
20806
|
//#endregion
|
|
20610
20807
|
//#region src/local/front-door-server.ts
|
|
20611
20808
|
/** Default per-request upstream timeout — a hung replica yields a 504, not a hang. */
|
|
@@ -20646,14 +20843,56 @@ async function startFrontDoorServer(opts) {
|
|
|
20646
20843
|
}
|
|
20647
20844
|
};
|
|
20648
20845
|
}
|
|
20846
|
+
/**
|
|
20847
|
+
* Resolve the dispatch target for a request path from whichever selector the
|
|
20848
|
+
* caller supplied. `selectTarget` (#123, ECS-or-Lambda) wins over the
|
|
20849
|
+
* pool-only `selectPool`; a `selectPool` hit is adapted to a `kind: 'pool'`
|
|
20850
|
+
* target so the request handler has a single code path.
|
|
20851
|
+
*/
|
|
20852
|
+
function resolveDispatchTarget(opts, requestPath) {
|
|
20853
|
+
if (opts.selectTarget) return opts.selectTarget(requestPath);
|
|
20854
|
+
if (opts.selectPool) {
|
|
20855
|
+
const pool = opts.selectPool(requestPath);
|
|
20856
|
+
return pool ? {
|
|
20857
|
+
kind: "pool",
|
|
20858
|
+
pool
|
|
20859
|
+
} : void 0;
|
|
20860
|
+
}
|
|
20861
|
+
}
|
|
20649
20862
|
function handleProxyRequest(req, res, opts) {
|
|
20863
|
+
const url = req.url ?? "/";
|
|
20864
|
+
if (opts.route) {
|
|
20865
|
+
const action = opts.route({
|
|
20866
|
+
path: url,
|
|
20867
|
+
...hostHeader(req)
|
|
20868
|
+
});
|
|
20869
|
+
if (!action) return reply404(req, res, opts);
|
|
20870
|
+
if (action.kind === "redirect" || action.kind === "fixed-response") {
|
|
20871
|
+
req.resume();
|
|
20872
|
+
if (action.kind === "redirect") writeRedirect(res, action, req, opts.listenerPort);
|
|
20873
|
+
else writeFixedResponse(res, action);
|
|
20874
|
+
return Promise.resolve();
|
|
20875
|
+
}
|
|
20876
|
+
const picked = pickWeightedTarget(action.pools);
|
|
20877
|
+
if (!picked) {
|
|
20878
|
+
writeError(res, 502, `No forward target selected behind ${opts.label} (every weighted target has weight 0).`);
|
|
20879
|
+
return Promise.resolve();
|
|
20880
|
+
}
|
|
20881
|
+
if ("lambda" in picked) return handleLambdaRequest(req, res, picked.lambda, opts);
|
|
20882
|
+
return handlePoolRequest(req, res, picked.pool, opts);
|
|
20883
|
+
}
|
|
20884
|
+
const target = resolveDispatchTarget(opts, url);
|
|
20885
|
+
if (!target) return reply404(req, res, opts);
|
|
20886
|
+
if (target.kind === "lambda") return handleLambdaRequest(req, res, target.lambda, opts);
|
|
20887
|
+
return handlePoolRequest(req, res, target.pool, opts);
|
|
20888
|
+
}
|
|
20889
|
+
/** Reply 404 — an ALB listener with no matching rule and no default action. */
|
|
20890
|
+
function reply404(req, res, opts) {
|
|
20891
|
+
writeError(res, 404, `No listener rule matched '${req.url ?? "/"}' on ${opts.label}, and the listener has no default action forwarding to a local target.`);
|
|
20892
|
+
return Promise.resolve();
|
|
20893
|
+
}
|
|
20894
|
+
function handlePoolRequest(req, res, pool, opts) {
|
|
20650
20895
|
return new Promise((resolve) => {
|
|
20651
|
-
const pool = opts.selectPool(req.url ?? "/");
|
|
20652
|
-
if (!pool) {
|
|
20653
|
-
writeError(res, 404, `No listener rule matched '${req.url ?? "/"}' on ${opts.label}, and the listener has no default action forwarding to a local target.`);
|
|
20654
|
-
resolve();
|
|
20655
|
-
return;
|
|
20656
|
-
}
|
|
20657
20896
|
const endpoint = pool.next();
|
|
20658
20897
|
if (!endpoint) {
|
|
20659
20898
|
writeError(res, 503, `No running replicas behind ${opts.label} for the matched target. The front-door has no healthy target to forward to.`);
|
|
@@ -20703,6 +20942,154 @@ function handleProxyRequest(req, res, opts) {
|
|
|
20703
20942
|
req.pipe(proxyReq);
|
|
20704
20943
|
});
|
|
20705
20944
|
}
|
|
20945
|
+
/** Extract the request `Host` header (string) for host-header rule matching. */
|
|
20946
|
+
function hostHeader(req) {
|
|
20947
|
+
const raw = req.headers.host;
|
|
20948
|
+
const host = Array.isArray(raw) ? raw[0] : raw;
|
|
20949
|
+
return host ? { host } : {};
|
|
20950
|
+
}
|
|
20951
|
+
/**
|
|
20952
|
+
* Pick one weighted target from a forward set: weighted random over the
|
|
20953
|
+
* non-zero weights. A single-entry set short-circuits to that entry. Returns
|
|
20954
|
+
* `undefined` when every weight is 0 (an ALB-valid but un-routable forward).
|
|
20955
|
+
* Used for forwards that may mix ECS pools and Lambda invokers.
|
|
20956
|
+
*/
|
|
20957
|
+
function pickWeightedTarget(targets) {
|
|
20958
|
+
if (targets.length === 0) return void 0;
|
|
20959
|
+
if (targets.length === 1) return targets[0].weight > 0 ? targets[0] : void 0;
|
|
20960
|
+
const total = targets.reduce((sum, t) => sum + Math.max(0, t.weight), 0);
|
|
20961
|
+
if (total <= 0) return void 0;
|
|
20962
|
+
let roll = Math.random() * total;
|
|
20963
|
+
for (const t of targets) {
|
|
20964
|
+
const w = Math.max(0, t.weight);
|
|
20965
|
+
if (w === 0) continue;
|
|
20966
|
+
roll -= w;
|
|
20967
|
+
if (roll < 0) return t;
|
|
20968
|
+
}
|
|
20969
|
+
for (let i = targets.length - 1; i >= 0; i--) if (Math.max(0, targets[i].weight) > 0) return targets[i];
|
|
20970
|
+
}
|
|
20971
|
+
/**
|
|
20972
|
+
* Synthesize an ALB-style redirect (301 / 302). ALB builds the `Location` from
|
|
20973
|
+
* the action fields with `#{protocol}` / `#{host}` / `#{port}` / `#{path}` /
|
|
20974
|
+
* `#{query}` placeholders filled from the incoming request. We resolve those
|
|
20975
|
+
* placeholders against the request the front-door received.
|
|
20976
|
+
*/
|
|
20977
|
+
function writeRedirect(res, action, req, listenerPort) {
|
|
20978
|
+
const location = buildRedirectLocation(action, req, listenerPort);
|
|
20979
|
+
res.writeHead(action.statusCode, {
|
|
20980
|
+
location,
|
|
20981
|
+
"content-type": "text/plain; charset=utf-8",
|
|
20982
|
+
"content-length": "0"
|
|
20983
|
+
});
|
|
20984
|
+
res.end();
|
|
20985
|
+
}
|
|
20986
|
+
/** Build the `Location` URL for a redirect action, resolving ALB `#{...}` placeholders. */
|
|
20987
|
+
function buildRedirectLocation(action, req, listenerPort) {
|
|
20988
|
+
const url = req.url ?? "/";
|
|
20989
|
+
const qIndex = url.indexOf("?");
|
|
20990
|
+
const reqPath = qIndex === -1 ? url : url.slice(0, qIndex);
|
|
20991
|
+
const reqQuery = qIndex === -1 ? "" : url.slice(qIndex + 1);
|
|
20992
|
+
const rawHost = req.headers["host"];
|
|
20993
|
+
const placeholders = {
|
|
20994
|
+
protocol: "http",
|
|
20995
|
+
host: ((Array.isArray(rawHost) ? rawHost[0] : rawHost) ?? "").split(":")[0] ?? "",
|
|
20996
|
+
port: String(listenerPort),
|
|
20997
|
+
path: reqPath.replace(/^\//, ""),
|
|
20998
|
+
query: reqQuery
|
|
20999
|
+
};
|
|
21000
|
+
const fill = (template) => template.replace(/#\{(protocol|host|port|path|query)\}/g, (_m, key) => placeholders[key] ?? "");
|
|
21001
|
+
const protocol = (action.protocol ? fill(action.protocol) : placeholders["protocol"]).toLowerCase();
|
|
21002
|
+
const host = action.host ? fill(action.host) : placeholders["host"];
|
|
21003
|
+
const port = action.port ? fill(action.port) : placeholders["port"];
|
|
21004
|
+
const path = fill(action.path ?? "/#{path}");
|
|
21005
|
+
const query = fill(action.query ?? "#{query}");
|
|
21006
|
+
return `${protocol}://${protocol === "http" && port === "80" || protocol === "https" && port === "443" || port === "" ? host : `${host}:${port}`}${path.startsWith("/") ? path : `/${path}`}${query ? `?${query}` : ""}`;
|
|
21007
|
+
}
|
|
21008
|
+
/** Synthesize an ALB-style fixed-response. */
|
|
21009
|
+
function writeFixedResponse(res, action) {
|
|
21010
|
+
const body = action.messageBody ?? "";
|
|
21011
|
+
res.writeHead(action.statusCode, {
|
|
21012
|
+
"content-type": action.contentType ?? "text/plain; charset=utf-8",
|
|
21013
|
+
"content-length": String(Buffer.byteLength(body))
|
|
21014
|
+
});
|
|
21015
|
+
res.end(body);
|
|
21016
|
+
}
|
|
21017
|
+
/** Maximum request body the ALB Lambda-target path buffers (ALB's own limit is 1 MB). */
|
|
21018
|
+
const ALB_LAMBDA_MAX_BODY_BYTES = 1024 * 1024;
|
|
21019
|
+
/**
|
|
21020
|
+
* Serve a request that resolved to a Lambda forward target (#123). Buffers the
|
|
21021
|
+
* request body (ALB caps the Lambda-target request body at 1 MB), translates
|
|
21022
|
+
* the request into the ALB Lambda-target event, invokes the function locally,
|
|
21023
|
+
* and writes the translated response. A malformed handler response or an
|
|
21024
|
+
* invoke failure surfaces as 502 — mirroring a real ALB.
|
|
21025
|
+
*/
|
|
21026
|
+
function handleLambdaRequest(req, res, lambda, opts) {
|
|
21027
|
+
const logger = getLogger().child("front-door");
|
|
21028
|
+
return new Promise((resolve) => {
|
|
21029
|
+
let settled = false;
|
|
21030
|
+
const done = () => {
|
|
21031
|
+
if (settled) return;
|
|
21032
|
+
settled = true;
|
|
21033
|
+
resolve();
|
|
21034
|
+
};
|
|
21035
|
+
const chunks = [];
|
|
21036
|
+
let total = 0;
|
|
21037
|
+
let aborted = false;
|
|
21038
|
+
req.on("data", (chunk) => {
|
|
21039
|
+
if (aborted) return;
|
|
21040
|
+
total += chunk.length;
|
|
21041
|
+
if (total > ALB_LAMBDA_MAX_BODY_BYTES) {
|
|
21042
|
+
aborted = true;
|
|
21043
|
+
writeError(res, 413, `Request body exceeds the ${ALB_LAMBDA_MAX_BODY_BYTES}-byte Lambda-target limit on ${opts.label}.`);
|
|
21044
|
+
req.destroy();
|
|
21045
|
+
done();
|
|
21046
|
+
return;
|
|
21047
|
+
}
|
|
21048
|
+
chunks.push(chunk);
|
|
21049
|
+
});
|
|
21050
|
+
req.on("error", () => {
|
|
21051
|
+
if (!res.headersSent) writeError(res, 400, `Failed to read request body on ${opts.label}.`);
|
|
21052
|
+
done();
|
|
21053
|
+
});
|
|
21054
|
+
req.on("end", () => {
|
|
21055
|
+
if (aborted) return;
|
|
21056
|
+
const serveLambda = async () => {
|
|
21057
|
+
try {
|
|
21058
|
+
const body = Buffer.concat(chunks);
|
|
21059
|
+
const forwardHeaders = { ...req.headers };
|
|
21060
|
+
stripHopByHopHeaders(forwardHeaders);
|
|
21061
|
+
appendForwardedHeaders(forwardHeaders, req, opts.listenerPort);
|
|
21062
|
+
const snapshot = snapshotFromIncoming(req, body);
|
|
21063
|
+
for (const [name, value] of Object.entries(forwardHeaders)) {
|
|
21064
|
+
if (value === void 0) continue;
|
|
21065
|
+
snapshot.headers[name] = Array.isArray(value) ? value : [value];
|
|
21066
|
+
}
|
|
21067
|
+
const event = buildAlbLambdaEvent(snapshot, {
|
|
21068
|
+
targetGroupArn: lambda.targetGroupArn,
|
|
21069
|
+
multiValueHeaders: lambda.multiValueHeaders
|
|
21070
|
+
});
|
|
21071
|
+
const translated = translateAlbLambdaResponse(await lambda.invoke(event));
|
|
21072
|
+
if (res.headersSent || res.writableEnded) {
|
|
21073
|
+
done();
|
|
21074
|
+
return;
|
|
21075
|
+
}
|
|
21076
|
+
const outHeaders = {};
|
|
21077
|
+
for (const [name, values] of Object.entries(translated.headers)) outHeaders[name] = values.length === 1 ? values[0] : values;
|
|
21078
|
+
if (translated.statusDescription) res.writeHead(translated.statusCode, translated.statusDescription, outHeaders);
|
|
21079
|
+
else res.writeHead(translated.statusCode, outHeaders);
|
|
21080
|
+
res.end(translated.body);
|
|
21081
|
+
done();
|
|
21082
|
+
} catch (err) {
|
|
21083
|
+
logger.debug(`Lambda target '${lambda.label}' request failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
21084
|
+
if (!res.headersSent) writeError(res, 502, `Lambda target '${lambda.label}' behind ${opts.label} failed.`);
|
|
21085
|
+
else if (!res.writableEnded) res.destroy();
|
|
21086
|
+
done();
|
|
21087
|
+
}
|
|
21088
|
+
};
|
|
21089
|
+
serveLambda();
|
|
21090
|
+
});
|
|
21091
|
+
});
|
|
21092
|
+
}
|
|
20706
21093
|
/** Standard hop-by-hop headers (RFC 7230 §6.1) — a proxy must not forward these. */
|
|
20707
21094
|
const HOP_BY_HOP_HEADERS = [
|
|
20708
21095
|
"connection",
|
|
@@ -20751,21 +21138,39 @@ function writeError(res, statusCode, message) {
|
|
|
20751
21138
|
//#endregion
|
|
20752
21139
|
//#region src/local/alb-path-matcher.ts
|
|
20753
21140
|
/**
|
|
20754
|
-
* Return the target of the highest-priority rule whose
|
|
20755
|
-
* `
|
|
20756
|
-
*
|
|
21141
|
+
* Return the target of the highest-priority rule whose conditions all match
|
|
21142
|
+
* `req`, or `undefined` when none match (caller uses the default). Rules are
|
|
21143
|
+
* evaluated in ascending priority; the input order is irrelevant.
|
|
21144
|
+
*
|
|
21145
|
+
* Accepts either a request facts object or a bare path string (the path-only
|
|
21146
|
+
* form keeps the original signature working for callers that have no Host).
|
|
20757
21147
|
*/
|
|
20758
|
-
function matchAlbPathRule(
|
|
20759
|
-
const path =
|
|
21148
|
+
function matchAlbPathRule(req, rules) {
|
|
21149
|
+
const { path, host } = typeof req === "string" ? {
|
|
21150
|
+
path: req,
|
|
21151
|
+
host: void 0
|
|
21152
|
+
} : req;
|
|
21153
|
+
const requestPath = pathOf(path);
|
|
21154
|
+
const requestHost = host !== void 0 ? hostOf(host) : void 0;
|
|
20760
21155
|
const ordered = [...rules].sort((a, b) => a.priority - b.priority);
|
|
20761
|
-
for (const rule of ordered) if (rule
|
|
21156
|
+
for (const rule of ordered) if (ruleMatches(rule, requestPath, requestHost)) return rule.target;
|
|
20762
21157
|
}
|
|
20763
21158
|
/**
|
|
20764
|
-
* Whether a single
|
|
20765
|
-
*
|
|
21159
|
+
* Whether a single rule's conditions all match. A `path-pattern` /
|
|
21160
|
+
* `host-header` condition is satisfied when ANY of its values match (OR); a
|
|
21161
|
+
* rule with both fields requires both to match (AND). An empty pattern list
|
|
21162
|
+
* for a field means "no constraint on that field" (the condition was absent).
|
|
20766
21163
|
*/
|
|
20767
|
-
function
|
|
20768
|
-
|
|
21164
|
+
function ruleMatches(rule, requestPath, requestHost) {
|
|
21165
|
+
if (rule.pathPatterns.length > 0) {
|
|
21166
|
+
if (!rule.pathPatterns.some((pattern) => globToRegExp(pattern, false).test(requestPath))) return false;
|
|
21167
|
+
}
|
|
21168
|
+
const hostPatterns = rule.hostPatterns ?? [];
|
|
21169
|
+
if (hostPatterns.length > 0) {
|
|
21170
|
+
if (requestHost === void 0) return false;
|
|
21171
|
+
if (!hostPatterns.some((pattern) => globToRegExp(pattern, true).test(requestHost))) return false;
|
|
21172
|
+
}
|
|
21173
|
+
return true;
|
|
20769
21174
|
}
|
|
20770
21175
|
/** Strip the query string / fragment so only the URL path is matched. */
|
|
20771
21176
|
function pathOf(url) {
|
|
@@ -20776,21 +21181,219 @@ function pathOf(url) {
|
|
|
20776
21181
|
if (h !== -1 && h < end) end = h;
|
|
20777
21182
|
return url.slice(0, end);
|
|
20778
21183
|
}
|
|
21184
|
+
/**
|
|
21185
|
+
* Normalize a `Host` header for matching: drop any `:port` suffix and lower-case
|
|
21186
|
+
* it (DNS hostnames are case-insensitive). IPv6 literals (`[::1]:8080`) keep the
|
|
21187
|
+
* bracketed address and only the trailing port is removed.
|
|
21188
|
+
*/
|
|
21189
|
+
function hostOf(host) {
|
|
21190
|
+
const trimmed = host.trim();
|
|
21191
|
+
if (trimmed.startsWith("[")) {
|
|
21192
|
+
const close = trimmed.indexOf("]");
|
|
21193
|
+
if (close !== -1) return trimmed.slice(0, close + 1).toLowerCase();
|
|
21194
|
+
return trimmed.toLowerCase();
|
|
21195
|
+
}
|
|
21196
|
+
const colon = trimmed.indexOf(":");
|
|
21197
|
+
return (colon === -1 ? trimmed : trimmed.slice(0, colon)).toLowerCase();
|
|
21198
|
+
}
|
|
20779
21199
|
const REGEXP_META = /[.+^${}()|[\]\\]/;
|
|
20780
21200
|
/**
|
|
20781
|
-
* Translate an ALB
|
|
20782
|
-
*
|
|
20783
|
-
*
|
|
21201
|
+
* Translate an ALB `*` / `?` glob into an anchored RegExp: `*` -> `.*`,
|
|
21202
|
+
* `?` -> `.`, every other character is escaped and matched literally. Host
|
|
21203
|
+
* patterns match case-insensitively (the `i` flag) and the pattern is
|
|
21204
|
+
* lower-cased to pair with the lower-cased host; path patterns are
|
|
21205
|
+
* case-sensitive.
|
|
20784
21206
|
*/
|
|
20785
|
-
function globToRegExp(pattern) {
|
|
21207
|
+
function globToRegExp(pattern, caseInsensitive) {
|
|
21208
|
+
const source = caseInsensitive ? pattern.toLowerCase() : pattern;
|
|
20786
21209
|
let body = "";
|
|
20787
|
-
for (const ch of
|
|
21210
|
+
for (const ch of source) if (ch === "*") body += ".*";
|
|
20788
21211
|
else if (ch === "?") body += ".";
|
|
20789
21212
|
else if (REGEXP_META.test(ch)) body += `\\${ch}`;
|
|
20790
21213
|
else body += ch;
|
|
20791
21214
|
return new RegExp(`^${body}$`);
|
|
20792
21215
|
}
|
|
20793
21216
|
|
|
21217
|
+
//#endregion
|
|
21218
|
+
//#region src/local/front-door-lambda-runner.ts
|
|
21219
|
+
/** Forward the dev shell's AWS credential / region env into the Lambda container. */
|
|
21220
|
+
function forwardAwsEnv() {
|
|
21221
|
+
const env = {};
|
|
21222
|
+
for (const key of [
|
|
21223
|
+
"AWS_ACCESS_KEY_ID",
|
|
21224
|
+
"AWS_SECRET_ACCESS_KEY",
|
|
21225
|
+
"AWS_SESSION_TOKEN",
|
|
21226
|
+
"AWS_REGION",
|
|
21227
|
+
"AWS_DEFAULT_REGION"
|
|
21228
|
+
]) {
|
|
21229
|
+
const value = process.env[key];
|
|
21230
|
+
if (value !== void 0) env[key] = value;
|
|
21231
|
+
}
|
|
21232
|
+
return env;
|
|
21233
|
+
}
|
|
21234
|
+
/**
|
|
21235
|
+
* Materialize an inline (`Code.ZipFile`) ZIP Lambda's source into a temp dir at
|
|
21236
|
+
* the path implied by `handler`, returning the dir to bind-mount. Mirrors
|
|
21237
|
+
* `local-invoke.ts:materializeInlineCode`.
|
|
21238
|
+
*/
|
|
21239
|
+
function materializeInlineCode(handler, source, fileExtension) {
|
|
21240
|
+
const lastDot = handler.lastIndexOf(".");
|
|
21241
|
+
if (lastDot <= 0) throw new Error(`Handler '${handler}' is malformed: expected '<modulePath>.<exportName>'.`);
|
|
21242
|
+
const modulePath = handler.substring(0, lastDot);
|
|
21243
|
+
const dir = mkdtempSync(path.join(tmpdir(), `${getEmbedConfig().resourceNamePrefix}-alb-lambda-`));
|
|
21244
|
+
const filePath = path.join(dir, `${modulePath}${fileExtension}`);
|
|
21245
|
+
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
21246
|
+
writeFileSync(filePath, source, "utf-8");
|
|
21247
|
+
return dir;
|
|
21248
|
+
}
|
|
21249
|
+
async function resolveZipImagePlan(lambda, opts) {
|
|
21250
|
+
let inlineTmpDir;
|
|
21251
|
+
let codeDir = lambda.codePath;
|
|
21252
|
+
if (codeDir === null) {
|
|
21253
|
+
inlineTmpDir = materializeInlineCode(lambda.handler, lambda.inlineCode ?? "", resolveRuntimeFileExtension(lambda.runtime));
|
|
21254
|
+
codeDir = inlineTmpDir;
|
|
21255
|
+
}
|
|
21256
|
+
const image = resolveRuntimeImage(lambda.runtime);
|
|
21257
|
+
await pullImage(image, opts.skipPull === true);
|
|
21258
|
+
const containerCodePath = resolveRuntimeCodeMountPath(lambda.runtime);
|
|
21259
|
+
return {
|
|
21260
|
+
image,
|
|
21261
|
+
mounts: [{
|
|
21262
|
+
hostPath: codeDir,
|
|
21263
|
+
containerPath: containerCodePath,
|
|
21264
|
+
readOnly: true
|
|
21265
|
+
}],
|
|
21266
|
+
cmd: [lambda.handler],
|
|
21267
|
+
...inlineTmpDir !== void 0 && { inlineTmpDir }
|
|
21268
|
+
};
|
|
21269
|
+
}
|
|
21270
|
+
async function resolveContainerImagePlan(lambda, opts) {
|
|
21271
|
+
const logger = getLogger().child("front-door-lambda");
|
|
21272
|
+
const platform = opts.platformOverride ?? architectureToPlatform(lambda.architecture);
|
|
21273
|
+
const manifestPath = lambda.stack.assetManifestPath;
|
|
21274
|
+
let imageRef;
|
|
21275
|
+
let localBuilt = false;
|
|
21276
|
+
if (manifestPath) {
|
|
21277
|
+
const cdkOutDir = path.dirname(manifestPath);
|
|
21278
|
+
const manifest = await new AssetManifestLoader().loadManifest(cdkOutDir, lambda.stack.stackName);
|
|
21279
|
+
const entry = manifest ? getDockerImageBySourceHash(manifest, lambda.imageUri) : void 0;
|
|
21280
|
+
if (entry) {
|
|
21281
|
+
imageRef = await buildContainerImage(entry.asset, cdkOutDir, { architecture: lambda.architecture });
|
|
21282
|
+
localBuilt = true;
|
|
21283
|
+
}
|
|
21284
|
+
}
|
|
21285
|
+
if (!localBuilt) {
|
|
21286
|
+
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.`);
|
|
21287
|
+
logger.info(`No matching cdk.out asset for ${lambda.imageUri}; falling back to ECR pull...`);
|
|
21288
|
+
imageRef = await pullEcrImage(lambda.imageUri, {
|
|
21289
|
+
skipPull: opts.skipPull === true,
|
|
21290
|
+
...opts.region !== void 0 && { region: opts.region },
|
|
21291
|
+
...opts.ecrRoleArn !== void 0 && { ecrRoleArn: opts.ecrRoleArn }
|
|
21292
|
+
});
|
|
21293
|
+
}
|
|
21294
|
+
return {
|
|
21295
|
+
image: imageRef,
|
|
21296
|
+
mounts: [],
|
|
21297
|
+
cmd: lambda.imageConfig.command ?? [],
|
|
21298
|
+
platform,
|
|
21299
|
+
...lambda.imageConfig.entryPoint && lambda.imageConfig.entryPoint.length > 0 && { entryPoint: lambda.imageConfig.entryPoint },
|
|
21300
|
+
...lambda.imageConfig.workingDirectory !== void 0 && { workingDir: lambda.imageConfig.workingDirectory }
|
|
21301
|
+
};
|
|
21302
|
+
}
|
|
21303
|
+
/**
|
|
21304
|
+
* Build a {@link FrontDoorLambdaRunner} for a resolved Lambda. Construction is
|
|
21305
|
+
* pure (no docker work); `start()` does the boot. The invoke timeout defaults
|
|
21306
|
+
* to `max(30s, timeoutSec * 2 * 1000)` — same formula as `cdkl invoke`.
|
|
21307
|
+
*/
|
|
21308
|
+
function createFrontDoorLambdaRunner(lambda, opts) {
|
|
21309
|
+
const logger = getLogger().child("front-door-lambda");
|
|
21310
|
+
const defaultTimeoutMs = Math.max(3e4, lambda.timeoutSec * 2 * 1e3);
|
|
21311
|
+
let plan;
|
|
21312
|
+
let containerId;
|
|
21313
|
+
let hostPort;
|
|
21314
|
+
let stopLogStream;
|
|
21315
|
+
let starting;
|
|
21316
|
+
let stopped = false;
|
|
21317
|
+
async function doStart() {
|
|
21318
|
+
plan = lambda.kind === "zip" ? await resolveZipImagePlan(lambda, opts) : await resolveContainerImagePlan(lambda, opts);
|
|
21319
|
+
const port = await pickFreePort();
|
|
21320
|
+
hostPort = port;
|
|
21321
|
+
const name = `${getEmbedConfig().resourceNamePrefix}-alblambda-${lambda.logicalId}-${process.pid}-${Math.floor(Math.random() * 1e6)}`;
|
|
21322
|
+
const env = {
|
|
21323
|
+
AWS_LAMBDA_FUNCTION_NAME: lambda.logicalId,
|
|
21324
|
+
AWS_LAMBDA_FUNCTION_MEMORY_SIZE: String(lambda.memoryMb),
|
|
21325
|
+
AWS_LAMBDA_FUNCTION_TIMEOUT: String(lambda.timeoutSec),
|
|
21326
|
+
AWS_LAMBDA_FUNCTION_VERSION: "$LATEST",
|
|
21327
|
+
AWS_LAMBDA_LOG_GROUP_NAME: `/aws/lambda/${lambda.logicalId}`,
|
|
21328
|
+
AWS_LAMBDA_LOG_STREAM_NAME: "local",
|
|
21329
|
+
...forwardAwsEnv()
|
|
21330
|
+
};
|
|
21331
|
+
logger.info(`Starting Lambda target container for ${lambda.logicalId} (image=${plan.image}, port=${port})...`);
|
|
21332
|
+
const id = await runDetached({
|
|
21333
|
+
image: plan.image,
|
|
21334
|
+
mounts: plan.mounts,
|
|
21335
|
+
env,
|
|
21336
|
+
cmd: plan.cmd,
|
|
21337
|
+
hostPort: port,
|
|
21338
|
+
host: opts.containerHost,
|
|
21339
|
+
name,
|
|
21340
|
+
...plan.platform !== void 0 && { platform: plan.platform },
|
|
21341
|
+
...plan.entryPoint !== void 0 && { entryPoint: plan.entryPoint },
|
|
21342
|
+
...plan.workingDir !== void 0 && { workingDir: plan.workingDir }
|
|
21343
|
+
});
|
|
21344
|
+
containerId = id;
|
|
21345
|
+
stopLogStream = opts.streamLogs === false ? void 0 : streamLogs(id);
|
|
21346
|
+
try {
|
|
21347
|
+
await waitForRieReady(opts.containerHost, port, 3e4);
|
|
21348
|
+
} catch (err) {
|
|
21349
|
+
try {
|
|
21350
|
+
stopLogStream?.();
|
|
21351
|
+
} catch {}
|
|
21352
|
+
await removeContainer(id).catch(() => void 0);
|
|
21353
|
+
containerId = void 0;
|
|
21354
|
+
throw err;
|
|
21355
|
+
}
|
|
21356
|
+
}
|
|
21357
|
+
return {
|
|
21358
|
+
logicalId: lambda.logicalId,
|
|
21359
|
+
async start() {
|
|
21360
|
+
if (stopped) throw new Error("FrontDoorLambdaRunner.start called after stop");
|
|
21361
|
+
if (containerId) return;
|
|
21362
|
+
if (!starting) starting = doStart();
|
|
21363
|
+
await starting;
|
|
21364
|
+
},
|
|
21365
|
+
async invoke(event, timeoutMs) {
|
|
21366
|
+
if (!containerId || hostPort === void 0) throw new Error(`FrontDoorLambdaRunner('${lambda.logicalId}').invoke called before start() completed.`);
|
|
21367
|
+
return (await invokeRie(opts.containerHost, hostPort, event, timeoutMs ?? defaultTimeoutMs)).payload;
|
|
21368
|
+
},
|
|
21369
|
+
async stop() {
|
|
21370
|
+
if (stopped) return;
|
|
21371
|
+
stopped = true;
|
|
21372
|
+
try {
|
|
21373
|
+
stopLogStream?.();
|
|
21374
|
+
} catch (err) {
|
|
21375
|
+
logger.debug(`stopLogStream(${lambda.logicalId}) failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
21376
|
+
}
|
|
21377
|
+
if (containerId) {
|
|
21378
|
+
try {
|
|
21379
|
+
await removeContainer(containerId);
|
|
21380
|
+
} catch (err) {
|
|
21381
|
+
logger.warn(`Failed to remove Lambda target container for ${lambda.logicalId}: ${err instanceof Error ? err.message : String(err)}. Continuing cleanup.`);
|
|
21382
|
+
}
|
|
21383
|
+
containerId = void 0;
|
|
21384
|
+
}
|
|
21385
|
+
if (plan?.inlineTmpDir) try {
|
|
21386
|
+
rmSync(plan.inlineTmpDir, {
|
|
21387
|
+
recursive: true,
|
|
21388
|
+
force: true
|
|
21389
|
+
});
|
|
21390
|
+
} catch (err) {
|
|
21391
|
+
logger.debug(`Failed to remove inline-code tmpdir ${plan.inlineTmpDir}: ${err instanceof Error ? err.message : String(err)}`);
|
|
21392
|
+
}
|
|
21393
|
+
}
|
|
21394
|
+
};
|
|
21395
|
+
}
|
|
21396
|
+
|
|
20794
21397
|
//#endregion
|
|
20795
21398
|
//#region src/cli/commands/ecs-service-emulator.ts
|
|
20796
21399
|
/**
|
|
@@ -20811,6 +21414,7 @@ async function runEcsServiceEmulator(targets, options, strategy, extraStateProvi
|
|
|
20811
21414
|
let profileCredsFile;
|
|
20812
21415
|
let frontDoorServers = [];
|
|
20813
21416
|
let frontDoorByService = /* @__PURE__ */ new Map();
|
|
21417
|
+
let frontDoorLambdaRunners = [];
|
|
20814
21418
|
const cleanup = singleFlight(async () => {
|
|
20815
21419
|
await Promise.allSettled(perTarget.map(async (pt) => {
|
|
20816
21420
|
if (pt.controller) await pt.controller.shutdown();
|
|
@@ -20821,6 +21425,8 @@ async function runEcsServiceEmulator(targets, options, strategy, extraStateProvi
|
|
|
20821
21425
|
}));
|
|
20822
21426
|
await Promise.allSettled(frontDoorServers.map((s) => s.close().catch((err) => getLogger().warn(`front-door server teardown failed: ${err instanceof Error ? err.message : String(err)}`))));
|
|
20823
21427
|
frontDoorServers = [];
|
|
21428
|
+
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)}`))));
|
|
21429
|
+
frontDoorLambdaRunners = [];
|
|
20824
21430
|
if (profileCredsFile) {
|
|
20825
21431
|
try {
|
|
20826
21432
|
await profileCredsFile.dispose();
|
|
@@ -20865,7 +21471,8 @@ async function runEcsServiceEmulator(targets, options, strategy, extraStateProvi
|
|
|
20865
21471
|
});
|
|
20866
21472
|
const { boots, frontDoor, warnings } = strategy.resolveBoots(stacks, resolvedTargets);
|
|
20867
21473
|
for (const w of warnings) logger.warn(w);
|
|
20868
|
-
|
|
21474
|
+
const hasFrontDoorListeners = !!frontDoor && frontDoor.listeners.length > 0;
|
|
21475
|
+
if (boots.length === 0 && !hasFrontDoorListeners) throw new LocalStartServiceError(`No runnable target resolved from ${resolvedTargets.join(", ")}.`);
|
|
20869
21476
|
rejectExplicitCfnStackWithMultipleStacks(options, boots.length);
|
|
20870
21477
|
perTarget = boots.map((boot) => ({
|
|
20871
21478
|
boot,
|
|
@@ -20896,9 +21503,10 @@ async function runEcsServiceEmulator(targets, options, strategy, extraStateProvi
|
|
|
20896
21503
|
sharedNetwork
|
|
20897
21504
|
};
|
|
20898
21505
|
if (frontDoor && frontDoor.listeners.length > 0) {
|
|
20899
|
-
const built = await buildFrontDoor(frontDoor, options
|
|
21506
|
+
const built = await buildFrontDoor(frontDoor, options, logger);
|
|
20900
21507
|
frontDoorServers = built.servers;
|
|
20901
21508
|
frontDoorByService = built.frontDoorByService;
|
|
21509
|
+
frontDoorLambdaRunners = built.lambdaRunners;
|
|
20902
21510
|
}
|
|
20903
21511
|
sigintHandler = () => {
|
|
20904
21512
|
sigintCount += 1;
|
|
@@ -20912,10 +21520,13 @@ async function runEcsServiceEmulator(targets, options, strategy, extraStateProvi
|
|
|
20912
21520
|
process.on("SIGINT", sigintHandler);
|
|
20913
21521
|
process.on("SIGTERM", sigintHandler);
|
|
20914
21522
|
for (const pt of perTarget) pt.controller = await bootOneTarget(pt.boot, pt.runState, stacks, options, discovery, skipPull, extraStateProviders, profileCredsFile, frontDoorByService.get(pt.boot.target));
|
|
20915
|
-
|
|
20916
|
-
|
|
21523
|
+
if (perTarget.length > 0) {
|
|
21524
|
+
const summary = perTarget.map((pt) => `${pt.controller.service.serviceName} (${pt.controller.activeReplicaCount()} replica(s))`).join(", ");
|
|
21525
|
+
logger.info(`Service(s) running: ${summary}.`);
|
|
21526
|
+
} else logger.info(`Service(s) running: ${frontDoorLambdaRunners.length} Lambda target(s) behind the ALB front-door.`);
|
|
20917
21527
|
logger.info("Press ^C to shut down.");
|
|
20918
|
-
await Promise.all(perTarget.map((pt) => pt.controller.waitForShutdown()));
|
|
21528
|
+
if (perTarget.length > 0) await Promise.all(perTarget.map((pt) => pt.controller.waitForShutdown()));
|
|
21529
|
+
else await new Promise(() => {});
|
|
20919
21530
|
} finally {
|
|
20920
21531
|
if (sigintHandler) {
|
|
20921
21532
|
process.off("SIGINT", sigintHandler);
|
|
@@ -21013,32 +21624,92 @@ async function runOneTarget(boot, runState, stacks, options, discovery, skipPull
|
|
|
21013
21624
|
* already in use) every server started so far is closed and the error is
|
|
21014
21625
|
* re-thrown with a `--lb-port` hint.
|
|
21015
21626
|
*/
|
|
21016
|
-
async function buildFrontDoor(plan,
|
|
21627
|
+
async function buildFrontDoor(plan, options, logger) {
|
|
21628
|
+
const containerHost = options.containerHost;
|
|
21017
21629
|
const servers = [];
|
|
21018
|
-
const
|
|
21019
|
-
const
|
|
21630
|
+
const poolRegistry = /* @__PURE__ */ new Map();
|
|
21631
|
+
const lambdaRegistry = /* @__PURE__ */ new Map();
|
|
21632
|
+
const dispatchFor = (t) => {
|
|
21633
|
+
if (t.kind === "lambda") {
|
|
21634
|
+
let runner = lambdaRegistry.get(t.lambda.logicalId);
|
|
21635
|
+
if (!runner) {
|
|
21636
|
+
runner = createFrontDoorLambdaRunner(t.lambda, {
|
|
21637
|
+
containerHost,
|
|
21638
|
+
skipPull: options.pull === false,
|
|
21639
|
+
...options.platform !== void 0 && { platformOverride: options.platform },
|
|
21640
|
+
...options.ecrRoleArn !== void 0 && { ecrRoleArn: options.ecrRoleArn },
|
|
21641
|
+
...options.region !== void 0 && { region: options.region }
|
|
21642
|
+
});
|
|
21643
|
+
lambdaRegistry.set(t.lambda.logicalId, runner);
|
|
21644
|
+
}
|
|
21645
|
+
const boundRunner = runner;
|
|
21646
|
+
return {
|
|
21647
|
+
kind: "lambda",
|
|
21648
|
+
lambda: {
|
|
21649
|
+
targetGroupArn: t.targetGroupArn,
|
|
21650
|
+
multiValueHeaders: t.multiValueHeaders,
|
|
21651
|
+
label: t.lambda.logicalId,
|
|
21652
|
+
invoke: (event) => boundRunner.invoke(event)
|
|
21653
|
+
}
|
|
21654
|
+
};
|
|
21655
|
+
}
|
|
21020
21656
|
const key = `${t.serviceTarget} ${t.targetContainerName} ${t.targetContainerPort}`;
|
|
21021
|
-
let entry =
|
|
21657
|
+
let entry = poolRegistry.get(key);
|
|
21022
21658
|
if (!entry) {
|
|
21023
21659
|
entry = {
|
|
21024
21660
|
pool: new FrontDoorEndpointPool(),
|
|
21025
21661
|
target: t
|
|
21026
21662
|
};
|
|
21027
|
-
|
|
21663
|
+
poolRegistry.set(key, entry);
|
|
21028
21664
|
}
|
|
21029
|
-
return
|
|
21665
|
+
return {
|
|
21666
|
+
kind: "pool",
|
|
21667
|
+
pool: entry.pool
|
|
21668
|
+
};
|
|
21669
|
+
};
|
|
21670
|
+
const weightedTargetFor = (t) => {
|
|
21671
|
+
const dispatch = dispatchFor(t);
|
|
21672
|
+
return dispatch.kind === "lambda" ? {
|
|
21673
|
+
lambda: dispatch.lambda,
|
|
21674
|
+
weight: t.weight
|
|
21675
|
+
} : {
|
|
21676
|
+
pool: dispatch.pool,
|
|
21677
|
+
weight: t.weight
|
|
21678
|
+
};
|
|
21679
|
+
};
|
|
21680
|
+
const toRouteAction = (action) => {
|
|
21681
|
+
if (action.kind === "forward") return {
|
|
21682
|
+
kind: "forward",
|
|
21683
|
+
pools: action.targets.map(weightedTargetFor)
|
|
21684
|
+
};
|
|
21685
|
+
if (action.kind === "redirect") return {
|
|
21686
|
+
kind: "redirect",
|
|
21687
|
+
statusCode: action.statusCode,
|
|
21688
|
+
...action.protocol !== void 0 && { protocol: action.protocol },
|
|
21689
|
+
...action.host !== void 0 && { host: action.host },
|
|
21690
|
+
...action.port !== void 0 && { port: action.port },
|
|
21691
|
+
...action.path !== void 0 && { path: action.path },
|
|
21692
|
+
...action.query !== void 0 && { query: action.query }
|
|
21693
|
+
};
|
|
21694
|
+
return {
|
|
21695
|
+
kind: "fixed-response",
|
|
21696
|
+
statusCode: action.statusCode,
|
|
21697
|
+
...action.contentType !== void 0 && { contentType: action.contentType },
|
|
21698
|
+
...action.messageBody !== void 0 && { messageBody: action.messageBody }
|
|
21699
|
+
};
|
|
21030
21700
|
};
|
|
21031
21701
|
try {
|
|
21032
21702
|
for (const listener of plan.listeners) {
|
|
21033
|
-
const
|
|
21703
|
+
const defaultRoute = listener.defaultAction ? toRouteAction(listener.defaultAction) : void 0;
|
|
21034
21704
|
const ruleRoutes = listener.rules.map((r) => ({
|
|
21035
21705
|
priority: r.priority,
|
|
21036
21706
|
pathPatterns: r.pathPatterns,
|
|
21037
|
-
|
|
21707
|
+
hostPatterns: r.hostPatterns,
|
|
21708
|
+
target: toRouteAction(r.action)
|
|
21038
21709
|
}));
|
|
21039
|
-
const
|
|
21710
|
+
const route = (req) => matchAlbPathRule(req, ruleRoutes) ?? defaultRoute;
|
|
21040
21711
|
const server = await startFrontDoorServer({
|
|
21041
|
-
|
|
21712
|
+
route,
|
|
21042
21713
|
port: listener.hostPort,
|
|
21043
21714
|
host: containerHost,
|
|
21044
21715
|
listenerPort: listener.listenerPort,
|
|
@@ -21046,16 +21717,21 @@ async function buildFrontDoor(plan, containerHost, logger) {
|
|
|
21046
21717
|
});
|
|
21047
21718
|
servers.push(server);
|
|
21048
21719
|
logger.info(`ALB front-door: http://${server.host}:${server.port} (listener port ${listener.listenerPort})`);
|
|
21049
|
-
if (listener.
|
|
21050
|
-
for (const r of [...listener.rules].sort((a, b) => a.priority - b.priority)) logger.info(`
|
|
21051
|
-
if (!listener.
|
|
21720
|
+
if (listener.defaultAction) logger.info(` default -> ${describeAction(listener.defaultAction)}`);
|
|
21721
|
+
for (const r of [...listener.rules].sort((a, b) => a.priority - b.priority)) logger.info(` ${describeConditions(r)} (priority ${r.priority}) -> ${describeAction(r.action)}`);
|
|
21722
|
+
if (!listener.defaultAction) logger.info(" (no default action: unmatched requests return 404)");
|
|
21723
|
+
}
|
|
21724
|
+
for (const runner of lambdaRegistry.values()) {
|
|
21725
|
+
logger.info(`Booting Lambda target '${runner.logicalId}' behind the ALB front-door...`);
|
|
21726
|
+
await runner.start();
|
|
21052
21727
|
}
|
|
21053
21728
|
} catch (err) {
|
|
21054
21729
|
await Promise.allSettled(servers.map((s) => s.close()));
|
|
21730
|
+
await Promise.allSettled([...lambdaRegistry.values()].map((r) => r.stop()));
|
|
21055
21731
|
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).`);
|
|
21056
21732
|
}
|
|
21057
21733
|
const frontDoorByService = /* @__PURE__ */ new Map();
|
|
21058
|
-
for (const { pool, target } of
|
|
21734
|
+
for (const { pool, target } of poolRegistry.values()) {
|
|
21059
21735
|
const list = frontDoorByService.get(target.serviceTarget) ?? [];
|
|
21060
21736
|
list.push({
|
|
21061
21737
|
pool,
|
|
@@ -21066,11 +21742,32 @@ async function buildFrontDoor(plan, containerHost, logger) {
|
|
|
21066
21742
|
}
|
|
21067
21743
|
return {
|
|
21068
21744
|
servers,
|
|
21069
|
-
frontDoorByService
|
|
21745
|
+
frontDoorByService,
|
|
21746
|
+
lambdaRunners: [...lambdaRegistry.values()]
|
|
21070
21747
|
};
|
|
21071
21748
|
}
|
|
21749
|
+
/** Human-readable summary of a planned rule's path / host conditions (for the boot banner). */
|
|
21750
|
+
function describeConditions(rule) {
|
|
21751
|
+
const parts = [];
|
|
21752
|
+
if (rule.pathPatterns.length > 0) parts.push(`path ${rule.pathPatterns.join(", ")}`);
|
|
21753
|
+
if (rule.hostPatterns.length > 0) parts.push(`host ${rule.hostPatterns.join(", ")}`);
|
|
21754
|
+
return parts.join(" AND ") || "(no condition)";
|
|
21755
|
+
}
|
|
21756
|
+
/** Human-readable summary of a planned action (for the boot banner). */
|
|
21757
|
+
function describeAction(action) {
|
|
21758
|
+
if (action.kind === "redirect") return `redirect ${action.statusCode}`;
|
|
21759
|
+
if (action.kind === "fixed-response") return `fixed-response ${action.statusCode}`;
|
|
21760
|
+
if (action.targets.length === 1) return describeTarget(action.targets[0]);
|
|
21761
|
+
return `weighted forward [${action.targets.map((t) => `${describeTargetShort(t)}@${t.weight}`).join(", ")}]`;
|
|
21762
|
+
}
|
|
21763
|
+
/** One forward target, described in full (for a single-target forward banner). */
|
|
21072
21764
|
function describeTarget(t) {
|
|
21073
|
-
|
|
21765
|
+
if (t.kind === "lambda") return `Lambda ${t.lambda.logicalId} (invoke)`;
|
|
21766
|
+
return `${t.serviceTarget} (container ${t.targetContainerName}:${t.targetContainerPort}) (round-robin)`;
|
|
21767
|
+
}
|
|
21768
|
+
/** One forward target, described compactly (for a weighted-forward banner). */
|
|
21769
|
+
function describeTargetShort(t) {
|
|
21770
|
+
return t.kind === "lambda" ? `Lambda ${t.lambda.logicalId}` : t.serviceTarget;
|
|
21074
21771
|
}
|
|
21075
21772
|
async function resolvePlaceholderAccount(arn, region) {
|
|
21076
21773
|
if (!arn.includes("${AWS::AccountId}")) return arn;
|
|
@@ -21275,39 +21972,50 @@ function createLocalStartServiceCommand(opts = {}) {
|
|
|
21275
21972
|
* cdk-local discovers the services behind it (mirroring how `start-api` names
|
|
21276
21973
|
* the API and discovers the backing Lambdas).
|
|
21277
21974
|
*
|
|
21278
|
-
* The synthesized linkage (confirmed against real `cdk synth` of
|
|
21279
|
-
* `
|
|
21975
|
+
* The synthesized linkage (confirmed against real `cdk synth` of an
|
|
21976
|
+
* `ApplicationLoadBalancer` + `addAction` rules):
|
|
21280
21977
|
*
|
|
21281
21978
|
* ```
|
|
21282
21979
|
* ElasticLoadBalancingV2::LoadBalancer (the ALB you name)
|
|
21283
21980
|
* ElasticLoadBalancingV2::Listener : { LoadBalancerArn:{Ref:<ALB>}, Port, Protocol,
|
|
21284
|
-
* DefaultActions:[
|
|
21981
|
+
* DefaultActions:[ <action> ] }
|
|
21285
21982
|
* ElasticLoadBalancingV2::ListenerRule : { ListenerArn:{Ref:<Listener>}, Priority,
|
|
21286
|
-
* Conditions:[{ Field:"path-pattern", PathPatternConfig:{ Values:["/api/*"] } }
|
|
21287
|
-
*
|
|
21983
|
+
* Conditions:[{ Field:"path-pattern", PathPatternConfig:{ Values:["/api/*"] } },
|
|
21984
|
+
* { Field:"host-header", HostHeaderConfig:{ Values:["api.example.com"] } }],
|
|
21985
|
+
* Actions:[ <action> ] }
|
|
21288
21986
|
* ElasticLoadBalancingV2::TargetGroup : { Port, Protocol, TargetType:"ip" }
|
|
21289
21987
|
* ECS::Service.LoadBalancers[] -> { ContainerName, ContainerPort, TargetGroupArn:{Ref:<TG>} }
|
|
21290
21988
|
* ```
|
|
21291
21989
|
*
|
|
21990
|
+
* Each `<action>` is one of:
|
|
21991
|
+
* - `forward` — `{ Type:"forward", TargetGroupArn:{Ref} }` (single target) OR
|
|
21992
|
+
* `{ Type:"forward", ForwardConfig:{ TargetGroups:[{ TargetGroupArn:{Ref}, Weight }] } }`
|
|
21993
|
+
* (one or more weighted targets);
|
|
21994
|
+
* - `redirect` — `{ Type:"redirect", RedirectConfig:{ Protocol/Host/Port/Path/Query/StatusCode } }`;
|
|
21995
|
+
* - `fixed-response` — `{ Type:"fixed-response", FixedResponseConfig:{ StatusCode/ContentType/MessageBody } }`.
|
|
21996
|
+
*
|
|
21292
21997
|
* Resolution walks ALB -> listeners (by `LoadBalancerArn` Ref) -> their default
|
|
21293
|
-
*
|
|
21998
|
+
* action AND any ListenerRules -> for each `forward`, the ECS Service whose
|
|
21294
21999
|
* `LoadBalancers[]` references each target group (a reverse scan; there is no
|
|
21295
22000
|
* direct TG -> service pointer). Output is a per-listener routing table: a
|
|
21296
|
-
* default
|
|
21297
|
-
*
|
|
21298
|
-
*
|
|
21299
|
-
* Scope: HTTP listeners
|
|
21300
|
-
*
|
|
21301
|
-
* `TargetType:"lambda"` target groups
|
|
21302
|
-
*
|
|
21303
|
-
*
|
|
21304
|
-
*
|
|
22001
|
+
* default action plus the ordered rules, each carrying its resolved action and
|
|
22002
|
+
* its path / host conditions.
|
|
22003
|
+
*
|
|
22004
|
+
* Scope: HTTP listeners; `path-pattern` + `host-header` conditions; `forward`
|
|
22005
|
+
* (single or weighted) to ECS services AND/OR Lambda functions
|
|
22006
|
+
* (`TargetType:"lambda"` target groups — #123: the TG -> backing
|
|
22007
|
+
* `AWS::Lambda::Function` is resolved and the front-door invokes it locally per
|
|
22008
|
+
* request); `redirect` / `fixed-response` actions. A single weighted forward may
|
|
22009
|
+
* mix ECS and Lambda targets. Skipped with a warning: HTTPS/TLS listeners, the
|
|
22010
|
+
* other condition fields (http-header / http-request-method / query-string /
|
|
22011
|
+
* source-ip), and `authenticate-cognito` / `authenticate-oidc` actions.
|
|
21305
22012
|
*/
|
|
21306
22013
|
const ALB_TYPE = "AWS::ElasticLoadBalancingV2::LoadBalancer";
|
|
21307
22014
|
const LISTENER_TYPE = "AWS::ElasticLoadBalancingV2::Listener";
|
|
21308
22015
|
const LISTENER_RULE_TYPE = "AWS::ElasticLoadBalancingV2::ListenerRule";
|
|
21309
22016
|
const TARGET_GROUP_TYPE = "AWS::ElasticLoadBalancingV2::TargetGroup";
|
|
21310
22017
|
const SERVICE_TYPE = "AWS::ECS::Service";
|
|
22018
|
+
const LAMBDA_FUNCTION_TYPE = "AWS::Lambda::Function";
|
|
21311
22019
|
/**
|
|
21312
22020
|
* Resolve an ALB into its front-door listeners + routing tables. Pure — reads
|
|
21313
22021
|
* only the supplied stack template. Returns an empty `listeners` array (with
|
|
@@ -21331,31 +22039,32 @@ function resolveAlbFrontDoor(stack, albLogicalId) {
|
|
|
21331
22039
|
warnings.push(`Listener '${listenerLogicalId}' on port ${port} uses protocol ${protocol}; the local ALB front-door supports HTTP listeners only (TLS termination is deferred). Skipping it.`);
|
|
21332
22040
|
continue;
|
|
21333
22041
|
}
|
|
21334
|
-
const
|
|
22042
|
+
const defaultAction = resolveAction(props["DefaultActions"], resources, tgToService, stackName, `Listener '${listenerLogicalId}' (port ${port}) default action`, warnings);
|
|
21335
22043
|
const rules = [];
|
|
21336
22044
|
for (const { ruleLogicalId, ruleProps } of rulesByListener.get(listenerLogicalId) ?? []) {
|
|
21337
22045
|
const priority = parsePriority(ruleProps["Priority"]);
|
|
21338
22046
|
const ruleLabel = `Listener rule '${ruleLogicalId}' (priority ${priority})`;
|
|
21339
|
-
const {
|
|
22047
|
+
const { pathPatterns, hostPatterns, unsupported } = parseRuleConditions(ruleProps["Conditions"]);
|
|
21340
22048
|
if (unsupported.length > 0) {
|
|
21341
|
-
warnings.push(`${ruleLabel} uses unsupported condition(s): ${unsupported.join(", ")}. The local ALB front-door supports path-pattern
|
|
22049
|
+
warnings.push(`${ruleLabel} uses unsupported condition(s): ${unsupported.join(", ")}. The local ALB front-door supports path-pattern and host-header conditions only (http-header / query-string / http-request-method / source-ip deferred). Skipping it.`);
|
|
21342
22050
|
continue;
|
|
21343
22051
|
}
|
|
21344
|
-
if (
|
|
21345
|
-
const
|
|
21346
|
-
if (!
|
|
22052
|
+
if (pathPatterns.length === 0 && hostPatterns.length === 0) continue;
|
|
22053
|
+
const action = resolveAction(ruleProps["Actions"], resources, tgToService, stackName, `${ruleLabel} action`, warnings);
|
|
22054
|
+
if (!action) continue;
|
|
21347
22055
|
rules.push({
|
|
21348
22056
|
priority,
|
|
21349
|
-
pathPatterns
|
|
21350
|
-
|
|
22057
|
+
pathPatterns,
|
|
22058
|
+
hostPatterns,
|
|
22059
|
+
action
|
|
21351
22060
|
});
|
|
21352
22061
|
}
|
|
21353
|
-
if (!
|
|
22062
|
+
if (!defaultAction && rules.length === 0) continue;
|
|
21354
22063
|
listeners.push({
|
|
21355
22064
|
listenerPort: port,
|
|
21356
22065
|
listenerProtocol: "HTTP",
|
|
21357
22066
|
listenerLogicalId,
|
|
21358
|
-
...
|
|
22067
|
+
...defaultAction ? { defaultAction } : {},
|
|
21359
22068
|
rules
|
|
21360
22069
|
});
|
|
21361
22070
|
}
|
|
@@ -21371,41 +22080,168 @@ function isApplicationLoadBalancer(resource) {
|
|
|
21371
22080
|
return type === void 0 || type === "application";
|
|
21372
22081
|
}
|
|
21373
22082
|
/**
|
|
21374
|
-
* Resolve a listener / rule `Actions` (or `DefaultActions`) array to
|
|
21375
|
-
*
|
|
21376
|
-
*
|
|
22083
|
+
* Resolve a listener / rule `Actions` (or `DefaultActions`) array to the single
|
|
22084
|
+
* action the local front-door serves, or `undefined` when it is not resolvable
|
|
22085
|
+
* (a warning is emitted for the cases worth surfacing). ALB allows exactly one
|
|
22086
|
+
* non-authenticate terminal action per action set, optionally preceded by
|
|
22087
|
+
* authenticate-* actions (which the local front-door does not enforce — they
|
|
22088
|
+
* are skipped with a warning and the terminal action is honored).
|
|
21377
22089
|
*/
|
|
21378
|
-
function
|
|
21379
|
-
|
|
21380
|
-
|
|
21381
|
-
|
|
21382
|
-
|
|
22090
|
+
function resolveAction(actions, resources, tgToService, stackName, label, warnings) {
|
|
22091
|
+
if (!Array.isArray(actions)) return void 0;
|
|
22092
|
+
let sawAuthenticate = false;
|
|
22093
|
+
for (const action of actions) {
|
|
22094
|
+
if (!action || typeof action !== "object") continue;
|
|
22095
|
+
const a = action;
|
|
22096
|
+
const type = a["Type"];
|
|
22097
|
+
if (type === "authenticate-cognito" || type === "authenticate-oidc") {
|
|
22098
|
+
sawAuthenticate = true;
|
|
22099
|
+
continue;
|
|
22100
|
+
}
|
|
22101
|
+
if (type === "forward") return resolveForwardAction(a, resources, tgToService, stackName, label, warnings);
|
|
22102
|
+
if (type === "redirect") {
|
|
22103
|
+
const redirect = resolveRedirectAction(a, label, warnings);
|
|
22104
|
+
if (redirect) return redirect;
|
|
22105
|
+
continue;
|
|
22106
|
+
}
|
|
22107
|
+
if (type === "fixed-response") return resolveFixedResponseAction(a);
|
|
22108
|
+
if (typeof type === "string") warnings.push(`${label} uses an unsupported action type '${type}'. The local ALB front-door supports forward / redirect / fixed-response actions only. Skipping it.`);
|
|
21383
22109
|
}
|
|
21384
|
-
if (
|
|
21385
|
-
|
|
22110
|
+
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.`);
|
|
22111
|
+
}
|
|
22112
|
+
/**
|
|
22113
|
+
* Resolve a `forward` action into one or more weighted targets. Each target
|
|
22114
|
+
* group is either an ECS service (the original `start-alb` path) or a
|
|
22115
|
+
* `TargetType: lambda` group backed by an in-stack Lambda (#123); a single
|
|
22116
|
+
* weighted forward may mix both.
|
|
22117
|
+
*/
|
|
22118
|
+
function resolveForwardAction(action, resources, tgToService, stackName, label, warnings) {
|
|
22119
|
+
const refs = collectForwardTargetGroupRefs(action);
|
|
22120
|
+
if (refs.length === 0) {
|
|
22121
|
+
if (hasUnresolvableForward(action)) warnings.push(`${label} forwards to a non-Ref TargetGroupArn (literal / cross-stack / imported); the local front-door only supports in-stack target groups. Skipping it.`);
|
|
21386
22122
|
return;
|
|
21387
22123
|
}
|
|
21388
|
-
const
|
|
21389
|
-
const
|
|
21390
|
-
|
|
21391
|
-
|
|
21392
|
-
|
|
22124
|
+
const targets = [];
|
|
22125
|
+
for (const { tgRef, weight } of refs) {
|
|
22126
|
+
const tg = resources[tgRef];
|
|
22127
|
+
if (!tg || tg.Type !== TARGET_GROUP_TYPE) {
|
|
22128
|
+
warnings.push(`${label} forwards to target group '${tgRef}', but no ${TARGET_GROUP_TYPE} with that logical id exists in ${stackName}. Skipping that target group.`);
|
|
22129
|
+
continue;
|
|
22130
|
+
}
|
|
22131
|
+
const tgProps = tg.Properties ?? {};
|
|
22132
|
+
if (tgProps["TargetType"] === "lambda") {
|
|
22133
|
+
const lambdaTarget = resolveLambdaForwardTarget(tgProps, tgRef, resources, stackName, label, warnings);
|
|
22134
|
+
if (lambdaTarget) targets.push({
|
|
22135
|
+
...lambdaTarget,
|
|
22136
|
+
weight
|
|
22137
|
+
});
|
|
22138
|
+
continue;
|
|
22139
|
+
}
|
|
22140
|
+
const backing = tgToService.get(tgRef);
|
|
22141
|
+
if (!backing) {
|
|
22142
|
+
warnings.push(`${label} forwards to target group '${tgRef}', which is not referenced by any ${SERVICE_TYPE}.LoadBalancers[] in ${stackName}; cdk-local has no ECS service to front behind it. Skipping that target group.`);
|
|
22143
|
+
continue;
|
|
22144
|
+
}
|
|
22145
|
+
targets.push({
|
|
22146
|
+
kind: "ecs",
|
|
22147
|
+
serviceLogicalId: backing.serviceLogicalId,
|
|
22148
|
+
targetContainerName: backing.containerName,
|
|
22149
|
+
targetContainerPort: backing.containerPort,
|
|
22150
|
+
targetGroupLogicalId: tgRef,
|
|
22151
|
+
weight
|
|
22152
|
+
});
|
|
21393
22153
|
}
|
|
21394
|
-
if (
|
|
21395
|
-
|
|
22154
|
+
if (targets.length === 0) return void 0;
|
|
22155
|
+
return {
|
|
22156
|
+
kind: "forward",
|
|
22157
|
+
targets
|
|
22158
|
+
};
|
|
22159
|
+
}
|
|
22160
|
+
/**
|
|
22161
|
+
* Resolve a `TargetType: lambda` target group into its `FrontDoorLambdaTarget`
|
|
22162
|
+
* (weight applied by the caller), or `undefined` (with a warning) when the
|
|
22163
|
+
* backing function is not an in-stack `AWS::Lambda::Function` reference.
|
|
22164
|
+
*/
|
|
22165
|
+
function resolveLambdaForwardTarget(tgProps, tgRef, resources, stackName, label, warnings) {
|
|
22166
|
+
const lambdaLogicalId = resolveLambdaTargetLogicalId(tgProps["Targets"]);
|
|
22167
|
+
if (!lambdaLogicalId) {
|
|
22168
|
+
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.`);
|
|
21396
22169
|
return;
|
|
21397
22170
|
}
|
|
21398
|
-
const
|
|
21399
|
-
if (!
|
|
21400
|
-
warnings.push(`${label} forwards to target group '${tgRef}',
|
|
22171
|
+
const lambda = resources[lambdaLogicalId];
|
|
22172
|
+
if (!lambda || lambda.Type !== LAMBDA_FUNCTION_TYPE) {
|
|
22173
|
+
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.`);
|
|
21401
22174
|
return;
|
|
21402
22175
|
}
|
|
21403
22176
|
return {
|
|
21404
|
-
|
|
21405
|
-
|
|
21406
|
-
|
|
21407
|
-
|
|
22177
|
+
kind: "lambda",
|
|
22178
|
+
lambdaLogicalId,
|
|
22179
|
+
targetGroupLogicalId: tgRef,
|
|
22180
|
+
multiValueHeaders: readMultiValueHeadersAttribute(tgProps["TargetGroupAttributes"])
|
|
22181
|
+
};
|
|
22182
|
+
}
|
|
22183
|
+
/** Resolve a `redirect` action into its `Location`-template fields + status code. */
|
|
22184
|
+
function resolveRedirectAction(action, label, warnings) {
|
|
22185
|
+
const cfg = action["RedirectConfig"];
|
|
22186
|
+
if (!cfg || typeof cfg !== "object") {
|
|
22187
|
+
warnings.push(`${label} is a redirect with no RedirectConfig; skipping it.`);
|
|
22188
|
+
return;
|
|
22189
|
+
}
|
|
22190
|
+
const c = cfg;
|
|
22191
|
+
const out = {
|
|
22192
|
+
kind: "redirect",
|
|
22193
|
+
statusCode: parseRedirectStatusCode(c["StatusCode"])
|
|
22194
|
+
};
|
|
22195
|
+
if (typeof c["Protocol"] === "string") out.protocol = c["Protocol"];
|
|
22196
|
+
if (typeof c["Host"] === "string") out.host = c["Host"];
|
|
22197
|
+
if (typeof c["Port"] === "string") out.port = c["Port"];
|
|
22198
|
+
if (typeof c["Path"] === "string") out.path = c["Path"];
|
|
22199
|
+
if (typeof c["Query"] === "string") out.query = c["Query"];
|
|
22200
|
+
return out;
|
|
22201
|
+
}
|
|
22202
|
+
/** Resolve a `fixed-response` action into its status / content-type / body. */
|
|
22203
|
+
function resolveFixedResponseAction(action) {
|
|
22204
|
+
const cfg = action["FixedResponseConfig"];
|
|
22205
|
+
const c = cfg && typeof cfg === "object" ? cfg : {};
|
|
22206
|
+
const out = {
|
|
22207
|
+
kind: "fixed-response",
|
|
22208
|
+
statusCode: parseFixedResponseStatusCode(c["StatusCode"])
|
|
21408
22209
|
};
|
|
22210
|
+
if (typeof c["ContentType"] === "string") out.contentType = c["ContentType"];
|
|
22211
|
+
if (typeof c["MessageBody"] === "string") out.messageBody = c["MessageBody"];
|
|
22212
|
+
return out;
|
|
22213
|
+
}
|
|
22214
|
+
/**
|
|
22215
|
+
* Resolve a `TargetType: lambda` target group's backing Lambda logical id from
|
|
22216
|
+
* its `Targets[].Id`. CDK synthesizes the registration as
|
|
22217
|
+
* `Targets: [{ Id: { "Fn::GetAtt": [<FnLogicalId>, "Arn"] } }]`; a `Ref` to the
|
|
22218
|
+
* function (its name) is also accepted. Returns the logical id, or `undefined`
|
|
22219
|
+
* when the target is a literal / imported ARN (not an in-stack reference).
|
|
22220
|
+
*/
|
|
22221
|
+
function resolveLambdaTargetLogicalId(targets) {
|
|
22222
|
+
if (!Array.isArray(targets) || targets.length === 0) return void 0;
|
|
22223
|
+
const first = targets[0];
|
|
22224
|
+
if (!first || typeof first !== "object") return void 0;
|
|
22225
|
+
const id = first["Id"];
|
|
22226
|
+
if (!id || typeof id !== "object" || Array.isArray(id)) return void 0;
|
|
22227
|
+
const idObj = id;
|
|
22228
|
+
const getAtt = idObj["Fn::GetAtt"];
|
|
22229
|
+
if (Array.isArray(getAtt) && typeof getAtt[0] === "string" && getAtt[0].length > 0) return getAtt[0];
|
|
22230
|
+
const ref = idObj["Ref"];
|
|
22231
|
+
if (typeof ref === "string" && ref.length > 0) return ref;
|
|
22232
|
+
}
|
|
22233
|
+
/**
|
|
22234
|
+
* Read the `lambda.multi_value_headers.enabled` target-group attribute (a
|
|
22235
|
+
* string `"true"` / `"false"` in CFn). Defaults to `false` when absent.
|
|
22236
|
+
*/
|
|
22237
|
+
function readMultiValueHeadersAttribute(attributes) {
|
|
22238
|
+
if (!Array.isArray(attributes)) return false;
|
|
22239
|
+
for (const attr of attributes) {
|
|
22240
|
+
if (!attr || typeof attr !== "object") continue;
|
|
22241
|
+
const a = attr;
|
|
22242
|
+
if (a["Key"] === "lambda.multi_value_headers.enabled") return String(a["Value"]).toLowerCase() === "true";
|
|
22243
|
+
}
|
|
22244
|
+
return false;
|
|
21409
22245
|
}
|
|
21410
22246
|
/**
|
|
21411
22247
|
* Build a `targetGroupLogicalId -> backing ECS service` index by scanning every
|
|
@@ -21452,78 +22288,81 @@ function indexRulesByListener(resources) {
|
|
|
21452
22288
|
return index;
|
|
21453
22289
|
}
|
|
21454
22290
|
/**
|
|
21455
|
-
* Parse a ListenerRule's `Conditions` into its `path-pattern`
|
|
21456
|
-
* field names of any
|
|
21457
|
-
*
|
|
21458
|
-
*
|
|
21459
|
-
*
|
|
22291
|
+
* Parse a ListenerRule's `Conditions` into its supported `path-pattern` and
|
|
22292
|
+
* `host-header` values plus the field names of any unsupported conditions
|
|
22293
|
+
* (which make the rule unsupported). ALB ANDs conditions of different fields,
|
|
22294
|
+
* so a rule with an unsupported field cannot be honored locally. Multiple
|
|
22295
|
+
* conditions of the same field merge their values (each field OR-matches).
|
|
21460
22296
|
*/
|
|
21461
|
-
function
|
|
21462
|
-
const
|
|
22297
|
+
function parseRuleConditions(conditions) {
|
|
22298
|
+
const pathPatterns = [];
|
|
22299
|
+
const hostPatterns = [];
|
|
21463
22300
|
const unsupported = [];
|
|
21464
22301
|
if (!Array.isArray(conditions)) return {
|
|
21465
|
-
|
|
22302
|
+
pathPatterns,
|
|
22303
|
+
hostPatterns,
|
|
21466
22304
|
unsupported
|
|
21467
22305
|
};
|
|
21468
22306
|
for (const cond of conditions) {
|
|
21469
22307
|
if (!cond || typeof cond !== "object") continue;
|
|
21470
22308
|
const c = cond;
|
|
21471
22309
|
const field = typeof c["Field"] === "string" ? c["Field"] : "(unknown)";
|
|
21472
|
-
if (field
|
|
21473
|
-
|
|
21474
|
-
|
|
21475
|
-
}
|
|
21476
|
-
const cfg = c["PathPatternConfig"];
|
|
21477
|
-
const values = cfg && typeof cfg === "object" && Array.isArray(cfg["Values"]) ? cfg["Values"] : Array.isArray(c["Values"]) ? c["Values"] : [];
|
|
21478
|
-
for (const v of values) if (typeof v === "string") patterns.push(v);
|
|
22310
|
+
if (field === "path-pattern") pathPatterns.push(...conditionValues(c, "PathPatternConfig"));
|
|
22311
|
+
else if (field === "host-header") hostPatterns.push(...conditionValues(c, "HostHeaderConfig"));
|
|
22312
|
+
else unsupported.push(field);
|
|
21479
22313
|
}
|
|
21480
22314
|
return {
|
|
21481
|
-
|
|
22315
|
+
pathPatterns,
|
|
22316
|
+
hostPatterns,
|
|
21482
22317
|
unsupported
|
|
21483
22318
|
};
|
|
21484
22319
|
}
|
|
21485
|
-
|
|
21486
|
-
|
|
21487
|
-
|
|
21488
|
-
|
|
21489
|
-
|
|
21490
|
-
|
|
21491
|
-
|
|
21492
|
-
|
|
21493
|
-
|
|
21494
|
-
|
|
21495
|
-
|
|
21496
|
-
|
|
21497
|
-
|
|
21498
|
-
|
|
21499
|
-
|
|
21500
|
-
|
|
21501
|
-
|
|
22320
|
+
/**
|
|
22321
|
+
* Extract a condition's string values from either the typed `<Field>Config`
|
|
22322
|
+
* sub-object's `Values` or the legacy top-level `Values` array.
|
|
22323
|
+
*/
|
|
22324
|
+
function conditionValues(cond, configKey) {
|
|
22325
|
+
const cfg = cond[configKey];
|
|
22326
|
+
return (cfg && typeof cfg === "object" && Array.isArray(cfg["Values"]) ? cfg["Values"] : Array.isArray(cond["Values"]) ? cond["Values"] : []).filter((v) => typeof v === "string");
|
|
22327
|
+
}
|
|
22328
|
+
/** Collect a forward action's `(targetGroupRef, weight)` pairs (single + ForwardConfig forms). */
|
|
22329
|
+
function collectForwardTargetGroupRefs(action) {
|
|
22330
|
+
const out = [];
|
|
22331
|
+
const direct = refOf(action["TargetGroupArn"]);
|
|
22332
|
+
if (direct) out.push({
|
|
22333
|
+
tgRef: direct,
|
|
22334
|
+
weight: 1
|
|
22335
|
+
});
|
|
22336
|
+
const forwardConfig = action["ForwardConfig"];
|
|
22337
|
+
if (forwardConfig && typeof forwardConfig === "object") {
|
|
22338
|
+
const groups = forwardConfig["TargetGroups"];
|
|
22339
|
+
if (Array.isArray(groups)) for (const g of groups) {
|
|
22340
|
+
if (!g || typeof g !== "object") continue;
|
|
22341
|
+
const gObj = g;
|
|
22342
|
+
const ref = refOf(gObj["TargetGroupArn"]);
|
|
22343
|
+
if (ref) out.push({
|
|
22344
|
+
tgRef: ref,
|
|
22345
|
+
weight: parseWeight(gObj["Weight"])
|
|
22346
|
+
});
|
|
21502
22347
|
}
|
|
21503
22348
|
}
|
|
21504
|
-
return
|
|
22349
|
+
return out;
|
|
21505
22350
|
}
|
|
21506
22351
|
/**
|
|
21507
|
-
* True when `
|
|
21508
|
-
*
|
|
21509
|
-
*
|
|
21510
|
-
*
|
|
22352
|
+
* True when `action` is a `forward` that references a target group via a
|
|
22353
|
+
* NON-`Ref` arn (literal / `Fn::GetAtt` / cross-stack) — i.e. a forward we
|
|
22354
|
+
* could not resolve to an in-stack target group. Used to warn rather than
|
|
22355
|
+
* silently skip.
|
|
21511
22356
|
*/
|
|
21512
|
-
function hasUnresolvableForward(
|
|
21513
|
-
if (
|
|
21514
|
-
|
|
21515
|
-
|
|
21516
|
-
const
|
|
21517
|
-
if (
|
|
21518
|
-
|
|
21519
|
-
|
|
21520
|
-
|
|
21521
|
-
const groups = forwardConfig["TargetGroups"];
|
|
21522
|
-
if (Array.isArray(groups)) for (const g of groups) {
|
|
21523
|
-
if (!g || typeof g !== "object") continue;
|
|
21524
|
-
const arn = g["TargetGroupArn"];
|
|
21525
|
-
if (arn !== void 0 && refOf(arn) === void 0) return true;
|
|
21526
|
-
}
|
|
22357
|
+
function hasUnresolvableForward(action) {
|
|
22358
|
+
if (action["TargetGroupArn"] !== void 0 && refOf(action["TargetGroupArn"]) === void 0) return true;
|
|
22359
|
+
const forwardConfig = action["ForwardConfig"];
|
|
22360
|
+
if (forwardConfig && typeof forwardConfig === "object") {
|
|
22361
|
+
const groups = forwardConfig["TargetGroups"];
|
|
22362
|
+
if (Array.isArray(groups)) for (const g of groups) {
|
|
22363
|
+
if (!g || typeof g !== "object") continue;
|
|
22364
|
+
const arn = g["TargetGroupArn"];
|
|
22365
|
+
if (arn !== void 0 && refOf(arn) === void 0) return true;
|
|
21527
22366
|
}
|
|
21528
22367
|
}
|
|
21529
22368
|
return false;
|
|
@@ -21545,6 +22384,27 @@ function parseContainerPort(raw) {
|
|
|
21545
22384
|
return parsePort(raw);
|
|
21546
22385
|
}
|
|
21547
22386
|
/**
|
|
22387
|
+
* Parse a `ForwardConfig.TargetGroups[].Weight`. ALB weights are 0-999; a
|
|
22388
|
+
* missing weight defaults to 1 (CDK's `weightedForward` always emits one, but
|
|
22389
|
+
* a hand-rolled template may omit it). Negative / non-numeric clamps to 0.
|
|
22390
|
+
*/
|
|
22391
|
+
function parseWeight(raw) {
|
|
22392
|
+
if (typeof raw === "number" && Number.isFinite(raw)) return raw < 0 ? 0 : raw;
|
|
22393
|
+
if (typeof raw === "string" && /^\d+$/.test(raw)) return parseInt(raw, 10);
|
|
22394
|
+
return 1;
|
|
22395
|
+
}
|
|
22396
|
+
/** ALB emits redirect status as `HTTP_301` / `HTTP_302`; default to 302 when absent / unknown. */
|
|
22397
|
+
function parseRedirectStatusCode(raw) {
|
|
22398
|
+
if (raw === "HTTP_301" || raw === "301" || raw === 301) return 301;
|
|
22399
|
+
return 302;
|
|
22400
|
+
}
|
|
22401
|
+
/** Parse a `FixedResponseConfig.StatusCode` (a numeric string); default 200 when absent. */
|
|
22402
|
+
function parseFixedResponseStatusCode(raw) {
|
|
22403
|
+
if (typeof raw === "number" && Number.isInteger(raw)) return raw;
|
|
22404
|
+
if (typeof raw === "string" && /^\d+$/.test(raw)) return parseInt(raw, 10);
|
|
22405
|
+
return 200;
|
|
22406
|
+
}
|
|
22407
|
+
/**
|
|
21548
22408
|
* Parse a ListenerRule `Priority` (ALB priorities are 1-50000, lower = higher
|
|
21549
22409
|
* precedence). A missing / unparseable priority sorts last so an explicitly
|
|
21550
22410
|
* prioritized rule always wins over it.
|
|
@@ -21643,13 +22503,43 @@ function albStrategy(options) {
|
|
|
21643
22503
|
const { stack, albLogicalId } = resolveAlbTarget(albTarget, stacks);
|
|
21644
22504
|
const resolution = resolveAlbFrontDoor(stack, albLogicalId);
|
|
21645
22505
|
warnings.push(...resolution.warnings);
|
|
21646
|
-
const
|
|
22506
|
+
const qualifyTarget = (t) => {
|
|
22507
|
+
if (t.kind === "lambda") return {
|
|
22508
|
+
kind: "lambda",
|
|
22509
|
+
lambda: resolveLambdaTarget(`${stack.stackName}:${t.lambdaLogicalId}`, stacks),
|
|
22510
|
+
targetGroupArn: `${stack.stackName}:${t.targetGroupLogicalId}`,
|
|
22511
|
+
multiValueHeaders: t.multiValueHeaders,
|
|
22512
|
+
weight: t.weight
|
|
22513
|
+
};
|
|
21647
22514
|
const serviceTarget = `${stack.stackName}:${t.serviceLogicalId}`;
|
|
21648
22515
|
serviceTargets.add(serviceTarget);
|
|
21649
22516
|
return {
|
|
22517
|
+
kind: "ecs",
|
|
21650
22518
|
serviceTarget,
|
|
21651
22519
|
targetContainerName: t.targetContainerName,
|
|
21652
|
-
targetContainerPort: t.targetContainerPort
|
|
22520
|
+
targetContainerPort: t.targetContainerPort,
|
|
22521
|
+
weight: t.weight
|
|
22522
|
+
};
|
|
22523
|
+
};
|
|
22524
|
+
const qualify = (action) => {
|
|
22525
|
+
if (action.kind === "forward") return {
|
|
22526
|
+
kind: "forward",
|
|
22527
|
+
targets: action.targets.map(qualifyTarget)
|
|
22528
|
+
};
|
|
22529
|
+
if (action.kind === "redirect") return {
|
|
22530
|
+
kind: "redirect",
|
|
22531
|
+
statusCode: action.statusCode,
|
|
22532
|
+
...action.protocol !== void 0 && { protocol: action.protocol },
|
|
22533
|
+
...action.host !== void 0 && { host: action.host },
|
|
22534
|
+
...action.port !== void 0 && { port: action.port },
|
|
22535
|
+
...action.path !== void 0 && { path: action.path },
|
|
22536
|
+
...action.query !== void 0 && { query: action.query }
|
|
22537
|
+
};
|
|
22538
|
+
return {
|
|
22539
|
+
kind: "fixed-response",
|
|
22540
|
+
statusCode: action.statusCode,
|
|
22541
|
+
...action.contentType !== void 0 && { contentType: action.contentType },
|
|
22542
|
+
...action.messageBody !== void 0 && { messageBody: action.messageBody }
|
|
21653
22543
|
};
|
|
21654
22544
|
};
|
|
21655
22545
|
for (const listener of resolution.listeners) {
|
|
@@ -21663,11 +22553,12 @@ function albStrategy(options) {
|
|
|
21663
22553
|
listeners.push({
|
|
21664
22554
|
listenerPort: listener.listenerPort,
|
|
21665
22555
|
hostPort,
|
|
21666
|
-
...listener.
|
|
22556
|
+
...listener.defaultAction ? { defaultAction: qualify(listener.defaultAction) } : {},
|
|
21667
22557
|
rules: listener.rules.map((r) => ({
|
|
21668
22558
|
priority: r.priority,
|
|
21669
22559
|
pathPatterns: r.pathPatterns,
|
|
21670
|
-
|
|
22560
|
+
hostPatterns: r.hostPatterns,
|
|
22561
|
+
action: qualify(r.action)
|
|
21671
22562
|
}))
|
|
21672
22563
|
});
|
|
21673
22564
|
}
|
|
@@ -21696,7 +22587,7 @@ function albStrategy(options) {
|
|
|
21696
22587
|
*/
|
|
21697
22588
|
function createLocalStartAlbCommand(opts = {}) {
|
|
21698
22589
|
setEmbedConfig(opts.embedConfig);
|
|
21699
|
-
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
|
|
22590
|
+
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) => {
|
|
21700
22591
|
await runEcsServiceEmulator(targets, options, albStrategy(options), opts.extraStateProviders);
|
|
21701
22592
|
})));
|
|
21702
22593
|
}
|
|
@@ -21773,4 +22664,4 @@ function createLocalListCommand(opts = {}) {
|
|
|
21773
22664
|
|
|
21774
22665
|
//#endregion
|
|
21775
22666
|
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 };
|
|
21776
|
-
//# sourceMappingURL=local-list-
|
|
22667
|
+
//# sourceMappingURL=local-list-jk2HzwoN.js.map
|