cdk-local 0.36.1 → 0.37.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 +30 -1
- package/dist/cli.js +3 -2
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +78 -69
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/{local-list-D1MAmo5o.js → local-list-Dklrlnu1.js} +777 -97
- package/dist/local-list-Dklrlnu1.js.map +1 -0
- package/package.json +1 -1
- package/dist/local-list-D1MAmo5o.js.map +0 -1
|
@@ -24,7 +24,7 @@ import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
|
24
24
|
import { readFile } from "fs/promises";
|
|
25
25
|
import { join as join$1 } from "path";
|
|
26
26
|
import { WebSocketServer } from "ws";
|
|
27
|
-
import { createServer as createServer$1 } from "node:http";
|
|
27
|
+
import { createServer as createServer$1, request } from "node:http";
|
|
28
28
|
import { createServer as createServer$2 } from "node:https";
|
|
29
29
|
import * as chokidar from "chokidar";
|
|
30
30
|
import graphlib from "graphlib";
|
|
@@ -794,7 +794,7 @@ function parseTarget(target) {
|
|
|
794
794
|
function resolveLambdaTarget(target, stacks) {
|
|
795
795
|
if (stacks.length === 0) throw new LocalInvokeResolutionError("No stacks found in the synthesized assembly.");
|
|
796
796
|
const parsed = parseTarget(target);
|
|
797
|
-
const stack = pickStack$
|
|
797
|
+
const stack = pickStack$4(parsed, stacks);
|
|
798
798
|
const template = stack.template;
|
|
799
799
|
const resources = template.Resources ?? {};
|
|
800
800
|
let match;
|
|
@@ -828,7 +828,7 @@ function resolveLambdaTarget(target, stacks) {
|
|
|
828
828
|
* user may omit the stack prefix. Otherwise an explicit stack pattern is
|
|
829
829
|
* required.
|
|
830
830
|
*/
|
|
831
|
-
function pickStack$
|
|
831
|
+
function pickStack$4(parsed, stacks) {
|
|
832
832
|
if (parsed.stackPattern === null) {
|
|
833
833
|
if (stacks.length === 1) return stacks[0];
|
|
834
834
|
throw new LocalInvokeResolutionError(`Multiple stacks in app, target '${parsed.pathOrId}' is missing a stack prefix. Use 'StackName:${parsed.pathOrId}' or 'StackName/...' (path form). Available stacks: ${stacks.map((s) => s.stackName).join(", ")}.`);
|
|
@@ -1222,7 +1222,7 @@ var AgentCoreResolutionError = class AgentCoreResolutionError extends Error {
|
|
|
1222
1222
|
function resolveAgentCoreTarget(target, stacks, imageContext) {
|
|
1223
1223
|
if (stacks.length === 0) throw new AgentCoreResolutionError("No stacks found in the synthesized assembly.");
|
|
1224
1224
|
const parsed = parseTarget(target);
|
|
1225
|
-
const stack = pickStack$
|
|
1225
|
+
const stack = pickStack$3(parsed, stacks);
|
|
1226
1226
|
const resources = stack.template.Resources ?? {};
|
|
1227
1227
|
const { logicalId, resource } = matchRuntime(parsed, target, stack, resources);
|
|
1228
1228
|
if (resource.Type !== "AWS::BedrockAgentCore::Runtime") throw new AgentCoreResolutionError(`Resource '${logicalId}' in ${stack.stackName} is ${resource.Type}, not ${AGENTCORE_RUNTIME_TYPE}. ${getEmbedConfig().cliName} invoke-agentcore only runs Bedrock AgentCore Runtime resources.`);
|
|
@@ -1234,7 +1234,7 @@ function resolveAgentCoreTarget(target, stacks, imageContext) {
|
|
|
1234
1234
|
* Mirrors the Lambda / ECS resolvers' behavior via the shared
|
|
1235
1235
|
* stack-matcher.
|
|
1236
1236
|
*/
|
|
1237
|
-
function pickStack$
|
|
1237
|
+
function pickStack$3(parsed, stacks) {
|
|
1238
1238
|
if (parsed.stackPattern === null) {
|
|
1239
1239
|
if (stacks.length === 1) return stacks[0];
|
|
1240
1240
|
throw new AgentCoreResolutionError(`Multiple stacks in app, target '${parsed.pathOrId}' is missing a stack prefix. Use 'StackName:${parsed.pathOrId}' or 'StackName/...' (path form). Available stacks: ${stacks.map((s) => s.stackName).join(", ")}.`);
|
|
@@ -2974,12 +2974,30 @@ function listApiSurfaces(stacks) {
|
|
|
2974
2974
|
return [...byKey.values()];
|
|
2975
2975
|
}
|
|
2976
2976
|
/**
|
|
2977
|
-
*
|
|
2978
|
-
* `
|
|
2977
|
+
* Enumerate the application Load Balancers `cdkl start-alb` can front. Only
|
|
2978
|
+
* `Type: application` (the default) is included — NLBs / Gateway LBs are a
|
|
2979
|
+
* different architecture the local front-door does not emulate.
|
|
2980
|
+
*/
|
|
2981
|
+
function scanApplicationLoadBalancers(stacks) {
|
|
2982
|
+
const entries = [];
|
|
2983
|
+
for (const stack of stacks) {
|
|
2984
|
+
const resources = stack.template.Resources ?? {};
|
|
2985
|
+
for (const [logicalId, resource] of Object.entries(resources)) {
|
|
2986
|
+
if (resource.Type !== "AWS::ElasticLoadBalancingV2::LoadBalancer") continue;
|
|
2987
|
+
const type = resource.Properties?.["Type"];
|
|
2988
|
+
if (type !== void 0 && type !== "application") continue;
|
|
2989
|
+
entries.push(makeEntry(stack.stackName, logicalId, readCdkPathOrUndefined(resource)));
|
|
2990
|
+
}
|
|
2991
|
+
}
|
|
2992
|
+
return entries;
|
|
2993
|
+
}
|
|
2994
|
+
/**
|
|
2995
|
+
* Walk every synthesized stack and collect the resources the `cdkl` commands
|
|
2996
|
+
* can run locally, grouped by command.
|
|
2979
2997
|
*
|
|
2980
2998
|
* Pure over the synthesized {@link StackInfo}[] — no Docker / AWS /
|
|
2981
|
-
* filesystem access — so both `cdkl list` and
|
|
2982
|
-
*
|
|
2999
|
+
* filesystem access — so both `cdkl list` and the interactive target
|
|
3000
|
+
* pickers can share it.
|
|
2983
3001
|
*/
|
|
2984
3002
|
function listTargets(stacks) {
|
|
2985
3003
|
return {
|
|
@@ -2987,7 +3005,8 @@ function listTargets(stacks) {
|
|
|
2987
3005
|
apis: sortApiEntries(listApiSurfaces(stacks)),
|
|
2988
3006
|
ecsServices: sortEntries(scanByType(stacks, "AWS::ECS::Service")),
|
|
2989
3007
|
ecsTaskDefinitions: sortEntries(scanByType(stacks, "AWS::ECS::TaskDefinition")),
|
|
2990
|
-
agentCoreRuntimes: sortEntries(scanByType(stacks, AGENTCORE_RUNTIME_TYPE))
|
|
3008
|
+
agentCoreRuntimes: sortEntries(scanByType(stacks, AGENTCORE_RUNTIME_TYPE)),
|
|
3009
|
+
loadBalancers: sortEntries(scanApplicationLoadBalancers(stacks))
|
|
2991
3010
|
};
|
|
2992
3011
|
}
|
|
2993
3012
|
const pathOf = (e) => e.displayPath ?? e.qualifiedId;
|
|
@@ -3019,7 +3038,7 @@ function sortApiEntries(entries) {
|
|
|
3019
3038
|
}
|
|
3020
3039
|
/** Total number of targets across every category. */
|
|
3021
3040
|
function countTargets(listing) {
|
|
3022
|
-
return listing.lambdas.length + listing.apis.length + listing.ecsServices.length + listing.ecsTaskDefinitions.length + listing.agentCoreRuntimes.length;
|
|
3041
|
+
return listing.lambdas.length + listing.apis.length + listing.ecsServices.length + listing.ecsTaskDefinitions.length + listing.agentCoreRuntimes.length + listing.loadBalancers.length;
|
|
3023
3042
|
}
|
|
3024
3043
|
|
|
3025
3044
|
//#endregion
|
|
@@ -5333,7 +5352,7 @@ function parseEcsTarget(target) {
|
|
|
5333
5352
|
function resolveEcsTaskTarget(target, stacks, context) {
|
|
5334
5353
|
if (stacks.length === 0) throw new EcsTaskResolutionError("No stacks found in the synthesized assembly.");
|
|
5335
5354
|
const parsed = parseEcsTarget(target);
|
|
5336
|
-
const stack = pickStack$
|
|
5355
|
+
const stack = pickStack$2(parsed, stacks);
|
|
5337
5356
|
const resources = stack.template.Resources ?? {};
|
|
5338
5357
|
let logicalId;
|
|
5339
5358
|
let resource;
|
|
@@ -5354,7 +5373,7 @@ function resolveEcsTaskTarget(target, stacks, context) {
|
|
|
5354
5373
|
if (resource.Type !== "AWS::ECS::TaskDefinition") throw new EcsTaskResolutionError(`Resource '${logicalId}' in ${stack.stackName} is ${resource.Type}, not an AWS::ECS::TaskDefinition.`);
|
|
5355
5374
|
return extractTaskDefinitionProperties(stack, logicalId, resource, context);
|
|
5356
5375
|
}
|
|
5357
|
-
function pickStack$
|
|
5376
|
+
function pickStack$2(parsed, stacks) {
|
|
5358
5377
|
if (parsed.stackPattern === null) {
|
|
5359
5378
|
if (stacks.length === 1) return stacks[0];
|
|
5360
5379
|
throw new EcsTaskResolutionError(`Multiple stacks in app, target '${parsed.pathOrId}' is missing a stack prefix. Use 'StackName:${parsed.pathOrId}' or 'StackName/...' (path form). Available stacks: ${stacks.map((s) => s.stackName).join(", ")}.`);
|
|
@@ -13976,7 +13995,7 @@ async function startApiServer(opts) {
|
|
|
13976
13995
|
const requestHandler = (req, res) => {
|
|
13977
13996
|
handleRequest(req, res, currentState, opts).catch((err) => {
|
|
13978
13997
|
logger.error(`Unhandled request error: ${err instanceof Error ? err.stack ?? err.message : String(err)}`);
|
|
13979
|
-
if (!res.headersSent) writeError(res, 502);
|
|
13998
|
+
if (!res.headersSent) writeError$1(res, 502);
|
|
13980
13999
|
});
|
|
13981
14000
|
};
|
|
13982
14001
|
const server = opts.mtls ? createServer$2({
|
|
@@ -14049,7 +14068,7 @@ async function handleRequest(req, res, state, opts) {
|
|
|
14049
14068
|
if (await opts.preDispatch(req, res)) return;
|
|
14050
14069
|
} catch (err) {
|
|
14051
14070
|
logger.error(`preDispatch hook threw: ${err instanceof Error ? err.stack ?? err.message : String(err)}`);
|
|
14052
|
-
if (!res.headersSent) writeError(res, 502);
|
|
14071
|
+
if (!res.headersSent) writeError$1(res, 502);
|
|
14053
14072
|
return;
|
|
14054
14073
|
}
|
|
14055
14074
|
const bodyBuf = await readBody(req);
|
|
@@ -14059,7 +14078,7 @@ async function handleRequest(req, res, state, opts) {
|
|
|
14059
14078
|
if (method === "OPTIONS" ? maybeHandleCorsPreflight(req, res, requestPath, state) : false) return;
|
|
14060
14079
|
const match = matchRoute(method, requestPath, state.routes.map((r) => r.route));
|
|
14061
14080
|
if (!match) {
|
|
14062
|
-
writeError(res, 404, "{\"message\":\"Not Found\"}");
|
|
14081
|
+
writeError$1(res, 404, "{\"message\":\"Not Found\"}");
|
|
14063
14082
|
return;
|
|
14064
14083
|
}
|
|
14065
14084
|
const requestOrigin = pickFirstHeaderValue(collectHeaders(req), "origin") ?? void 0;
|
|
@@ -14125,7 +14144,7 @@ async function handleRequest(req, res, state, opts) {
|
|
|
14125
14144
|
logger.error(`REST v1 ${match.route.restV1Integration.kind} dispatch failed for ${match.route.declaredAt}: ${err instanceof Error ? err.message : String(err)}`);
|
|
14126
14145
|
if (!res.headersSent) {
|
|
14127
14146
|
applyCors();
|
|
14128
|
-
writeError(res, 502);
|
|
14147
|
+
writeError$1(res, 502);
|
|
14129
14148
|
} else res.end();
|
|
14130
14149
|
}
|
|
14131
14150
|
return;
|
|
@@ -14136,7 +14155,7 @@ async function handleRequest(req, res, state, opts) {
|
|
|
14136
14155
|
} catch (err) {
|
|
14137
14156
|
logger.error(`Failed to acquire container for ${match.route.lambdaLogicalId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
14138
14157
|
applyCors();
|
|
14139
|
-
writeError(res, 502);
|
|
14158
|
+
writeError$1(res, 502);
|
|
14140
14159
|
return;
|
|
14141
14160
|
}
|
|
14142
14161
|
if (match.route.invokeMode === "RESPONSE_STREAM") {
|
|
@@ -14156,7 +14175,7 @@ async function handleRequest(req, res, state, opts) {
|
|
|
14156
14175
|
logger.error(`RIE streaming invoke failed for ${match.route.lambdaLogicalId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
14157
14176
|
if (!res.headersSent) {
|
|
14158
14177
|
applyCors();
|
|
14159
|
-
writeError(res, 502);
|
|
14178
|
+
writeError$1(res, 502);
|
|
14160
14179
|
} else res.end();
|
|
14161
14180
|
state.pool.release(handle);
|
|
14162
14181
|
return;
|
|
@@ -14173,7 +14192,7 @@ async function handleRequest(req, res, state, opts) {
|
|
|
14173
14192
|
logger.error(`RIE invoke failed for ${match.route.lambdaLogicalId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
14174
14193
|
if (!res.headersSent) {
|
|
14175
14194
|
applyCors();
|
|
14176
|
-
writeError(res, 502);
|
|
14195
|
+
writeError$1(res, 502);
|
|
14177
14196
|
} else res.end();
|
|
14178
14197
|
} finally {
|
|
14179
14198
|
state.pool.release(handle);
|
|
@@ -14582,25 +14601,25 @@ function buildOverlay(authorizer, result, routeApiVersion) {
|
|
|
14582
14601
|
function writeAuthRejection(res, apiVersion, denyKind, authorizerKind) {
|
|
14583
14602
|
if (apiVersion === "v1" && authorizerKind === "iam") {
|
|
14584
14603
|
if (denyKind === "missing-identity") {
|
|
14585
|
-
writeError(res, 403, "{\"message\":\"Missing Authentication Token\"}");
|
|
14604
|
+
writeError$1(res, 403, "{\"message\":\"Missing Authentication Token\"}");
|
|
14586
14605
|
return;
|
|
14587
14606
|
}
|
|
14588
|
-
writeError(res, 403, "{\"message\":\"Forbidden\"}");
|
|
14607
|
+
writeError$1(res, 403, "{\"message\":\"Forbidden\"}");
|
|
14589
14608
|
return;
|
|
14590
14609
|
}
|
|
14591
14610
|
if (apiVersion === "v2" && authorizerKind === "iam") {
|
|
14592
|
-
writeError(res, 403, "{\"Message\":\"Forbidden\"}");
|
|
14611
|
+
writeError$1(res, 403, "{\"Message\":\"Forbidden\"}");
|
|
14593
14612
|
return;
|
|
14594
14613
|
}
|
|
14595
14614
|
if (apiVersion === "v2") {
|
|
14596
|
-
writeError(res, 401, "{\"message\":\"Unauthorized\"}");
|
|
14615
|
+
writeError$1(res, 401, "{\"message\":\"Unauthorized\"}");
|
|
14597
14616
|
return;
|
|
14598
14617
|
}
|
|
14599
14618
|
if (denyKind === "missing-identity") {
|
|
14600
|
-
writeError(res, 401, "{\"message\":\"Unauthorized\"}");
|
|
14619
|
+
writeError$1(res, 401, "{\"message\":\"Unauthorized\"}");
|
|
14601
14620
|
return;
|
|
14602
14621
|
}
|
|
14603
|
-
writeError(res, 403, "{\"message\":\"Forbidden\"}");
|
|
14622
|
+
writeError$1(res, 403, "{\"message\":\"Forbidden\"}");
|
|
14604
14623
|
}
|
|
14605
14624
|
function hashOne(value) {
|
|
14606
14625
|
return value;
|
|
@@ -14688,7 +14707,7 @@ function collectHeaders(req) {
|
|
|
14688
14707
|
* the handler at all (no matching route, container acquire failed, RIE
|
|
14689
14708
|
* unreachable).
|
|
14690
14709
|
*/
|
|
14691
|
-
function writeError(res, statusCode, body = "{\"message\":\"Internal server error\"}") {
|
|
14710
|
+
function writeError$1(res, statusCode, body = "{\"message\":\"Internal server error\"}") {
|
|
14692
14711
|
res.statusCode = statusCode;
|
|
14693
14712
|
res.setHeader("content-type", "application/json");
|
|
14694
14713
|
res.setHeader("content-length", String(Buffer.byteLength(body, "utf-8")));
|
|
@@ -14723,7 +14742,7 @@ async function handleServiceIntegrationRequest(req, res, match, bodyBuf, opts, a
|
|
|
14723
14742
|
const route = match.route;
|
|
14724
14743
|
const svc = route.serviceIntegration;
|
|
14725
14744
|
if (!svc) {
|
|
14726
|
-
writeError(res, 500);
|
|
14745
|
+
writeError$1(res, 500);
|
|
14727
14746
|
return;
|
|
14728
14747
|
}
|
|
14729
14748
|
const rawUrl = req.url ?? "/";
|
|
@@ -18007,6 +18026,7 @@ async function runEcsTask(task, options, state) {
|
|
|
18007
18026
|
sidecarIp: state.network.sidecarIp,
|
|
18008
18027
|
...options.skipHostPortPublish ? { skipHostPortPublish: true } : {},
|
|
18009
18028
|
...options.hostPortOverrides ? { hostPortOverrides: options.hostPortOverrides } : {},
|
|
18029
|
+
...options.ephemeralPublishContainerPorts && options.ephemeralPublishContainerPorts.length > 0 ? { ephemeralPublishContainerPorts: options.ephemeralPublishContainerPorts } : {},
|
|
18010
18030
|
...options.addHostFlags && options.addHostFlags.length > 0 ? { addHostFlags: options.addHostFlags } : {},
|
|
18011
18031
|
...(options.networkAliasesByContainer?.get(container.name)?.length ?? 0) > 0 ? { networkAliases: options.networkAliasesByContainer.get(container.name) } : {},
|
|
18012
18032
|
...options.profileCredentialsFile && { profileCredentialsFile: options.profileCredentialsFile }
|
|
@@ -18368,12 +18388,22 @@ function buildDockerRunArgs(opts) {
|
|
|
18368
18388
|
if (opts.addHostFlags && opts.addHostFlags.length > 0) for (const f of opts.addHostFlags) args.push(f);
|
|
18369
18389
|
if (opts.platformOverride) args.push("--platform", opts.platformOverride);
|
|
18370
18390
|
else if (task.runtimePlatform) args.push("--platform", task.runtimePlatform.cpuArchitecture === "ARM64" ? "linux/arm64" : "linux/amd64");
|
|
18391
|
+
const ephemeralPorts = new Set(opts.ephemeralPublishContainerPorts ?? []);
|
|
18371
18392
|
if (!opts.skipHostPortPublish) for (const pm of container.portMappings) {
|
|
18393
|
+
if (ephemeralPorts.has(pm.containerPort)) continue;
|
|
18372
18394
|
const declaredHostPort = pm.hostPort ?? pm.containerPort;
|
|
18373
18395
|
const hostPort = opts.hostPortOverrides?.[pm.containerPort] ?? declaredHostPort;
|
|
18374
18396
|
if (hostPort !== declaredHostPort) getLogger().child("ecs").info(`Container '${container.name}' container port ${pm.containerPort} published on ${containerHost}:${hostPort} (--host-port override). Reach it at ${containerHost}:${hostPort}.`);
|
|
18375
18397
|
args.push("-p", `${containerHost}:${hostPort}:${pm.containerPort}/${pm.protocol}`);
|
|
18376
18398
|
}
|
|
18399
|
+
if (ephemeralPorts.size > 0) {
|
|
18400
|
+
const alreadyEphemeral = /* @__PURE__ */ new Set();
|
|
18401
|
+
for (const pm of container.portMappings) {
|
|
18402
|
+
if (!ephemeralPorts.has(pm.containerPort) || alreadyEphemeral.has(pm.containerPort)) continue;
|
|
18403
|
+
alreadyEphemeral.add(pm.containerPort);
|
|
18404
|
+
args.push("-p", `${containerHost}::${pm.containerPort}/${pm.protocol}`);
|
|
18405
|
+
}
|
|
18406
|
+
}
|
|
18377
18407
|
if (opts.profileCredentialsFile) args.push("-v", `${opts.profileCredentialsFile.hostPath}:${opts.profileCredentialsFile.containerPath}:ro`);
|
|
18378
18408
|
for (const mp of container.mountPoints) {
|
|
18379
18409
|
const v = volumeByName.get(mp.sourceVolume);
|
|
@@ -18772,7 +18802,7 @@ function createLocalRunTaskCommand(opts = {}) {
|
|
|
18772
18802
|
function resolveEcsServiceTarget(target, stacks, context) {
|
|
18773
18803
|
if (stacks.length === 0) throw new EcsTaskResolutionError("No stacks found in the synthesized assembly.");
|
|
18774
18804
|
const parsed = parseEcsTarget(target);
|
|
18775
|
-
const stack = pickStack(parsed, stacks);
|
|
18805
|
+
const stack = pickStack$1(parsed, stacks);
|
|
18776
18806
|
const resources = stack.template.Resources ?? {};
|
|
18777
18807
|
let serviceLogicalId;
|
|
18778
18808
|
let serviceResource;
|
|
@@ -18974,7 +19004,7 @@ function parseServiceName(raw, serviceLogicalId) {
|
|
|
18974
19004
|
* service-specific extensions (e.g. cross-stack service-to-task refs)
|
|
18975
19005
|
* can diverge without breaking the run-task code path.
|
|
18976
19006
|
*/
|
|
18977
|
-
function pickStack(parsed, stacks) {
|
|
19007
|
+
function pickStack$1(parsed, stacks) {
|
|
18978
19008
|
if (parsed.stackPattern === null) {
|
|
18979
19009
|
if (stacks.length === 1) return stacks[0];
|
|
18980
19010
|
throw new EcsTaskResolutionError(`Target has no stack prefix, and the assembly contains ${stacks.length} stacks: ${stacks.map((s) => s.stackName).join(", ")}. Pass the target as 'Stack/Path' or 'Stack:LogicalId'.`);
|
|
@@ -19042,6 +19072,35 @@ async function getContainerNetworkIp(containerId, networkName) {
|
|
|
19042
19072
|
return;
|
|
19043
19073
|
}
|
|
19044
19074
|
}
|
|
19075
|
+
/**
|
|
19076
|
+
* Issue #86 v1 helper — read back the docker-assigned host port a container
|
|
19077
|
+
* port was published on. The local ALB front-door publishes each replica's
|
|
19078
|
+
* target container port on an ephemeral host port (`-p <host>::<containerPort>`)
|
|
19079
|
+
* and round-robins to `127.0.0.1:<hostPort>`; this resolves that host port
|
|
19080
|
+
* after the replica boots.
|
|
19081
|
+
*
|
|
19082
|
+
* Uses a nil-safe Go template (`with index`) so a container that did NOT
|
|
19083
|
+
* publish the port yields empty output instead of a template error. Returns
|
|
19084
|
+
* `undefined` when the port is unpublished, the container vanished, or the
|
|
19085
|
+
* value isn't a parseable port.
|
|
19086
|
+
*/
|
|
19087
|
+
async function getPublishedHostPort(containerId, containerPort, protocol = "tcp") {
|
|
19088
|
+
const format = `{{with index .NetworkSettings.Ports "${`${containerPort}/${protocol}`}"}}{{with index . 0}}{{.HostPort}}{{end}}{{end}}`;
|
|
19089
|
+
try {
|
|
19090
|
+
const { stdout } = await execFileAsync(getDockerCmd(), [
|
|
19091
|
+
"inspect",
|
|
19092
|
+
"--format",
|
|
19093
|
+
format,
|
|
19094
|
+
containerId
|
|
19095
|
+
]);
|
|
19096
|
+
const raw = stdout.trim();
|
|
19097
|
+
if (!raw) return void 0;
|
|
19098
|
+
const port = parseInt(raw, 10);
|
|
19099
|
+
return Number.isInteger(port) && port >= 1 && port <= 65535 ? port : void 0;
|
|
19100
|
+
} catch {
|
|
19101
|
+
return;
|
|
19102
|
+
}
|
|
19103
|
+
}
|
|
19045
19104
|
|
|
19046
19105
|
//#endregion
|
|
19047
19106
|
//#region src/local/ecs-service-runner.ts
|
|
@@ -19172,7 +19231,8 @@ async function startEcsService(service, options, runState) {
|
|
|
19172
19231
|
restartCount: 0,
|
|
19173
19232
|
shuttingDown: false,
|
|
19174
19233
|
inFlightBoot: void 0,
|
|
19175
|
-
cloudMapHandles: []
|
|
19234
|
+
cloudMapHandles: [],
|
|
19235
|
+
frontDoorOwnerKey: void 0
|
|
19176
19236
|
};
|
|
19177
19237
|
runState.replicas.push(instance);
|
|
19178
19238
|
const bootPromise = bootReplica(service, options, instance);
|
|
@@ -19260,6 +19320,7 @@ var ServiceController = class {
|
|
|
19260
19320
|
} catch {}
|
|
19261
19321
|
instance.cloudMapHandles = [];
|
|
19262
19322
|
}
|
|
19323
|
+
unregisterReplicaFromFrontDoor(instance, this.options.frontDoor);
|
|
19263
19324
|
try {
|
|
19264
19325
|
await cleanupEcsRun(instance.state, { keepRunning: this.options.taskOptions.keepRunning });
|
|
19265
19326
|
} catch (err) {
|
|
@@ -19316,6 +19377,7 @@ async function bootReplica(service, options, instance) {
|
|
|
19316
19377
|
const sharedNetwork = options.discovery?.sharedNetwork;
|
|
19317
19378
|
const networkAliasesByContainer = buildNetworkAliasesByContainer(service);
|
|
19318
19379
|
const skipHostPortPublish = computeReplicaCount(service.desiredCount, options.maxTasks) > 1;
|
|
19380
|
+
const ephemeralPublishContainerPorts = options.frontDoor ? [...new Set(options.frontDoor.pools.map((p) => p.targetContainerPort))] : [];
|
|
19319
19381
|
const perReplicaTaskOptions = {
|
|
19320
19382
|
...options.taskOptions,
|
|
19321
19383
|
cluster: perReplicaCluster,
|
|
@@ -19323,11 +19385,13 @@ async function bootReplica(service, options, instance) {
|
|
|
19323
19385
|
...skipHostPortPublish ? { skipHostPortPublish: true } : {},
|
|
19324
19386
|
...sharedNetwork ? { existingNetwork: sharedNetwork } : { subnetOctet: pickSubnetOctet(instance.index) },
|
|
19325
19387
|
...addHostFlags.length > 0 ? { addHostFlags } : {},
|
|
19326
|
-
...networkAliasesByContainer.size > 0 ? { networkAliasesByContainer } : {}
|
|
19388
|
+
...networkAliasesByContainer.size > 0 ? { networkAliasesByContainer } : {},
|
|
19389
|
+
...ephemeralPublishContainerPorts.length > 0 ? { ephemeralPublishContainerPorts } : {}
|
|
19327
19390
|
};
|
|
19328
19391
|
logger.info(`Booting replica ${instance.index} (${perReplicaCluster})`);
|
|
19329
19392
|
await runEcsTask(service.task, perReplicaTaskOptions, instance.state);
|
|
19330
19393
|
if (options.discovery) await publishReplicaToCloudMap(service, instance, options.discovery, ownerKeyPrefix);
|
|
19394
|
+
if (options.frontDoor) await publishReplicaToFrontDoor(service, instance, options.frontDoor, options.taskOptions.containerHost, ownerKeyPrefix);
|
|
19331
19395
|
}
|
|
19332
19396
|
/**
|
|
19333
19397
|
* After the replica's main container is up, discover its docker
|
|
@@ -19408,6 +19472,57 @@ async function publishReplicaToCloudMap(service, instance, discovery, ownerKeyPr
|
|
|
19408
19472
|
}
|
|
19409
19473
|
}
|
|
19410
19474
|
/**
|
|
19475
|
+
* Issue #86 v1 — register this replica's host-reachable endpoint into every
|
|
19476
|
+
* front-door pool. For each pool, find the target container's docker id, read
|
|
19477
|
+
* back the ephemeral host port docker assigned to its target container port
|
|
19478
|
+
* (`-p <host>::<port>`), and register `<containerHost>:<hostPort>` under the
|
|
19479
|
+
* per-replica owner key.
|
|
19480
|
+
*
|
|
19481
|
+
* Best-effort, mirroring `publishReplicaToCloudMap`: a missing container / port
|
|
19482
|
+
* logs a warn and skips that pool rather than aborting the replica (the
|
|
19483
|
+
* replica is alive; the front-door just can't route to it until it re-boots).
|
|
19484
|
+
* The owner key is stamped on the instance so the restart / shutdown paths can
|
|
19485
|
+
* `pool.unregister` symmetrically.
|
|
19486
|
+
*/
|
|
19487
|
+
async function publishReplicaToFrontDoor(service, instance, frontDoor, containerHost, ownerKeyPrefix) {
|
|
19488
|
+
const logger = getLogger().child("ecs-service");
|
|
19489
|
+
instance.frontDoorOwnerKey = ownerKeyPrefix;
|
|
19490
|
+
for (const target of frontDoor.pools) {
|
|
19491
|
+
const started = instance.state.startedContainers.find((c) => c.name === target.targetContainerName);
|
|
19492
|
+
if (!started) {
|
|
19493
|
+
logger.warn(`ECS Service '${service.serviceLogicalId}' front-door: container '${target.targetContainerName}' did not start for replica ${instance.index}; the front-door cannot route to it.`);
|
|
19494
|
+
continue;
|
|
19495
|
+
}
|
|
19496
|
+
let hostPort;
|
|
19497
|
+
try {
|
|
19498
|
+
hostPort = await getPublishedHostPort(started.id, target.targetContainerPort);
|
|
19499
|
+
} catch (err) {
|
|
19500
|
+
logger.warn(`Replica ${instance.index}: docker inspect failed before front-door publish: ${err instanceof Error ? err.message : String(err)}`);
|
|
19501
|
+
continue;
|
|
19502
|
+
}
|
|
19503
|
+
if (hostPort === void 0) {
|
|
19504
|
+
logger.warn(`Replica ${instance.index}: container port ${target.targetContainerPort} on '${target.targetContainerName}' was not published on a host port; skipping front-door registration for this replica.`);
|
|
19505
|
+
continue;
|
|
19506
|
+
}
|
|
19507
|
+
target.pool.register(ownerKeyPrefix, {
|
|
19508
|
+
host: containerHost,
|
|
19509
|
+
port: hostPort
|
|
19510
|
+
});
|
|
19511
|
+
logger.debug(`Front-door: replica ${instance.index} registered at ${containerHost}:${hostPort} (container ${target.targetContainerName}:${target.targetContainerPort}).`);
|
|
19512
|
+
}
|
|
19513
|
+
}
|
|
19514
|
+
/**
|
|
19515
|
+
* Drop this replica's endpoint from every front-door pool. Idempotent; called
|
|
19516
|
+
* from the watcher restart branch and the controller shutdown path so a
|
|
19517
|
+
* dying / restarting replica is removed from round-robin before its container
|
|
19518
|
+
* is torn down.
|
|
19519
|
+
*/
|
|
19520
|
+
function unregisterReplicaFromFrontDoor(instance, frontDoor) {
|
|
19521
|
+
if (!frontDoor || !instance.frontDoorOwnerKey) return;
|
|
19522
|
+
for (const target of frontDoor.pools) target.pool.unregister(instance.frontDoorOwnerKey);
|
|
19523
|
+
instance.frontDoorOwnerKey = void 0;
|
|
19524
|
+
}
|
|
19525
|
+
/**
|
|
19411
19526
|
* Long-running watcher loop for one replica. Polls the essential
|
|
19412
19527
|
* container's exit code via `docker wait`; on exit, decides whether to
|
|
19413
19528
|
* restart per `restartPolicy` + applies exponential backoff. The loop
|
|
@@ -19447,6 +19562,7 @@ async function watchReplica(service, options, instance, runState) {
|
|
|
19447
19562
|
} catch {}
|
|
19448
19563
|
instance.cloudMapHandles = [];
|
|
19449
19564
|
}
|
|
19565
|
+
unregisterReplicaFromFrontDoor(instance, options.frontDoor);
|
|
19450
19566
|
try {
|
|
19451
19567
|
await cleanupEcsRun(instance.state, { keepRunning: false });
|
|
19452
19568
|
} catch (err) {
|
|
@@ -19859,14 +19975,176 @@ function extractDnsRecords(serviceProps) {
|
|
|
19859
19975
|
}
|
|
19860
19976
|
|
|
19861
19977
|
//#endregion
|
|
19862
|
-
//#region src/
|
|
19978
|
+
//#region src/local/front-door-pool.ts
|
|
19979
|
+
var FrontDoorEndpointPool = class {
|
|
19980
|
+
entries = [];
|
|
19981
|
+
/** Monotonic counter; `next()` rotates over the current entries by index. */
|
|
19982
|
+
cursor = 0;
|
|
19983
|
+
/**
|
|
19984
|
+
* Register (or idempotently replace) the endpoint for `ownerKey`. A replica
|
|
19985
|
+
* restart calls this again with the same key and the new ephemeral port; the
|
|
19986
|
+
* prior entry for that key is replaced rather than duplicated.
|
|
19987
|
+
*/
|
|
19988
|
+
register(ownerKey, endpoint) {
|
|
19989
|
+
if (!ownerKey) throw new Error("FrontDoorEndpointPool.register: ownerKey must be non-empty.");
|
|
19990
|
+
const next = this.entries.filter((e) => e.ownerKey !== ownerKey);
|
|
19991
|
+
next.push({
|
|
19992
|
+
ownerKey,
|
|
19993
|
+
host: endpoint.host,
|
|
19994
|
+
port: endpoint.port
|
|
19995
|
+
});
|
|
19996
|
+
this.entries = next;
|
|
19997
|
+
}
|
|
19998
|
+
/** Drop the endpoint for `ownerKey`. Idempotent; returns whether one was removed. */
|
|
19999
|
+
unregister(ownerKey) {
|
|
20000
|
+
const next = this.entries.filter((e) => e.ownerKey !== ownerKey);
|
|
20001
|
+
const removed = next.length !== this.entries.length;
|
|
20002
|
+
this.entries = next;
|
|
20003
|
+
return removed;
|
|
20004
|
+
}
|
|
20005
|
+
/**
|
|
20006
|
+
* Round-robin the next live endpoint, or `undefined` when the pool is empty
|
|
20007
|
+
* (the front-door server replies 503 in that case). The cursor advances per
|
|
20008
|
+
* call; modulo by the current length tolerates entries being added / removed
|
|
20009
|
+
* between calls.
|
|
20010
|
+
*/
|
|
20011
|
+
next() {
|
|
20012
|
+
if (this.entries.length === 0) return void 0;
|
|
20013
|
+
const entry = this.entries[this.cursor % this.entries.length];
|
|
20014
|
+
this.cursor = (this.cursor + 1) % Number.MAX_SAFE_INTEGER;
|
|
20015
|
+
return {
|
|
20016
|
+
host: entry.host,
|
|
20017
|
+
port: entry.port
|
|
20018
|
+
};
|
|
20019
|
+
}
|
|
20020
|
+
/** Snapshot of the current endpoints (for diagnostics / tests). */
|
|
20021
|
+
list() {
|
|
20022
|
+
return this.entries.map((e) => ({
|
|
20023
|
+
host: e.host,
|
|
20024
|
+
port: e.port
|
|
20025
|
+
}));
|
|
20026
|
+
}
|
|
20027
|
+
/** Number of live endpoints. */
|
|
20028
|
+
size() {
|
|
20029
|
+
return this.entries.length;
|
|
20030
|
+
}
|
|
20031
|
+
};
|
|
20032
|
+
|
|
20033
|
+
//#endregion
|
|
20034
|
+
//#region src/local/front-door-server.ts
|
|
20035
|
+
/** Default per-request upstream timeout — a hung replica yields a 504, not a hang. */
|
|
20036
|
+
const DEFAULT_UPSTREAM_TIMEOUT_MS = 3e4;
|
|
20037
|
+
async function startFrontDoorServer(opts) {
|
|
20038
|
+
const logger = getLogger().child("front-door");
|
|
20039
|
+
const host = opts.host ?? "127.0.0.1";
|
|
20040
|
+
const server = createServer$1((req, res) => {
|
|
20041
|
+
handleProxyRequest(req, res, opts).catch((err) => {
|
|
20042
|
+
logger.debug(`front-door request error: ${err instanceof Error ? err.message : String(err)}`);
|
|
20043
|
+
if (!res.headersSent) writeError(res, 502, "Bad Gateway");
|
|
20044
|
+
});
|
|
20045
|
+
});
|
|
20046
|
+
server.on("connection", (socket) => socket.setNoDelay(true));
|
|
20047
|
+
const boundPort = await new Promise((resolve, reject) => {
|
|
20048
|
+
server.once("error", reject);
|
|
20049
|
+
server.listen(opts.port, host, () => {
|
|
20050
|
+
const addr = server.address();
|
|
20051
|
+
if (addr === null || typeof addr === "string") {
|
|
20052
|
+
reject(/* @__PURE__ */ new Error("Could not determine front-door listening address"));
|
|
20053
|
+
return;
|
|
20054
|
+
}
|
|
20055
|
+
resolve(addr.port);
|
|
20056
|
+
});
|
|
20057
|
+
});
|
|
20058
|
+
let closed = false;
|
|
20059
|
+
return {
|
|
20060
|
+
port: boundPort,
|
|
20061
|
+
host,
|
|
20062
|
+
server,
|
|
20063
|
+
close: async () => {
|
|
20064
|
+
if (closed) return;
|
|
20065
|
+
closed = true;
|
|
20066
|
+
await new Promise((resolve) => {
|
|
20067
|
+
server.close(() => resolve());
|
|
20068
|
+
server.closeAllConnections?.();
|
|
20069
|
+
});
|
|
20070
|
+
}
|
|
20071
|
+
};
|
|
20072
|
+
}
|
|
20073
|
+
function handleProxyRequest(req, res, opts) {
|
|
20074
|
+
return new Promise((resolve) => {
|
|
20075
|
+
const endpoint = opts.pool.next();
|
|
20076
|
+
if (!endpoint) {
|
|
20077
|
+
writeError(res, 503, `No running replicas for service '${opts.serviceName}'. The front-door has no healthy target to forward to.`);
|
|
20078
|
+
resolve();
|
|
20079
|
+
return;
|
|
20080
|
+
}
|
|
20081
|
+
let settled = false;
|
|
20082
|
+
const done = () => {
|
|
20083
|
+
if (settled) return;
|
|
20084
|
+
settled = true;
|
|
20085
|
+
resolve();
|
|
20086
|
+
};
|
|
20087
|
+
const headers = { ...req.headers };
|
|
20088
|
+
appendForwardedHeaders(headers, req, opts.listenerPort);
|
|
20089
|
+
const proxyReq = request({
|
|
20090
|
+
host: endpoint.host,
|
|
20091
|
+
port: endpoint.port,
|
|
20092
|
+
method: req.method,
|
|
20093
|
+
path: req.url,
|
|
20094
|
+
headers
|
|
20095
|
+
}, (proxyRes) => {
|
|
20096
|
+
res.writeHead(proxyRes.statusCode ?? 502, proxyRes.headers);
|
|
20097
|
+
proxyRes.pipe(res);
|
|
20098
|
+
proxyRes.on("end", done);
|
|
20099
|
+
proxyRes.on("error", () => {
|
|
20100
|
+
if (!res.writableEnded) res.destroy();
|
|
20101
|
+
done();
|
|
20102
|
+
});
|
|
20103
|
+
});
|
|
20104
|
+
proxyReq.setTimeout(opts.upstreamTimeoutMs ?? 3e4, () => {
|
|
20105
|
+
if (!res.headersSent) writeError(res, 504, `Replica ${endpoint.host}:${endpoint.port} for service '${opts.serviceName}' did not respond in time.`);
|
|
20106
|
+
else if (!res.writableEnded) res.destroy();
|
|
20107
|
+
proxyReq.destroy();
|
|
20108
|
+
done();
|
|
20109
|
+
});
|
|
20110
|
+
proxyReq.on("error", () => {
|
|
20111
|
+
if (!res.headersSent) writeError(res, 502, `Failed to reach replica ${endpoint.host}:${endpoint.port} for service '${opts.serviceName}'.`);
|
|
20112
|
+
else if (!res.writableEnded) res.destroy();
|
|
20113
|
+
done();
|
|
20114
|
+
});
|
|
20115
|
+
res.on("close", () => {
|
|
20116
|
+
if (!res.writableEnded) proxyReq.destroy();
|
|
20117
|
+
});
|
|
20118
|
+
req.pipe(proxyReq);
|
|
20119
|
+
});
|
|
20120
|
+
}
|
|
19863
20121
|
/**
|
|
19864
|
-
*
|
|
19865
|
-
*
|
|
19866
|
-
*
|
|
19867
|
-
|
|
20122
|
+
* Inject the ALB-style forwarding headers a downstream app may read. Appends
|
|
20123
|
+
* the client IP to any existing `X-Forwarded-For` chain (ALB appends rather
|
|
20124
|
+
* than replaces) and stamps the scheme / listener port.
|
|
20125
|
+
*/
|
|
20126
|
+
function appendForwardedHeaders(headers, req, listenerPort) {
|
|
20127
|
+
const clientIp = req.socket.remoteAddress ?? "";
|
|
20128
|
+
const existing = headers["x-forwarded-for"];
|
|
20129
|
+
const chain = Array.isArray(existing) ? existing.join(", ") : existing;
|
|
20130
|
+
headers["x-forwarded-for"] = chain ? `${chain}, ${clientIp}` : clientIp;
|
|
20131
|
+
headers["x-forwarded-proto"] = "http";
|
|
20132
|
+
headers["x-forwarded-port"] = String(listenerPort);
|
|
20133
|
+
}
|
|
20134
|
+
function writeError(res, statusCode, message) {
|
|
20135
|
+
res.writeHead(statusCode, { "content-type": "text/plain; charset=utf-8" });
|
|
20136
|
+
res.end(`${message}\n`);
|
|
20137
|
+
}
|
|
20138
|
+
|
|
20139
|
+
//#endregion
|
|
20140
|
+
//#region src/cli/commands/ecs-service-emulator.ts
|
|
20141
|
+
/**
|
|
20142
|
+
* Long-running ECS-service emulator. Synths the app, resolves the strategy's
|
|
20143
|
+
* targets into service boots, boots every replica pool (with optional
|
|
20144
|
+
* front-door), and blocks until `^C`. Idempotent single-flight cleanup tears
|
|
20145
|
+
* down every replica + front-door server + the shared network + sidecar.
|
|
19868
20146
|
*/
|
|
19869
|
-
async function
|
|
20147
|
+
async function runEcsServiceEmulator(targets, options, strategy, extraStateProviders) {
|
|
19870
20148
|
const logger = getLogger();
|
|
19871
20149
|
if (options.verbose) logger.setLevel("debug");
|
|
19872
20150
|
warnIfDeprecatedRegion(options);
|
|
@@ -19883,6 +20161,7 @@ async function localStartServiceCommand(targets, options, extraStateProviders) {
|
|
|
19883
20161
|
await Promise.allSettled(pt.runState.replicas.map((r) => r.inFlightBoot).filter((p) => p !== void 0));
|
|
19884
20162
|
await Promise.allSettled(pt.runState.replicas.map((r) => cleanupEcsRun(r.state, { keepRunning: false }).catch(() => void 0)));
|
|
19885
20163
|
}
|
|
20164
|
+
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)}`))));
|
|
19886
20165
|
}));
|
|
19887
20166
|
if (profileCredsFile) {
|
|
19888
20167
|
try {
|
|
@@ -19921,14 +20200,17 @@ async function localStartServiceCommand(targets, options, extraStateProviders) {
|
|
|
19921
20200
|
};
|
|
19922
20201
|
const { stacks } = await synthesizer.synthesize(synthOpts);
|
|
19923
20202
|
const resolvedTargets = await resolveMultiTarget(targets, {
|
|
19924
|
-
entries:
|
|
19925
|
-
message:
|
|
19926
|
-
noun:
|
|
19927
|
-
onMissing: () =>
|
|
20203
|
+
entries: strategy.pickEntries(stacks),
|
|
20204
|
+
message: strategy.pickerMessage,
|
|
20205
|
+
noun: strategy.pickerNoun,
|
|
20206
|
+
onMissing: () => strategy.onMissing()
|
|
19928
20207
|
});
|
|
19929
|
-
|
|
19930
|
-
|
|
19931
|
-
|
|
20208
|
+
const { boots, warnings } = strategy.resolveBoots(stacks, resolvedTargets);
|
|
20209
|
+
for (const w of warnings) logger.warn(w);
|
|
20210
|
+
if (boots.length === 0) throw new LocalStartServiceError(`No runnable ECS service resolved from ${resolvedTargets.join(", ")}.`);
|
|
20211
|
+
rejectExplicitCfnStackWithMultipleStacks(options, boots.length);
|
|
20212
|
+
perTarget = boots.map((boot) => ({
|
|
20213
|
+
boot,
|
|
19932
20214
|
runState: createServiceRunState()
|
|
19933
20215
|
}));
|
|
19934
20216
|
const cloudMapIndexByStack = /* @__PURE__ */ new Map();
|
|
@@ -19966,7 +20248,11 @@ async function localStartServiceCommand(targets, options, extraStateProviders) {
|
|
|
19966
20248
|
};
|
|
19967
20249
|
process.on("SIGINT", sigintHandler);
|
|
19968
20250
|
process.on("SIGTERM", sigintHandler);
|
|
19969
|
-
for (const pt of perTarget)
|
|
20251
|
+
for (const pt of perTarget) {
|
|
20252
|
+
const booted = await bootOneTarget(pt.boot, pt.runState, stacks, options, discovery, skipPull, extraStateProviders, profileCredsFile, strategy.lbPortOverrides);
|
|
20253
|
+
pt.controller = booted.controller;
|
|
20254
|
+
pt.frontDoorServers = booted.frontDoorServers;
|
|
20255
|
+
}
|
|
19970
20256
|
const summary = perTarget.map((pt) => `${pt.controller.service.serviceName} (${pt.controller.activeReplicaCount()} replica(s))`).join(", ");
|
|
19971
20257
|
logger.info(`Service(s) running: ${summary}.`);
|
|
19972
20258
|
logger.info("Press ^C to shut down.");
|
|
@@ -19979,21 +20265,18 @@ async function localStartServiceCommand(targets, options, extraStateProviders) {
|
|
|
19979
20265
|
await cleanup();
|
|
19980
20266
|
}
|
|
19981
20267
|
}
|
|
19982
|
-
|
|
19983
|
-
|
|
19984
|
-
* to wait + tear down.
|
|
19985
|
-
*/
|
|
19986
|
-
async function bootOneTarget(target, runState, stacks, options, discovery, skipPull, extraStateProviders, profileCredsFile) {
|
|
19987
|
-
const candidate = pickCandidateStack(parseEcsTarget(target).stackPattern, stacks);
|
|
20268
|
+
async function bootOneTarget(boot, runState, stacks, options, discovery, skipPull, extraStateProviders, profileCredsFile, lbPortOverrides) {
|
|
20269
|
+
const candidate = pickCandidateStack(parseEcsTarget(boot.target).stackPattern, stacks);
|
|
19988
20270
|
const stateProvider = createLocalStateProvider(options, candidate?.stackName ?? "", await resolveCfnFallbackRegion(options, candidate?.region), extraStateProviders);
|
|
19989
20271
|
try {
|
|
19990
|
-
return await runOneTarget(
|
|
20272
|
+
return await runOneTarget(boot, runState, stacks, options, discovery, skipPull, stateProvider, profileCredsFile, lbPortOverrides);
|
|
19991
20273
|
} finally {
|
|
19992
20274
|
if (stateProvider) stateProvider.dispose();
|
|
19993
20275
|
}
|
|
19994
20276
|
}
|
|
19995
|
-
async function runOneTarget(
|
|
20277
|
+
async function runOneTarget(boot, runState, stacks, options, discovery, skipPull, stateProvider, profileCredsFile, lbPortOverrides) {
|
|
19996
20278
|
const logger = getLogger();
|
|
20279
|
+
const target = boot.target;
|
|
19997
20280
|
const imageContext = await buildEcsImageResolutionContext(target, stacks, options, stateProvider);
|
|
19998
20281
|
const service = resolveEcsServiceTarget(target, stacks, imageContext);
|
|
19999
20282
|
logger.info(`Target: ${service.stack.stackName}/${service.serviceLogicalId} (service=${service.serviceName}, desiredCount=${service.desiredCount}, task=${service.task.taskDefinitionLogicalId})`);
|
|
@@ -20047,12 +20330,72 @@ async function runOneTarget(target, runState, stacks, options, discovery, skipPu
|
|
|
20047
20330
|
containerPath: profileCredsFile.containerPath,
|
|
20048
20331
|
profileName: profileCredsFile.profileName
|
|
20049
20332
|
};
|
|
20050
|
-
|
|
20333
|
+
const { frontDoorContext, frontDoorServers } = await startFrontDoorServers(boot.frontDoorTargets, service, options.containerHost, lbPortOverrides, logger);
|
|
20334
|
+
const runnerOpts = {
|
|
20051
20335
|
maxTasks: options.maxTasks,
|
|
20052
20336
|
restartPolicy: options.restartPolicy,
|
|
20053
20337
|
taskOptions: taskOpts,
|
|
20054
|
-
discovery
|
|
20055
|
-
|
|
20338
|
+
discovery,
|
|
20339
|
+
...frontDoorContext ? { frontDoor: frontDoorContext } : {}
|
|
20340
|
+
};
|
|
20341
|
+
let controller;
|
|
20342
|
+
try {
|
|
20343
|
+
controller = await startEcsService(service, runnerOpts, runState);
|
|
20344
|
+
} catch (err) {
|
|
20345
|
+
await Promise.allSettled(frontDoorServers.map((s) => s.close()));
|
|
20346
|
+
throw err;
|
|
20347
|
+
}
|
|
20348
|
+
return {
|
|
20349
|
+
controller,
|
|
20350
|
+
frontDoorServers
|
|
20351
|
+
};
|
|
20352
|
+
}
|
|
20353
|
+
/**
|
|
20354
|
+
* Issue #86 v1 — stand up one host-side reverse-proxy server per resolved
|
|
20355
|
+
* front-door listener and return the {@link FrontDoorRunnerContext} to thread
|
|
20356
|
+
* into the runner (so each replica publishes + registers its ephemeral
|
|
20357
|
+
* endpoint), plus the started servers for teardown. Pure front-door MECHANISM:
|
|
20358
|
+
* the ALB-specific resolution that produced `frontDoorTargets` lives in the
|
|
20359
|
+
* `start-alb` command. Returns an undefined context + empty servers when there
|
|
20360
|
+
* are no front-door targets (the pure-compute path).
|
|
20361
|
+
*
|
|
20362
|
+
* On a bind failure (e.g. EACCES on a privileged listener port, or the port is
|
|
20363
|
+
* already in use) every server started so far is closed and the error is
|
|
20364
|
+
* re-thrown with a `--lb-port` hint.
|
|
20365
|
+
*/
|
|
20366
|
+
async function startFrontDoorServers(frontDoorTargets, service, containerHost, lbPortOverrides, logger) {
|
|
20367
|
+
if (frontDoorTargets.length === 0) return {
|
|
20368
|
+
frontDoorContext: void 0,
|
|
20369
|
+
frontDoorServers: []
|
|
20370
|
+
};
|
|
20371
|
+
const frontDoorServers = [];
|
|
20372
|
+
const pools = [];
|
|
20373
|
+
try {
|
|
20374
|
+
for (const t of frontDoorTargets) {
|
|
20375
|
+
const pool = new FrontDoorEndpointPool();
|
|
20376
|
+
const server = await startFrontDoorServer({
|
|
20377
|
+
pool,
|
|
20378
|
+
port: lbPortOverrides[t.listenerPort] ?? t.listenerPort,
|
|
20379
|
+
host: containerHost,
|
|
20380
|
+
listenerPort: t.listenerPort,
|
|
20381
|
+
serviceName: service.serviceName
|
|
20382
|
+
});
|
|
20383
|
+
frontDoorServers.push(server);
|
|
20384
|
+
pools.push({
|
|
20385
|
+
pool,
|
|
20386
|
+
targetContainerName: t.targetContainerName,
|
|
20387
|
+
targetContainerPort: t.targetContainerPort
|
|
20388
|
+
});
|
|
20389
|
+
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)`);
|
|
20390
|
+
}
|
|
20391
|
+
} catch (err) {
|
|
20392
|
+
await Promise.allSettled(frontDoorServers.map((s) => s.close()));
|
|
20393
|
+
throw new LocalStartServiceError(`Failed to start ALB front-door for service '${service.serviceName}': ${err instanceof Error ? err.message : String(err)}. If the listener port is privileged (< 1024), remap it to a non-privileged host port with --lb-port <listenerPort>=<hostPort> (e.g. --lb-port 80=8080).`);
|
|
20394
|
+
}
|
|
20395
|
+
return {
|
|
20396
|
+
frontDoorContext: { pools },
|
|
20397
|
+
frontDoorServers
|
|
20398
|
+
};
|
|
20056
20399
|
}
|
|
20057
20400
|
async function resolvePlaceholderAccount(arn, region) {
|
|
20058
20401
|
if (!arn.includes("${AWS::AccountId}")) return arn;
|
|
@@ -20086,11 +20429,9 @@ async function assumeTaskRole(roleArn, region) {
|
|
|
20086
20429
|
}
|
|
20087
20430
|
}
|
|
20088
20431
|
/**
|
|
20089
|
-
* Build the substitution context the ECS resolver consumes.
|
|
20090
|
-
*
|
|
20091
|
-
*
|
|
20092
|
-
* SSM-parameter resolution call (issue #94) into this start-service call site
|
|
20093
|
-
* (mirrors `local-run-task`'s exported helper).
|
|
20432
|
+
* Build the substitution context the ECS resolver consumes. Exported for the
|
|
20433
|
+
* site-level binding test that locks the `--from-cfn-stack` SSM-parameter
|
|
20434
|
+
* resolution call (issue #94).
|
|
20094
20435
|
*/
|
|
20095
20436
|
async function buildEcsImageResolutionContext(target, stacks, options, stateProvider) {
|
|
20096
20437
|
const logger = getLogger();
|
|
@@ -20175,9 +20516,8 @@ function parsePositiveInt(raw, flagName) {
|
|
|
20175
20516
|
return parsed;
|
|
20176
20517
|
}
|
|
20177
20518
|
/**
|
|
20178
|
-
* Hard cap on `--max-tasks` driven by the per-replica subnet allocator
|
|
20179
|
-
*
|
|
20180
|
-
* link-local /24 range `169.254.170.0..169.254.253.0` and skips 171.
|
|
20519
|
+
* Hard cap on `--max-tasks` driven by the per-replica subnet allocator in
|
|
20520
|
+
* `ecs-service-runner.ts:pickSubnetOctet`.
|
|
20181
20521
|
*/
|
|
20182
20522
|
const MAX_TASKS_SUBNET_RANGE_CAP = 83;
|
|
20183
20523
|
function parseMaxTasks(raw) {
|
|
@@ -20190,37 +20530,23 @@ function parseRestartPolicy(raw) {
|
|
|
20190
20530
|
throw new LocalStartServiceError(`--restart-policy must be one of 'on-failure', 'always', or 'none' (got '${raw}').`);
|
|
20191
20531
|
}
|
|
20192
20532
|
/**
|
|
20193
|
-
*
|
|
20194
|
-
*
|
|
20195
|
-
*
|
|
20196
|
-
*
|
|
20197
|
-
*
|
|
20198
|
-
* (the SDK's default credential provider chain — SSO / IAM
|
|
20199
|
-
* Identity Center / fromIni / role-assumption). NEW in this PR.
|
|
20200
|
-
* 2. Not set → `undefined`; the sidecar runs with its own default
|
|
20201
|
-
* credential chain (typically empty inside a fresh container —
|
|
20202
|
-
* user containers will get 4xx from the credentials endpoint).
|
|
20203
|
-
*
|
|
20204
|
-
* Note: per-service `--assume-task-role <Service>=<arn>` overrides are
|
|
20205
|
-
* INTENTIONALLY NOT consulted here. The shared sidecar has no concept
|
|
20206
|
-
* of per-service IAM — per-service `TaskRoleArn` flows into each
|
|
20207
|
-
* container's env via `buildMetadataEnv` at boot time, where the
|
|
20208
|
-
* sidecar's `/role/<role-arn>` path resolves per-request. The shared
|
|
20209
|
-
* sidecar's OWN startup credentials govern only the fallback path
|
|
20210
|
-
* (containers that did not bind a `TaskRoleArn`).
|
|
20211
|
-
*
|
|
20212
|
-
* Extracted as an exported helper so a unit test can exercise both
|
|
20213
|
-
* branches without having to mock the full Synth + Docker + AWS
|
|
20214
|
-
* pipeline (the strategy used for the Lambda container path).
|
|
20533
|
+
* Resolve the credentials forwarded to the AWS-published metadata-endpoints
|
|
20534
|
+
* sidecar (shared across every replica boot in one CLI invocation). `--profile`
|
|
20535
|
+
* resolves via the SDK default chain; unset yields `undefined`. Per-service
|
|
20536
|
+
* `--assume-task-role` overrides are intentionally NOT consulted here. Exported
|
|
20537
|
+
* for a unit test that exercises both branches.
|
|
20215
20538
|
*/
|
|
20216
20539
|
async function resolveSharedSidecarCredentials(options) {
|
|
20217
20540
|
if (options.profile) return resolveProfileCredentials(options.profile);
|
|
20218
20541
|
}
|
|
20219
|
-
|
|
20220
|
-
|
|
20221
|
-
|
|
20222
|
-
|
|
20223
|
-
|
|
20542
|
+
/**
|
|
20543
|
+
* Add the CLI options shared by both ECS-service commands (`start-service` and
|
|
20544
|
+
* `start-alb`) to a command. The command-specific argument / description and
|
|
20545
|
+
* the one unique option (`--host-port` vs `--lb-port`) are added by each
|
|
20546
|
+
* factory.
|
|
20547
|
+
*/
|
|
20548
|
+
function addCommonEcsServiceOptions(cmd) {
|
|
20549
|
+
cmd.addOption(new Option("--cluster <name>", "Cluster name surfaced to ECS_CONTAINER_METADATA_URI_V4 and used as the docker network prefix").default(getEmbedConfig().resourceNamePrefix)).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"ContainerName\":{\"KEY\":\"VALUE\"}, \"Parameters\":{}})")).addOption(new Option("--container-host <ip>", "Host IP to bind published container ports to. Must be a numeric IP (Docker rejects hostnames here)").default("127.0.0.1")).addOption(new Option("--assume-task-role [arn]", "Assume the task definition's TaskRoleArn (or the supplied ARN) and forward STS-issued temp credentials via the metadata sidecar so containers run with the deployed task role. Bare flag uses the template's TaskRoleArn; pass an explicit ARN to override.")).addOption(new Option("--no-pull", "Skip docker pull for every container image and the metadata sidecar")).addOption(new Option("--ecr-role-arn <arn>", "Role ARN to assume before authenticating against ECR for cross-account / centralized registries.")).addOption(new Option("--platform <platform>", "Force docker --platform (linux/amd64 or linux/arm64). Default: inferred from task RuntimePlatform.CpuArchitecture")).addOption(new Option("--max-tasks <n>", `Hard cap on local replica count. Caps the template DesiredCount so local dev machines don't run an unbounded number of containers. Cannot exceed ${83} due to the per-replica link-local /24 subnet allocator's range.`).default(3).argParser(parseMaxTasks)).addOption(new Option("--restart-policy <policy>", "How to react when an essential container exits. 'on-failure' (default) restarts only on non-zero exit; 'always' restarts on every exit; 'none' shuts the replica down and runs the service degraded.").default("on-failure").argParser(parseRestartPolicy)).addOption(new Option("--from-cfn-stack [cfn-stack-name]", `Read a deployed CloudFormation stack via ListStackResources and substitute Ref / Fn::ImportValue in container env vars / secrets / image URIs with the deployed physical IDs / exports. Use for CDK apps deployed via the upstream CDK CLI (\`cdk deploy\`). Bare form uses the ${getEmbedConfig().binaryName} stack name; pass an explicit value when the CFn stack name differs. Fn::GetAtt is warn-and-dropped in v1 (CFn ListStackResources does not return per-attribute values).`)).addOption(new Option("--stack-region <region>", "Region of the state record to read. Used with --from-cfn-stack as the CFn client region."));
|
|
20224
20550
|
[
|
|
20225
20551
|
...commonOptions(),
|
|
20226
20552
|
...appOptions(),
|
|
@@ -20230,6 +20556,359 @@ function createLocalStartServiceCommand(opts = {}) {
|
|
|
20230
20556
|
return cmd;
|
|
20231
20557
|
}
|
|
20232
20558
|
|
|
20559
|
+
//#endregion
|
|
20560
|
+
//#region src/cli/commands/local-start-service.ts
|
|
20561
|
+
/**
|
|
20562
|
+
* `cdkl start-service` strategy — a pure ECS replica runner. It picks
|
|
20563
|
+
* `AWS::ECS::Service` targets and boots each with NO front-door (the ALB
|
|
20564
|
+
* front-door is its own command, `cdkl start-alb`). This keeps `start-service`
|
|
20565
|
+
* a leaf compute runner, symmetric with `invoke` / `run-task`.
|
|
20566
|
+
*/
|
|
20567
|
+
function serviceStrategy() {
|
|
20568
|
+
return {
|
|
20569
|
+
pickEntries: (stacks) => listTargets(stacks).ecsServices,
|
|
20570
|
+
pickerMessage: "Select one or more ECS services to run",
|
|
20571
|
+
pickerNoun: "ECS services",
|
|
20572
|
+
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.`),
|
|
20573
|
+
resolveBoots: (_stacks, chosenTargets) => ({
|
|
20574
|
+
boots: chosenTargets.map((target) => ({
|
|
20575
|
+
target,
|
|
20576
|
+
frontDoorTargets: []
|
|
20577
|
+
})),
|
|
20578
|
+
warnings: []
|
|
20579
|
+
}),
|
|
20580
|
+
lbPortOverrides: {}
|
|
20581
|
+
};
|
|
20582
|
+
}
|
|
20583
|
+
/**
|
|
20584
|
+
* `cdkl start-service <Stack/Service>` — Phase 2 of #262. Spins up
|
|
20585
|
+
* `DesiredCount` task replicas locally (clamped by `--max-tasks`) using the
|
|
20586
|
+
* existing `ecs-task-runner` per replica. Long-running; ^C cleans every replica
|
|
20587
|
+
* + sidecar + shared network. Pure compute: to put a local ALB front-door in
|
|
20588
|
+
* front of an ALB-fronted service, use `cdkl start-alb`.
|
|
20589
|
+
*/
|
|
20590
|
+
function createLocalStartServiceCommand(opts = {}) {
|
|
20591
|
+
setEmbedConfig(opts.embedConfig);
|
|
20592
|
+
return addCommonEcsServiceOptions(new Command("start-service").description(`Run one or more AWS::ECS::Service resources locally as a long-running emulator. Spins up DesiredCount task replicas per service (clamped by --max-tasks) using the same per-task docker network + metadata sidecar pattern as \`${getEmbedConfig().cliName} run-task\`, then keeps each replica running and restarts it on exit per --restart-policy. ^C tears every replica + sidecar + network down. Each <target> accepts a CDK display path (MyStack/MyService) or stack-qualified logical ID (MyStack:MyServiceXYZ); single-stack apps may omit the stack prefix. When two or more <target>s are supplied, every service is booted into a shared Cloud Map / Service Connect registry so peer services discover each other via docker --add-host overlay. Omit <targets> in an interactive terminal to multi-select the services from a list. To put a local ALB front-door in front of an ALB-fronted service, use \`${getEmbedConfig().cliName} start-alb\` instead.`).argument("[targets...]", "One or more CDK display paths or stack-qualified logical IDs of the AWS::ECS::Service resources to run (omit to multi-select interactively in a TTY)").addOption(new Option("--host-port <containerPort=hostPort...>", "Publish a container port on a specific host port (e.g. 80=8080); repeatable. Default: host port == container port. Use this on macOS to map a privileged container port (< 1024) to a non-privileged host port and avoid the Docker Desktop admin-password prompt. (Single-replica services only — multi-replica services do not publish host ports.)")).action(withErrorHandling(async (targets, options) => {
|
|
20593
|
+
await runEcsServiceEmulator(targets, options, serviceStrategy(), opts.extraStateProviders);
|
|
20594
|
+
})));
|
|
20595
|
+
}
|
|
20596
|
+
|
|
20597
|
+
//#endregion
|
|
20598
|
+
//#region src/local/elb-front-door-resolver.ts
|
|
20599
|
+
/**
|
|
20600
|
+
* Issue #86 v1 — resolve an `AWS::ElasticLoadBalancingV2::LoadBalancer` (an
|
|
20601
|
+
* ALB) into the backing ECS service(s) and the host listener port(s) a local
|
|
20602
|
+
* front-door should expose for each. This is the `cdkl start-alb` entry: you
|
|
20603
|
+
* name the ALB, and cdk-local discovers the services behind it (mirroring how
|
|
20604
|
+
* `start-api` names the API and discovers the backing Lambdas).
|
|
20605
|
+
*
|
|
20606
|
+
* The synthesized linkage (confirmed against real `cdk synth` of
|
|
20607
|
+
* `ApplicationLoadBalancedFargateService`):
|
|
20608
|
+
*
|
|
20609
|
+
* ```
|
|
20610
|
+
* ElasticLoadBalancingV2::LoadBalancer (the ALB you name)
|
|
20611
|
+
* ElasticLoadBalancingV2::Listener : { LoadBalancerArn:{Ref:<ALB>}, Port, Protocol,
|
|
20612
|
+
* DefaultActions:[{ Type:"forward", TargetGroupArn:{Ref:<TG>} }] }
|
|
20613
|
+
* ElasticLoadBalancingV2::TargetGroup : { Port, Protocol, TargetType:"ip" }
|
|
20614
|
+
* ECS::Service.LoadBalancers[] -> { ContainerName, ContainerPort, TargetGroupArn:{Ref:<TG>} }
|
|
20615
|
+
* ```
|
|
20616
|
+
*
|
|
20617
|
+
* Resolution walks ALB -> listeners (by `LoadBalancerArn` Ref) -> default
|
|
20618
|
+
* `forward` target groups -> the ECS Service whose `LoadBalancers[]` references
|
|
20619
|
+
* that target group (a reverse scan; there is no direct TG -> service pointer).
|
|
20620
|
+
*
|
|
20621
|
+
* v1 scope (single forward): only listener `DefaultActions` are honored.
|
|
20622
|
+
* `AWS::ElasticLoadBalancingV2::ListenerRule` (path / host / weighted routing)
|
|
20623
|
+
* is ignored — tracked in #123. HTTPS / TLS listeners and `TargetType:"lambda"`
|
|
20624
|
+
* target groups are skipped with a warning.
|
|
20625
|
+
*/
|
|
20626
|
+
const ALB_TYPE = "AWS::ElasticLoadBalancingV2::LoadBalancer";
|
|
20627
|
+
const LISTENER_TYPE = "AWS::ElasticLoadBalancingV2::Listener";
|
|
20628
|
+
const TARGET_GROUP_TYPE = "AWS::ElasticLoadBalancingV2::TargetGroup";
|
|
20629
|
+
const SERVICE_TYPE = "AWS::ECS::Service";
|
|
20630
|
+
/**
|
|
20631
|
+
* Resolve an ALB into its backing services + front-door targets. Pure — reads
|
|
20632
|
+
* only the supplied stack template. Returns an empty `services` array (with
|
|
20633
|
+
* warnings) when the ALB fronts nothing cdk-local can serve locally.
|
|
20634
|
+
*/
|
|
20635
|
+
function resolveAlbFrontDoor(stack, albLogicalId) {
|
|
20636
|
+
const warnings = [];
|
|
20637
|
+
const resources = stack.template.Resources ?? {};
|
|
20638
|
+
const tgToService = indexTargetGroupToService(resources);
|
|
20639
|
+
const byService = /* @__PURE__ */ new Map();
|
|
20640
|
+
const seenPortsByService = /* @__PURE__ */ new Map();
|
|
20641
|
+
for (const [listenerLogicalId, resource] of Object.entries(resources)) {
|
|
20642
|
+
if (resource.Type !== LISTENER_TYPE) continue;
|
|
20643
|
+
const props = resource.Properties ?? {};
|
|
20644
|
+
if (refOf(props["LoadBalancerArn"]) !== albLogicalId) continue;
|
|
20645
|
+
const port = parsePort(props["Port"]);
|
|
20646
|
+
if (port === void 0) continue;
|
|
20647
|
+
const protocol = typeof props["Protocol"] === "string" ? props["Protocol"] : "HTTP";
|
|
20648
|
+
const tgRefs = collectForwardTargetGroupRefs(props["DefaultActions"]);
|
|
20649
|
+
if (tgRefs.size === 0) {
|
|
20650
|
+
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.`);
|
|
20651
|
+
continue;
|
|
20652
|
+
}
|
|
20653
|
+
if (protocol !== "HTTP") {
|
|
20654
|
+
warnings.push(`Listener '${listenerLogicalId}' on port ${port} uses protocol ${protocol}; the local ALB front-door supports HTTP listeners only in v1 (TLS termination is deferred). Skipping it.`);
|
|
20655
|
+
continue;
|
|
20656
|
+
}
|
|
20657
|
+
for (const tgRef of tgRefs) {
|
|
20658
|
+
const tg = resources[tgRef];
|
|
20659
|
+
if (!tg || tg.Type !== TARGET_GROUP_TYPE) {
|
|
20660
|
+
warnings.push(`Listener '${listenerLogicalId}' forwards to target group '${tgRef}', but no ${TARGET_GROUP_TYPE} with that logical id exists in ${stack.stackName}. Skipping it.`);
|
|
20661
|
+
continue;
|
|
20662
|
+
}
|
|
20663
|
+
if (tg.Properties?.["TargetType"] === "lambda") {
|
|
20664
|
+
warnings.push(`Target group '${tgRef}' is a Lambda target (TargetType: lambda). The local ALB front-door supports ECS targets only in v1; Lambda targets are deferred to a follow-up. Skipping it.`);
|
|
20665
|
+
continue;
|
|
20666
|
+
}
|
|
20667
|
+
const backing = tgToService.get(tgRef);
|
|
20668
|
+
if (!backing) {
|
|
20669
|
+
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.`);
|
|
20670
|
+
continue;
|
|
20671
|
+
}
|
|
20672
|
+
const seenPorts = seenPortsByService.get(backing.serviceLogicalId) ?? /* @__PURE__ */ new Set();
|
|
20673
|
+
if (seenPorts.has(port)) {
|
|
20674
|
+
warnings.push(`Service '${backing.serviceLogicalId}' is fronted by more than one listener on host port ${port}; the local front-door fronts only the first.`);
|
|
20675
|
+
continue;
|
|
20676
|
+
}
|
|
20677
|
+
seenPorts.add(port);
|
|
20678
|
+
seenPortsByService.set(backing.serviceLogicalId, seenPorts);
|
|
20679
|
+
const targets = byService.get(backing.serviceLogicalId) ?? [];
|
|
20680
|
+
targets.push({
|
|
20681
|
+
listenerPort: port,
|
|
20682
|
+
listenerProtocol: "HTTP",
|
|
20683
|
+
targetContainerName: backing.containerName,
|
|
20684
|
+
targetContainerPort: backing.containerPort,
|
|
20685
|
+
targetGroupLogicalId: tgRef,
|
|
20686
|
+
listenerLogicalId
|
|
20687
|
+
});
|
|
20688
|
+
byService.set(backing.serviceLogicalId, targets);
|
|
20689
|
+
}
|
|
20690
|
+
}
|
|
20691
|
+
return {
|
|
20692
|
+
services: [...byService.entries()].map(([serviceLogicalId, targets]) => ({
|
|
20693
|
+
serviceLogicalId,
|
|
20694
|
+
targets
|
|
20695
|
+
})),
|
|
20696
|
+
warnings
|
|
20697
|
+
};
|
|
20698
|
+
}
|
|
20699
|
+
/** True when the resource is an application Load Balancer (the `start-alb` target type). */
|
|
20700
|
+
function isApplicationLoadBalancer(resource) {
|
|
20701
|
+
if (resource.Type !== ALB_TYPE) return false;
|
|
20702
|
+
const type = resource.Properties?.["Type"];
|
|
20703
|
+
return type === void 0 || type === "application";
|
|
20704
|
+
}
|
|
20705
|
+
/**
|
|
20706
|
+
* Build a `targetGroupLogicalId -> backing ECS service` index by scanning every
|
|
20707
|
+
* `AWS::ECS::Service.LoadBalancers[]`. First service wins on a shared target
|
|
20708
|
+
* group (unusual; would only happen with a hand-rolled template).
|
|
20709
|
+
*/
|
|
20710
|
+
function indexTargetGroupToService(resources) {
|
|
20711
|
+
const index = /* @__PURE__ */ new Map();
|
|
20712
|
+
for (const [serviceLogicalId, resource] of Object.entries(resources)) {
|
|
20713
|
+
if (resource.Type !== SERVICE_TYPE) continue;
|
|
20714
|
+
const lbs = resource.Properties?.["LoadBalancers"];
|
|
20715
|
+
if (!Array.isArray(lbs)) continue;
|
|
20716
|
+
for (const entry of lbs) {
|
|
20717
|
+
if (!entry || typeof entry !== "object") continue;
|
|
20718
|
+
const e = entry;
|
|
20719
|
+
const tgRef = refOf(e["TargetGroupArn"]);
|
|
20720
|
+
const containerName = typeof e["ContainerName"] === "string" ? e["ContainerName"] : void 0;
|
|
20721
|
+
const containerPort = parseContainerPort(e["ContainerPort"]);
|
|
20722
|
+
if (!tgRef || !containerName || containerPort === void 0) continue;
|
|
20723
|
+
if (!index.has(tgRef)) index.set(tgRef, {
|
|
20724
|
+
serviceLogicalId,
|
|
20725
|
+
containerName,
|
|
20726
|
+
containerPort
|
|
20727
|
+
});
|
|
20728
|
+
}
|
|
20729
|
+
}
|
|
20730
|
+
return index;
|
|
20731
|
+
}
|
|
20732
|
+
function collectForwardTargetGroupRefs(defaultActions) {
|
|
20733
|
+
const refs = /* @__PURE__ */ new Set();
|
|
20734
|
+
if (!Array.isArray(defaultActions)) return refs;
|
|
20735
|
+
for (const action of defaultActions) {
|
|
20736
|
+
if (!action || typeof action !== "object") continue;
|
|
20737
|
+
const a = action;
|
|
20738
|
+
if (a["Type"] !== "forward") continue;
|
|
20739
|
+
const direct = refOf(a["TargetGroupArn"]);
|
|
20740
|
+
if (direct) refs.add(direct);
|
|
20741
|
+
const forwardConfig = a["ForwardConfig"];
|
|
20742
|
+
if (forwardConfig && typeof forwardConfig === "object") {
|
|
20743
|
+
const groups = forwardConfig["TargetGroups"];
|
|
20744
|
+
if (Array.isArray(groups)) for (const g of groups) {
|
|
20745
|
+
if (!g || typeof g !== "object") continue;
|
|
20746
|
+
const ref = refOf(g["TargetGroupArn"]);
|
|
20747
|
+
if (ref) refs.add(ref);
|
|
20748
|
+
}
|
|
20749
|
+
}
|
|
20750
|
+
}
|
|
20751
|
+
return refs;
|
|
20752
|
+
}
|
|
20753
|
+
/**
|
|
20754
|
+
* True when `DefaultActions` has at least one `forward` action that references
|
|
20755
|
+
* a target group via a NON-`Ref` arn (literal / `Fn::GetAtt` / cross-stack) —
|
|
20756
|
+
* i.e. a forward we could not resolve to an in-stack target group. Used to warn
|
|
20757
|
+
* rather than silently skip such a listener.
|
|
20758
|
+
*/
|
|
20759
|
+
function hasUnresolvableForward(defaultActions) {
|
|
20760
|
+
if (!Array.isArray(defaultActions)) return false;
|
|
20761
|
+
for (const action of defaultActions) {
|
|
20762
|
+
if (!action || typeof action !== "object") continue;
|
|
20763
|
+
const a = action;
|
|
20764
|
+
if (a["Type"] !== "forward") continue;
|
|
20765
|
+
if (a["TargetGroupArn"] !== void 0 && refOf(a["TargetGroupArn"]) === void 0) return true;
|
|
20766
|
+
const forwardConfig = a["ForwardConfig"];
|
|
20767
|
+
if (forwardConfig && typeof forwardConfig === "object") {
|
|
20768
|
+
const groups = forwardConfig["TargetGroups"];
|
|
20769
|
+
if (Array.isArray(groups)) for (const g of groups) {
|
|
20770
|
+
if (!g || typeof g !== "object") continue;
|
|
20771
|
+
const arn = g["TargetGroupArn"];
|
|
20772
|
+
if (arn !== void 0 && refOf(arn) === void 0) return true;
|
|
20773
|
+
}
|
|
20774
|
+
}
|
|
20775
|
+
}
|
|
20776
|
+
return false;
|
|
20777
|
+
}
|
|
20778
|
+
function refOf(raw) {
|
|
20779
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
20780
|
+
const ref = raw["Ref"];
|
|
20781
|
+
if (typeof ref === "string" && ref.length > 0) return ref;
|
|
20782
|
+
}
|
|
20783
|
+
}
|
|
20784
|
+
function parsePort(raw) {
|
|
20785
|
+
if (typeof raw === "number" && Number.isInteger(raw) && raw >= 1 && raw <= 65535) return raw;
|
|
20786
|
+
if (typeof raw === "string" && /^\d+$/.test(raw)) {
|
|
20787
|
+
const n = parseInt(raw, 10);
|
|
20788
|
+
if (n >= 1 && n <= 65535) return n;
|
|
20789
|
+
}
|
|
20790
|
+
}
|
|
20791
|
+
function parseContainerPort(raw) {
|
|
20792
|
+
return parsePort(raw);
|
|
20793
|
+
}
|
|
20794
|
+
|
|
20795
|
+
//#endregion
|
|
20796
|
+
//#region src/cli/commands/local-start-alb.ts
|
|
20797
|
+
/**
|
|
20798
|
+
* Issue #86 v1 — parse `--lb-port <listenerPort>=<hostPort>` overrides into a
|
|
20799
|
+
* `listenerPort -> hostPort` map. The local ALB front-door binds the listener
|
|
20800
|
+
* port on the host by default, but a privileged listener port (e.g. 80 / 443)
|
|
20801
|
+
* fails to bind as non-root on macOS, so the user opts in to a non-privileged
|
|
20802
|
+
* host port (e.g. `--lb-port 80=8080`). Repeatable; each value is
|
|
20803
|
+
* `<listenerPort>=<hostPort>` with both in 1-65535.
|
|
20804
|
+
*/
|
|
20805
|
+
function parseLbPortOverrides(values) {
|
|
20806
|
+
const out = {};
|
|
20807
|
+
for (const raw of values ?? []) {
|
|
20808
|
+
const m = /^(\d+)=(\d+)$/.exec(raw.trim());
|
|
20809
|
+
if (!m) throw new LocalStartServiceError(`Invalid --lb-port '${raw}'. Expected <listenerPort>=<hostPort> (e.g. 80=8080).`);
|
|
20810
|
+
const listenerPort = Number(m[1]);
|
|
20811
|
+
const hostPort = Number(m[2]);
|
|
20812
|
+
for (const [label, p] of [["listener", listenerPort], ["host", hostPort]]) if (p < 1 || p > 65535) throw new LocalStartServiceError(`Invalid --lb-port '${raw}': ${label} port must be 1-65535.`);
|
|
20813
|
+
out[listenerPort] = hostPort;
|
|
20814
|
+
}
|
|
20815
|
+
return out;
|
|
20816
|
+
}
|
|
20817
|
+
/**
|
|
20818
|
+
* Resolve an ALB target string (`Stack/Path` display path or `Stack:LogicalId`)
|
|
20819
|
+
* to its stack + `AWS::ElasticLoadBalancingV2::LoadBalancer` logical id. Mirrors
|
|
20820
|
+
* the ECS service resolver's target grammar.
|
|
20821
|
+
*/
|
|
20822
|
+
function resolveAlbTarget(target, stacks) {
|
|
20823
|
+
if (stacks.length === 0) throw new LocalStartServiceError("No stacks found in the synthesized assembly.");
|
|
20824
|
+
const parsed = parseEcsTarget(target);
|
|
20825
|
+
const stack = pickStack(parsed.stackPattern, stacks, target);
|
|
20826
|
+
const resources = stack.template.Resources ?? {};
|
|
20827
|
+
if (parsed.isPath) {
|
|
20828
|
+
const index = buildCdkPathIndex(stack.template);
|
|
20829
|
+
const albs = resolveCdkPathToLogicalIds(parsed.pathOrId, index).filter(({ logicalId }) => {
|
|
20830
|
+
const r = resources[logicalId];
|
|
20831
|
+
return r !== void 0 && isApplicationLoadBalancer(r);
|
|
20832
|
+
});
|
|
20833
|
+
if (albs.length === 0) throw notFound(target, stack, resources);
|
|
20834
|
+
if (albs.length > 1) throw new LocalStartServiceError(`Target '${target}' matches ${albs.length} load balancers in ${stack.stackName}: ${albs.map((a) => a.logicalId).join(", ")}. Refine the path or use the stack:LogicalId form.`);
|
|
20835
|
+
return {
|
|
20836
|
+
stack,
|
|
20837
|
+
albLogicalId: albs[0].logicalId
|
|
20838
|
+
};
|
|
20839
|
+
}
|
|
20840
|
+
const res = resources[parsed.pathOrId];
|
|
20841
|
+
if (!res || !isApplicationLoadBalancer(res)) throw notFound(target, stack, resources);
|
|
20842
|
+
return {
|
|
20843
|
+
stack,
|
|
20844
|
+
albLogicalId: parsed.pathOrId
|
|
20845
|
+
};
|
|
20846
|
+
}
|
|
20847
|
+
function pickStack(stackPattern, stacks, target) {
|
|
20848
|
+
if (stackPattern === null) {
|
|
20849
|
+
if (stacks.length === 1) return stacks[0];
|
|
20850
|
+
throw new LocalStartServiceError(`Target '${target}' has no stack prefix, and the assembly contains ${stacks.length} stacks: ${stacks.map((s) => s.stackName).join(", ")}. Pass it as 'Stack/Path' or 'Stack:LogicalId'.`);
|
|
20851
|
+
}
|
|
20852
|
+
const matched = matchStacks(stacks, [stackPattern]);
|
|
20853
|
+
if (matched.length === 0) throw new LocalStartServiceError(`No stack matches '${stackPattern}'. Available stacks: ${stacks.map((s) => s.stackName).join(", ")}.`);
|
|
20854
|
+
if (matched.length > 1) throw new LocalStartServiceError(`Multiple stacks match '${stackPattern}': ${matched.map((s) => s.stackName).join(", ")}. Refine the pattern.`);
|
|
20855
|
+
return matched[0];
|
|
20856
|
+
}
|
|
20857
|
+
function notFound(target, stack, resources) {
|
|
20858
|
+
const albs = Object.entries(resources).filter(([, r]) => r.Type === "AWS::ElasticLoadBalancingV2::LoadBalancer").map(([logicalId]) => logicalId);
|
|
20859
|
+
const available = albs.length > 0 ? ` Available load balancers in ${stack.stackName}: ${albs.join(", ")}.` : ` ${stack.stackName} declares no AWS::ElasticLoadBalancingV2::LoadBalancer resources.`;
|
|
20860
|
+
return new LocalStartServiceError(`Target '${target}' did not match an application Load Balancer in ${stack.stackName}.${available}`);
|
|
20861
|
+
}
|
|
20862
|
+
/**
|
|
20863
|
+
* `cdkl start-alb` strategy — name the ALB, boot the ECS service(s) behind it,
|
|
20864
|
+
* and expose each listener via a local front-door. Mirrors how `start-api`
|
|
20865
|
+
* names the API and serves its backing Lambdas.
|
|
20866
|
+
*/
|
|
20867
|
+
function albStrategy(options) {
|
|
20868
|
+
return {
|
|
20869
|
+
pickEntries: (stacks) => listTargets(stacks).loadBalancers,
|
|
20870
|
+
pickerMessage: "Select one or more Application Load Balancers to run",
|
|
20871
|
+
pickerNoun: "Application Load Balancers",
|
|
20872
|
+
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.`),
|
|
20873
|
+
resolveBoots: (stacks, chosenTargets) => {
|
|
20874
|
+
const byServiceTarget = /* @__PURE__ */ new Map();
|
|
20875
|
+
const warnings = [];
|
|
20876
|
+
for (const albTarget of chosenTargets) {
|
|
20877
|
+
const { stack, albLogicalId } = resolveAlbTarget(albTarget, stacks);
|
|
20878
|
+
const resolution = resolveAlbFrontDoor(stack, albLogicalId);
|
|
20879
|
+
warnings.push(...resolution.warnings);
|
|
20880
|
+
for (const svc of resolution.services) {
|
|
20881
|
+
const target = `${stack.stackName}:${svc.serviceLogicalId}`;
|
|
20882
|
+
const existing = byServiceTarget.get(target);
|
|
20883
|
+
if (existing) existing.frontDoorTargets.push(...svc.targets);
|
|
20884
|
+
else byServiceTarget.set(target, {
|
|
20885
|
+
target,
|
|
20886
|
+
frontDoorTargets: [...svc.targets]
|
|
20887
|
+
});
|
|
20888
|
+
}
|
|
20889
|
+
}
|
|
20890
|
+
return {
|
|
20891
|
+
boots: [...byServiceTarget.values()],
|
|
20892
|
+
warnings
|
|
20893
|
+
};
|
|
20894
|
+
},
|
|
20895
|
+
lbPortOverrides: parseLbPortOverrides(options.lbPort)
|
|
20896
|
+
};
|
|
20897
|
+
}
|
|
20898
|
+
/**
|
|
20899
|
+
* `cdkl start-alb <Stack/Alb>` — Issue #86 v1. Names an
|
|
20900
|
+
* `AWS::ElasticLoadBalancingV2::LoadBalancer`, discovers the ECS service(s)
|
|
20901
|
+
* behind its HTTP `forward` listeners, boots their replicas, and stands up a
|
|
20902
|
+
* local front-door on each listener port that round-robins across the replicas.
|
|
20903
|
+
* The symmetric ALB counterpart of `start-api`.
|
|
20904
|
+
*/
|
|
20905
|
+
function createLocalStartAlbCommand(opts = {}) {
|
|
20906
|
+
setEmbedConfig(opts.embedConfig);
|
|
20907
|
+
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 forward listeners and stands up a local front-door on each listener port that round-robins across the running replicas — a single 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. v1 supports a single default-action forward to an HTTP listener; HTTPS listeners and Lambda target groups are skipped with a warning, and listener-rule routing is tracked separately. 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) => {
|
|
20908
|
+
await runEcsServiceEmulator(targets, options, albStrategy(options), opts.extraStateProviders);
|
|
20909
|
+
})));
|
|
20910
|
+
}
|
|
20911
|
+
|
|
20233
20912
|
//#endregion
|
|
20234
20913
|
//#region src/cli/commands/local-list.ts
|
|
20235
20914
|
async function localListCommand(options) {
|
|
@@ -20265,14 +20944,15 @@ async function localListCommand(options) {
|
|
|
20265
20944
|
* without running synthesis.
|
|
20266
20945
|
*/
|
|
20267
20946
|
function formatTargetListing(listing, cliName, options = {}) {
|
|
20268
|
-
if (countTargets(listing) === 0) return `No runnable targets (Lambda functions, APIs, ECS services / tasks, AgentCore Runtimes) found in this CDK app.`;
|
|
20947
|
+
if (countTargets(listing) === 0) return `No runnable targets (Lambda functions, APIs, ECS services / tasks, AgentCore Runtimes, load balancers) found in this CDK app.`;
|
|
20269
20948
|
const long = options.long ?? false;
|
|
20270
20949
|
return "\n" + [
|
|
20271
20950
|
formatSection("Lambda Functions", `${cliName} invoke <target>`, listing.lambdas, long),
|
|
20272
20951
|
formatSection("APIs", `${cliName} start-api [target...]`, listing.apis, long),
|
|
20273
20952
|
formatSection("ECS Services", `${cliName} start-service <target...>`, listing.ecsServices, long),
|
|
20274
20953
|
formatSection("ECS Task Definitions", `${cliName} run-task <target>`, listing.ecsTaskDefinitions, long),
|
|
20275
|
-
formatSection("AgentCore Runtimes", `${cliName} invoke-agentcore <target>`, listing.agentCoreRuntimes, long)
|
|
20954
|
+
formatSection("AgentCore Runtimes", `${cliName} invoke-agentcore <target>`, listing.agentCoreRuntimes, long),
|
|
20955
|
+
formatSection("Application Load Balancers", `${cliName} start-alb <target...>`, listing.loadBalancers, long)
|
|
20276
20956
|
].filter((lines) => lines.length > 0).map((lines) => lines.join("\n")).join("\n\n");
|
|
20277
20957
|
}
|
|
20278
20958
|
function formatSection(title, command, entries, long) {
|
|
@@ -20287,7 +20967,7 @@ function formatSection(title, command, entries, long) {
|
|
|
20287
20967
|
}
|
|
20288
20968
|
function createLocalListCommand(opts = {}) {
|
|
20289
20969
|
setEmbedConfig(opts.embedConfig);
|
|
20290
|
-
const cmd = new Command("list").alias("ls").description("List the runnable targets in the synthesized CDK app, grouped by the command that runs them: Lambda functions (invoke), API Gateway REST v1 / HTTP v2 / Function URL / WebSocket surfaces (start-api), ECS services (start-service), ECS task definitions (run-task),
|
|
20970
|
+
const cmd = new Command("list").alias("ls").description("List the runnable targets in the synthesized CDK app, grouped by the command that runs them: Lambda functions (invoke), API Gateway REST v1 / HTTP v2 / Function URL / WebSocket surfaces (start-api), ECS services (start-service), ECS task definitions (run-task), AgentCore Runtimes (invoke-agentcore), and Application Load Balancers (start-alb). Each target is shown by its CDK display path; pass -l to also print the stack-qualified logical ID. Tip: you usually do not need to copy these — just run the command (e.g. `invoke`) with no target in a terminal and pick from the list.").addOption(new Option("-l, --long", "Also print each target's stack-qualified logical ID (<Stack>:<LogicalId>) beneath it").default(false)).action(withErrorHandling(async (options) => {
|
|
20291
20971
|
await localListCommand(options);
|
|
20292
20972
|
}));
|
|
20293
20973
|
[
|
|
@@ -20300,5 +20980,5 @@ function createLocalListCommand(opts = {}) {
|
|
|
20300
20980
|
}
|
|
20301
20981
|
|
|
20302
20982
|
//#endregion
|
|
20303
|
-
export {
|
|
20304
|
-
//# sourceMappingURL=local-list-
|
|
20983
|
+
export { probeHostGatewaySupport as $, verifyJwtAuthorizer as A, resolveCfnStackName as At, isFunctionUrlOacFronted as B, discoverRoutes as Bt, resolveSelectionExpression as C, materializeLayerFromArn as Ct, buildJwksUrlFromIssuer as D, rejectExplicitCfnStackWithMultipleStacks as Dt, buildCognitoJwksUrl as E, isCfnFlagPresent as Et, invokeTokenAuthorizer as F, countTargets as Ft, buildHttpApiV2Event as G, AgentCoreResolutionError as Gt, matchRoute as H, resolveLambdaArnIntrinsic as Ht, attachAuthorizers as I, listTargets as It, pickResponseTemplate as J, substituteImagePlaceholders as Jt, buildRestV1Event as K, resolveAgentCoreTarget as Kt, applyCorsResponseHeaders as L, discoverWebSocketApis as Lt, computeRequestIdentityHash as M, collectSsmParameterRefs as Mt, evaluateCachedLambdaPolicy as N, resolveSsmParameters as Nt, createJwksCache as O, resolveCfnFallbackRegion as Ot, invokeRequestAuthorizer as P, resolveWatchConfig as Pt, HOST_GATEWAY_MIN_VERSION as Q, buildCorsConfigByApiId as R, discoverWebSocketApisOrThrow as Rt, startApiServer as S, resolveEnvVars as St, defaultCredentialsLoader as T, createLocalStateProvider as Tt, translateLambdaResponse as U, AGENTCORE_HTTP_PROTOCOL as Ut, matchPreflight as V, pickRefLogicalId as Vt, applyAuthorizerOverlay as W, AGENTCORE_RUNTIME_TYPE as Wt, tryParseStatus as X, LocalInvokeBuildError as Xt, selectIntegrationResponse as Y, tryResolveImageFnJoin as Yt, VtlEvaluationError as Z, availableApiIdentifiers as _, EcsTaskResolutionError as _t, buildCloudMapIndex as a, buildConnectEvent as at, groupRoutesByServer as b, substituteEnvVarsFromState as bt, createLocalRunTaskCommand as c, AGENTCORE_SESSION_ID_HEADER as ct, createWatchPredicates as d, createLocalInvokeCommand as dt, bufferToBody as et, resolveApiTargetSubset as f, architectureToPlatform as ft, buildStageMap as g, resolveRuntimeImage as gt, attachStageContext as h, resolveRuntimeFileExtension as ht, createLocalStartServiceCommand as i, parseConnectionsPath as it, buildMethodArn as j, CfnLocalStateProvider as jt, verifyCognitoJwt as k, resolveCfnRegion as kt, createLocalInvokeAgentCoreCommand as l, invokeAgentCore as lt, createFileWatcher as m, resolveRuntimeCodeMountPath as mt, formatTargetListing as n, buildMgmtEndpointEnvUrl as nt, CloudMapRegistry as o, buildDisconnectEvent as ot, createAuthorizerCache as p, buildContainerImage as pt, evaluateResponseParameters as q, derivePseudoParametersFromRegion as qt, createLocalStartAlbCommand as r, handleConnectionsRequest as rt, getContainerNetworkIp as s, buildMessageEvent as st, createLocalListCommand as t, ConnectionRegistry as tt, createLocalStartApiCommand as u, waitForAgentCorePing as ut, filterRoutesByApiIdentifier as v, substituteAgainstState as vt, resolveServiceIntegrationParameters as w, LocalStateSourceError as wt, readMtlsMaterialsFromDisk as x, substituteEnvVarsFromStateAsync as xt, filterRoutesByApiIdentifiers as y, substituteAgainstStateAsync as yt, buildCorsConfigFromCloudFrontChain as z, parseSelectionExpressionPath as zt };
|
|
20984
|
+
//# sourceMappingURL=local-list-Dklrlnu1.js.map
|