cdk-local 0.39.0 → 0.41.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 +4 -3
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +53 -54
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/{local-list-iRB83Vtx.js → local-list-DDsa8FJV.js} +354 -146
- package/dist/local-list-DDsa8FJV.js.map +1 -0
- package/package.json +1 -1
- package/dist/local-list-iRB83Vtx.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. */
|
|
@@ -20230,9 +20230,15 @@ async function startFrontDoorServer(opts) {
|
|
|
20230
20230
|
}
|
|
20231
20231
|
function handleProxyRequest(req, res, opts) {
|
|
20232
20232
|
return new Promise((resolve) => {
|
|
20233
|
-
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();
|
|
20234
20240
|
if (!endpoint) {
|
|
20235
|
-
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.`);
|
|
20236
20242
|
resolve();
|
|
20237
20243
|
return;
|
|
20238
20244
|
}
|
|
@@ -20243,6 +20249,7 @@ function handleProxyRequest(req, res, opts) {
|
|
|
20243
20249
|
resolve();
|
|
20244
20250
|
};
|
|
20245
20251
|
const headers = { ...req.headers };
|
|
20252
|
+
stripHopByHopHeaders(headers);
|
|
20246
20253
|
appendForwardedHeaders(headers, req, opts.listenerPort);
|
|
20247
20254
|
const proxyReq = request({
|
|
20248
20255
|
host: endpoint.host,
|
|
@@ -20251,7 +20258,9 @@ function handleProxyRequest(req, res, opts) {
|
|
|
20251
20258
|
path: req.url,
|
|
20252
20259
|
headers
|
|
20253
20260
|
}, (proxyRes) => {
|
|
20254
|
-
|
|
20261
|
+
const resHeaders = { ...proxyRes.headers };
|
|
20262
|
+
stripHopByHopHeaders(resHeaders);
|
|
20263
|
+
res.writeHead(proxyRes.statusCode ?? 502, resHeaders);
|
|
20255
20264
|
proxyRes.pipe(res);
|
|
20256
20265
|
proxyRes.on("end", done);
|
|
20257
20266
|
proxyRes.on("error", () => {
|
|
@@ -20260,13 +20269,13 @@ function handleProxyRequest(req, res, opts) {
|
|
|
20260
20269
|
});
|
|
20261
20270
|
});
|
|
20262
20271
|
proxyReq.setTimeout(opts.upstreamTimeoutMs ?? 3e4, () => {
|
|
20263
|
-
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.`);
|
|
20264
20273
|
else if (!res.writableEnded) res.destroy();
|
|
20265
20274
|
proxyReq.destroy();
|
|
20266
20275
|
done();
|
|
20267
20276
|
});
|
|
20268
20277
|
proxyReq.on("error", () => {
|
|
20269
|
-
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}.`);
|
|
20270
20279
|
else if (!res.writableEnded) res.destroy();
|
|
20271
20280
|
done();
|
|
20272
20281
|
});
|
|
@@ -20276,6 +20285,33 @@ function handleProxyRequest(req, res, opts) {
|
|
|
20276
20285
|
req.pipe(proxyReq);
|
|
20277
20286
|
});
|
|
20278
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
|
+
}
|
|
20279
20315
|
/**
|
|
20280
20316
|
* Inject the ALB-style forwarding headers a downstream app may read. Appends
|
|
20281
20317
|
* the client IP to any existing `X-Forwarded-For` chain (ALB appends rather
|
|
@@ -20294,6 +20330,49 @@ function writeError(res, statusCode, message) {
|
|
|
20294
20330
|
res.end(`${message}\n`);
|
|
20295
20331
|
}
|
|
20296
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
|
+
|
|
20297
20376
|
//#endregion
|
|
20298
20377
|
//#region src/cli/commands/ecs-service-emulator.ts
|
|
20299
20378
|
/**
|
|
@@ -20312,6 +20391,8 @@ async function runEcsServiceEmulator(targets, options, strategy, extraStateProvi
|
|
|
20312
20391
|
let sigintCount = 0;
|
|
20313
20392
|
let sharedNetwork;
|
|
20314
20393
|
let profileCredsFile;
|
|
20394
|
+
let frontDoorServers = [];
|
|
20395
|
+
let frontDoorByService = /* @__PURE__ */ new Map();
|
|
20315
20396
|
const cleanup = singleFlight(async () => {
|
|
20316
20397
|
await Promise.allSettled(perTarget.map(async (pt) => {
|
|
20317
20398
|
if (pt.controller) await pt.controller.shutdown();
|
|
@@ -20319,8 +20400,9 @@ async function runEcsServiceEmulator(targets, options, strategy, extraStateProvi
|
|
|
20319
20400
|
await Promise.allSettled(pt.runState.replicas.map((r) => r.inFlightBoot).filter((p) => p !== void 0));
|
|
20320
20401
|
await Promise.allSettled(pt.runState.replicas.map((r) => cleanupEcsRun(r.state, { keepRunning: false }).catch(() => void 0)));
|
|
20321
20402
|
}
|
|
20322
|
-
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)}`))));
|
|
20323
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 = [];
|
|
20324
20406
|
if (profileCredsFile) {
|
|
20325
20407
|
try {
|
|
20326
20408
|
await profileCredsFile.dispose();
|
|
@@ -20363,7 +20445,7 @@ async function runEcsServiceEmulator(targets, options, strategy, extraStateProvi
|
|
|
20363
20445
|
noun: strategy.pickerNoun,
|
|
20364
20446
|
onMissing: () => strategy.onMissing()
|
|
20365
20447
|
});
|
|
20366
|
-
const { boots, warnings } = strategy.resolveBoots(stacks, resolvedTargets);
|
|
20448
|
+
const { boots, frontDoor, warnings } = strategy.resolveBoots(stacks, resolvedTargets);
|
|
20367
20449
|
for (const w of warnings) logger.warn(w);
|
|
20368
20450
|
if (boots.length === 0) throw new LocalStartServiceError(`No runnable ECS service resolved from ${resolvedTargets.join(", ")}.`);
|
|
20369
20451
|
rejectExplicitCfnStackWithMultipleStacks(options, boots.length);
|
|
@@ -20395,6 +20477,11 @@ async function runEcsServiceEmulator(targets, options, strategy, extraStateProvi
|
|
|
20395
20477
|
cloudMapIndexByStack,
|
|
20396
20478
|
sharedNetwork
|
|
20397
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
|
+
}
|
|
20398
20485
|
sigintHandler = () => {
|
|
20399
20486
|
sigintCount += 1;
|
|
20400
20487
|
if (sigintCount >= 2) {
|
|
@@ -20406,11 +20493,7 @@ async function runEcsServiceEmulator(targets, options, strategy, extraStateProvi
|
|
|
20406
20493
|
};
|
|
20407
20494
|
process.on("SIGINT", sigintHandler);
|
|
20408
20495
|
process.on("SIGTERM", sigintHandler);
|
|
20409
|
-
for (const pt of perTarget)
|
|
20410
|
-
const booted = await bootOneTarget(pt.boot, pt.runState, stacks, options, discovery, skipPull, extraStateProviders, profileCredsFile, strategy.lbPortOverrides);
|
|
20411
|
-
pt.controller = booted.controller;
|
|
20412
|
-
pt.frontDoorServers = booted.frontDoorServers;
|
|
20413
|
-
}
|
|
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));
|
|
20414
20497
|
const summary = perTarget.map((pt) => `${pt.controller.service.serviceName} (${pt.controller.activeReplicaCount()} replica(s))`).join(", ");
|
|
20415
20498
|
logger.info(`Service(s) running: ${summary}.`);
|
|
20416
20499
|
logger.info("Press ^C to shut down.");
|
|
@@ -20423,16 +20506,16 @@ async function runEcsServiceEmulator(targets, options, strategy, extraStateProvi
|
|
|
20423
20506
|
await cleanup();
|
|
20424
20507
|
}
|
|
20425
20508
|
}
|
|
20426
|
-
async function bootOneTarget(boot, runState, stacks, options, discovery, skipPull, extraStateProviders, profileCredsFile,
|
|
20509
|
+
async function bootOneTarget(boot, runState, stacks, options, discovery, skipPull, extraStateProviders, profileCredsFile, frontDoorPools) {
|
|
20427
20510
|
const candidate = pickCandidateStack(parseEcsTarget(boot.target).stackPattern, stacks);
|
|
20428
20511
|
const stateProvider = createLocalStateProvider(options, candidate?.stackName ?? "", await resolveCfnFallbackRegion(options, candidate?.region), extraStateProviders);
|
|
20429
20512
|
try {
|
|
20430
|
-
return await runOneTarget(boot, runState, stacks, options, discovery, skipPull, stateProvider, profileCredsFile,
|
|
20513
|
+
return await runOneTarget(boot, runState, stacks, options, discovery, skipPull, stateProvider, profileCredsFile, frontDoorPools);
|
|
20431
20514
|
} finally {
|
|
20432
20515
|
if (stateProvider) stateProvider.dispose();
|
|
20433
20516
|
}
|
|
20434
20517
|
}
|
|
20435
|
-
async function runOneTarget(boot, runState, stacks, options, discovery, skipPull, stateProvider, profileCredsFile,
|
|
20518
|
+
async function runOneTarget(boot, runState, stacks, options, discovery, skipPull, stateProvider, profileCredsFile, frontDoorPools) {
|
|
20436
20519
|
const logger = getLogger();
|
|
20437
20520
|
const target = boot.target;
|
|
20438
20521
|
const imageContext = await buildEcsImageResolutionContext(target, stacks, options, stateProvider);
|
|
@@ -20488,73 +20571,89 @@ async function runOneTarget(boot, runState, stacks, options, discovery, skipPull
|
|
|
20488
20571
|
containerPath: profileCredsFile.containerPath,
|
|
20489
20572
|
profileName: profileCredsFile.profileName
|
|
20490
20573
|
};
|
|
20491
|
-
|
|
20492
|
-
const runnerOpts = {
|
|
20574
|
+
return startEcsService(service, {
|
|
20493
20575
|
maxTasks: options.maxTasks,
|
|
20494
20576
|
restartPolicy: options.restartPolicy,
|
|
20495
20577
|
taskOptions: taskOpts,
|
|
20496
20578
|
discovery,
|
|
20497
|
-
...
|
|
20498
|
-
};
|
|
20499
|
-
let controller;
|
|
20500
|
-
try {
|
|
20501
|
-
controller = await startEcsService(service, runnerOpts, runState);
|
|
20502
|
-
} catch (err) {
|
|
20503
|
-
await Promise.allSettled(frontDoorServers.map((s) => s.close()));
|
|
20504
|
-
throw err;
|
|
20505
|
-
}
|
|
20506
|
-
return {
|
|
20507
|
-
controller,
|
|
20508
|
-
frontDoorServers
|
|
20509
|
-
};
|
|
20579
|
+
...frontDoorPools && frontDoorPools.length > 0 ? { frontDoor: { pools: frontDoorPools } } : {}
|
|
20580
|
+
}, runState);
|
|
20510
20581
|
}
|
|
20511
20582
|
/**
|
|
20512
|
-
*
|
|
20513
|
-
*
|
|
20514
|
-
*
|
|
20515
|
-
*
|
|
20516
|
-
*
|
|
20517
|
-
*
|
|
20518
|
-
*
|
|
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.
|
|
20519
20593
|
*
|
|
20520
20594
|
* On a bind failure (e.g. EACCES on a privileged listener port, or the port is
|
|
20521
20595
|
* already in use) every server started so far is closed and the error is
|
|
20522
20596
|
* re-thrown with a `--lb-port` hint.
|
|
20523
20597
|
*/
|
|
20524
|
-
async function
|
|
20525
|
-
|
|
20526
|
-
|
|
20527
|
-
|
|
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;
|
|
20528
20612
|
};
|
|
20529
|
-
const frontDoorServers = [];
|
|
20530
|
-
const pools = [];
|
|
20531
20613
|
try {
|
|
20532
|
-
for (const
|
|
20533
|
-
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;
|
|
20534
20622
|
const server = await startFrontDoorServer({
|
|
20535
|
-
|
|
20536
|
-
port:
|
|
20623
|
+
selectPool,
|
|
20624
|
+
port: listener.hostPort,
|
|
20537
20625
|
host: containerHost,
|
|
20538
|
-
listenerPort:
|
|
20539
|
-
|
|
20540
|
-
});
|
|
20541
|
-
frontDoorServers.push(server);
|
|
20542
|
-
pools.push({
|
|
20543
|
-
pool,
|
|
20544
|
-
targetContainerName: t.targetContainerName,
|
|
20545
|
-
targetContainerPort: t.targetContainerPort
|
|
20626
|
+
listenerPort: listener.listenerPort,
|
|
20627
|
+
label: `listener port ${listener.listenerPort}`
|
|
20546
20628
|
});
|
|
20547
|
-
|
|
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)");
|
|
20548
20634
|
}
|
|
20549
20635
|
} catch (err) {
|
|
20550
|
-
await Promise.allSettled(
|
|
20551
|
-
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);
|
|
20552
20648
|
}
|
|
20553
20649
|
return {
|
|
20554
|
-
|
|
20555
|
-
|
|
20650
|
+
servers,
|
|
20651
|
+
frontDoorByService
|
|
20556
20652
|
};
|
|
20557
20653
|
}
|
|
20654
|
+
function describeTarget(t) {
|
|
20655
|
+
return `${t.serviceTarget} (container ${t.targetContainerName}:${t.targetContainerPort})`;
|
|
20656
|
+
}
|
|
20558
20657
|
async function resolvePlaceholderAccount(arn, region) {
|
|
20559
20658
|
if (!arn.includes("${AWS::AccountId}")) return arn;
|
|
20560
20659
|
const { STSClient, GetCallerIdentityCommand } = await import("@aws-sdk/client-sts");
|
|
@@ -20729,10 +20828,7 @@ function serviceStrategy() {
|
|
|
20729
20828
|
pickerNoun: "ECS services",
|
|
20730
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.`),
|
|
20731
20830
|
resolveBoots: (_stacks, chosenTargets) => ({
|
|
20732
|
-
boots: chosenTargets.map((target) => ({
|
|
20733
|
-
target,
|
|
20734
|
-
frontDoorTargets: []
|
|
20735
|
-
})),
|
|
20831
|
+
boots: chosenTargets.map((target) => ({ target })),
|
|
20736
20832
|
warnings: []
|
|
20737
20833
|
}),
|
|
20738
20834
|
lbPortOverrides: {}
|
|
@@ -20755,47 +20851,57 @@ function createLocalStartServiceCommand(opts = {}) {
|
|
|
20755
20851
|
//#endregion
|
|
20756
20852
|
//#region src/local/elb-front-door-resolver.ts
|
|
20757
20853
|
/**
|
|
20758
|
-
*
|
|
20759
|
-
*
|
|
20760
|
-
*
|
|
20761
|
-
*
|
|
20762
|
-
*
|
|
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).
|
|
20763
20859
|
*
|
|
20764
20860
|
* The synthesized linkage (confirmed against real `cdk synth` of
|
|
20765
|
-
* `ApplicationLoadBalancedFargateService`):
|
|
20861
|
+
* `ApplicationLoadBalancedFargateService` + an `addAction` path rule):
|
|
20766
20862
|
*
|
|
20767
20863
|
* ```
|
|
20768
20864
|
* ElasticLoadBalancingV2::LoadBalancer (the ALB you name)
|
|
20769
20865
|
* ElasticLoadBalancingV2::Listener : { LoadBalancerArn:{Ref:<ALB>}, Port, Protocol,
|
|
20770
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>} }] }
|
|
20771
20870
|
* ElasticLoadBalancingV2::TargetGroup : { Port, Protocol, TargetType:"ip" }
|
|
20772
20871
|
* ECS::Service.LoadBalancers[] -> { ContainerName, ContainerPort, TargetGroupArn:{Ref:<TG>} }
|
|
20773
20872
|
* ```
|
|
20774
20873
|
*
|
|
20775
|
-
* Resolution walks ALB -> listeners (by `LoadBalancerArn` Ref) -> default
|
|
20776
|
-
* `forward`
|
|
20777
|
-
*
|
|
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.
|
|
20778
20880
|
*
|
|
20779
|
-
*
|
|
20780
|
-
*
|
|
20781
|
-
*
|
|
20782
|
-
*
|
|
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.
|
|
20783
20887
|
*/
|
|
20784
20888
|
const ALB_TYPE = "AWS::ElasticLoadBalancingV2::LoadBalancer";
|
|
20785
20889
|
const LISTENER_TYPE = "AWS::ElasticLoadBalancingV2::Listener";
|
|
20890
|
+
const LISTENER_RULE_TYPE = "AWS::ElasticLoadBalancingV2::ListenerRule";
|
|
20786
20891
|
const TARGET_GROUP_TYPE = "AWS::ElasticLoadBalancingV2::TargetGroup";
|
|
20787
20892
|
const SERVICE_TYPE = "AWS::ECS::Service";
|
|
20788
20893
|
/**
|
|
20789
|
-
* Resolve an ALB into its
|
|
20790
|
-
* 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
|
|
20791
20896
|
* warnings) when the ALB fronts nothing cdk-local can serve locally.
|
|
20792
20897
|
*/
|
|
20793
20898
|
function resolveAlbFrontDoor(stack, albLogicalId) {
|
|
20794
20899
|
const warnings = [];
|
|
20795
20900
|
const resources = stack.template.Resources ?? {};
|
|
20901
|
+
const stackName = stack.stackName;
|
|
20796
20902
|
const tgToService = indexTargetGroupToService(resources);
|
|
20797
|
-
const
|
|
20798
|
-
const
|
|
20903
|
+
const rulesByListener = indexRulesByListener(resources);
|
|
20904
|
+
const listeners = [];
|
|
20799
20905
|
for (const [listenerLogicalId, resource] of Object.entries(resources)) {
|
|
20800
20906
|
if (resource.Type !== LISTENER_TYPE) continue;
|
|
20801
20907
|
const props = resource.Properties ?? {};
|
|
@@ -20803,54 +20909,40 @@ function resolveAlbFrontDoor(stack, albLogicalId) {
|
|
|
20803
20909
|
const port = parsePort(props["Port"]);
|
|
20804
20910
|
if (port === void 0) continue;
|
|
20805
20911
|
const protocol = typeof props["Protocol"] === "string" ? props["Protocol"] : "HTTP";
|
|
20806
|
-
const tgRefs = collectForwardTargetGroupRefs(props["DefaultActions"]);
|
|
20807
|
-
if (tgRefs.size === 0) {
|
|
20808
|
-
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.`);
|
|
20809
|
-
continue;
|
|
20810
|
-
}
|
|
20811
20912
|
if (protocol !== "HTTP") {
|
|
20812
|
-
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.`);
|
|
20813
20914
|
continue;
|
|
20814
20915
|
}
|
|
20815
|
-
|
|
20816
|
-
|
|
20817
|
-
|
|
20818
|
-
|
|
20819
|
-
|
|
20820
|
-
}
|
|
20821
|
-
if (
|
|
20822
|
-
warnings.push(
|
|
20823
|
-
continue;
|
|
20824
|
-
}
|
|
20825
|
-
const backing = tgToService.get(tgRef);
|
|
20826
|
-
if (!backing) {
|
|
20827
|
-
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.`);
|
|
20828
20924
|
continue;
|
|
20829
20925
|
}
|
|
20830
|
-
|
|
20831
|
-
|
|
20832
|
-
|
|
20833
|
-
|
|
20834
|
-
|
|
20835
|
-
|
|
20836
|
-
|
|
20837
|
-
const targets = byService.get(backing.serviceLogicalId) ?? [];
|
|
20838
|
-
targets.push({
|
|
20839
|
-
listenerPort: port,
|
|
20840
|
-
listenerProtocol: "HTTP",
|
|
20841
|
-
targetContainerName: backing.containerName,
|
|
20842
|
-
targetContainerPort: backing.containerPort,
|
|
20843
|
-
targetGroupLogicalId: tgRef,
|
|
20844
|
-
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
|
|
20845
20933
|
});
|
|
20846
|
-
byService.set(backing.serviceLogicalId, targets);
|
|
20847
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
|
+
});
|
|
20848
20943
|
}
|
|
20849
20944
|
return {
|
|
20850
|
-
|
|
20851
|
-
serviceLogicalId,
|
|
20852
|
-
targets
|
|
20853
|
-
})),
|
|
20945
|
+
listeners,
|
|
20854
20946
|
warnings
|
|
20855
20947
|
};
|
|
20856
20948
|
}
|
|
@@ -20861,6 +20953,43 @@ function isApplicationLoadBalancer(resource) {
|
|
|
20861
20953
|
return type === void 0 || type === "application";
|
|
20862
20954
|
}
|
|
20863
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
|
+
/**
|
|
20864
20993
|
* Build a `targetGroupLogicalId -> backing ECS service` index by scanning every
|
|
20865
20994
|
* `AWS::ECS::Service.LoadBalancers[]`. First service wins on a shared target
|
|
20866
20995
|
* group (unusual; would only happen with a hand-rolled template).
|
|
@@ -20887,10 +21016,58 @@ function indexTargetGroupToService(resources) {
|
|
|
20887
21016
|
}
|
|
20888
21017
|
return index;
|
|
20889
21018
|
}
|
|
20890
|
-
|
|
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) {
|
|
20891
21068
|
const refs = /* @__PURE__ */ new Set();
|
|
20892
|
-
if (!Array.isArray(
|
|
20893
|
-
for (const action of
|
|
21069
|
+
if (!Array.isArray(actions)) return refs;
|
|
21070
|
+
for (const action of actions) {
|
|
20894
21071
|
if (!action || typeof action !== "object") continue;
|
|
20895
21072
|
const a = action;
|
|
20896
21073
|
if (a["Type"] !== "forward") continue;
|
|
@@ -20909,14 +21086,14 @@ function collectForwardTargetGroupRefs(defaultActions) {
|
|
|
20909
21086
|
return refs;
|
|
20910
21087
|
}
|
|
20911
21088
|
/**
|
|
20912
|
-
* True when `
|
|
20913
|
-
*
|
|
20914
|
-
*
|
|
20915
|
-
*
|
|
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.
|
|
20916
21093
|
*/
|
|
20917
|
-
function hasUnresolvableForward(
|
|
20918
|
-
if (!Array.isArray(
|
|
20919
|
-
for (const action of
|
|
21094
|
+
function hasUnresolvableForward(actions) {
|
|
21095
|
+
if (!Array.isArray(actions)) return false;
|
|
21096
|
+
for (const action of actions) {
|
|
20920
21097
|
if (!action || typeof action !== "object") continue;
|
|
20921
21098
|
const a = action;
|
|
20922
21099
|
if (a["Type"] !== "forward") continue;
|
|
@@ -20949,6 +21126,16 @@ function parsePort(raw) {
|
|
|
20949
21126
|
function parseContainerPort(raw) {
|
|
20950
21127
|
return parsePort(raw);
|
|
20951
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
|
+
}
|
|
20952
21139
|
|
|
20953
21140
|
//#endregion
|
|
20954
21141
|
//#region src/cli/commands/local-start-alb.ts
|
|
@@ -21030,31 +21217,52 @@ function albStrategy(options) {
|
|
|
21030
21217
|
pickerNoun: "Application Load Balancers",
|
|
21031
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.`),
|
|
21032
21219
|
resolveBoots: (stacks, chosenTargets) => {
|
|
21033
|
-
const byServiceTarget = /* @__PURE__ */ new Map();
|
|
21034
21220
|
const warnings = [];
|
|
21221
|
+
const serviceTargets = /* @__PURE__ */ new Set();
|
|
21222
|
+
const listeners = [];
|
|
21223
|
+
const claimedHostPorts = /* @__PURE__ */ new Map();
|
|
21035
21224
|
for (const albTarget of chosenTargets) {
|
|
21036
21225
|
const { stack, albLogicalId } = resolveAlbTarget(albTarget, stacks);
|
|
21037
21226
|
const resolution = resolveAlbFrontDoor(stack, albLogicalId);
|
|
21038
21227
|
warnings.push(...resolution.warnings);
|
|
21039
|
-
|
|
21040
|
-
const
|
|
21041
|
-
|
|
21042
|
-
|
|
21043
|
-
|
|
21044
|
-
|
|
21045
|
-
|
|
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
|
+
}))
|
|
21046
21254
|
});
|
|
21047
21255
|
}
|
|
21048
21256
|
}
|
|
21049
|
-
const boots = [...
|
|
21050
|
-
const resolvedPorts =
|
|
21051
|
-
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));
|
|
21052
21259
|
for (const portStr of Object.keys(lbPortOverrides)) {
|
|
21053
21260
|
const port = Number(portStr);
|
|
21054
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.`);
|
|
21055
21262
|
}
|
|
21056
21263
|
return {
|
|
21057
21264
|
boots,
|
|
21265
|
+
...listeners.length > 0 ? { frontDoor: { listeners } } : {},
|
|
21058
21266
|
warnings
|
|
21059
21267
|
};
|
|
21060
21268
|
},
|
|
@@ -21070,7 +21278,7 @@ function albStrategy(options) {
|
|
|
21070
21278
|
*/
|
|
21071
21279
|
function createLocalStartAlbCommand(opts = {}) {
|
|
21072
21280
|
setEmbedConfig(opts.embedConfig);
|
|
21073
|
-
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) => {
|
|
21074
21282
|
await runEcsServiceEmulator(targets, options, albStrategy(options), opts.extraStateProviders);
|
|
21075
21283
|
})));
|
|
21076
21284
|
}
|
|
@@ -21147,4 +21355,4 @@ function createLocalListCommand(opts = {}) {
|
|
|
21147
21355
|
|
|
21148
21356
|
//#endregion
|
|
21149
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 };
|
|
21150
|
-
//# sourceMappingURL=local-list-
|
|
21358
|
+
//# sourceMappingURL=local-list-DDsa8FJV.js.map
|