cdk-local 0.42.0 → 0.44.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 +38 -200
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +22 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/{local-list-qObdm4O8.js → local-list-EweOsy3A.js} +621 -162
- package/dist/local-list-EweOsy3A.js.map +1 -0
- package/package.json +1 -1
- package/dist/local-list-qObdm4O8.js.map +0 -1
|
@@ -1278,7 +1278,7 @@ function notFoundError$2(target, stack, resources) {
|
|
|
1278
1278
|
function extractRuntimeProperties(stack, logicalId, resource, resources, imageContext) {
|
|
1279
1279
|
const props = resource.Properties ?? {};
|
|
1280
1280
|
const protocol = extractProtocol(props["ProtocolConfiguration"], logicalId, stack.stackName);
|
|
1281
|
-
const
|
|
1281
|
+
const artifact = extractArtifact(props["AgentRuntimeArtifact"], logicalId, stack.stackName, resources, stack.region, imageContext);
|
|
1282
1282
|
const environmentVariables = props["EnvironmentVariables"] && typeof props["EnvironmentVariables"] === "object" && !Array.isArray(props["EnvironmentVariables"]) ? props["EnvironmentVariables"] : {};
|
|
1283
1283
|
const roleArn = typeof props["RoleArn"] === "string" ? props["RoleArn"] : void 0;
|
|
1284
1284
|
const jwtAuthorizer = extractJwtAuthorizer(props["AuthorizerConfiguration"], logicalId);
|
|
@@ -1286,7 +1286,7 @@ function extractRuntimeProperties(stack, logicalId, resource, resources, imageCo
|
|
|
1286
1286
|
stack,
|
|
1287
1287
|
logicalId,
|
|
1288
1288
|
resource,
|
|
1289
|
-
containerUri,
|
|
1289
|
+
...artifact.kind === "container" ? { containerUri: artifact.containerUri } : { codeArtifact: artifact.codeArtifact },
|
|
1290
1290
|
environmentVariables,
|
|
1291
1291
|
protocol,
|
|
1292
1292
|
...roleArn !== void 0 && { roleArn },
|
|
@@ -1330,19 +1330,50 @@ function extractProtocol(value, logicalId, stackName) {
|
|
|
1330
1330
|
return value;
|
|
1331
1331
|
}
|
|
1332
1332
|
/**
|
|
1333
|
-
*
|
|
1334
|
-
*
|
|
1335
|
-
*
|
|
1333
|
+
* Resolve `AgentRuntimeArtifact` to either a container image URI or a code
|
|
1334
|
+
* artifact (managed runtime). A `ContainerConfiguration` yields the resolved
|
|
1335
|
+
* `ContainerUri`; a `CodeConfiguration` yields its `Runtime` / `EntryPoint` +
|
|
1336
|
+
* the cdk.out asset hash the command uses to locate the bundle source.
|
|
1336
1337
|
*/
|
|
1337
|
-
function
|
|
1338
|
+
function extractArtifact(artifact, logicalId, stackName, resources, region, imageContext) {
|
|
1338
1339
|
if (!artifact || typeof artifact !== "object" || Array.isArray(artifact)) throw new AgentCoreResolutionError(`AgentCore Runtime '${logicalId}' in ${stackName} has no AgentRuntimeArtifact.`);
|
|
1339
1340
|
const art = artifact;
|
|
1340
|
-
if (art["CodeConfiguration"] && !art["ContainerConfiguration"])
|
|
1341
|
+
if (art["CodeConfiguration"] && !art["ContainerConfiguration"]) return {
|
|
1342
|
+
kind: "code",
|
|
1343
|
+
codeArtifact: extractCodeArtifact(art["CodeConfiguration"], logicalId, stackName)
|
|
1344
|
+
};
|
|
1341
1345
|
const container = art["ContainerConfiguration"];
|
|
1342
1346
|
if (!container || typeof container !== "object" || Array.isArray(container)) throw new AgentCoreResolutionError(`AgentCore Runtime '${logicalId}' in ${stackName} has no ContainerConfiguration in its AgentRuntimeArtifact.`);
|
|
1343
1347
|
const uri = resolveImageUri(container["ContainerUri"], resources, region, imageContext);
|
|
1344
1348
|
if (uri === void 0) throw new AgentCoreResolutionError(`AgentCore Runtime '${logicalId}' in ${stackName} has a ContainerConfiguration.ContainerUri that ${getEmbedConfig().cliName} invoke-agentcore cannot resolve. v1 resolves a literal image URI, an Fn::Sub asset URI (the fromAsset / Dockerfile path), and an imported-ECR Fn::Join. A same-stack AWS::ECR::Repository reference is not supported — build the agent as a fromAsset image, or pin a literal / imported ECR image URI.`);
|
|
1345
|
-
return
|
|
1349
|
+
return {
|
|
1350
|
+
kind: "container",
|
|
1351
|
+
containerUri: uri
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
/**
|
|
1355
|
+
* Extract a `CodeConfiguration` (managed-runtime) artifact. Reads `Runtime`,
|
|
1356
|
+
* `EntryPoint`, and the cdk.out file-asset hash from `Code.S3.Prefix`
|
|
1357
|
+
* (`<hash>.zip`, the `fromCodeAsset` shape). A non-literal `Code.S3.Prefix`
|
|
1358
|
+
* (an intrinsic, or a `fromS3` object key the command can't map to a local
|
|
1359
|
+
* asset) hard-errors — downloading a pre-existing S3 bundle is not supported
|
|
1360
|
+
* locally yet.
|
|
1361
|
+
*/
|
|
1362
|
+
function extractCodeArtifact(codeConfig, logicalId, stackName) {
|
|
1363
|
+
const cfg = codeConfig && typeof codeConfig === "object" && !Array.isArray(codeConfig) ? codeConfig : {};
|
|
1364
|
+
const runtime = cfg["Runtime"];
|
|
1365
|
+
if (typeof runtime !== "string" || runtime.length === 0) throw new AgentCoreResolutionError(`AgentCore Runtime '${logicalId}' in ${stackName} has a CodeConfiguration with no string Runtime.`);
|
|
1366
|
+
const entryPointRaw = cfg["EntryPoint"];
|
|
1367
|
+
const entryPoint = Array.isArray(entryPointRaw) ? entryPointRaw.filter((x) => typeof x === "string") : [];
|
|
1368
|
+
if (entryPoint.length === 0) throw new AgentCoreResolutionError(`AgentCore Runtime '${logicalId}' in ${stackName} has a CodeConfiguration with no EntryPoint.`);
|
|
1369
|
+
const s3 = cfg["Code"] && typeof cfg["Code"] === "object" ? cfg["Code"]["S3"] : void 0;
|
|
1370
|
+
const prefix = s3 && typeof s3 === "object" ? s3["Prefix"] : void 0;
|
|
1371
|
+
if (typeof prefix !== "string" || prefix.length === 0) throw new AgentCoreResolutionError(`AgentCore Runtime '${logicalId}' in ${stackName} has a CodeConfiguration whose Code.S3.Prefix is not a literal string. ${getEmbedConfig().cliName} invoke-agentcore runs a local from-source build of the fromCodeAsset bundle; downloading a pre-existing S3 bundle (fromS3) is not supported yet.`);
|
|
1372
|
+
return {
|
|
1373
|
+
runtime,
|
|
1374
|
+
entryPoint,
|
|
1375
|
+
codeAssetHash: prefix.replace(/^.*\//, "").replace(/\.zip$/, "")
|
|
1376
|
+
};
|
|
1346
1377
|
}
|
|
1347
1378
|
/**
|
|
1348
1379
|
* Resolve a `ContainerUri` value to a string. Handles a literal string,
|
|
@@ -8142,6 +8173,116 @@ function createLocalInvokeCommand(opts = {}) {
|
|
|
8142
8173
|
return invoke;
|
|
8143
8174
|
}
|
|
8144
8175
|
|
|
8176
|
+
//#endregion
|
|
8177
|
+
//#region src/local/agentcore-code-build.ts
|
|
8178
|
+
/**
|
|
8179
|
+
* Local from-source build for an AgentCore Runtime `CodeConfiguration`
|
|
8180
|
+
* (managed-runtime) artifact.
|
|
8181
|
+
*
|
|
8182
|
+
* Unlike the container artifact (which ships its own Dockerfile/image), a code
|
|
8183
|
+
* artifact is just source + an `EntryPoint` + a `Runtime`; AWS's managed
|
|
8184
|
+
* runtime runs the entrypoint, which self-serves the AgentCore HTTP contract
|
|
8185
|
+
* (`POST /invocations` + `GET /ping` on 8080) — typically via the
|
|
8186
|
+
* `bedrock-agentcore` SDK. We replicate that locally: generate a Dockerfile
|
|
8187
|
+
* for the runtime's base image, install the bundle's dependencies, and run the
|
|
8188
|
+
* entrypoint. The resulting container speaks the same 8080 contract, so the
|
|
8189
|
+
* existing HTTP client drives it unchanged.
|
|
8190
|
+
*/
|
|
8191
|
+
/** AgentCore CodeConfiguration `Runtime` enum → local Docker base image. */
|
|
8192
|
+
const RUNTIME_BASE_IMAGES = {
|
|
8193
|
+
PYTHON_3_10: "public.ecr.aws/docker/library/python:3.10-slim",
|
|
8194
|
+
PYTHON_3_11: "public.ecr.aws/docker/library/python:3.11-slim",
|
|
8195
|
+
PYTHON_3_12: "public.ecr.aws/docker/library/python:3.12-slim",
|
|
8196
|
+
PYTHON_3_13: "public.ecr.aws/docker/library/python:3.13-slim",
|
|
8197
|
+
PYTHON_3_14: "public.ecr.aws/docker/library/python:3.14-slim",
|
|
8198
|
+
NODE_22: "public.ecr.aws/docker/library/node:22-slim"
|
|
8199
|
+
};
|
|
8200
|
+
/** Runtimes this CLI can build a from-source image for. */
|
|
8201
|
+
const SUPPORTED_CODE_RUNTIMES = Object.keys(RUNTIME_BASE_IMAGES);
|
|
8202
|
+
/**
|
|
8203
|
+
* Build (or, with `noBuild`, verify) a local image for a code artifact and
|
|
8204
|
+
* return its tag. The generated Dockerfile is written to a temp dir and built
|
|
8205
|
+
* with the source dir as the context, so the cdk.out asset is never mutated.
|
|
8206
|
+
*/
|
|
8207
|
+
async function buildAgentCoreCodeImage(options) {
|
|
8208
|
+
const logger = getLogger();
|
|
8209
|
+
const base = RUNTIME_BASE_IMAGES[options.runtime];
|
|
8210
|
+
if (!base) throw new LocalInvokeBuildError(`AgentCore CodeConfiguration runtime '${options.runtime}' is not supported for local execution. Supported runtimes: ${SUPPORTED_CODE_RUNTIMES.join(", ")}.`);
|
|
8211
|
+
const isNode = options.runtime.startsWith("NODE");
|
|
8212
|
+
const dockerfile = renderCodeDockerfile(base, options.entryPoint, isNode);
|
|
8213
|
+
const tag = computeCodeImageTag(options.sourceDir, options.runtime, options.entryPoint, dockerfile);
|
|
8214
|
+
const platform = options.architecture === "x86_64" ? "linux/amd64" : "linux/arm64";
|
|
8215
|
+
if (options.noBuild === true) {
|
|
8216
|
+
logger.info(`Skipping docker build (--no-build). Verifying ${tag} is in local registry...`);
|
|
8217
|
+
if (!await isImageInLocalCache(tag)) throw new LocalInvokeBuildError(`image '${tag}' not in local registry and --no-build is set; remove --no-build or run the build manually first.`);
|
|
8218
|
+
return tag;
|
|
8219
|
+
}
|
|
8220
|
+
logger.info(`Building agent image from source (runtime=${options.runtime}, platform=${platform})...`);
|
|
8221
|
+
logger.debug(`Local tag: ${tag}`);
|
|
8222
|
+
const buildDir = await mkdtemp(join(tmpdir(), `${getEmbedConfig().resourceNamePrefix}-agentcore-code-`));
|
|
8223
|
+
const dockerfilePath = join(buildDir, "Dockerfile");
|
|
8224
|
+
try {
|
|
8225
|
+
await writeFile(dockerfilePath, dockerfile, "utf-8");
|
|
8226
|
+
await runDockerStreaming([
|
|
8227
|
+
"build",
|
|
8228
|
+
"--platform",
|
|
8229
|
+
platform,
|
|
8230
|
+
"--tag",
|
|
8231
|
+
tag,
|
|
8232
|
+
"--file",
|
|
8233
|
+
dockerfilePath,
|
|
8234
|
+
options.sourceDir
|
|
8235
|
+
]);
|
|
8236
|
+
} catch (err) {
|
|
8237
|
+
const stderr = err.stderr?.trim();
|
|
8238
|
+
throw new LocalInvokeBuildError(`docker build failed for AgentCore code artifact (${options.sourceDir})${stderr ? `: ${stderr}` : ""}`);
|
|
8239
|
+
} finally {
|
|
8240
|
+
await rm(buildDir, {
|
|
8241
|
+
recursive: true,
|
|
8242
|
+
force: true
|
|
8243
|
+
}).catch(() => void 0);
|
|
8244
|
+
}
|
|
8245
|
+
return tag;
|
|
8246
|
+
}
|
|
8247
|
+
/**
|
|
8248
|
+
* Render the generated Dockerfile. Dependencies are installed conditionally
|
|
8249
|
+
* (a bundle may vendor them or ship none), and the EntryPoint is mapped to a
|
|
8250
|
+
* CMD: a bare script (`app.py` / `server.js`) is run by the interpreter, while
|
|
8251
|
+
* an explicit launcher (e.g. `opentelemetry-instrument`) is run verbatim.
|
|
8252
|
+
*/
|
|
8253
|
+
function renderCodeDockerfile(base, entryPoint, isNode) {
|
|
8254
|
+
const installStep = isNode ? "RUN if [ -f package.json ]; then npm install --omit=dev; fi" : "RUN if [ -f requirements.txt ]; then pip install --no-cache-dir -r requirements.txt; elif [ -f pyproject.toml ]; then pip install --no-cache-dir .; fi";
|
|
8255
|
+
return [
|
|
8256
|
+
`FROM ${base}`,
|
|
8257
|
+
"WORKDIR /app",
|
|
8258
|
+
"COPY . /app",
|
|
8259
|
+
installStep,
|
|
8260
|
+
"EXPOSE 8080",
|
|
8261
|
+
`CMD ${JSON.stringify(toCmdArgv(entryPoint, isNode))}`
|
|
8262
|
+
].join("\n") + "\n";
|
|
8263
|
+
}
|
|
8264
|
+
/**
|
|
8265
|
+
* Map the EntryPoint argv to a Docker CMD argv. The managed runtime execs the
|
|
8266
|
+
* entrypoint as the program; a bare script file is run by the language
|
|
8267
|
+
* interpreter (`python` / `node`), while a non-script first token (a launcher
|
|
8268
|
+
* already on PATH, e.g. `opentelemetry-instrument`) is run verbatim.
|
|
8269
|
+
*/
|
|
8270
|
+
function toCmdArgv(entryPoint, isNode) {
|
|
8271
|
+
const first = entryPoint[0] ?? "";
|
|
8272
|
+
if (!(isNode ? /\.[cm]?js$/.test(first) : /\.py$/.test(first))) return entryPoint;
|
|
8273
|
+
return [isNode ? "node" : "python", ...entryPoint];
|
|
8274
|
+
}
|
|
8275
|
+
/** Deterministic local tag, stable for identical source + runtime + entrypoint. */
|
|
8276
|
+
function computeCodeImageTag(sourceDir, runtime, entryPoint, dockerfile) {
|
|
8277
|
+
const hash = createHash("sha256").update([
|
|
8278
|
+
sourceDir,
|
|
8279
|
+
runtime,
|
|
8280
|
+
entryPoint.join(" "),
|
|
8281
|
+
dockerfile
|
|
8282
|
+
].join("\0")).digest("hex").slice(0, 16);
|
|
8283
|
+
return `${getEmbedConfig().resourceNamePrefix}-agentcore-code-${hash}`;
|
|
8284
|
+
}
|
|
8285
|
+
|
|
8145
8286
|
//#endregion
|
|
8146
8287
|
//#region src/local/agentcore-client.ts
|
|
8147
8288
|
/**
|
|
@@ -17461,9 +17602,10 @@ function createLocalStartApiCommand(opts = {}) {
|
|
|
17461
17602
|
* locally and invoke it once over the AgentCore HTTP contract. Resolves
|
|
17462
17603
|
* the `AWS::BedrockAgentCore::Runtime`, pulls / builds its container,
|
|
17463
17604
|
* starts it on port 8080, waits for `GET /ping`, POSTs the event to
|
|
17464
|
-
* `POST /invocations`, prints the response, and tears down.
|
|
17465
|
-
* container artifact
|
|
17466
|
-
*
|
|
17605
|
+
* `POST /invocations`, prints the response, and tears down. Covers the
|
|
17606
|
+
* container artifact and the CodeConfiguration managed-runtime artifact
|
|
17607
|
+
* (fromCodeAsset, built from source) on the HTTP + MCP protocols; the agent's
|
|
17608
|
+
* calls to real AWS go to real AWS (credentials injected like `cdkl invoke`).
|
|
17467
17609
|
*/
|
|
17468
17610
|
async function localInvokeAgentCoreCommand(target, options, extraStateProviders) {
|
|
17469
17611
|
const logger = getLogger();
|
|
@@ -17606,36 +17748,74 @@ async function resolveInboundAuthorization(resolved, options) {
|
|
|
17606
17748
|
return header;
|
|
17607
17749
|
}
|
|
17608
17750
|
/**
|
|
17609
|
-
* Acquire the agent
|
|
17610
|
-
*
|
|
17611
|
-
*
|
|
17751
|
+
* Acquire the agent image. A CODE artifact (managed runtime) is built from
|
|
17752
|
+
* source (generated Dockerfile over the bundle's cdk.out asset). A CONTAINER
|
|
17753
|
+
* artifact mirrors the container-Lambda path: build from a local cdk.out asset
|
|
17754
|
+
* when the URI matches one, else pull from ECR, else pull a plain registry image.
|
|
17612
17755
|
*/
|
|
17613
17756
|
async function resolveAgentCoreImage(resolved, options) {
|
|
17614
17757
|
const logger = getLogger();
|
|
17615
17758
|
const architecture = platformToArchitecture(options.platform);
|
|
17759
|
+
if (resolved.codeArtifact) return resolveAgentCoreCodeImage(resolved, resolved.codeArtifact, options, architecture);
|
|
17760
|
+
const containerUri = resolved.containerUri;
|
|
17761
|
+
if (containerUri === void 0) throw new CdkLocalError(`AgentCore Runtime '${resolved.logicalId}' has neither a container image nor a code artifact to run.`, "LOCAL_INVOKE_AGENTCORE_NO_ARTIFACT");
|
|
17616
17762
|
const manifestPath = resolved.stack.assetManifestPath;
|
|
17617
17763
|
if (manifestPath) {
|
|
17618
17764
|
const cdkOutDir = dirname(manifestPath);
|
|
17619
17765
|
const manifest = await new AssetManifestLoader().loadManifest(cdkOutDir, resolved.stack.stackName);
|
|
17620
17766
|
if (manifest) {
|
|
17621
|
-
const entry = getDockerImageBySourceHash(manifest,
|
|
17767
|
+
const entry = getDockerImageBySourceHash(manifest, containerUri);
|
|
17622
17768
|
if (entry) return buildContainerImage(entry.asset, cdkOutDir, {
|
|
17623
17769
|
architecture,
|
|
17624
17770
|
noBuild: options.build === false
|
|
17625
17771
|
});
|
|
17626
17772
|
}
|
|
17627
17773
|
}
|
|
17628
|
-
if (parseEcrUri(
|
|
17629
|
-
logger.info(`Pulling agent image from ECR: ${
|
|
17630
|
-
return pullEcrImage(
|
|
17774
|
+
if (parseEcrUri(containerUri)) {
|
|
17775
|
+
logger.info(`Pulling agent image from ECR: ${containerUri}`);
|
|
17776
|
+
return pullEcrImage(containerUri, {
|
|
17631
17777
|
skipPull: options.pull === false,
|
|
17632
17778
|
...options.region !== void 0 && { region: options.region },
|
|
17633
17779
|
...options.ecrRoleArn !== void 0 && { ecrRoleArn: options.ecrRoleArn },
|
|
17634
17780
|
...options.profile !== void 0 && { profile: options.profile }
|
|
17635
17781
|
});
|
|
17636
17782
|
}
|
|
17637
|
-
await pullImage(
|
|
17638
|
-
return
|
|
17783
|
+
await pullImage(containerUri, options.pull === false);
|
|
17784
|
+
return containerUri;
|
|
17785
|
+
}
|
|
17786
|
+
/**
|
|
17787
|
+
* Build a local image from a `CodeConfiguration` (managed-runtime) bundle:
|
|
17788
|
+
* locate the fromCodeAsset source dir in cdk.out via its asset hash, then run
|
|
17789
|
+
* the from-source build (generated Dockerfile → install deps → run EntryPoint).
|
|
17790
|
+
* A bundle with no local asset (fromS3) hard-errors — not supported yet.
|
|
17791
|
+
*/
|
|
17792
|
+
async function resolveAgentCoreCodeImage(resolved, code, options, architecture) {
|
|
17793
|
+
const manifestPath = resolved.stack.assetManifestPath;
|
|
17794
|
+
if (!manifestPath) throw new CdkLocalError(`AgentCore Runtime '${resolved.logicalId}' uses a code artifact, but its stack has no asset manifest in cdk.out to read the bundle source from.`, "LOCAL_INVOKE_AGENTCORE_CODE_NO_MANIFEST");
|
|
17795
|
+
const cdkOutDir = dirname(manifestPath);
|
|
17796
|
+
const loader = new AssetManifestLoader();
|
|
17797
|
+
const manifest = await loader.loadManifest(cdkOutDir, resolved.stack.stackName);
|
|
17798
|
+
const fileAssets = manifest ? loader.getFileAssets(manifest) : void 0;
|
|
17799
|
+
const asset = fileAssets ? fileAssets.get(code.codeAssetHash) ?? findFileAssetByObjectKey(fileAssets, code.codeAssetHash) : void 0;
|
|
17800
|
+
if (!asset) throw new CdkLocalError(`AgentCore Runtime '${resolved.logicalId}' code bundle (asset ${code.codeAssetHash}) was not found in the cdk.out asset manifest. ${getEmbedConfig().cliName} invoke-agentcore runs a local from-source build of a fromCodeAsset bundle; a fromS3 bundle (a pre-existing S3 object) is not supported yet.`, "LOCAL_INVOKE_AGENTCORE_CODE_ASSET_NOT_FOUND");
|
|
17801
|
+
const sourceDir = loader.getAssetSourcePath(cdkOutDir, asset);
|
|
17802
|
+
if (!existsSync(sourceDir) || !statSync(sourceDir).isDirectory()) throw new CdkLocalError(`AgentCore Runtime '${resolved.logicalId}' code bundle source '${sourceDir}' does not exist or is not a directory. Re-synthesize the app and retry.`, "LOCAL_INVOKE_AGENTCORE_CODE_SOURCE_MISSING");
|
|
17803
|
+
return buildAgentCoreCodeImage({
|
|
17804
|
+
sourceDir,
|
|
17805
|
+
runtime: code.runtime,
|
|
17806
|
+
entryPoint: code.entryPoint,
|
|
17807
|
+
architecture,
|
|
17808
|
+
noBuild: options.build === false
|
|
17809
|
+
});
|
|
17810
|
+
}
|
|
17811
|
+
/**
|
|
17812
|
+
* Find the file asset whose destination objectKey is `<hash>.zip` (matching the
|
|
17813
|
+
* `Code.S3.Prefix`'s hash) when the source-hash-keyed lookup misses — covers a
|
|
17814
|
+
* synthesizer whose source hash differs from the destination objectKey.
|
|
17815
|
+
*/
|
|
17816
|
+
function findFileAssetByObjectKey(fileAssets, hash) {
|
|
17817
|
+
const zip = `${hash}.zip`;
|
|
17818
|
+
for (const asset of fileAssets.values()) if (Object.values(asset.destinations).some((d) => d.objectKey === zip || d.objectKey.endsWith(`/${zip}`))) return asset;
|
|
17639
17819
|
}
|
|
17640
17820
|
/**
|
|
17641
17821
|
* Build the container env: resolved template env vars (+ `--env-vars`
|
|
@@ -17858,7 +18038,7 @@ function readEnvOverridesFile$2(filePath) {
|
|
|
17858
18038
|
}
|
|
17859
18039
|
function createLocalInvokeAgentCoreCommand(opts = {}) {
|
|
17860
18040
|
setEmbedConfig(opts.embedConfig);
|
|
17861
|
-
const cmd = new Command("invoke-agentcore").description("Run a Bedrock AgentCore Runtime container locally and invoke it once over its protocol contract: HTTP (POST /invocations + GET /ping on 8080) or MCP (POST /mcp Streamable HTTP on 8000). Resolves the AWS::BedrockAgentCore::Runtime, pulls/builds its container, injects env vars + AWS credentials, and prints the response. For an MCP runtime, runs the session handshake then sends one JSON-RPC request (tools/list by default, or the method/params from --event). Target accepts a CDK display path (MyStack/MyAgent) or stack-qualified logical ID (MyStack:MyAgentRuntime1234). Single-stack apps may omit the stack prefix. Omit <target> in an interactive terminal to pick from a list. Supports the container artifact on the HTTP + MCP protocols; the agent calls real AWS for managed services.").argument("[target]", "CDK display path or stack-qualified logical ID of the AgentCore Runtime to invoke (omit to pick interactively in a TTY)").addOption(new Option("-e, --event <file>", "JSON event payload file (default: {})")).addOption(new Option("--event-stdin", "Read event JSON from stdin").default(false)).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"LogicalId\":{\"KEY\":\"VALUE\"}})")).addOption(new Option("--session-id <id>", "AgentCore runtime session id header value (default: a random UUID)")).addOption(new Option("--bearer-token <jwt>", "Bearer JWT to present when the runtime declares a customJwtAuthorizer. Verified against the runtime OIDC discovery URL (signature / issuer / expiry / audience) before the container starts, then forwarded to /invocations as Authorization: Bearer <jwt>.")).addOption(new Option("--no-verify-auth", "Skip inbound JWT verification even when the runtime declares a customJwtAuthorizer (local-dev escape hatch). A --bearer-token, if given, is still forwarded.")).addOption(new Option("--platform <platform>", "docker --platform for the agent container (linux/amd64 or linux/arm64)").choices(["linux/amd64", "linux/arm64"]).default("linux/arm64")).addOption(new Option("--no-pull", "Skip docker pull (use cached image) — no-op for the local-build path")).addOption(new Option("--no-build", "Skip docker build on the local-asset path (use the previously-built tag). No-op for the ECR / registry pull paths.")).addOption(new Option("--container-host <host>", "Host to bind the agent port to").default("127.0.0.1")).addOption(new Option("--assume-role [arn]", "Assume the runtime's execution role and forward STS-issued temp credentials to the container so the agent runs with the deployed role. Three forms: (1) `--assume-role <arn>` assumes the explicit ARN; (2) `--assume-role` (bare) uses the runtime's RoleArn when it is a literal ARN; (3) `--no-assume-role` opts out. Off by default — the developer's shell credentials are forwarded unchanged.")).addOption(new Option("--ecr-role-arn <arn>", "Role ARN to assume before authenticating against ECR for cross-account / centralized registries. Same-account / same-region pulls do not need this flag.")).addOption(new Option("--from-cfn-stack [cfn-stack-name]", "Read a deployed CloudFormation stack via ListStackResources and substitute Ref / Fn::ImportValue in env vars with the deployed physical IDs / exports. Bare form uses the resolved stack name; pass an explicit value when the CFn stack name differs.")).addOption(new Option("--stack-region <region>", "Region of the state record to read. Used with --from-cfn-stack as the CFn client region.")).action(withErrorHandling(async (target, options) => {
|
|
18041
|
+
const cmd = new Command("invoke-agentcore").description("Run a Bedrock AgentCore Runtime container locally and invoke it once over its protocol contract: HTTP (POST /invocations + GET /ping on 8080) or MCP (POST /mcp Streamable HTTP on 8000). Resolves the AWS::BedrockAgentCore::Runtime, pulls/builds its container, injects env vars + AWS credentials, and prints the response. For an MCP runtime, runs the session handshake then sends one JSON-RPC request (tools/list by default, or the method/params from --event). Target accepts a CDK display path (MyStack/MyAgent) or stack-qualified logical ID (MyStack:MyAgentRuntime1234). Single-stack apps may omit the stack prefix. Omit <target> in an interactive terminal to pick from a list. Supports the container artifact and the CodeConfiguration managed-runtime artifact (fromCodeAsset, built from source) on the HTTP + MCP protocols; the agent calls real AWS for managed services.").argument("[target]", "CDK display path or stack-qualified logical ID of the AgentCore Runtime to invoke (omit to pick interactively in a TTY)").addOption(new Option("-e, --event <file>", "JSON event payload file (default: {})")).addOption(new Option("--event-stdin", "Read event JSON from stdin").default(false)).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"LogicalId\":{\"KEY\":\"VALUE\"}})")).addOption(new Option("--session-id <id>", "AgentCore runtime session id header value (default: a random UUID)")).addOption(new Option("--bearer-token <jwt>", "Bearer JWT to present when the runtime declares a customJwtAuthorizer. Verified against the runtime OIDC discovery URL (signature / issuer / expiry / audience) before the container starts, then forwarded to /invocations as Authorization: Bearer <jwt>.")).addOption(new Option("--no-verify-auth", "Skip inbound JWT verification even when the runtime declares a customJwtAuthorizer (local-dev escape hatch). A --bearer-token, if given, is still forwarded.")).addOption(new Option("--platform <platform>", "docker --platform for the agent container (linux/amd64 or linux/arm64)").choices(["linux/amd64", "linux/arm64"]).default("linux/arm64")).addOption(new Option("--no-pull", "Skip docker pull (use cached image) — no-op for the local-build path")).addOption(new Option("--no-build", "Skip docker build on the local-asset path (use the previously-built tag). No-op for the ECR / registry pull paths.")).addOption(new Option("--container-host <host>", "Host to bind the agent port to").default("127.0.0.1")).addOption(new Option("--assume-role [arn]", "Assume the runtime's execution role and forward STS-issued temp credentials to the container so the agent runs with the deployed role. Three forms: (1) `--assume-role <arn>` assumes the explicit ARN; (2) `--assume-role` (bare) uses the runtime's RoleArn when it is a literal ARN; (3) `--no-assume-role` opts out. Off by default — the developer's shell credentials are forwarded unchanged.")).addOption(new Option("--ecr-role-arn <arn>", "Role ARN to assume before authenticating against ECR for cross-account / centralized registries. Same-account / same-region pulls do not need this flag.")).addOption(new Option("--from-cfn-stack [cfn-stack-name]", "Read a deployed CloudFormation stack via ListStackResources and substitute Ref / Fn::ImportValue in env vars with the deployed physical IDs / exports. Bare form uses the resolved stack name; pass an explicit value when the CFn stack name differs.")).addOption(new Option("--stack-region <region>", "Region of the state record to read. Used with --from-cfn-stack as the CFn client region.")).action(withErrorHandling(async (target, options) => {
|
|
17862
18042
|
await localInvokeAgentCoreCommand(target, options, opts.extraStateProviders);
|
|
17863
18043
|
}));
|
|
17864
18044
|
[
|
|
@@ -20468,9 +20648,26 @@ async function startFrontDoorServer(opts) {
|
|
|
20468
20648
|
}
|
|
20469
20649
|
function handleProxyRequest(req, res, opts) {
|
|
20470
20650
|
return new Promise((resolve) => {
|
|
20471
|
-
const
|
|
20651
|
+
const url = req.url ?? "/";
|
|
20652
|
+
const action = opts.route({
|
|
20653
|
+
path: url,
|
|
20654
|
+
...hostHeader(req)
|
|
20655
|
+
});
|
|
20656
|
+
if (!action) {
|
|
20657
|
+
writeError(res, 404, `No listener rule matched '${url}' on ${opts.label}, and the listener has no default action forwarding to a local target.`);
|
|
20658
|
+
resolve();
|
|
20659
|
+
return;
|
|
20660
|
+
}
|
|
20661
|
+
if (action.kind === "redirect" || action.kind === "fixed-response") {
|
|
20662
|
+
req.resume();
|
|
20663
|
+
if (action.kind === "redirect") writeRedirect(res, action, req, opts.listenerPort);
|
|
20664
|
+
else writeFixedResponse(res, action);
|
|
20665
|
+
resolve();
|
|
20666
|
+
return;
|
|
20667
|
+
}
|
|
20668
|
+
const pool = pickWeightedPool(action.pools);
|
|
20472
20669
|
if (!pool) {
|
|
20473
|
-
writeError(res,
|
|
20670
|
+
writeError(res, 502, `No forward target selected behind ${opts.label} (every weighted target has weight 0).`);
|
|
20474
20671
|
resolve();
|
|
20475
20672
|
return;
|
|
20476
20673
|
}
|
|
@@ -20523,6 +20720,77 @@ function handleProxyRequest(req, res, opts) {
|
|
|
20523
20720
|
req.pipe(proxyReq);
|
|
20524
20721
|
});
|
|
20525
20722
|
}
|
|
20723
|
+
/** Extract the request `Host` header (string) for host-header rule matching. */
|
|
20724
|
+
function hostHeader(req) {
|
|
20725
|
+
const raw = req.headers.host;
|
|
20726
|
+
const host = Array.isArray(raw) ? raw[0] : raw;
|
|
20727
|
+
return host ? { host } : {};
|
|
20728
|
+
}
|
|
20729
|
+
/**
|
|
20730
|
+
* Pick one pool from a weighted set: weighted random over the non-zero weights.
|
|
20731
|
+
* A single-entry set short-circuits to that pool. Returns `undefined` when
|
|
20732
|
+
* every weight is 0 (an ALB-valid but un-routable forward).
|
|
20733
|
+
*/
|
|
20734
|
+
function pickWeightedPool(pools) {
|
|
20735
|
+
if (pools.length === 0) return void 0;
|
|
20736
|
+
if (pools.length === 1) return pools[0].weight > 0 ? pools[0].pool : void 0;
|
|
20737
|
+
const total = pools.reduce((sum, p) => sum + Math.max(0, p.weight), 0);
|
|
20738
|
+
if (total <= 0) return void 0;
|
|
20739
|
+
let roll = Math.random() * total;
|
|
20740
|
+
for (const p of pools) {
|
|
20741
|
+
const w = Math.max(0, p.weight);
|
|
20742
|
+
if (w === 0) continue;
|
|
20743
|
+
roll -= w;
|
|
20744
|
+
if (roll < 0) return p.pool;
|
|
20745
|
+
}
|
|
20746
|
+
for (let i = pools.length - 1; i >= 0; i--) if (Math.max(0, pools[i].weight) > 0) return pools[i].pool;
|
|
20747
|
+
}
|
|
20748
|
+
/**
|
|
20749
|
+
* Synthesize an ALB-style redirect (301 / 302). ALB builds the `Location` from
|
|
20750
|
+
* the action fields with `#{protocol}` / `#{host}` / `#{port}` / `#{path}` /
|
|
20751
|
+
* `#{query}` placeholders filled from the incoming request. We resolve those
|
|
20752
|
+
* placeholders against the request the front-door received.
|
|
20753
|
+
*/
|
|
20754
|
+
function writeRedirect(res, action, req, listenerPort) {
|
|
20755
|
+
const location = buildRedirectLocation(action, req, listenerPort);
|
|
20756
|
+
res.writeHead(action.statusCode, {
|
|
20757
|
+
location,
|
|
20758
|
+
"content-type": "text/plain; charset=utf-8",
|
|
20759
|
+
"content-length": "0"
|
|
20760
|
+
});
|
|
20761
|
+
res.end();
|
|
20762
|
+
}
|
|
20763
|
+
/** Build the `Location` URL for a redirect action, resolving ALB `#{...}` placeholders. */
|
|
20764
|
+
function buildRedirectLocation(action, req, listenerPort) {
|
|
20765
|
+
const url = req.url ?? "/";
|
|
20766
|
+
const qIndex = url.indexOf("?");
|
|
20767
|
+
const reqPath = qIndex === -1 ? url : url.slice(0, qIndex);
|
|
20768
|
+
const reqQuery = qIndex === -1 ? "" : url.slice(qIndex + 1);
|
|
20769
|
+
const rawHost = req.headers["host"];
|
|
20770
|
+
const placeholders = {
|
|
20771
|
+
protocol: "http",
|
|
20772
|
+
host: ((Array.isArray(rawHost) ? rawHost[0] : rawHost) ?? "").split(":")[0] ?? "",
|
|
20773
|
+
port: String(listenerPort),
|
|
20774
|
+
path: reqPath.replace(/^\//, ""),
|
|
20775
|
+
query: reqQuery
|
|
20776
|
+
};
|
|
20777
|
+
const fill = (template) => template.replace(/#\{(protocol|host|port|path|query)\}/g, (_m, key) => placeholders[key] ?? "");
|
|
20778
|
+
const protocol = (action.protocol ? fill(action.protocol) : placeholders["protocol"]).toLowerCase();
|
|
20779
|
+
const host = action.host ? fill(action.host) : placeholders["host"];
|
|
20780
|
+
const port = action.port ? fill(action.port) : placeholders["port"];
|
|
20781
|
+
const path = fill(action.path ?? "/#{path}");
|
|
20782
|
+
const query = fill(action.query ?? "#{query}");
|
|
20783
|
+
return `${protocol}://${protocol === "http" && port === "80" || protocol === "https" && port === "443" || port === "" ? host : `${host}:${port}`}${path.startsWith("/") ? path : `/${path}`}${query ? `?${query}` : ""}`;
|
|
20784
|
+
}
|
|
20785
|
+
/** Synthesize an ALB-style fixed-response. */
|
|
20786
|
+
function writeFixedResponse(res, action) {
|
|
20787
|
+
const body = action.messageBody ?? "";
|
|
20788
|
+
res.writeHead(action.statusCode, {
|
|
20789
|
+
"content-type": action.contentType ?? "text/plain; charset=utf-8",
|
|
20790
|
+
"content-length": String(Buffer.byteLength(body))
|
|
20791
|
+
});
|
|
20792
|
+
res.end(body);
|
|
20793
|
+
}
|
|
20526
20794
|
/** Standard hop-by-hop headers (RFC 7230 §6.1) — a proxy must not forward these. */
|
|
20527
20795
|
const HOP_BY_HOP_HEADERS = [
|
|
20528
20796
|
"connection",
|
|
@@ -20571,21 +20839,39 @@ function writeError(res, statusCode, message) {
|
|
|
20571
20839
|
//#endregion
|
|
20572
20840
|
//#region src/local/alb-path-matcher.ts
|
|
20573
20841
|
/**
|
|
20574
|
-
* Return the target of the highest-priority rule whose
|
|
20575
|
-
* `
|
|
20576
|
-
*
|
|
20842
|
+
* Return the target of the highest-priority rule whose conditions all match
|
|
20843
|
+
* `req`, or `undefined` when none match (caller uses the default). Rules are
|
|
20844
|
+
* evaluated in ascending priority; the input order is irrelevant.
|
|
20845
|
+
*
|
|
20846
|
+
* Accepts either a request facts object or a bare path string (the path-only
|
|
20847
|
+
* form keeps the original signature working for callers that have no Host).
|
|
20577
20848
|
*/
|
|
20578
|
-
function matchAlbPathRule(
|
|
20579
|
-
const path =
|
|
20849
|
+
function matchAlbPathRule(req, rules) {
|
|
20850
|
+
const { path, host } = typeof req === "string" ? {
|
|
20851
|
+
path: req,
|
|
20852
|
+
host: void 0
|
|
20853
|
+
} : req;
|
|
20854
|
+
const requestPath = pathOf(path);
|
|
20855
|
+
const requestHost = host !== void 0 ? hostOf(host) : void 0;
|
|
20580
20856
|
const ordered = [...rules].sort((a, b) => a.priority - b.priority);
|
|
20581
|
-
for (const rule of ordered) if (rule
|
|
20857
|
+
for (const rule of ordered) if (ruleMatches(rule, requestPath, requestHost)) return rule.target;
|
|
20582
20858
|
}
|
|
20583
20859
|
/**
|
|
20584
|
-
* Whether a single
|
|
20585
|
-
*
|
|
20860
|
+
* Whether a single rule's conditions all match. A `path-pattern` /
|
|
20861
|
+
* `host-header` condition is satisfied when ANY of its values match (OR); a
|
|
20862
|
+
* rule with both fields requires both to match (AND). An empty pattern list
|
|
20863
|
+
* for a field means "no constraint on that field" (the condition was absent).
|
|
20586
20864
|
*/
|
|
20587
|
-
function
|
|
20588
|
-
|
|
20865
|
+
function ruleMatches(rule, requestPath, requestHost) {
|
|
20866
|
+
if (rule.pathPatterns.length > 0) {
|
|
20867
|
+
if (!rule.pathPatterns.some((pattern) => globToRegExp(pattern, false).test(requestPath))) return false;
|
|
20868
|
+
}
|
|
20869
|
+
const hostPatterns = rule.hostPatterns ?? [];
|
|
20870
|
+
if (hostPatterns.length > 0) {
|
|
20871
|
+
if (requestHost === void 0) return false;
|
|
20872
|
+
if (!hostPatterns.some((pattern) => globToRegExp(pattern, true).test(requestHost))) return false;
|
|
20873
|
+
}
|
|
20874
|
+
return true;
|
|
20589
20875
|
}
|
|
20590
20876
|
/** Strip the query string / fragment so only the URL path is matched. */
|
|
20591
20877
|
function pathOf(url) {
|
|
@@ -20596,15 +20882,33 @@ function pathOf(url) {
|
|
|
20596
20882
|
if (h !== -1 && h < end) end = h;
|
|
20597
20883
|
return url.slice(0, end);
|
|
20598
20884
|
}
|
|
20885
|
+
/**
|
|
20886
|
+
* Normalize a `Host` header for matching: drop any `:port` suffix and lower-case
|
|
20887
|
+
* it (DNS hostnames are case-insensitive). IPv6 literals (`[::1]:8080`) keep the
|
|
20888
|
+
* bracketed address and only the trailing port is removed.
|
|
20889
|
+
*/
|
|
20890
|
+
function hostOf(host) {
|
|
20891
|
+
const trimmed = host.trim();
|
|
20892
|
+
if (trimmed.startsWith("[")) {
|
|
20893
|
+
const close = trimmed.indexOf("]");
|
|
20894
|
+
if (close !== -1) return trimmed.slice(0, close + 1).toLowerCase();
|
|
20895
|
+
return trimmed.toLowerCase();
|
|
20896
|
+
}
|
|
20897
|
+
const colon = trimmed.indexOf(":");
|
|
20898
|
+
return (colon === -1 ? trimmed : trimmed.slice(0, colon)).toLowerCase();
|
|
20899
|
+
}
|
|
20599
20900
|
const REGEXP_META = /[.+^${}()|[\]\\]/;
|
|
20600
20901
|
/**
|
|
20601
|
-
* Translate an ALB
|
|
20602
|
-
*
|
|
20603
|
-
*
|
|
20902
|
+
* Translate an ALB `*` / `?` glob into an anchored RegExp: `*` -> `.*`,
|
|
20903
|
+
* `?` -> `.`, every other character is escaped and matched literally. Host
|
|
20904
|
+
* patterns match case-insensitively (the `i` flag) and the pattern is
|
|
20905
|
+
* lower-cased to pair with the lower-cased host; path patterns are
|
|
20906
|
+
* case-sensitive.
|
|
20604
20907
|
*/
|
|
20605
|
-
function globToRegExp(pattern) {
|
|
20908
|
+
function globToRegExp(pattern, caseInsensitive) {
|
|
20909
|
+
const source = caseInsensitive ? pattern.toLowerCase() : pattern;
|
|
20606
20910
|
let body = "";
|
|
20607
|
-
for (const ch of
|
|
20911
|
+
for (const ch of source) if (ch === "*") body += ".*";
|
|
20608
20912
|
else if (ch === "?") body += ".";
|
|
20609
20913
|
else if (REGEXP_META.test(ch)) body += `\\${ch}`;
|
|
20610
20914
|
else body += ch;
|
|
@@ -20848,17 +21152,42 @@ async function buildFrontDoor(plan, containerHost, logger) {
|
|
|
20848
21152
|
}
|
|
20849
21153
|
return entry.pool;
|
|
20850
21154
|
};
|
|
21155
|
+
const toRouteAction = (action) => {
|
|
21156
|
+
if (action.kind === "forward") return {
|
|
21157
|
+
kind: "forward",
|
|
21158
|
+
pools: action.targets.map((t) => ({
|
|
21159
|
+
pool: poolFor(t),
|
|
21160
|
+
weight: t.weight
|
|
21161
|
+
}))
|
|
21162
|
+
};
|
|
21163
|
+
if (action.kind === "redirect") return {
|
|
21164
|
+
kind: "redirect",
|
|
21165
|
+
statusCode: action.statusCode,
|
|
21166
|
+
...action.protocol !== void 0 && { protocol: action.protocol },
|
|
21167
|
+
...action.host !== void 0 && { host: action.host },
|
|
21168
|
+
...action.port !== void 0 && { port: action.port },
|
|
21169
|
+
...action.path !== void 0 && { path: action.path },
|
|
21170
|
+
...action.query !== void 0 && { query: action.query }
|
|
21171
|
+
};
|
|
21172
|
+
return {
|
|
21173
|
+
kind: "fixed-response",
|
|
21174
|
+
statusCode: action.statusCode,
|
|
21175
|
+
...action.contentType !== void 0 && { contentType: action.contentType },
|
|
21176
|
+
...action.messageBody !== void 0 && { messageBody: action.messageBody }
|
|
21177
|
+
};
|
|
21178
|
+
};
|
|
20851
21179
|
try {
|
|
20852
21180
|
for (const listener of plan.listeners) {
|
|
20853
|
-
const
|
|
21181
|
+
const defaultRoute = listener.defaultAction ? toRouteAction(listener.defaultAction) : void 0;
|
|
20854
21182
|
const ruleRoutes = listener.rules.map((r) => ({
|
|
20855
21183
|
priority: r.priority,
|
|
20856
21184
|
pathPatterns: r.pathPatterns,
|
|
20857
|
-
|
|
21185
|
+
hostPatterns: r.hostPatterns,
|
|
21186
|
+
target: toRouteAction(r.action)
|
|
20858
21187
|
}));
|
|
20859
|
-
const
|
|
21188
|
+
const route = (req) => matchAlbPathRule(req, ruleRoutes) ?? defaultRoute;
|
|
20860
21189
|
const server = await startFrontDoorServer({
|
|
20861
|
-
|
|
21190
|
+
route,
|
|
20862
21191
|
port: listener.hostPort,
|
|
20863
21192
|
host: containerHost,
|
|
20864
21193
|
listenerPort: listener.listenerPort,
|
|
@@ -20866,9 +21195,9 @@ async function buildFrontDoor(plan, containerHost, logger) {
|
|
|
20866
21195
|
});
|
|
20867
21196
|
servers.push(server);
|
|
20868
21197
|
logger.info(`ALB front-door: http://${server.host}:${server.port} (listener port ${listener.listenerPort})`);
|
|
20869
|
-
if (listener.
|
|
20870
|
-
for (const r of [...listener.rules].sort((a, b) => a.priority - b.priority)) logger.info(`
|
|
20871
|
-
if (!listener.
|
|
21198
|
+
if (listener.defaultAction) logger.info(` default -> ${describeAction(listener.defaultAction)}`);
|
|
21199
|
+
for (const r of [...listener.rules].sort((a, b) => a.priority - b.priority)) logger.info(` ${describeConditions(r)} (priority ${r.priority}) -> ${describeAction(r.action)}`);
|
|
21200
|
+
if (!listener.defaultAction) logger.info(" (no default action: unmatched requests return 404)");
|
|
20872
21201
|
}
|
|
20873
21202
|
} catch (err) {
|
|
20874
21203
|
await Promise.allSettled(servers.map((s) => s.close()));
|
|
@@ -20889,8 +21218,22 @@ async function buildFrontDoor(plan, containerHost, logger) {
|
|
|
20889
21218
|
frontDoorByService
|
|
20890
21219
|
};
|
|
20891
21220
|
}
|
|
20892
|
-
|
|
20893
|
-
|
|
21221
|
+
/** Human-readable summary of a planned rule's path / host conditions (for the boot banner). */
|
|
21222
|
+
function describeConditions(rule) {
|
|
21223
|
+
const parts = [];
|
|
21224
|
+
if (rule.pathPatterns.length > 0) parts.push(`path ${rule.pathPatterns.join(", ")}`);
|
|
21225
|
+
if (rule.hostPatterns.length > 0) parts.push(`host ${rule.hostPatterns.join(", ")}`);
|
|
21226
|
+
return parts.join(" AND ") || "(no condition)";
|
|
21227
|
+
}
|
|
21228
|
+
/** Human-readable summary of a planned action (for the boot banner). */
|
|
21229
|
+
function describeAction(action) {
|
|
21230
|
+
if (action.kind === "redirect") return `redirect ${action.statusCode}`;
|
|
21231
|
+
if (action.kind === "fixed-response") return `fixed-response ${action.statusCode}`;
|
|
21232
|
+
if (action.targets.length === 1) {
|
|
21233
|
+
const t = action.targets[0];
|
|
21234
|
+
return `${t.serviceTarget} (container ${t.targetContainerName}:${t.targetContainerPort}) (round-robin)`;
|
|
21235
|
+
}
|
|
21236
|
+
return `weighted forward [${action.targets.map((t) => `${t.serviceTarget}@${t.weight}`).join(", ")}]`;
|
|
20894
21237
|
}
|
|
20895
21238
|
async function resolvePlaceholderAccount(arn, region) {
|
|
20896
21239
|
if (!arn.includes("${AWS::AccountId}")) return arn;
|
|
@@ -21095,33 +21438,41 @@ function createLocalStartServiceCommand(opts = {}) {
|
|
|
21095
21438
|
* cdk-local discovers the services behind it (mirroring how `start-api` names
|
|
21096
21439
|
* the API and discovers the backing Lambdas).
|
|
21097
21440
|
*
|
|
21098
|
-
* The synthesized linkage (confirmed against real `cdk synth` of
|
|
21099
|
-
* `
|
|
21441
|
+
* The synthesized linkage (confirmed against real `cdk synth` of an
|
|
21442
|
+
* `ApplicationLoadBalancer` + `addAction` rules):
|
|
21100
21443
|
*
|
|
21101
21444
|
* ```
|
|
21102
21445
|
* ElasticLoadBalancingV2::LoadBalancer (the ALB you name)
|
|
21103
21446
|
* ElasticLoadBalancingV2::Listener : { LoadBalancerArn:{Ref:<ALB>}, Port, Protocol,
|
|
21104
|
-
* DefaultActions:[
|
|
21447
|
+
* DefaultActions:[ <action> ] }
|
|
21105
21448
|
* ElasticLoadBalancingV2::ListenerRule : { ListenerArn:{Ref:<Listener>}, Priority,
|
|
21106
|
-
* Conditions:[{ Field:"path-pattern", PathPatternConfig:{ Values:["/api/*"] } }
|
|
21107
|
-
*
|
|
21449
|
+
* Conditions:[{ Field:"path-pattern", PathPatternConfig:{ Values:["/api/*"] } },
|
|
21450
|
+
* { Field:"host-header", HostHeaderConfig:{ Values:["api.example.com"] } }],
|
|
21451
|
+
* Actions:[ <action> ] }
|
|
21108
21452
|
* ElasticLoadBalancingV2::TargetGroup : { Port, Protocol, TargetType:"ip" }
|
|
21109
21453
|
* ECS::Service.LoadBalancers[] -> { ContainerName, ContainerPort, TargetGroupArn:{Ref:<TG>} }
|
|
21110
21454
|
* ```
|
|
21111
21455
|
*
|
|
21456
|
+
* Each `<action>` is one of:
|
|
21457
|
+
* - `forward` — `{ Type:"forward", TargetGroupArn:{Ref} }` (single target) OR
|
|
21458
|
+
* `{ Type:"forward", ForwardConfig:{ TargetGroups:[{ TargetGroupArn:{Ref}, Weight }] } }`
|
|
21459
|
+
* (one or more weighted targets);
|
|
21460
|
+
* - `redirect` — `{ Type:"redirect", RedirectConfig:{ Protocol/Host/Port/Path/Query/StatusCode } }`;
|
|
21461
|
+
* - `fixed-response` — `{ Type:"fixed-response", FixedResponseConfig:{ StatusCode/ContentType/MessageBody } }`.
|
|
21462
|
+
*
|
|
21112
21463
|
* Resolution walks ALB -> listeners (by `LoadBalancerArn` Ref) -> their default
|
|
21113
|
-
*
|
|
21464
|
+
* action AND any ListenerRules -> for each `forward`, the ECS Service whose
|
|
21114
21465
|
* `LoadBalancers[]` references each target group (a reverse scan; there is no
|
|
21115
21466
|
* direct TG -> service pointer). Output is a per-listener routing table: a
|
|
21116
|
-
* default
|
|
21117
|
-
*
|
|
21467
|
+
* default action plus the ordered rules, each carrying its resolved action and
|
|
21468
|
+
* its path / host conditions.
|
|
21118
21469
|
*
|
|
21119
|
-
* Scope: HTTP listeners
|
|
21120
|
-
*
|
|
21121
|
-
* `TargetType:"lambda"` target
|
|
21122
|
-
*
|
|
21123
|
-
* /
|
|
21124
|
-
* remaining listener-rule features are tracked in #123.
|
|
21470
|
+
* Scope: HTTP listeners; `path-pattern` + `host-header` conditions; `forward`
|
|
21471
|
+
* (single or weighted) to ECS services; `redirect` / `fixed-response` actions.
|
|
21472
|
+
* Skipped with a warning: HTTPS/TLS listeners, `TargetType:"lambda"` target
|
|
21473
|
+
* groups, the other condition fields (http-header / http-request-method /
|
|
21474
|
+
* query-string / source-ip), and `authenticate-cognito` / `authenticate-oidc`
|
|
21475
|
+
* actions. Those remaining listener-rule features are tracked in #123.
|
|
21125
21476
|
*/
|
|
21126
21477
|
const ALB_TYPE = "AWS::ElasticLoadBalancingV2::LoadBalancer";
|
|
21127
21478
|
const LISTENER_TYPE = "AWS::ElasticLoadBalancingV2::Listener";
|
|
@@ -21151,31 +21502,32 @@ function resolveAlbFrontDoor(stack, albLogicalId) {
|
|
|
21151
21502
|
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.`);
|
|
21152
21503
|
continue;
|
|
21153
21504
|
}
|
|
21154
|
-
const
|
|
21505
|
+
const defaultAction = resolveAction(props["DefaultActions"], resources, tgToService, stackName, `Listener '${listenerLogicalId}' (port ${port}) default action`, warnings);
|
|
21155
21506
|
const rules = [];
|
|
21156
21507
|
for (const { ruleLogicalId, ruleProps } of rulesByListener.get(listenerLogicalId) ?? []) {
|
|
21157
21508
|
const priority = parsePriority(ruleProps["Priority"]);
|
|
21158
21509
|
const ruleLabel = `Listener rule '${ruleLogicalId}' (priority ${priority})`;
|
|
21159
|
-
const {
|
|
21510
|
+
const { pathPatterns, hostPatterns, unsupported } = parseRuleConditions(ruleProps["Conditions"]);
|
|
21160
21511
|
if (unsupported.length > 0) {
|
|
21161
|
-
warnings.push(`${ruleLabel} uses unsupported condition(s): ${unsupported.join(", ")}. The local ALB front-door supports path-pattern
|
|
21512
|
+
warnings.push(`${ruleLabel} uses unsupported condition(s): ${unsupported.join(", ")}. The local ALB front-door supports path-pattern and host-header conditions only (http-header / query-string / http-request-method / source-ip deferred). Skipping it.`);
|
|
21162
21513
|
continue;
|
|
21163
21514
|
}
|
|
21164
|
-
if (
|
|
21165
|
-
const
|
|
21166
|
-
if (!
|
|
21515
|
+
if (pathPatterns.length === 0 && hostPatterns.length === 0) continue;
|
|
21516
|
+
const action = resolveAction(ruleProps["Actions"], resources, tgToService, stackName, `${ruleLabel} action`, warnings);
|
|
21517
|
+
if (!action) continue;
|
|
21167
21518
|
rules.push({
|
|
21168
21519
|
priority,
|
|
21169
|
-
pathPatterns
|
|
21170
|
-
|
|
21520
|
+
pathPatterns,
|
|
21521
|
+
hostPatterns,
|
|
21522
|
+
action
|
|
21171
21523
|
});
|
|
21172
21524
|
}
|
|
21173
|
-
if (!
|
|
21525
|
+
if (!defaultAction && rules.length === 0) continue;
|
|
21174
21526
|
listeners.push({
|
|
21175
21527
|
listenerPort: port,
|
|
21176
21528
|
listenerProtocol: "HTTP",
|
|
21177
21529
|
listenerLogicalId,
|
|
21178
|
-
...
|
|
21530
|
+
...defaultAction ? { defaultAction } : {},
|
|
21179
21531
|
rules
|
|
21180
21532
|
});
|
|
21181
21533
|
}
|
|
@@ -21191,41 +21543,102 @@ function isApplicationLoadBalancer(resource) {
|
|
|
21191
21543
|
return type === void 0 || type === "application";
|
|
21192
21544
|
}
|
|
21193
21545
|
/**
|
|
21194
|
-
* Resolve a listener / rule `Actions` (or `DefaultActions`) array to
|
|
21195
|
-
*
|
|
21196
|
-
*
|
|
21546
|
+
* Resolve a listener / rule `Actions` (or `DefaultActions`) array to the single
|
|
21547
|
+
* action the local front-door serves, or `undefined` when it is not resolvable
|
|
21548
|
+
* (a warning is emitted for the cases worth surfacing). ALB allows exactly one
|
|
21549
|
+
* non-authenticate terminal action per action set, optionally preceded by
|
|
21550
|
+
* authenticate-* actions (which the local front-door does not enforce — they
|
|
21551
|
+
* are skipped with a warning and the terminal action is honored).
|
|
21197
21552
|
*/
|
|
21198
|
-
function
|
|
21199
|
-
|
|
21200
|
-
|
|
21201
|
-
|
|
21202
|
-
|
|
21203
|
-
|
|
21204
|
-
|
|
21205
|
-
|
|
21206
|
-
|
|
21553
|
+
function resolveAction(actions, resources, tgToService, stackName, label, warnings) {
|
|
21554
|
+
if (!Array.isArray(actions)) return void 0;
|
|
21555
|
+
let sawAuthenticate = false;
|
|
21556
|
+
for (const action of actions) {
|
|
21557
|
+
if (!action || typeof action !== "object") continue;
|
|
21558
|
+
const a = action;
|
|
21559
|
+
const type = a["Type"];
|
|
21560
|
+
if (type === "authenticate-cognito" || type === "authenticate-oidc") {
|
|
21561
|
+
sawAuthenticate = true;
|
|
21562
|
+
continue;
|
|
21563
|
+
}
|
|
21564
|
+
if (type === "forward") return resolveForwardAction(a, resources, tgToService, stackName, label, warnings);
|
|
21565
|
+
if (type === "redirect") {
|
|
21566
|
+
const redirect = resolveRedirectAction(a, label, warnings);
|
|
21567
|
+
if (redirect) return redirect;
|
|
21568
|
+
continue;
|
|
21569
|
+
}
|
|
21570
|
+
if (type === "fixed-response") return resolveFixedResponseAction(a);
|
|
21571
|
+
if (typeof type === "string") warnings.push(`${label} uses an unsupported action type '${type}'. The local ALB front-door supports forward / redirect / fixed-response actions only. Skipping it.`);
|
|
21207
21572
|
}
|
|
21208
|
-
|
|
21209
|
-
|
|
21210
|
-
|
|
21211
|
-
|
|
21573
|
+
if (sawAuthenticate) warnings.push(`${label} is an authenticate-* action with no local-servable terminal action. The local ALB front-door does not enforce authenticate-cognito / authenticate-oidc; skipping it.`);
|
|
21574
|
+
}
|
|
21575
|
+
/** Resolve a `forward` action into one or more weighted ECS-service targets. */
|
|
21576
|
+
function resolveForwardAction(action, resources, tgToService, stackName, label, warnings) {
|
|
21577
|
+
const refs = collectForwardTargetGroupRefs(action);
|
|
21578
|
+
if (refs.length === 0) {
|
|
21579
|
+
if (hasUnresolvableForward(action)) warnings.push(`${label} forwards to a non-Ref TargetGroupArn (literal / cross-stack / imported); the local front-door only supports in-stack target groups. Skipping it.`);
|
|
21212
21580
|
return;
|
|
21213
21581
|
}
|
|
21214
|
-
|
|
21215
|
-
|
|
21216
|
-
|
|
21582
|
+
const targets = [];
|
|
21583
|
+
for (const { tgRef, weight } of refs) {
|
|
21584
|
+
const tg = resources[tgRef];
|
|
21585
|
+
if (!tg || tg.Type !== TARGET_GROUP_TYPE) {
|
|
21586
|
+
warnings.push(`${label} forwards to target group '${tgRef}', but no ${TARGET_GROUP_TYPE} with that logical id exists in ${stackName}. Skipping that target group.`);
|
|
21587
|
+
continue;
|
|
21588
|
+
}
|
|
21589
|
+
if (tg.Properties?.["TargetType"] === "lambda") {
|
|
21590
|
+
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 that target group.`);
|
|
21591
|
+
continue;
|
|
21592
|
+
}
|
|
21593
|
+
const backing = tgToService.get(tgRef);
|
|
21594
|
+
if (!backing) {
|
|
21595
|
+
warnings.push(`${label} forwards to target group '${tgRef}', which is not referenced by any ${SERVICE_TYPE}.LoadBalancers[] in ${stackName}; cdk-local has no ECS service to front behind it. Skipping that target group.`);
|
|
21596
|
+
continue;
|
|
21597
|
+
}
|
|
21598
|
+
targets.push({
|
|
21599
|
+
serviceLogicalId: backing.serviceLogicalId,
|
|
21600
|
+
targetContainerName: backing.containerName,
|
|
21601
|
+
targetContainerPort: backing.containerPort,
|
|
21602
|
+
targetGroupLogicalId: tgRef,
|
|
21603
|
+
weight
|
|
21604
|
+
});
|
|
21217
21605
|
}
|
|
21218
|
-
|
|
21219
|
-
|
|
21220
|
-
|
|
21606
|
+
if (targets.length === 0) return void 0;
|
|
21607
|
+
return {
|
|
21608
|
+
kind: "forward",
|
|
21609
|
+
targets
|
|
21610
|
+
};
|
|
21611
|
+
}
|
|
21612
|
+
/** Resolve a `redirect` action into its `Location`-template fields + status code. */
|
|
21613
|
+
function resolveRedirectAction(action, label, warnings) {
|
|
21614
|
+
const cfg = action["RedirectConfig"];
|
|
21615
|
+
if (!cfg || typeof cfg !== "object") {
|
|
21616
|
+
warnings.push(`${label} is a redirect with no RedirectConfig; skipping it.`);
|
|
21221
21617
|
return;
|
|
21222
21618
|
}
|
|
21223
|
-
|
|
21224
|
-
|
|
21225
|
-
|
|
21226
|
-
|
|
21227
|
-
|
|
21619
|
+
const c = cfg;
|
|
21620
|
+
const out = {
|
|
21621
|
+
kind: "redirect",
|
|
21622
|
+
statusCode: parseRedirectStatusCode(c["StatusCode"])
|
|
21623
|
+
};
|
|
21624
|
+
if (typeof c["Protocol"] === "string") out.protocol = c["Protocol"];
|
|
21625
|
+
if (typeof c["Host"] === "string") out.host = c["Host"];
|
|
21626
|
+
if (typeof c["Port"] === "string") out.port = c["Port"];
|
|
21627
|
+
if (typeof c["Path"] === "string") out.path = c["Path"];
|
|
21628
|
+
if (typeof c["Query"] === "string") out.query = c["Query"];
|
|
21629
|
+
return out;
|
|
21630
|
+
}
|
|
21631
|
+
/** Resolve a `fixed-response` action into its status / content-type / body. */
|
|
21632
|
+
function resolveFixedResponseAction(action) {
|
|
21633
|
+
const cfg = action["FixedResponseConfig"];
|
|
21634
|
+
const c = cfg && typeof cfg === "object" ? cfg : {};
|
|
21635
|
+
const out = {
|
|
21636
|
+
kind: "fixed-response",
|
|
21637
|
+
statusCode: parseFixedResponseStatusCode(c["StatusCode"])
|
|
21228
21638
|
};
|
|
21639
|
+
if (typeof c["ContentType"] === "string") out.contentType = c["ContentType"];
|
|
21640
|
+
if (typeof c["MessageBody"] === "string") out.messageBody = c["MessageBody"];
|
|
21641
|
+
return out;
|
|
21229
21642
|
}
|
|
21230
21643
|
/**
|
|
21231
21644
|
* Build a `targetGroupLogicalId -> backing ECS service` index by scanning every
|
|
@@ -21272,78 +21685,81 @@ function indexRulesByListener(resources) {
|
|
|
21272
21685
|
return index;
|
|
21273
21686
|
}
|
|
21274
21687
|
/**
|
|
21275
|
-
* Parse a ListenerRule's `Conditions` into its `path-pattern`
|
|
21276
|
-
* field names of any
|
|
21277
|
-
*
|
|
21278
|
-
*
|
|
21279
|
-
*
|
|
21688
|
+
* Parse a ListenerRule's `Conditions` into its supported `path-pattern` and
|
|
21689
|
+
* `host-header` values plus the field names of any unsupported conditions
|
|
21690
|
+
* (which make the rule unsupported). ALB ANDs conditions of different fields,
|
|
21691
|
+
* so a rule with an unsupported field cannot be honored locally. Multiple
|
|
21692
|
+
* conditions of the same field merge their values (each field OR-matches).
|
|
21280
21693
|
*/
|
|
21281
|
-
function
|
|
21282
|
-
const
|
|
21694
|
+
function parseRuleConditions(conditions) {
|
|
21695
|
+
const pathPatterns = [];
|
|
21696
|
+
const hostPatterns = [];
|
|
21283
21697
|
const unsupported = [];
|
|
21284
21698
|
if (!Array.isArray(conditions)) return {
|
|
21285
|
-
|
|
21699
|
+
pathPatterns,
|
|
21700
|
+
hostPatterns,
|
|
21286
21701
|
unsupported
|
|
21287
21702
|
};
|
|
21288
21703
|
for (const cond of conditions) {
|
|
21289
21704
|
if (!cond || typeof cond !== "object") continue;
|
|
21290
21705
|
const c = cond;
|
|
21291
21706
|
const field = typeof c["Field"] === "string" ? c["Field"] : "(unknown)";
|
|
21292
|
-
if (field
|
|
21293
|
-
|
|
21294
|
-
|
|
21295
|
-
}
|
|
21296
|
-
const cfg = c["PathPatternConfig"];
|
|
21297
|
-
const values = cfg && typeof cfg === "object" && Array.isArray(cfg["Values"]) ? cfg["Values"] : Array.isArray(c["Values"]) ? c["Values"] : [];
|
|
21298
|
-
for (const v of values) if (typeof v === "string") patterns.push(v);
|
|
21707
|
+
if (field === "path-pattern") pathPatterns.push(...conditionValues(c, "PathPatternConfig"));
|
|
21708
|
+
else if (field === "host-header") hostPatterns.push(...conditionValues(c, "HostHeaderConfig"));
|
|
21709
|
+
else unsupported.push(field);
|
|
21299
21710
|
}
|
|
21300
21711
|
return {
|
|
21301
|
-
|
|
21712
|
+
pathPatterns,
|
|
21713
|
+
hostPatterns,
|
|
21302
21714
|
unsupported
|
|
21303
21715
|
};
|
|
21304
21716
|
}
|
|
21305
|
-
|
|
21306
|
-
|
|
21307
|
-
|
|
21308
|
-
|
|
21309
|
-
|
|
21310
|
-
|
|
21311
|
-
|
|
21312
|
-
|
|
21313
|
-
|
|
21314
|
-
|
|
21315
|
-
|
|
21316
|
-
|
|
21317
|
-
|
|
21318
|
-
|
|
21319
|
-
|
|
21320
|
-
|
|
21321
|
-
|
|
21717
|
+
/**
|
|
21718
|
+
* Extract a condition's string values from either the typed `<Field>Config`
|
|
21719
|
+
* sub-object's `Values` or the legacy top-level `Values` array.
|
|
21720
|
+
*/
|
|
21721
|
+
function conditionValues(cond, configKey) {
|
|
21722
|
+
const cfg = cond[configKey];
|
|
21723
|
+
return (cfg && typeof cfg === "object" && Array.isArray(cfg["Values"]) ? cfg["Values"] : Array.isArray(cond["Values"]) ? cond["Values"] : []).filter((v) => typeof v === "string");
|
|
21724
|
+
}
|
|
21725
|
+
/** Collect a forward action's `(targetGroupRef, weight)` pairs (single + ForwardConfig forms). */
|
|
21726
|
+
function collectForwardTargetGroupRefs(action) {
|
|
21727
|
+
const out = [];
|
|
21728
|
+
const direct = refOf(action["TargetGroupArn"]);
|
|
21729
|
+
if (direct) out.push({
|
|
21730
|
+
tgRef: direct,
|
|
21731
|
+
weight: 1
|
|
21732
|
+
});
|
|
21733
|
+
const forwardConfig = action["ForwardConfig"];
|
|
21734
|
+
if (forwardConfig && typeof forwardConfig === "object") {
|
|
21735
|
+
const groups = forwardConfig["TargetGroups"];
|
|
21736
|
+
if (Array.isArray(groups)) for (const g of groups) {
|
|
21737
|
+
if (!g || typeof g !== "object") continue;
|
|
21738
|
+
const gObj = g;
|
|
21739
|
+
const ref = refOf(gObj["TargetGroupArn"]);
|
|
21740
|
+
if (ref) out.push({
|
|
21741
|
+
tgRef: ref,
|
|
21742
|
+
weight: parseWeight(gObj["Weight"])
|
|
21743
|
+
});
|
|
21322
21744
|
}
|
|
21323
21745
|
}
|
|
21324
|
-
return
|
|
21746
|
+
return out;
|
|
21325
21747
|
}
|
|
21326
21748
|
/**
|
|
21327
|
-
* True when `
|
|
21328
|
-
*
|
|
21329
|
-
*
|
|
21330
|
-
*
|
|
21749
|
+
* True when `action` is a `forward` that references a target group via a
|
|
21750
|
+
* NON-`Ref` arn (literal / `Fn::GetAtt` / cross-stack) — i.e. a forward we
|
|
21751
|
+
* could not resolve to an in-stack target group. Used to warn rather than
|
|
21752
|
+
* silently skip.
|
|
21331
21753
|
*/
|
|
21332
|
-
function hasUnresolvableForward(
|
|
21333
|
-
if (
|
|
21334
|
-
|
|
21335
|
-
|
|
21336
|
-
const
|
|
21337
|
-
if (
|
|
21338
|
-
|
|
21339
|
-
|
|
21340
|
-
|
|
21341
|
-
const groups = forwardConfig["TargetGroups"];
|
|
21342
|
-
if (Array.isArray(groups)) for (const g of groups) {
|
|
21343
|
-
if (!g || typeof g !== "object") continue;
|
|
21344
|
-
const arn = g["TargetGroupArn"];
|
|
21345
|
-
if (arn !== void 0 && refOf(arn) === void 0) return true;
|
|
21346
|
-
}
|
|
21754
|
+
function hasUnresolvableForward(action) {
|
|
21755
|
+
if (action["TargetGroupArn"] !== void 0 && refOf(action["TargetGroupArn"]) === void 0) return true;
|
|
21756
|
+
const forwardConfig = action["ForwardConfig"];
|
|
21757
|
+
if (forwardConfig && typeof forwardConfig === "object") {
|
|
21758
|
+
const groups = forwardConfig["TargetGroups"];
|
|
21759
|
+
if (Array.isArray(groups)) for (const g of groups) {
|
|
21760
|
+
if (!g || typeof g !== "object") continue;
|
|
21761
|
+
const arn = g["TargetGroupArn"];
|
|
21762
|
+
if (arn !== void 0 && refOf(arn) === void 0) return true;
|
|
21347
21763
|
}
|
|
21348
21764
|
}
|
|
21349
21765
|
return false;
|
|
@@ -21365,6 +21781,27 @@ function parseContainerPort(raw) {
|
|
|
21365
21781
|
return parsePort(raw);
|
|
21366
21782
|
}
|
|
21367
21783
|
/**
|
|
21784
|
+
* Parse a `ForwardConfig.TargetGroups[].Weight`. ALB weights are 0-999; a
|
|
21785
|
+
* missing weight defaults to 1 (CDK's `weightedForward` always emits one, but
|
|
21786
|
+
* a hand-rolled template may omit it). Negative / non-numeric clamps to 0.
|
|
21787
|
+
*/
|
|
21788
|
+
function parseWeight(raw) {
|
|
21789
|
+
if (typeof raw === "number" && Number.isFinite(raw)) return raw < 0 ? 0 : raw;
|
|
21790
|
+
if (typeof raw === "string" && /^\d+$/.test(raw)) return parseInt(raw, 10);
|
|
21791
|
+
return 1;
|
|
21792
|
+
}
|
|
21793
|
+
/** ALB emits redirect status as `HTTP_301` / `HTTP_302`; default to 302 when absent / unknown. */
|
|
21794
|
+
function parseRedirectStatusCode(raw) {
|
|
21795
|
+
if (raw === "HTTP_301" || raw === "301" || raw === 301) return 301;
|
|
21796
|
+
return 302;
|
|
21797
|
+
}
|
|
21798
|
+
/** Parse a `FixedResponseConfig.StatusCode` (a numeric string); default 200 when absent. */
|
|
21799
|
+
function parseFixedResponseStatusCode(raw) {
|
|
21800
|
+
if (typeof raw === "number" && Number.isInteger(raw)) return raw;
|
|
21801
|
+
if (typeof raw === "string" && /^\d+$/.test(raw)) return parseInt(raw, 10);
|
|
21802
|
+
return 200;
|
|
21803
|
+
}
|
|
21804
|
+
/**
|
|
21368
21805
|
* Parse a ListenerRule `Priority` (ALB priorities are 1-50000, lower = higher
|
|
21369
21806
|
* precedence). A missing / unparseable priority sorts last so an explicitly
|
|
21370
21807
|
* prioritized rule always wins over it.
|
|
@@ -21463,13 +21900,34 @@ function albStrategy(options) {
|
|
|
21463
21900
|
const { stack, albLogicalId } = resolveAlbTarget(albTarget, stacks);
|
|
21464
21901
|
const resolution = resolveAlbFrontDoor(stack, albLogicalId);
|
|
21465
21902
|
warnings.push(...resolution.warnings);
|
|
21466
|
-
const qualify = (
|
|
21467
|
-
|
|
21468
|
-
|
|
21903
|
+
const qualify = (action) => {
|
|
21904
|
+
if (action.kind === "forward") return {
|
|
21905
|
+
kind: "forward",
|
|
21906
|
+
targets: action.targets.map((t) => {
|
|
21907
|
+
const serviceTarget = `${stack.stackName}:${t.serviceLogicalId}`;
|
|
21908
|
+
serviceTargets.add(serviceTarget);
|
|
21909
|
+
return {
|
|
21910
|
+
serviceTarget,
|
|
21911
|
+
targetContainerName: t.targetContainerName,
|
|
21912
|
+
targetContainerPort: t.targetContainerPort,
|
|
21913
|
+
weight: t.weight
|
|
21914
|
+
};
|
|
21915
|
+
})
|
|
21916
|
+
};
|
|
21917
|
+
if (action.kind === "redirect") return {
|
|
21918
|
+
kind: "redirect",
|
|
21919
|
+
statusCode: action.statusCode,
|
|
21920
|
+
...action.protocol !== void 0 && { protocol: action.protocol },
|
|
21921
|
+
...action.host !== void 0 && { host: action.host },
|
|
21922
|
+
...action.port !== void 0 && { port: action.port },
|
|
21923
|
+
...action.path !== void 0 && { path: action.path },
|
|
21924
|
+
...action.query !== void 0 && { query: action.query }
|
|
21925
|
+
};
|
|
21469
21926
|
return {
|
|
21470
|
-
|
|
21471
|
-
|
|
21472
|
-
|
|
21927
|
+
kind: "fixed-response",
|
|
21928
|
+
statusCode: action.statusCode,
|
|
21929
|
+
...action.contentType !== void 0 && { contentType: action.contentType },
|
|
21930
|
+
...action.messageBody !== void 0 && { messageBody: action.messageBody }
|
|
21473
21931
|
};
|
|
21474
21932
|
};
|
|
21475
21933
|
for (const listener of resolution.listeners) {
|
|
@@ -21483,11 +21941,12 @@ function albStrategy(options) {
|
|
|
21483
21941
|
listeners.push({
|
|
21484
21942
|
listenerPort: listener.listenerPort,
|
|
21485
21943
|
hostPort,
|
|
21486
|
-
...listener.
|
|
21944
|
+
...listener.defaultAction ? { defaultAction: qualify(listener.defaultAction) } : {},
|
|
21487
21945
|
rules: listener.rules.map((r) => ({
|
|
21488
21946
|
priority: r.priority,
|
|
21489
21947
|
pathPatterns: r.pathPatterns,
|
|
21490
|
-
|
|
21948
|
+
hostPatterns: r.hostPatterns,
|
|
21949
|
+
action: qualify(r.action)
|
|
21491
21950
|
}))
|
|
21492
21951
|
});
|
|
21493
21952
|
}
|
|
@@ -21516,7 +21975,7 @@ function albStrategy(options) {
|
|
|
21516
21975
|
*/
|
|
21517
21976
|
function createLocalStartAlbCommand(opts = {}) {
|
|
21518
21977
|
setEmbedConfig(opts.embedConfig);
|
|
21519
|
-
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
|
|
21978
|
+
return addCommonEcsServiceOptions(new Command("start-alb").description("Run an Application Load Balancer locally: name the ALB, and cdk-local boots the ECS service(s) behind its HTTP listeners and stands up a local front-door on each listener port that round-robins across the running replicas and routes its listener rules across the backing services — a stable host endpoint, like behind a real load balancer. The symmetric ALB counterpart of `start-api`. Each <target> accepts a CDK display path (MyStack/MyAlb) or stack-qualified logical ID; single-stack apps may omit the stack prefix. Supports HTTP listeners; path-pattern and host-header rule conditions; forward (single and weighted), redirect, and fixed-response actions. HTTPS listeners, Lambda target groups, the other rule conditions (http-header / query-string / source-ip / http-request-method), and authenticate-* actions are skipped with a warning. Omit <targets> in an interactive terminal to multi-select the load balancers from a list.").argument("[targets...]", "One or more CDK display paths or stack-qualified logical IDs of the AWS::ElasticLoadBalancingV2::LoadBalancer resources to run (omit to multi-select interactively in a TTY)").addOption(new Option("--lb-port <listenerPort=hostPort...>", "Bind the local front-door on a specific host port (e.g. 80=8080); repeatable. Default: host port == ALB listener port. Use this on macOS to remap a privileged listener port (< 1024) to a non-privileged host port.")).action(withErrorHandling(async (targets, options) => {
|
|
21520
21979
|
await runEcsServiceEmulator(targets, options, albStrategy(options), opts.extraStateProviders);
|
|
21521
21980
|
})));
|
|
21522
21981
|
}
|
|
@@ -21592,5 +22051,5 @@ function createLocalListCommand(opts = {}) {
|
|
|
21592
22051
|
}
|
|
21593
22052
|
|
|
21594
22053
|
//#endregion
|
|
21595
|
-
export { createJwksCache as $,
|
|
21596
|
-
//# sourceMappingURL=local-list-
|
|
22054
|
+
export { createJwksCache as $, resolveLambdaArnIntrinsic as $t, invokeTokenAuthorizer as A, substituteAgainstStateAsync as At, VtlEvaluationError as B, resolveCfnRegion as Bt, resolveSelectionExpression as C, architectureToPlatform as Ct, computeRequestIdentityHash as D, resolveRuntimeImage as Dt, buildMethodArn as E, resolveRuntimeFileExtension as Et, buildRestV1Event as F, LocalStateSourceError as Ft, buildMgmtEndpointEnvUrl as G, resolveWatchConfig as Gt, probeHostGatewaySupport as H, CfnLocalStateProvider as Ht, evaluateResponseParameters as I, createLocalStateProvider as It, buildConnectEvent as J, discoverWebSocketApis as Jt, handleConnectionsRequest as K, countTargets as Kt, pickResponseTemplate as L, isCfnFlagPresent as Lt, translateLambdaResponse as M, substituteEnvVarsFromStateAsync as Mt, applyAuthorizerOverlay as N, resolveEnvVars as Nt, evaluateCachedLambdaPolicy as O, EcsTaskResolutionError as Ot, buildHttpApiV2Event as P, materializeLayerFromArn as Pt, buildJwksUrlFromIssuer as Q, pickRefLogicalId as Qt, selectIntegrationResponse as R, rejectExplicitCfnStackWithMultipleStacks as Rt, startApiServer as S, createLocalInvokeCommand as St, defaultCredentialsLoader as T, resolveRuntimeCodeMountPath as Tt, bufferToBody as U, collectSsmParameterRefs as Ut, HOST_GATEWAY_MIN_VERSION as V, resolveCfnStackName as Vt, ConnectionRegistry as W, resolveSsmParameters as Wt, buildMessageEvent as X, parseSelectionExpressionPath as Xt, buildDisconnectEvent as Y, discoverWebSocketApisOrThrow as Yt, buildCognitoJwksUrl as Z, discoverRoutes as Zt, availableApiIdentifiers as _, SUPPORTED_CODE_RUNTIMES as _t, buildCloudMapIndex as a, derivePseudoParametersFromRegion as an, buildCorsConfigByApiId as at, groupRoutesByServer as b, renderCodeDockerfile as bt, createLocalRunTaskCommand as c, LocalInvokeBuildError as cn, matchPreflight as ct, createWatchPredicates as d, MCP_PROTOCOL_VERSION as dt, AGENTCORE_HTTP_PROTOCOL as en, verifyCognitoJwt as et, resolveApiTargetSubset as f, mcpInvokeOnce as ft, buildStageMap as g, waitForAgentCorePing as gt, attachStageContext as h, invokeAgentCore as ht, createLocalStartServiceCommand as i, resolveAgentCoreTarget as in, applyCorsResponseHeaders as it, matchRoute as j, substituteEnvVarsFromState as jt, invokeRequestAuthorizer as k, substituteAgainstState as kt, createLocalInvokeAgentCoreCommand as l, MCP_CONTAINER_PORT as lt, createFileWatcher as m, AGENTCORE_SESSION_ID_HEADER as mt, formatTargetListing as n, AGENTCORE_RUNTIME_TYPE as nn, verifyJwtViaDiscovery as nt, CloudMapRegistry as o, substituteImagePlaceholders as on, buildCorsConfigFromCloudFrontChain as ot, createAuthorizerCache as p, parseSseForJsonRpc as pt, parseConnectionsPath as q, listTargets as qt, createLocalStartAlbCommand as r, AgentCoreResolutionError as rn, attachAuthorizers as rt, getContainerNetworkIp as s, tryResolveImageFnJoin as sn, isFunctionUrlOacFronted as st, createLocalListCommand as t, AGENTCORE_MCP_PROTOCOL as tn, verifyJwtAuthorizer as tt, createLocalStartApiCommand as u, MCP_PATH as ut, filterRoutesByApiIdentifier as v, buildAgentCoreCodeImage as vt, resolveServiceIntegrationParameters as w, buildContainerImage as wt, readMtlsMaterialsFromDisk as x, toCmdArgv as xt, filterRoutesByApiIdentifiers as y, computeCodeImageTag as yt, tryParseStatus as z, resolveCfnFallbackRegion as zt };
|
|
22055
|
+
//# sourceMappingURL=local-list-EweOsy3A.js.map
|