cdk-local 0.38.0 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -4
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +55 -54
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/{local-list-CVAyM_lE.js → local-list-DDsa8FJV.js} +402 -152
- package/dist/local-list-DDsa8FJV.js.map +1 -0
- package/package.json +1 -1
- package/dist/local-list-CVAyM_lE.js.map +0 -1
|
@@ -3036,10 +3036,10 @@ function listTargets(stacks) {
|
|
|
3036
3036
|
loadBalancers: sortEntries(scanApplicationLoadBalancers(stacks))
|
|
3037
3037
|
};
|
|
3038
3038
|
}
|
|
3039
|
-
const pathOf = (e) => e.displayPath ?? e.qualifiedId;
|
|
3039
|
+
const pathOf$1 = (e) => e.displayPath ?? e.qualifiedId;
|
|
3040
3040
|
/** Stable, human-readable ordering: by display path, falling back to the qualified ID. */
|
|
3041
3041
|
function sortEntries(entries) {
|
|
3042
|
-
return [...entries].sort((a, b) => pathOf(a).localeCompare(pathOf(b)));
|
|
3042
|
+
return [...entries].sort((a, b) => pathOf$1(a).localeCompare(pathOf$1(b)));
|
|
3043
3043
|
}
|
|
3044
3044
|
/** Display order for API surface kinds; entries are grouped in this order. */
|
|
3045
3045
|
const API_KIND_ORDER = [
|
|
@@ -3060,7 +3060,7 @@ function sortApiEntries(entries) {
|
|
|
3060
3060
|
const ka = API_KIND_ORDER.indexOf(a.kind ?? "");
|
|
3061
3061
|
const kb = API_KIND_ORDER.indexOf(b.kind ?? "");
|
|
3062
3062
|
if (ka !== kb) return ka - kb;
|
|
3063
|
-
return pathOf(a).localeCompare(pathOf(b));
|
|
3063
|
+
return pathOf$1(a).localeCompare(pathOf$1(b));
|
|
3064
3064
|
});
|
|
3065
3065
|
}
|
|
3066
3066
|
/** Total number of targets across every category. */
|
|
@@ -8232,10 +8232,12 @@ function isTransientNetworkError(err) {
|
|
|
8232
8232
|
}
|
|
8233
8233
|
/**
|
|
8234
8234
|
* POST the event body to the agent's `/invocations` endpoint with the
|
|
8235
|
-
* session-id header and a JSON content type
|
|
8236
|
-
*
|
|
8237
|
-
*
|
|
8238
|
-
*
|
|
8235
|
+
* session-id header and a JSON content type, then return the response.
|
|
8236
|
+
*
|
|
8237
|
+
* A `text/event-stream` response is streamed incrementally through
|
|
8238
|
+
* `options.onChunk` (when given) so the user sees tokens as they arrive — the
|
|
8239
|
+
* result is then `streamed: true` with an empty `raw`. Any other response (or
|
|
8240
|
+
* a missing sink) is buffered into `raw` and returned verbatim.
|
|
8239
8241
|
*/
|
|
8240
8242
|
async function invokeAgentCore(host, port, event, options) {
|
|
8241
8243
|
const url = `http://${host}:${port}${INVOCATIONS_PATH}`;
|
|
@@ -8254,11 +8256,22 @@ async function invokeAgentCore(host, port, event, options) {
|
|
|
8254
8256
|
body,
|
|
8255
8257
|
signal: controller.signal
|
|
8256
8258
|
});
|
|
8259
|
+
const contentType = response.headers.get("content-type");
|
|
8260
|
+
if ((contentType ?? "").includes("text/event-stream") && options.onChunk && response.body) {
|
|
8261
|
+
await streamBody(response.body, options.onChunk);
|
|
8262
|
+
return {
|
|
8263
|
+
status: response.status,
|
|
8264
|
+
contentType,
|
|
8265
|
+
raw: "",
|
|
8266
|
+
streamed: true
|
|
8267
|
+
};
|
|
8268
|
+
}
|
|
8257
8269
|
const raw = await response.text();
|
|
8258
8270
|
return {
|
|
8259
8271
|
status: response.status,
|
|
8260
|
-
contentType
|
|
8261
|
-
raw
|
|
8272
|
+
contentType,
|
|
8273
|
+
raw,
|
|
8274
|
+
streamed: false
|
|
8262
8275
|
};
|
|
8263
8276
|
} catch (err) {
|
|
8264
8277
|
if (err.name === "AbortError") throw new Error(`AgentCore invoke at ${url} timed out after ${options.timeoutMs}ms. The agent may be hung; check container logs.`);
|
|
@@ -8267,6 +8280,30 @@ async function invokeAgentCore(host, port, event, options) {
|
|
|
8267
8280
|
clearTimeout(timer);
|
|
8268
8281
|
}
|
|
8269
8282
|
}
|
|
8283
|
+
/**
|
|
8284
|
+
* Decode a response body stream to UTF-8 text and push each chunk to `onChunk`
|
|
8285
|
+
* as it arrives. Uses the reader API (portable across Node versions) and a
|
|
8286
|
+
* streaming TextDecoder so a multi-byte char split across chunk boundaries is
|
|
8287
|
+
* not corrupted.
|
|
8288
|
+
*/
|
|
8289
|
+
async function streamBody(body, onChunk) {
|
|
8290
|
+
const reader = body.getReader();
|
|
8291
|
+
const decoder = new TextDecoder();
|
|
8292
|
+
try {
|
|
8293
|
+
for (;;) {
|
|
8294
|
+
const { done, value } = await reader.read();
|
|
8295
|
+
if (done) break;
|
|
8296
|
+
if (value) {
|
|
8297
|
+
const text = decoder.decode(value, { stream: true });
|
|
8298
|
+
if (text) onChunk(text);
|
|
8299
|
+
}
|
|
8300
|
+
}
|
|
8301
|
+
const tail = decoder.decode();
|
|
8302
|
+
if (tail) onChunk(tail);
|
|
8303
|
+
} finally {
|
|
8304
|
+
reader.releaseLock();
|
|
8305
|
+
}
|
|
8306
|
+
}
|
|
8270
8307
|
|
|
8271
8308
|
//#endregion
|
|
8272
8309
|
//#region src/local/cors-handler.ts
|
|
@@ -17320,6 +17357,7 @@ async function localInvokeAgentCoreCommand(target, options, extraStateProviders)
|
|
|
17320
17357
|
const result = await invokeAgentCore(containerHost, hostPort, event, {
|
|
17321
17358
|
sessionId,
|
|
17322
17359
|
timeoutMs: 12e4,
|
|
17360
|
+
onChunk: (text) => process.stdout.write(text),
|
|
17323
17361
|
...authorization && { authorization }
|
|
17324
17362
|
});
|
|
17325
17363
|
await new Promise((r) => setTimeout(r, 250));
|
|
@@ -17496,6 +17534,10 @@ function emitResult(result) {
|
|
|
17496
17534
|
logger.warn(`Agent /invocations returned HTTP ${result.status}.`);
|
|
17497
17535
|
process.exitCode = 1;
|
|
17498
17536
|
}
|
|
17537
|
+
if (result.streamed) {
|
|
17538
|
+
process.stdout.write("\n");
|
|
17539
|
+
return;
|
|
17540
|
+
}
|
|
17499
17541
|
process.stdout.write(`${result.raw}\n`);
|
|
17500
17542
|
}
|
|
17501
17543
|
/** Map a `--platform` value to the architecture `buildContainerImage` expects. */
|
|
@@ -20188,9 +20230,15 @@ async function startFrontDoorServer(opts) {
|
|
|
20188
20230
|
}
|
|
20189
20231
|
function handleProxyRequest(req, res, opts) {
|
|
20190
20232
|
return new Promise((resolve) => {
|
|
20191
|
-
const
|
|
20233
|
+
const pool = opts.selectPool(req.url ?? "/");
|
|
20234
|
+
if (!pool) {
|
|
20235
|
+
writeError(res, 404, `No listener rule matched '${req.url ?? "/"}' on ${opts.label}, and the listener has no default action forwarding to a local target.`);
|
|
20236
|
+
resolve();
|
|
20237
|
+
return;
|
|
20238
|
+
}
|
|
20239
|
+
const endpoint = pool.next();
|
|
20192
20240
|
if (!endpoint) {
|
|
20193
|
-
writeError(res, 503, `No running replicas
|
|
20241
|
+
writeError(res, 503, `No running replicas behind ${opts.label} for the matched target. The front-door has no healthy target to forward to.`);
|
|
20194
20242
|
resolve();
|
|
20195
20243
|
return;
|
|
20196
20244
|
}
|
|
@@ -20201,6 +20249,7 @@ function handleProxyRequest(req, res, opts) {
|
|
|
20201
20249
|
resolve();
|
|
20202
20250
|
};
|
|
20203
20251
|
const headers = { ...req.headers };
|
|
20252
|
+
stripHopByHopHeaders(headers);
|
|
20204
20253
|
appendForwardedHeaders(headers, req, opts.listenerPort);
|
|
20205
20254
|
const proxyReq = request({
|
|
20206
20255
|
host: endpoint.host,
|
|
@@ -20209,7 +20258,9 @@ function handleProxyRequest(req, res, opts) {
|
|
|
20209
20258
|
path: req.url,
|
|
20210
20259
|
headers
|
|
20211
20260
|
}, (proxyRes) => {
|
|
20212
|
-
|
|
20261
|
+
const resHeaders = { ...proxyRes.headers };
|
|
20262
|
+
stripHopByHopHeaders(resHeaders);
|
|
20263
|
+
res.writeHead(proxyRes.statusCode ?? 502, resHeaders);
|
|
20213
20264
|
proxyRes.pipe(res);
|
|
20214
20265
|
proxyRes.on("end", done);
|
|
20215
20266
|
proxyRes.on("error", () => {
|
|
@@ -20218,13 +20269,13 @@ function handleProxyRequest(req, res, opts) {
|
|
|
20218
20269
|
});
|
|
20219
20270
|
});
|
|
20220
20271
|
proxyReq.setTimeout(opts.upstreamTimeoutMs ?? 3e4, () => {
|
|
20221
|
-
if (!res.headersSent) writeError(res, 504, `Replica ${endpoint.host}:${endpoint.port}
|
|
20272
|
+
if (!res.headersSent) writeError(res, 504, `Replica ${endpoint.host}:${endpoint.port} behind ${opts.label} did not respond in time.`);
|
|
20222
20273
|
else if (!res.writableEnded) res.destroy();
|
|
20223
20274
|
proxyReq.destroy();
|
|
20224
20275
|
done();
|
|
20225
20276
|
});
|
|
20226
20277
|
proxyReq.on("error", () => {
|
|
20227
|
-
if (!res.headersSent) writeError(res, 502, `Failed to reach replica ${endpoint.host}:${endpoint.port}
|
|
20278
|
+
if (!res.headersSent) writeError(res, 502, `Failed to reach replica ${endpoint.host}:${endpoint.port} behind ${opts.label}.`);
|
|
20228
20279
|
else if (!res.writableEnded) res.destroy();
|
|
20229
20280
|
done();
|
|
20230
20281
|
});
|
|
@@ -20234,6 +20285,33 @@ function handleProxyRequest(req, res, opts) {
|
|
|
20234
20285
|
req.pipe(proxyReq);
|
|
20235
20286
|
});
|
|
20236
20287
|
}
|
|
20288
|
+
/** Standard hop-by-hop headers (RFC 7230 §6.1) — a proxy must not forward these. */
|
|
20289
|
+
const HOP_BY_HOP_HEADERS = [
|
|
20290
|
+
"connection",
|
|
20291
|
+
"keep-alive",
|
|
20292
|
+
"proxy-authenticate",
|
|
20293
|
+
"proxy-authorization",
|
|
20294
|
+
"te",
|
|
20295
|
+
"trailer",
|
|
20296
|
+
"transfer-encoding",
|
|
20297
|
+
"upgrade"
|
|
20298
|
+
];
|
|
20299
|
+
/**
|
|
20300
|
+
* Strip hop-by-hop headers before relaying a request to / a response from the
|
|
20301
|
+
* upstream, mirroring what a real ALB does. Forwarding the upstream's
|
|
20302
|
+
* `Transfer-Encoding` / `Connection` verbatim while Node re-frames the body can
|
|
20303
|
+
* produce a malformed response; the headers named in a `Connection` token list
|
|
20304
|
+
* are also hop-by-hop and removed. Mutates `headers` in place.
|
|
20305
|
+
*/
|
|
20306
|
+
function stripHopByHopHeaders(headers) {
|
|
20307
|
+
const connection = headers["connection"];
|
|
20308
|
+
const connectionValue = Array.isArray(connection) ? connection.join(",") : connection;
|
|
20309
|
+
if (connectionValue) for (const token of connectionValue.split(",")) {
|
|
20310
|
+
const name = token.trim().toLowerCase();
|
|
20311
|
+
if (name) delete headers[name];
|
|
20312
|
+
}
|
|
20313
|
+
for (const name of HOP_BY_HOP_HEADERS) delete headers[name];
|
|
20314
|
+
}
|
|
20237
20315
|
/**
|
|
20238
20316
|
* Inject the ALB-style forwarding headers a downstream app may read. Appends
|
|
20239
20317
|
* the client IP to any existing `X-Forwarded-For` chain (ALB appends rather
|
|
@@ -20252,6 +20330,49 @@ function writeError(res, statusCode, message) {
|
|
|
20252
20330
|
res.end(`${message}\n`);
|
|
20253
20331
|
}
|
|
20254
20332
|
|
|
20333
|
+
//#endregion
|
|
20334
|
+
//#region src/local/alb-path-matcher.ts
|
|
20335
|
+
/**
|
|
20336
|
+
* Return the target of the highest-priority rule whose `path-pattern` matches
|
|
20337
|
+
* `requestPath`, or `undefined` when none match (caller uses the default).
|
|
20338
|
+
* Rules are evaluated in ascending priority; the input order is irrelevant.
|
|
20339
|
+
*/
|
|
20340
|
+
function matchAlbPathRule(requestPath, rules) {
|
|
20341
|
+
const path = pathOf(requestPath);
|
|
20342
|
+
const ordered = [...rules].sort((a, b) => a.priority - b.priority);
|
|
20343
|
+
for (const rule of ordered) if (rule.pathPatterns.some((pattern) => albPathPatternMatches(pattern, path))) return rule.target;
|
|
20344
|
+
}
|
|
20345
|
+
/**
|
|
20346
|
+
* Whether a single ALB `path-pattern` value matches a request path. The path
|
|
20347
|
+
* must already be query-stripped, or pass a raw URL and it is stripped here.
|
|
20348
|
+
*/
|
|
20349
|
+
function albPathPatternMatches(pattern, requestPath) {
|
|
20350
|
+
return globToRegExp(pattern).test(pathOf(requestPath));
|
|
20351
|
+
}
|
|
20352
|
+
/** Strip the query string / fragment so only the URL path is matched. */
|
|
20353
|
+
function pathOf(url) {
|
|
20354
|
+
let end = url.length;
|
|
20355
|
+
const q = url.indexOf("?");
|
|
20356
|
+
if (q !== -1) end = q;
|
|
20357
|
+
const h = url.indexOf("#");
|
|
20358
|
+
if (h !== -1 && h < end) end = h;
|
|
20359
|
+
return url.slice(0, end);
|
|
20360
|
+
}
|
|
20361
|
+
const REGEXP_META = /[.+^${}()|[\]\\]/;
|
|
20362
|
+
/**
|
|
20363
|
+
* Translate an ALB path-pattern glob into an anchored, case-sensitive RegExp:
|
|
20364
|
+
* `*` -> `.*`, `?` -> `.`, every other character is escaped and matched
|
|
20365
|
+
* literally.
|
|
20366
|
+
*/
|
|
20367
|
+
function globToRegExp(pattern) {
|
|
20368
|
+
let body = "";
|
|
20369
|
+
for (const ch of pattern) if (ch === "*") body += ".*";
|
|
20370
|
+
else if (ch === "?") body += ".";
|
|
20371
|
+
else if (REGEXP_META.test(ch)) body += `\\${ch}`;
|
|
20372
|
+
else body += ch;
|
|
20373
|
+
return new RegExp(`^${body}$`);
|
|
20374
|
+
}
|
|
20375
|
+
|
|
20255
20376
|
//#endregion
|
|
20256
20377
|
//#region src/cli/commands/ecs-service-emulator.ts
|
|
20257
20378
|
/**
|
|
@@ -20270,6 +20391,8 @@ async function runEcsServiceEmulator(targets, options, strategy, extraStateProvi
|
|
|
20270
20391
|
let sigintCount = 0;
|
|
20271
20392
|
let sharedNetwork;
|
|
20272
20393
|
let profileCredsFile;
|
|
20394
|
+
let frontDoorServers = [];
|
|
20395
|
+
let frontDoorByService = /* @__PURE__ */ new Map();
|
|
20273
20396
|
const cleanup = singleFlight(async () => {
|
|
20274
20397
|
await Promise.allSettled(perTarget.map(async (pt) => {
|
|
20275
20398
|
if (pt.controller) await pt.controller.shutdown();
|
|
@@ -20277,8 +20400,9 @@ async function runEcsServiceEmulator(targets, options, strategy, extraStateProvi
|
|
|
20277
20400
|
await Promise.allSettled(pt.runState.replicas.map((r) => r.inFlightBoot).filter((p) => p !== void 0));
|
|
20278
20401
|
await Promise.allSettled(pt.runState.replicas.map((r) => cleanupEcsRun(r.state, { keepRunning: false }).catch(() => void 0)));
|
|
20279
20402
|
}
|
|
20280
|
-
await Promise.allSettled((pt.frontDoorServers ?? []).map((s) => s.close().catch((err) => getLogger().warn(`front-door server teardown failed: ${err instanceof Error ? err.message : String(err)}`))));
|
|
20281
20403
|
}));
|
|
20404
|
+
await Promise.allSettled(frontDoorServers.map((s) => s.close().catch((err) => getLogger().warn(`front-door server teardown failed: ${err instanceof Error ? err.message : String(err)}`))));
|
|
20405
|
+
frontDoorServers = [];
|
|
20282
20406
|
if (profileCredsFile) {
|
|
20283
20407
|
try {
|
|
20284
20408
|
await profileCredsFile.dispose();
|
|
@@ -20321,7 +20445,7 @@ async function runEcsServiceEmulator(targets, options, strategy, extraStateProvi
|
|
|
20321
20445
|
noun: strategy.pickerNoun,
|
|
20322
20446
|
onMissing: () => strategy.onMissing()
|
|
20323
20447
|
});
|
|
20324
|
-
const { boots, warnings } = strategy.resolveBoots(stacks, resolvedTargets);
|
|
20448
|
+
const { boots, frontDoor, warnings } = strategy.resolveBoots(stacks, resolvedTargets);
|
|
20325
20449
|
for (const w of warnings) logger.warn(w);
|
|
20326
20450
|
if (boots.length === 0) throw new LocalStartServiceError(`No runnable ECS service resolved from ${resolvedTargets.join(", ")}.`);
|
|
20327
20451
|
rejectExplicitCfnStackWithMultipleStacks(options, boots.length);
|
|
@@ -20353,6 +20477,11 @@ async function runEcsServiceEmulator(targets, options, strategy, extraStateProvi
|
|
|
20353
20477
|
cloudMapIndexByStack,
|
|
20354
20478
|
sharedNetwork
|
|
20355
20479
|
};
|
|
20480
|
+
if (frontDoor && frontDoor.listeners.length > 0) {
|
|
20481
|
+
const built = await buildFrontDoor(frontDoor, options.containerHost, logger);
|
|
20482
|
+
frontDoorServers = built.servers;
|
|
20483
|
+
frontDoorByService = built.frontDoorByService;
|
|
20484
|
+
}
|
|
20356
20485
|
sigintHandler = () => {
|
|
20357
20486
|
sigintCount += 1;
|
|
20358
20487
|
if (sigintCount >= 2) {
|
|
@@ -20364,11 +20493,7 @@ async function runEcsServiceEmulator(targets, options, strategy, extraStateProvi
|
|
|
20364
20493
|
};
|
|
20365
20494
|
process.on("SIGINT", sigintHandler);
|
|
20366
20495
|
process.on("SIGTERM", sigintHandler);
|
|
20367
|
-
for (const pt of perTarget)
|
|
20368
|
-
const booted = await bootOneTarget(pt.boot, pt.runState, stacks, options, discovery, skipPull, extraStateProviders, profileCredsFile, strategy.lbPortOverrides);
|
|
20369
|
-
pt.controller = booted.controller;
|
|
20370
|
-
pt.frontDoorServers = booted.frontDoorServers;
|
|
20371
|
-
}
|
|
20496
|
+
for (const pt of perTarget) pt.controller = await bootOneTarget(pt.boot, pt.runState, stacks, options, discovery, skipPull, extraStateProviders, profileCredsFile, frontDoorByService.get(pt.boot.target));
|
|
20372
20497
|
const summary = perTarget.map((pt) => `${pt.controller.service.serviceName} (${pt.controller.activeReplicaCount()} replica(s))`).join(", ");
|
|
20373
20498
|
logger.info(`Service(s) running: ${summary}.`);
|
|
20374
20499
|
logger.info("Press ^C to shut down.");
|
|
@@ -20381,16 +20506,16 @@ async function runEcsServiceEmulator(targets, options, strategy, extraStateProvi
|
|
|
20381
20506
|
await cleanup();
|
|
20382
20507
|
}
|
|
20383
20508
|
}
|
|
20384
|
-
async function bootOneTarget(boot, runState, stacks, options, discovery, skipPull, extraStateProviders, profileCredsFile,
|
|
20509
|
+
async function bootOneTarget(boot, runState, stacks, options, discovery, skipPull, extraStateProviders, profileCredsFile, frontDoorPools) {
|
|
20385
20510
|
const candidate = pickCandidateStack(parseEcsTarget(boot.target).stackPattern, stacks);
|
|
20386
20511
|
const stateProvider = createLocalStateProvider(options, candidate?.stackName ?? "", await resolveCfnFallbackRegion(options, candidate?.region), extraStateProviders);
|
|
20387
20512
|
try {
|
|
20388
|
-
return await runOneTarget(boot, runState, stacks, options, discovery, skipPull, stateProvider, profileCredsFile,
|
|
20513
|
+
return await runOneTarget(boot, runState, stacks, options, discovery, skipPull, stateProvider, profileCredsFile, frontDoorPools);
|
|
20389
20514
|
} finally {
|
|
20390
20515
|
if (stateProvider) stateProvider.dispose();
|
|
20391
20516
|
}
|
|
20392
20517
|
}
|
|
20393
|
-
async function runOneTarget(boot, runState, stacks, options, discovery, skipPull, stateProvider, profileCredsFile,
|
|
20518
|
+
async function runOneTarget(boot, runState, stacks, options, discovery, skipPull, stateProvider, profileCredsFile, frontDoorPools) {
|
|
20394
20519
|
const logger = getLogger();
|
|
20395
20520
|
const target = boot.target;
|
|
20396
20521
|
const imageContext = await buildEcsImageResolutionContext(target, stacks, options, stateProvider);
|
|
@@ -20446,73 +20571,89 @@ async function runOneTarget(boot, runState, stacks, options, discovery, skipPull
|
|
|
20446
20571
|
containerPath: profileCredsFile.containerPath,
|
|
20447
20572
|
profileName: profileCredsFile.profileName
|
|
20448
20573
|
};
|
|
20449
|
-
|
|
20450
|
-
const runnerOpts = {
|
|
20574
|
+
return startEcsService(service, {
|
|
20451
20575
|
maxTasks: options.maxTasks,
|
|
20452
20576
|
restartPolicy: options.restartPolicy,
|
|
20453
20577
|
taskOptions: taskOpts,
|
|
20454
20578
|
discovery,
|
|
20455
|
-
...
|
|
20456
|
-
};
|
|
20457
|
-
let controller;
|
|
20458
|
-
try {
|
|
20459
|
-
controller = await startEcsService(service, runnerOpts, runState);
|
|
20460
|
-
} catch (err) {
|
|
20461
|
-
await Promise.allSettled(frontDoorServers.map((s) => s.close()));
|
|
20462
|
-
throw err;
|
|
20463
|
-
}
|
|
20464
|
-
return {
|
|
20465
|
-
controller,
|
|
20466
|
-
frontDoorServers
|
|
20467
|
-
};
|
|
20579
|
+
...frontDoorPools && frontDoorPools.length > 0 ? { frontDoor: { pools: frontDoorPools } } : {}
|
|
20580
|
+
}, runState);
|
|
20468
20581
|
}
|
|
20469
20582
|
/**
|
|
20470
|
-
*
|
|
20471
|
-
*
|
|
20472
|
-
*
|
|
20473
|
-
*
|
|
20474
|
-
*
|
|
20475
|
-
*
|
|
20476
|
-
*
|
|
20583
|
+
* Stand up one host-side reverse-proxy server PER LISTENER PORT from the
|
|
20584
|
+
* resolved {@link FrontDoorPlan}, path-routing each request across the services
|
|
20585
|
+
* the listener fronts, and return the started servers (for teardown) plus a
|
|
20586
|
+
* per-service-target pool list to thread into each service's runner (so every
|
|
20587
|
+
* replica publishes + registers its ephemeral endpoint into the right pool).
|
|
20588
|
+
*
|
|
20589
|
+
* One `FrontDoorEndpointPool` is created per distinct (service, container,
|
|
20590
|
+
* port) forward target and SHARED between the listener's routing table and the
|
|
20591
|
+
* owning service's runner context — same object on both sides, so a replica
|
|
20592
|
+
* registering itself is immediately reachable through the front-door.
|
|
20477
20593
|
*
|
|
20478
20594
|
* On a bind failure (e.g. EACCES on a privileged listener port, or the port is
|
|
20479
20595
|
* already in use) every server started so far is closed and the error is
|
|
20480
20596
|
* re-thrown with a `--lb-port` hint.
|
|
20481
20597
|
*/
|
|
20482
|
-
async function
|
|
20483
|
-
|
|
20484
|
-
|
|
20485
|
-
|
|
20598
|
+
async function buildFrontDoor(plan, containerHost, logger) {
|
|
20599
|
+
const servers = [];
|
|
20600
|
+
const registry = /* @__PURE__ */ new Map();
|
|
20601
|
+
const poolFor = (t) => {
|
|
20602
|
+
const key = `${t.serviceTarget} ${t.targetContainerName} ${t.targetContainerPort}`;
|
|
20603
|
+
let entry = registry.get(key);
|
|
20604
|
+
if (!entry) {
|
|
20605
|
+
entry = {
|
|
20606
|
+
pool: new FrontDoorEndpointPool(),
|
|
20607
|
+
target: t
|
|
20608
|
+
};
|
|
20609
|
+
registry.set(key, entry);
|
|
20610
|
+
}
|
|
20611
|
+
return entry.pool;
|
|
20486
20612
|
};
|
|
20487
|
-
const frontDoorServers = [];
|
|
20488
|
-
const pools = [];
|
|
20489
20613
|
try {
|
|
20490
|
-
for (const
|
|
20491
|
-
const
|
|
20614
|
+
for (const listener of plan.listeners) {
|
|
20615
|
+
const defaultPool = listener.defaultTarget ? poolFor(listener.defaultTarget) : void 0;
|
|
20616
|
+
const ruleRoutes = listener.rules.map((r) => ({
|
|
20617
|
+
priority: r.priority,
|
|
20618
|
+
pathPatterns: r.pathPatterns,
|
|
20619
|
+
target: poolFor(r.target)
|
|
20620
|
+
}));
|
|
20621
|
+
const selectPool = (requestPath) => matchAlbPathRule(requestPath, ruleRoutes) ?? defaultPool;
|
|
20492
20622
|
const server = await startFrontDoorServer({
|
|
20493
|
-
|
|
20494
|
-
port:
|
|
20623
|
+
selectPool,
|
|
20624
|
+
port: listener.hostPort,
|
|
20495
20625
|
host: containerHost,
|
|
20496
|
-
listenerPort:
|
|
20497
|
-
|
|
20626
|
+
listenerPort: listener.listenerPort,
|
|
20627
|
+
label: `listener port ${listener.listenerPort}`
|
|
20498
20628
|
});
|
|
20499
|
-
|
|
20500
|
-
|
|
20501
|
-
|
|
20502
|
-
|
|
20503
|
-
|
|
20504
|
-
});
|
|
20505
|
-
logger.info(`ALB front-door: http://${server.host}:${server.port} -> ${service.serviceName} (listener port ${t.listenerPort} -> container ${t.targetContainerName}:${t.targetContainerPort}, round-robin across replicas)`);
|
|
20629
|
+
servers.push(server);
|
|
20630
|
+
logger.info(`ALB front-door: http://${server.host}:${server.port} (listener port ${listener.listenerPort})`);
|
|
20631
|
+
if (listener.defaultTarget) logger.info(` default -> ${describeTarget(listener.defaultTarget)} (round-robin)`);
|
|
20632
|
+
for (const r of [...listener.rules].sort((a, b) => a.priority - b.priority)) logger.info(` path ${r.pathPatterns.join(", ")} (priority ${r.priority}) -> ${describeTarget(r.target)}`);
|
|
20633
|
+
if (!listener.defaultTarget) logger.info(" (no default action: unmatched paths return 404)");
|
|
20506
20634
|
}
|
|
20507
20635
|
} catch (err) {
|
|
20508
|
-
await Promise.allSettled(
|
|
20509
|
-
throw new LocalStartServiceError(`Failed to start ALB front-door
|
|
20636
|
+
await Promise.allSettled(servers.map((s) => s.close()));
|
|
20637
|
+
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).`);
|
|
20638
|
+
}
|
|
20639
|
+
const frontDoorByService = /* @__PURE__ */ new Map();
|
|
20640
|
+
for (const { pool, target } of registry.values()) {
|
|
20641
|
+
const list = frontDoorByService.get(target.serviceTarget) ?? [];
|
|
20642
|
+
list.push({
|
|
20643
|
+
pool,
|
|
20644
|
+
targetContainerName: target.targetContainerName,
|
|
20645
|
+
targetContainerPort: target.targetContainerPort
|
|
20646
|
+
});
|
|
20647
|
+
frontDoorByService.set(target.serviceTarget, list);
|
|
20510
20648
|
}
|
|
20511
20649
|
return {
|
|
20512
|
-
|
|
20513
|
-
|
|
20650
|
+
servers,
|
|
20651
|
+
frontDoorByService
|
|
20514
20652
|
};
|
|
20515
20653
|
}
|
|
20654
|
+
function describeTarget(t) {
|
|
20655
|
+
return `${t.serviceTarget} (container ${t.targetContainerName}:${t.targetContainerPort})`;
|
|
20656
|
+
}
|
|
20516
20657
|
async function resolvePlaceholderAccount(arn, region) {
|
|
20517
20658
|
if (!arn.includes("${AWS::AccountId}")) return arn;
|
|
20518
20659
|
const { STSClient, GetCallerIdentityCommand } = await import("@aws-sdk/client-sts");
|
|
@@ -20687,10 +20828,7 @@ function serviceStrategy() {
|
|
|
20687
20828
|
pickerNoun: "ECS services",
|
|
20688
20829
|
onMissing: () => new LocalStartServiceError(`${getEmbedConfig().cliName} start-service requires at least one <target>. Pass one or more service paths like 'Stack/Orders' 'Stack/Frontend', or run it in a TTY to pick interactively.`),
|
|
20689
20830
|
resolveBoots: (_stacks, chosenTargets) => ({
|
|
20690
|
-
boots: chosenTargets.map((target) => ({
|
|
20691
|
-
target,
|
|
20692
|
-
frontDoorTargets: []
|
|
20693
|
-
})),
|
|
20831
|
+
boots: chosenTargets.map((target) => ({ target })),
|
|
20694
20832
|
warnings: []
|
|
20695
20833
|
}),
|
|
20696
20834
|
lbPortOverrides: {}
|
|
@@ -20713,47 +20851,57 @@ function createLocalStartServiceCommand(opts = {}) {
|
|
|
20713
20851
|
//#endregion
|
|
20714
20852
|
//#region src/local/elb-front-door-resolver.ts
|
|
20715
20853
|
/**
|
|
20716
|
-
*
|
|
20717
|
-
*
|
|
20718
|
-
*
|
|
20719
|
-
*
|
|
20720
|
-
*
|
|
20854
|
+
* Resolve an `AWS::ElasticLoadBalancingV2::LoadBalancer` (an ALB) into the
|
|
20855
|
+
* backing ECS service(s) and the host listener port(s) a local front-door
|
|
20856
|
+
* should expose. This is the `cdkl start-alb` entry: you name the ALB, and
|
|
20857
|
+
* cdk-local discovers the services behind it (mirroring how `start-api` names
|
|
20858
|
+
* the API and discovers the backing Lambdas).
|
|
20721
20859
|
*
|
|
20722
20860
|
* The synthesized linkage (confirmed against real `cdk synth` of
|
|
20723
|
-
* `ApplicationLoadBalancedFargateService`):
|
|
20861
|
+
* `ApplicationLoadBalancedFargateService` + an `addAction` path rule):
|
|
20724
20862
|
*
|
|
20725
20863
|
* ```
|
|
20726
20864
|
* ElasticLoadBalancingV2::LoadBalancer (the ALB you name)
|
|
20727
20865
|
* ElasticLoadBalancingV2::Listener : { LoadBalancerArn:{Ref:<ALB>}, Port, Protocol,
|
|
20728
20866
|
* DefaultActions:[{ Type:"forward", TargetGroupArn:{Ref:<TG>} }] }
|
|
20867
|
+
* ElasticLoadBalancingV2::ListenerRule : { ListenerArn:{Ref:<Listener>}, Priority,
|
|
20868
|
+
* Conditions:[{ Field:"path-pattern", PathPatternConfig:{ Values:["/api/*"] } }],
|
|
20869
|
+
* Actions:[{ Type:"forward", TargetGroupArn:{Ref:<TG>} }] }
|
|
20729
20870
|
* ElasticLoadBalancingV2::TargetGroup : { Port, Protocol, TargetType:"ip" }
|
|
20730
20871
|
* ECS::Service.LoadBalancers[] -> { ContainerName, ContainerPort, TargetGroupArn:{Ref:<TG>} }
|
|
20731
20872
|
* ```
|
|
20732
20873
|
*
|
|
20733
|
-
* Resolution walks ALB -> listeners (by `LoadBalancerArn` Ref) -> default
|
|
20734
|
-
* `forward`
|
|
20735
|
-
*
|
|
20874
|
+
* Resolution walks ALB -> listeners (by `LoadBalancerArn` Ref) -> their default
|
|
20875
|
+
* `forward` action AND any `path-pattern` ListenerRules -> the ECS Service whose
|
|
20876
|
+
* `LoadBalancers[]` references each target group (a reverse scan; there is no
|
|
20877
|
+
* direct TG -> service pointer). Output is a per-listener routing table: a
|
|
20878
|
+
* default forward target (when the default action is a resolvable forward) plus
|
|
20879
|
+
* the ordered path-pattern rules.
|
|
20736
20880
|
*
|
|
20737
|
-
*
|
|
20738
|
-
*
|
|
20739
|
-
*
|
|
20740
|
-
*
|
|
20881
|
+
* Scope: HTTP listeners, `path-pattern` conditions, single-target `forward`
|
|
20882
|
+
* actions to ECS services. Skipped with a warning: HTTPS/TLS listeners,
|
|
20883
|
+
* `TargetType:"lambda"` target groups, weighted (multi-target) forwards, rules
|
|
20884
|
+
* with non-`path-pattern` conditions (host-header / http-header / query-string
|
|
20885
|
+
* / etc.), and `redirect` / `fixed-response` / `authenticate-*` actions. Those
|
|
20886
|
+
* remaining listener-rule features are tracked in #123.
|
|
20741
20887
|
*/
|
|
20742
20888
|
const ALB_TYPE = "AWS::ElasticLoadBalancingV2::LoadBalancer";
|
|
20743
20889
|
const LISTENER_TYPE = "AWS::ElasticLoadBalancingV2::Listener";
|
|
20890
|
+
const LISTENER_RULE_TYPE = "AWS::ElasticLoadBalancingV2::ListenerRule";
|
|
20744
20891
|
const TARGET_GROUP_TYPE = "AWS::ElasticLoadBalancingV2::TargetGroup";
|
|
20745
20892
|
const SERVICE_TYPE = "AWS::ECS::Service";
|
|
20746
20893
|
/**
|
|
20747
|
-
* Resolve an ALB into its
|
|
20748
|
-
* only the supplied stack template. Returns an empty `
|
|
20894
|
+
* Resolve an ALB into its front-door listeners + routing tables. Pure — reads
|
|
20895
|
+
* only the supplied stack template. Returns an empty `listeners` array (with
|
|
20749
20896
|
* warnings) when the ALB fronts nothing cdk-local can serve locally.
|
|
20750
20897
|
*/
|
|
20751
20898
|
function resolveAlbFrontDoor(stack, albLogicalId) {
|
|
20752
20899
|
const warnings = [];
|
|
20753
20900
|
const resources = stack.template.Resources ?? {};
|
|
20901
|
+
const stackName = stack.stackName;
|
|
20754
20902
|
const tgToService = indexTargetGroupToService(resources);
|
|
20755
|
-
const
|
|
20756
|
-
const
|
|
20903
|
+
const rulesByListener = indexRulesByListener(resources);
|
|
20904
|
+
const listeners = [];
|
|
20757
20905
|
for (const [listenerLogicalId, resource] of Object.entries(resources)) {
|
|
20758
20906
|
if (resource.Type !== LISTENER_TYPE) continue;
|
|
20759
20907
|
const props = resource.Properties ?? {};
|
|
@@ -20761,54 +20909,40 @@ function resolveAlbFrontDoor(stack, albLogicalId) {
|
|
|
20761
20909
|
const port = parsePort(props["Port"]);
|
|
20762
20910
|
if (port === void 0) continue;
|
|
20763
20911
|
const protocol = typeof props["Protocol"] === "string" ? props["Protocol"] : "HTTP";
|
|
20764
|
-
const tgRefs = collectForwardTargetGroupRefs(props["DefaultActions"]);
|
|
20765
|
-
if (tgRefs.size === 0) {
|
|
20766
|
-
if (hasUnresolvableForward(props["DefaultActions"])) warnings.push(`Listener '${listenerLogicalId}' on port ${port} forwards to a non-Ref TargetGroupArn (literal / cross-stack / imported); the local front-door only supports in-stack target groups. Skipping it.`);
|
|
20767
|
-
continue;
|
|
20768
|
-
}
|
|
20769
20912
|
if (protocol !== "HTTP") {
|
|
20770
|
-
warnings.push(`Listener '${listenerLogicalId}' on port ${port} uses protocol ${protocol}; the local ALB front-door supports HTTP listeners only
|
|
20913
|
+
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.`);
|
|
20771
20914
|
continue;
|
|
20772
20915
|
}
|
|
20773
|
-
|
|
20774
|
-
|
|
20775
|
-
|
|
20776
|
-
|
|
20777
|
-
|
|
20778
|
-
}
|
|
20779
|
-
if (
|
|
20780
|
-
warnings.push(
|
|
20781
|
-
continue;
|
|
20782
|
-
}
|
|
20783
|
-
const backing = tgToService.get(tgRef);
|
|
20784
|
-
if (!backing) {
|
|
20785
|
-
warnings.push(`Target group '${tgRef}' (listener '${listenerLogicalId}', port ${port}) is not referenced by any ${SERVICE_TYPE}.LoadBalancers[] in ${stack.stackName}; cdk-local has no ECS service to front behind it. Skipping it.`);
|
|
20916
|
+
const defaultTarget = resolveForwardTarget(props["DefaultActions"], resources, tgToService, stackName, `Listener '${listenerLogicalId}' (port ${port}) default action`, warnings);
|
|
20917
|
+
const rules = [];
|
|
20918
|
+
for (const { ruleLogicalId, ruleProps } of rulesByListener.get(listenerLogicalId) ?? []) {
|
|
20919
|
+
const priority = parsePriority(ruleProps["Priority"]);
|
|
20920
|
+
const ruleLabel = `Listener rule '${ruleLogicalId}' (priority ${priority})`;
|
|
20921
|
+
const { patterns, unsupported } = parseRulePathPatterns(ruleProps["Conditions"]);
|
|
20922
|
+
if (unsupported.length > 0) {
|
|
20923
|
+
warnings.push(`${ruleLabel} uses unsupported condition(s): ${unsupported.join(", ")}. The local ALB front-door supports path-pattern conditions only (host-header / http-header / query-string / http-request-method / source-ip deferred). Skipping it.`);
|
|
20786
20924
|
continue;
|
|
20787
20925
|
}
|
|
20788
|
-
|
|
20789
|
-
|
|
20790
|
-
|
|
20791
|
-
|
|
20792
|
-
|
|
20793
|
-
|
|
20794
|
-
|
|
20795
|
-
const targets = byService.get(backing.serviceLogicalId) ?? [];
|
|
20796
|
-
targets.push({
|
|
20797
|
-
listenerPort: port,
|
|
20798
|
-
listenerProtocol: "HTTP",
|
|
20799
|
-
targetContainerName: backing.containerName,
|
|
20800
|
-
targetContainerPort: backing.containerPort,
|
|
20801
|
-
targetGroupLogicalId: tgRef,
|
|
20802
|
-
listenerLogicalId
|
|
20926
|
+
if (patterns.length === 0) continue;
|
|
20927
|
+
const target = resolveForwardTarget(ruleProps["Actions"], resources, tgToService, stackName, `${ruleLabel} action`, warnings);
|
|
20928
|
+
if (!target) continue;
|
|
20929
|
+
rules.push({
|
|
20930
|
+
priority,
|
|
20931
|
+
pathPatterns: patterns,
|
|
20932
|
+
target
|
|
20803
20933
|
});
|
|
20804
|
-
byService.set(backing.serviceLogicalId, targets);
|
|
20805
20934
|
}
|
|
20935
|
+
if (!defaultTarget && rules.length === 0) continue;
|
|
20936
|
+
listeners.push({
|
|
20937
|
+
listenerPort: port,
|
|
20938
|
+
listenerProtocol: "HTTP",
|
|
20939
|
+
listenerLogicalId,
|
|
20940
|
+
...defaultTarget ? { defaultTarget } : {},
|
|
20941
|
+
rules
|
|
20942
|
+
});
|
|
20806
20943
|
}
|
|
20807
20944
|
return {
|
|
20808
|
-
|
|
20809
|
-
serviceLogicalId,
|
|
20810
|
-
targets
|
|
20811
|
-
})),
|
|
20945
|
+
listeners,
|
|
20812
20946
|
warnings
|
|
20813
20947
|
};
|
|
20814
20948
|
}
|
|
@@ -20819,6 +20953,43 @@ function isApplicationLoadBalancer(resource) {
|
|
|
20819
20953
|
return type === void 0 || type === "application";
|
|
20820
20954
|
}
|
|
20821
20955
|
/**
|
|
20956
|
+
* Resolve a listener / rule `Actions` (or `DefaultActions`) array to a single
|
|
20957
|
+
* ECS-service forward target, or `undefined` when it is not a resolvable
|
|
20958
|
+
* single forward (warning emitted for the cases worth surfacing).
|
|
20959
|
+
*/
|
|
20960
|
+
function resolveForwardTarget(actions, resources, tgToService, stackName, label, warnings) {
|
|
20961
|
+
const tgRefs = collectForwardTargetGroupRefs(actions);
|
|
20962
|
+
if (tgRefs.size === 0) {
|
|
20963
|
+
if (hasUnresolvableForward(actions)) 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.`);
|
|
20964
|
+
return;
|
|
20965
|
+
}
|
|
20966
|
+
if (tgRefs.size > 1) {
|
|
20967
|
+
warnings.push(`${label} is a weighted forward (multiple target groups); the local front-door supports a single target group per action (weighted routing deferred). Skipping it.`);
|
|
20968
|
+
return;
|
|
20969
|
+
}
|
|
20970
|
+
const tgRef = [...tgRefs][0];
|
|
20971
|
+
const tg = resources[tgRef];
|
|
20972
|
+
if (!tg || tg.Type !== TARGET_GROUP_TYPE) {
|
|
20973
|
+
warnings.push(`${label} forwards to target group '${tgRef}', but no ${TARGET_GROUP_TYPE} with that logical id exists in ${stackName}. Skipping it.`);
|
|
20974
|
+
return;
|
|
20975
|
+
}
|
|
20976
|
+
if (tg.Properties?.["TargetType"] === "lambda") {
|
|
20977
|
+
warnings.push(`${label} forwards to a Lambda target group '${tgRef}' (TargetType: lambda). The local ALB front-door supports ECS targets only; Lambda targets are deferred. Skipping it.`);
|
|
20978
|
+
return;
|
|
20979
|
+
}
|
|
20980
|
+
const backing = tgToService.get(tgRef);
|
|
20981
|
+
if (!backing) {
|
|
20982
|
+
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 it.`);
|
|
20983
|
+
return;
|
|
20984
|
+
}
|
|
20985
|
+
return {
|
|
20986
|
+
serviceLogicalId: backing.serviceLogicalId,
|
|
20987
|
+
targetContainerName: backing.containerName,
|
|
20988
|
+
targetContainerPort: backing.containerPort,
|
|
20989
|
+
targetGroupLogicalId: tgRef
|
|
20990
|
+
};
|
|
20991
|
+
}
|
|
20992
|
+
/**
|
|
20822
20993
|
* Build a `targetGroupLogicalId -> backing ECS service` index by scanning every
|
|
20823
20994
|
* `AWS::ECS::Service.LoadBalancers[]`. First service wins on a shared target
|
|
20824
20995
|
* group (unusual; would only happen with a hand-rolled template).
|
|
@@ -20845,10 +21016,58 @@ function indexTargetGroupToService(resources) {
|
|
|
20845
21016
|
}
|
|
20846
21017
|
return index;
|
|
20847
21018
|
}
|
|
20848
|
-
|
|
21019
|
+
/** Group every `AWS::ElasticLoadBalancingV2::ListenerRule` by the listener it references. */
|
|
21020
|
+
function indexRulesByListener(resources) {
|
|
21021
|
+
const index = /* @__PURE__ */ new Map();
|
|
21022
|
+
for (const [ruleLogicalId, resource] of Object.entries(resources)) {
|
|
21023
|
+
if (resource.Type !== LISTENER_RULE_TYPE) continue;
|
|
21024
|
+
const ruleProps = resource.Properties ?? {};
|
|
21025
|
+
const listenerRef = refOf(ruleProps["ListenerArn"]);
|
|
21026
|
+
if (!listenerRef) continue;
|
|
21027
|
+
const list = index.get(listenerRef) ?? [];
|
|
21028
|
+
list.push({
|
|
21029
|
+
ruleLogicalId,
|
|
21030
|
+
ruleProps
|
|
21031
|
+
});
|
|
21032
|
+
index.set(listenerRef, list);
|
|
21033
|
+
}
|
|
21034
|
+
return index;
|
|
21035
|
+
}
|
|
21036
|
+
/**
|
|
21037
|
+
* Parse a ListenerRule's `Conditions` into its `path-pattern` values plus the
|
|
21038
|
+
* field names of any non-`path-pattern` conditions (which make the rule
|
|
21039
|
+
* unsupported in this version). A rule is only usable when every condition is a
|
|
21040
|
+
* `path-pattern` — ALB ANDs conditions together, and we cannot honor the others
|
|
21041
|
+
* locally yet.
|
|
21042
|
+
*/
|
|
21043
|
+
function parseRulePathPatterns(conditions) {
|
|
21044
|
+
const patterns = [];
|
|
21045
|
+
const unsupported = [];
|
|
21046
|
+
if (!Array.isArray(conditions)) return {
|
|
21047
|
+
patterns,
|
|
21048
|
+
unsupported
|
|
21049
|
+
};
|
|
21050
|
+
for (const cond of conditions) {
|
|
21051
|
+
if (!cond || typeof cond !== "object") continue;
|
|
21052
|
+
const c = cond;
|
|
21053
|
+
const field = typeof c["Field"] === "string" ? c["Field"] : "(unknown)";
|
|
21054
|
+
if (field !== "path-pattern") {
|
|
21055
|
+
unsupported.push(field);
|
|
21056
|
+
continue;
|
|
21057
|
+
}
|
|
21058
|
+
const cfg = c["PathPatternConfig"];
|
|
21059
|
+
const values = cfg && typeof cfg === "object" && Array.isArray(cfg["Values"]) ? cfg["Values"] : Array.isArray(c["Values"]) ? c["Values"] : [];
|
|
21060
|
+
for (const v of values) if (typeof v === "string") patterns.push(v);
|
|
21061
|
+
}
|
|
21062
|
+
return {
|
|
21063
|
+
patterns,
|
|
21064
|
+
unsupported
|
|
21065
|
+
};
|
|
21066
|
+
}
|
|
21067
|
+
function collectForwardTargetGroupRefs(actions) {
|
|
20849
21068
|
const refs = /* @__PURE__ */ new Set();
|
|
20850
|
-
if (!Array.isArray(
|
|
20851
|
-
for (const action of
|
|
21069
|
+
if (!Array.isArray(actions)) return refs;
|
|
21070
|
+
for (const action of actions) {
|
|
20852
21071
|
if (!action || typeof action !== "object") continue;
|
|
20853
21072
|
const a = action;
|
|
20854
21073
|
if (a["Type"] !== "forward") continue;
|
|
@@ -20867,14 +21086,14 @@ function collectForwardTargetGroupRefs(defaultActions) {
|
|
|
20867
21086
|
return refs;
|
|
20868
21087
|
}
|
|
20869
21088
|
/**
|
|
20870
|
-
* True when `
|
|
20871
|
-
*
|
|
20872
|
-
*
|
|
20873
|
-
*
|
|
21089
|
+
* True when `actions` has at least one `forward` action that references a target
|
|
21090
|
+
* group via a NON-`Ref` arn (literal / `Fn::GetAtt` / cross-stack) — i.e. a
|
|
21091
|
+
* forward we could not resolve to an in-stack target group. Used to warn rather
|
|
21092
|
+
* than silently skip such a listener / rule.
|
|
20874
21093
|
*/
|
|
20875
|
-
function hasUnresolvableForward(
|
|
20876
|
-
if (!Array.isArray(
|
|
20877
|
-
for (const action of
|
|
21094
|
+
function hasUnresolvableForward(actions) {
|
|
21095
|
+
if (!Array.isArray(actions)) return false;
|
|
21096
|
+
for (const action of actions) {
|
|
20878
21097
|
if (!action || typeof action !== "object") continue;
|
|
20879
21098
|
const a = action;
|
|
20880
21099
|
if (a["Type"] !== "forward") continue;
|
|
@@ -20907,6 +21126,16 @@ function parsePort(raw) {
|
|
|
20907
21126
|
function parseContainerPort(raw) {
|
|
20908
21127
|
return parsePort(raw);
|
|
20909
21128
|
}
|
|
21129
|
+
/**
|
|
21130
|
+
* Parse a ListenerRule `Priority` (ALB priorities are 1-50000, lower = higher
|
|
21131
|
+
* precedence). A missing / unparseable priority sorts last so an explicitly
|
|
21132
|
+
* prioritized rule always wins over it.
|
|
21133
|
+
*/
|
|
21134
|
+
function parsePriority(raw) {
|
|
21135
|
+
if (typeof raw === "number" && Number.isFinite(raw)) return raw;
|
|
21136
|
+
if (typeof raw === "string" && /^\d+$/.test(raw)) return parseInt(raw, 10);
|
|
21137
|
+
return Number.MAX_SAFE_INTEGER;
|
|
21138
|
+
}
|
|
20910
21139
|
|
|
20911
21140
|
//#endregion
|
|
20912
21141
|
//#region src/cli/commands/local-start-alb.ts
|
|
@@ -20988,31 +21217,52 @@ function albStrategy(options) {
|
|
|
20988
21217
|
pickerNoun: "Application Load Balancers",
|
|
20989
21218
|
onMissing: () => new LocalStartServiceError(`${getEmbedConfig().cliName} start-alb requires at least one <target>. Pass one or more ALB paths like 'Stack/MyAlb', or run it in a TTY to pick interactively.`),
|
|
20990
21219
|
resolveBoots: (stacks, chosenTargets) => {
|
|
20991
|
-
const byServiceTarget = /* @__PURE__ */ new Map();
|
|
20992
21220
|
const warnings = [];
|
|
21221
|
+
const serviceTargets = /* @__PURE__ */ new Set();
|
|
21222
|
+
const listeners = [];
|
|
21223
|
+
const claimedHostPorts = /* @__PURE__ */ new Map();
|
|
20993
21224
|
for (const albTarget of chosenTargets) {
|
|
20994
21225
|
const { stack, albLogicalId } = resolveAlbTarget(albTarget, stacks);
|
|
20995
21226
|
const resolution = resolveAlbFrontDoor(stack, albLogicalId);
|
|
20996
21227
|
warnings.push(...resolution.warnings);
|
|
20997
|
-
|
|
20998
|
-
const
|
|
20999
|
-
|
|
21000
|
-
|
|
21001
|
-
|
|
21002
|
-
|
|
21003
|
-
|
|
21228
|
+
const qualify = (t) => {
|
|
21229
|
+
const serviceTarget = `${stack.stackName}:${t.serviceLogicalId}`;
|
|
21230
|
+
serviceTargets.add(serviceTarget);
|
|
21231
|
+
return {
|
|
21232
|
+
serviceTarget,
|
|
21233
|
+
targetContainerName: t.targetContainerName,
|
|
21234
|
+
targetContainerPort: t.targetContainerPort
|
|
21235
|
+
};
|
|
21236
|
+
};
|
|
21237
|
+
for (const listener of resolution.listeners) {
|
|
21238
|
+
const hostPort = lbPortOverrides[listener.listenerPort] ?? listener.listenerPort;
|
|
21239
|
+
const claimedBy = claimedHostPorts.get(hostPort);
|
|
21240
|
+
if (claimedBy !== void 0) {
|
|
21241
|
+
warnings.push(`Listener port ${listener.listenerPort} would bind host port ${hostPort}, already claimed by listener port ${claimedBy}; the local front-door fronts only the first. Use --lb-port to remap one of them.`);
|
|
21242
|
+
continue;
|
|
21243
|
+
}
|
|
21244
|
+
claimedHostPorts.set(hostPort, listener.listenerPort);
|
|
21245
|
+
listeners.push({
|
|
21246
|
+
listenerPort: listener.listenerPort,
|
|
21247
|
+
hostPort,
|
|
21248
|
+
...listener.defaultTarget ? { defaultTarget: qualify(listener.defaultTarget) } : {},
|
|
21249
|
+
rules: listener.rules.map((r) => ({
|
|
21250
|
+
priority: r.priority,
|
|
21251
|
+
pathPatterns: r.pathPatterns,
|
|
21252
|
+
target: qualify(r.target)
|
|
21253
|
+
}))
|
|
21004
21254
|
});
|
|
21005
21255
|
}
|
|
21006
21256
|
}
|
|
21007
|
-
const boots = [...
|
|
21008
|
-
const resolvedPorts =
|
|
21009
|
-
for (const b of boots) for (const t of b.frontDoorTargets) resolvedPorts.add(t.listenerPort);
|
|
21257
|
+
const boots = [...serviceTargets].map((target) => ({ target }));
|
|
21258
|
+
const resolvedPorts = new Set(listeners.map((l) => l.listenerPort));
|
|
21010
21259
|
for (const portStr of Object.keys(lbPortOverrides)) {
|
|
21011
21260
|
const port = Number(portStr);
|
|
21012
21261
|
if (!resolvedPorts.has(port)) warnings.push(`--lb-port override for listener port ${port} matched no ALB listener resolved for the named target(s); it was ignored.`);
|
|
21013
21262
|
}
|
|
21014
21263
|
return {
|
|
21015
21264
|
boots,
|
|
21265
|
+
...listeners.length > 0 ? { frontDoor: { listeners } } : {},
|
|
21016
21266
|
warnings
|
|
21017
21267
|
};
|
|
21018
21268
|
},
|
|
@@ -21028,7 +21278,7 @@ function albStrategy(options) {
|
|
|
21028
21278
|
*/
|
|
21029
21279
|
function createLocalStartAlbCommand(opts = {}) {
|
|
21030
21280
|
setEmbedConfig(opts.embedConfig);
|
|
21031
|
-
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
|
|
21281
|
+
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 path-routes its path-pattern 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, single-target forward actions, and path-pattern rules; HTTPS listeners, Lambda target groups, weighted forwards, other rule conditions, and redirect/fixed-response 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) => {
|
|
21032
21282
|
await runEcsServiceEmulator(targets, options, albStrategy(options), opts.extraStateProviders);
|
|
21033
21283
|
})));
|
|
21034
21284
|
}
|
|
@@ -21105,4 +21355,4 @@ function createLocalListCommand(opts = {}) {
|
|
|
21105
21355
|
|
|
21106
21356
|
//#endregion
|
|
21107
21357
|
export { createJwksCache as $, invokeTokenAuthorizer as A, resolveCfnRegion as At, VtlEvaluationError as B, parseSelectionExpressionPath as Bt, resolveSelectionExpression as C, resolveEnvVars as Ct, computeRequestIdentityHash as D, isCfnFlagPresent as Dt, buildMethodArn as E, createLocalStateProvider as Et, buildRestV1Event as F, resolveWatchConfig as Ft, buildMgmtEndpointEnvUrl as G, AGENTCORE_RUNTIME_TYPE as Gt, probeHostGatewaySupport as H, pickRefLogicalId as Ht, evaluateResponseParameters as I, countTargets as It, buildConnectEvent as J, derivePseudoParametersFromRegion as Jt, handleConnectionsRequest as K, AgentCoreResolutionError as Kt, pickResponseTemplate as L, listTargets as Lt, translateLambdaResponse as M, CfnLocalStateProvider as Mt, applyAuthorizerOverlay as N, collectSsmParameterRefs as Nt, evaluateCachedLambdaPolicy as O, rejectExplicitCfnStackWithMultipleStacks as Ot, buildHttpApiV2Event as P, resolveSsmParameters as Pt, buildJwksUrlFromIssuer as Q, selectIntegrationResponse as R, discoverWebSocketApis as Rt, startApiServer as S, substituteEnvVarsFromStateAsync as St, defaultCredentialsLoader as T, LocalStateSourceError as Tt, bufferToBody as U, resolveLambdaArnIntrinsic as Ut, HOST_GATEWAY_MIN_VERSION as V, discoverRoutes as Vt, ConnectionRegistry as W, AGENTCORE_HTTP_PROTOCOL as Wt, buildMessageEvent as X, tryResolveImageFnJoin as Xt, buildDisconnectEvent as Y, substituteImagePlaceholders as Yt, buildCognitoJwksUrl as Z, LocalInvokeBuildError as Zt, availableApiIdentifiers as _, resolveRuntimeImage as _t, buildCloudMapIndex as a, buildCorsConfigByApiId as at, groupRoutesByServer as b, substituteAgainstStateAsync as bt, createLocalRunTaskCommand as c, matchPreflight as ct, createWatchPredicates as d, waitForAgentCorePing as dt, verifyCognitoJwt as et, resolveApiTargetSubset as f, createLocalInvokeCommand as ft, buildStageMap as g, resolveRuntimeFileExtension as gt, attachStageContext as h, resolveRuntimeCodeMountPath as ht, createLocalStartServiceCommand as i, applyCorsResponseHeaders as it, matchRoute as j, resolveCfnStackName as jt, invokeRequestAuthorizer as k, resolveCfnFallbackRegion as kt, createLocalInvokeAgentCoreCommand as l, AGENTCORE_SESSION_ID_HEADER as lt, createFileWatcher as m, buildContainerImage as mt, formatTargetListing as n, verifyJwtViaDiscovery as nt, CloudMapRegistry as o, buildCorsConfigFromCloudFrontChain as ot, createAuthorizerCache as p, architectureToPlatform as pt, parseConnectionsPath as q, resolveAgentCoreTarget as qt, createLocalStartAlbCommand as r, attachAuthorizers as rt, getContainerNetworkIp as s, isFunctionUrlOacFronted as st, createLocalListCommand as t, verifyJwtAuthorizer as tt, createLocalStartApiCommand as u, invokeAgentCore as ut, filterRoutesByApiIdentifier as v, EcsTaskResolutionError as vt, resolveServiceIntegrationParameters as w, materializeLayerFromArn as wt, readMtlsMaterialsFromDisk as x, substituteEnvVarsFromState as xt, filterRoutesByApiIdentifiers as y, substituteAgainstState as yt, tryParseStatus as z, discoverWebSocketApisOrThrow as zt };
|
|
21108
|
-
//# sourceMappingURL=local-list-
|
|
21358
|
+
//# sourceMappingURL=local-list-DDsa8FJV.js.map
|