cdk-local 0.3.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/cli.js +2 -2
- package/dist/{docker-cmd-DTPLXRqh.js → docker-cmd-o4ovyAhR.js} +35 -3
- package/dist/docker-cmd-o4ovyAhR.js.map +1 -0
- package/dist/index.d.ts +15 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/{local-start-service-EZy1JNYK.js → local-start-service-DquKcP26.js} +366 -146
- package/dist/local-start-service-DquKcP26.js.map +1 -0
- package/package.json +1 -1
- package/dist/docker-cmd-DTPLXRqh.js.map +0 -1
- package/dist/local-start-service-EZy1JNYK.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as runDockerStreaming, i as runDockerForeground, n as formatDockerLoginError, o as spawnStreaming, r as getDockerCmd, s as getLogger } from "./docker-cmd-
|
|
1
|
+
import { a as runDockerStreaming, c as getEmbedConfig, i as runDockerForeground, l as setEmbedConfig, n as formatDockerLoginError, o as spawnStreaming, r as getDockerCmd, s as getLogger } from "./docker-cmd-o4ovyAhR.js";
|
|
2
2
|
import { cpSync, createWriteStream, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
@@ -40,18 +40,23 @@ function parseContextOptions(contextArgs) {
|
|
|
40
40
|
return context;
|
|
41
41
|
}
|
|
42
42
|
/**
|
|
43
|
-
* Options shared across every cdk-local command.
|
|
43
|
+
* Options shared across every cdk-local command. Built per-call (not a
|
|
44
|
+
* module-level const) so the `--role-arn` env-var hint reflects the active
|
|
45
|
+
* embed config, which the command factory installs before calling this.
|
|
44
46
|
*
|
|
45
47
|
* `--region` is intentionally NOT in `commonOptions` — local commands
|
|
46
48
|
* pick the region from `AWS_REGION` / profile / synthesized stack env.
|
|
47
49
|
* The deprecated flag below remains for muscle-memory compatibility.
|
|
48
50
|
*/
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
function commonOptions() {
|
|
52
|
+
const { envPrefix } = getEmbedConfig();
|
|
53
|
+
return [
|
|
54
|
+
new Option("--verbose", "Enable verbose logging").default(false),
|
|
55
|
+
new Option("--profile <profile>", "AWS profile"),
|
|
56
|
+
new Option("--role-arn <arn>", `IAM role ARN to assume for AWS API calls (env: ${envPrefix}_ROLE_ARN)`),
|
|
57
|
+
new Option("-y, --yes", "Automatically answer interactive prompts with the recommended response").default(false)
|
|
58
|
+
];
|
|
59
|
+
}
|
|
55
60
|
/**
|
|
56
61
|
* Deprecated `--region` option attached to every command.
|
|
57
62
|
*
|
|
@@ -68,13 +73,18 @@ function warnIfDeprecatedRegion(options) {
|
|
|
68
73
|
if (options.region !== void 0) process.stderr.write("Warning: --region is deprecated for this command and has no effect. Use the AWS_REGION environment variable or your AWS profile to override the SDK default region.\n");
|
|
69
74
|
}
|
|
70
75
|
/**
|
|
71
|
-
* App options.
|
|
76
|
+
* App options. Built per-call (not a module-level const) so the `--app`
|
|
77
|
+
* env-var hint reflects the active embed config.
|
|
72
78
|
*
|
|
73
|
-
* `--app` is optional: falls back to `
|
|
74
|
-
* `app` field. Accepts either a shell command (e.g.
|
|
75
|
-
* a path to a pre-synthesized cloud assembly directory
|
|
79
|
+
* `--app` is optional: falls back to `${envPrefix}_APP` env var, then
|
|
80
|
+
* `cdk.json` `app` field. Accepts either a shell command (e.g.
|
|
81
|
+
* `"node app.ts"`) or a path to a pre-synthesized cloud assembly directory
|
|
82
|
+
* (e.g. `"cdk.out"`).
|
|
76
83
|
*/
|
|
77
|
-
|
|
84
|
+
function appOptions() {
|
|
85
|
+
const { envPrefix } = getEmbedConfig();
|
|
86
|
+
return [new Option("-a, --app <command>", `CDK app command (e.g., "node app.ts") or path to a pre-synthesized cloud assembly directory. Falls back to cdk.json or ${envPrefix}_APP env`), new Option("--output <path>", "Output directory for synthesis").default("cdk.out")];
|
|
87
|
+
}
|
|
78
88
|
/**
|
|
79
89
|
* Context options.
|
|
80
90
|
*/
|
|
@@ -125,7 +135,7 @@ function effectiveAssumeRoleArn(logicalId, opt) {
|
|
|
125
135
|
* Default session duration is 1 hour.
|
|
126
136
|
*/
|
|
127
137
|
async function applyRoleArnIfSet(opts) {
|
|
128
|
-
const roleArn = opts.roleArn || process.env[
|
|
138
|
+
const roleArn = opts.roleArn || process.env[`${getEmbedConfig().envPrefix}_ROLE_ARN`];
|
|
129
139
|
if (!roleArn) return;
|
|
130
140
|
const logger = getLogger().child("role-arn");
|
|
131
141
|
logger.debug(`Assuming role ${roleArn}...`);
|
|
@@ -133,7 +143,7 @@ async function applyRoleArnIfSet(opts) {
|
|
|
133
143
|
try {
|
|
134
144
|
const response = await sts.send(new AssumeRoleCommand({
|
|
135
145
|
RoleArn: roleArn,
|
|
136
|
-
RoleSessionName:
|
|
146
|
+
RoleSessionName: `${getEmbedConfig().binaryName}-${Date.now()}`,
|
|
137
147
|
DurationSeconds: 3600
|
|
138
148
|
}));
|
|
139
149
|
if (!response.Credentials) throw new Error(`AssumeRole returned no credentials for role ${roleArn}`);
|
|
@@ -405,7 +415,7 @@ function loadCdkJson() {
|
|
|
405
415
|
*/
|
|
406
416
|
function resolveApp(cliApp) {
|
|
407
417
|
if (cliApp) return cliApp;
|
|
408
|
-
const envApp = process.env[
|
|
418
|
+
const envApp = process.env[`${getEmbedConfig().envPrefix}_APP`];
|
|
409
419
|
if (envApp) return envApp;
|
|
410
420
|
return loadCdkJson()?.app ?? void 0;
|
|
411
421
|
}
|
|
@@ -847,7 +857,7 @@ var LocalStateSourceError = class extends Error {
|
|
|
847
857
|
function rejectExplicitCfnStackWithMultipleStacks(options, routedStackCount) {
|
|
848
858
|
if (routedStackCount <= 1) return;
|
|
849
859
|
if (typeof options.fromCfnStack !== "string") return;
|
|
850
|
-
throw new LocalStateSourceError(`--from-cfn-stack <name> cannot be used with multiple routed stacks (got ${routedStackCount}). An explicit CFn stack name applies to one stack only and would silently mismap logical IDs across siblings. Use bare --from-cfn-stack (each stack uses its own name as the CFn stack name) or run one
|
|
860
|
+
throw new LocalStateSourceError(`--from-cfn-stack <name> cannot be used with multiple routed stacks (got ${routedStackCount}). An explicit CFn stack name applies to one stack only and would silently mismap logical IDs across siblings. Use bare --from-cfn-stack (each stack uses its own name as the CFn stack name) or run one ${getEmbedConfig().binaryName} invocation per stack.`);
|
|
851
861
|
}
|
|
852
862
|
/**
|
|
853
863
|
* Pick and construct the right `LocalStateProvider` for the supplied
|
|
@@ -1317,7 +1327,7 @@ function resolveLambdaTarget(target, stacks) {
|
|
|
1317
1327
|
const { logicalId, resource } = match;
|
|
1318
1328
|
if (resource.Type !== "AWS::Lambda::Function") {
|
|
1319
1329
|
if (resource.Type.startsWith("Custom::")) throw new LocalInvokeResolutionError(`Resource '${logicalId}' in ${stack.stackName} is a Custom Resource (${resource.Type}), not a Lambda function. Custom Resources are invoked by the deploy framework, not by users. If you want to test the underlying handler, target the ServiceToken Lambda directly.`);
|
|
1320
|
-
throw new LocalInvokeResolutionError(`Resource '${logicalId}' in ${stack.stackName} is ${resource.Type}, not a Lambda function.
|
|
1330
|
+
throw new LocalInvokeResolutionError(`Resource '${logicalId}' in ${stack.stackName} is ${resource.Type}, not a Lambda function. ${getEmbedConfig().cliName} invoke only works on AWS::Lambda::Function resources in v1.`);
|
|
1321
1331
|
}
|
|
1322
1332
|
return extractLambdaProperties(stack, logicalId, resource, resources);
|
|
1323
1333
|
}
|
|
@@ -1366,7 +1376,7 @@ function extractLambdaProperties(stack, logicalId, resource, resources) {
|
|
|
1366
1376
|
});
|
|
1367
1377
|
const runtime = typeof props["Runtime"] === "string" ? props["Runtime"] : "";
|
|
1368
1378
|
const handler = typeof props["Handler"] === "string" ? props["Handler"] : "";
|
|
1369
|
-
if (!runtime) throw new LocalInvokeResolutionError(`Lambda '${logicalId}' has no Runtime property and no Code.ImageUri.
|
|
1379
|
+
if (!runtime) throw new LocalInvokeResolutionError(`Lambda '${logicalId}' has no Runtime property and no Code.ImageUri. ${getEmbedConfig().productName} cannot tell if this is a ZIP or a container Lambda.`);
|
|
1370
1380
|
if (!handler) throw new LocalInvokeResolutionError(`Lambda '${logicalId}' has no Handler property.`);
|
|
1371
1381
|
const inlineCode = typeof code["ZipFile"] === "string" ? code["ZipFile"] : void 0;
|
|
1372
1382
|
let codePath = null;
|
|
@@ -1461,9 +1471,10 @@ function extractImageUri$1(value, logicalId, stackName, resources, region) {
|
|
|
1461
1471
|
const pseudoParameters = derivePseudoParametersFromRegion(region);
|
|
1462
1472
|
const joinResolved = tryResolveImageFnJoin(value, resources, pseudoParameters ? { pseudoParameters } : void 0);
|
|
1463
1473
|
if (joinResolved.kind === "resolved") return joinResolved.uri;
|
|
1464
|
-
if (joinResolved.kind === "needs-state") throw new LocalInvokeResolutionError(`Lambda '${logicalId}' in ${stackName} references same-stack ECR repository '${joinResolved.repoLogicalId}' via Fn::Join.
|
|
1465
|
-
if (joinResolved.kind === "unsupported-join") throw new LocalInvokeResolutionError(`Lambda '${logicalId}' in ${stackName} has an unsupported Fn::Join Code.ImageUri shape: ${joinResolved.reason}.
|
|
1466
|
-
|
|
1474
|
+
if (joinResolved.kind === "needs-state") throw new LocalInvokeResolutionError(`Lambda '${logicalId}' in ${stackName} references same-stack ECR repository '${joinResolved.repoLogicalId}' via Fn::Join. ${getEmbedConfig().cliName} invoke cannot resolve the repository URI without state — deploy the stack first (so ${getEmbedConfig().productName} records the repository physical id), rebuild via lambda.DockerImageCode.fromImageAsset, or pin a public image.`);
|
|
1475
|
+
if (joinResolved.kind === "unsupported-join") throw new LocalInvokeResolutionError(`Lambda '${logicalId}' in ${stackName} has an unsupported Fn::Join Code.ImageUri shape: ${joinResolved.reason}. ${getEmbedConfig().cliName} invoke recognizes the canonical CDK 2.x lambda.DockerImageCode.fromEcr Fn::Join shape (delimiter "" with nested Fn::Select/Fn::Split over an ECR Repository Arn GetAtt + Ref to the repo).`);
|
|
1476
|
+
const accountIdHint = pseudoParameters ? ` (likely \${AWS::AccountId}, which ${getEmbedConfig().productName} cannot derive without --from-state or STS)` : ` (${getEmbedConfig().productName} could not derive AWS pseudo parameters because stack.region was undefined)`;
|
|
1477
|
+
throw new LocalInvokeResolutionError(`Lambda '${logicalId}' in ${stackName} has an Fn::Join Code.ImageUri that ${getEmbedConfig().cliName} invoke cannot resolve${accountIdHint}. Workarounds: deploy first and run with --from-state, or pin a fully-literal public image URI.`);
|
|
1467
1478
|
}
|
|
1468
1479
|
}
|
|
1469
1480
|
}
|
|
@@ -1485,7 +1496,7 @@ function extractImageLambdaProperties(args) {
|
|
|
1485
1496
|
const first = arches[0];
|
|
1486
1497
|
if (first === "arm64") architecture = "arm64";
|
|
1487
1498
|
else if (first === "x86_64") architecture = "x86_64";
|
|
1488
|
-
else throw new LocalInvokeResolutionError(`Lambda '${logicalId}' has unsupported Architectures value '${String(first)}'.
|
|
1499
|
+
else throw new LocalInvokeResolutionError(`Lambda '${logicalId}' has unsupported Architectures value '${String(first)}'. ${getEmbedConfig().cliName} invoke supports x86_64 and arm64.`);
|
|
1489
1500
|
}
|
|
1490
1501
|
return {
|
|
1491
1502
|
kind: "image",
|
|
@@ -1515,7 +1526,7 @@ function extractImageLambdaProperties(args) {
|
|
|
1515
1526
|
*/
|
|
1516
1527
|
function resolveAssetCodePath$1(stack, logicalId, resource) {
|
|
1517
1528
|
const assetPath = resource.Metadata?.["aws:asset:path"];
|
|
1518
|
-
if (typeof assetPath !== "string" || assetPath.length === 0) throw new LocalInvokeResolutionError(`Lambda '${logicalId}' has no Metadata['aws:asset:path'].
|
|
1529
|
+
if (typeof assetPath !== "string" || assetPath.length === 0) throw new LocalInvokeResolutionError(`Lambda '${logicalId}' has no Metadata['aws:asset:path']. ${getEmbedConfig().cliName} invoke needs this hint to find the local asset directory. Re-synthesize the app (without \`--output <stale-dir>\`) and retry.`);
|
|
1519
1530
|
const cdkOutDir = stack.assetManifestPath ? dirname(stack.assetManifestPath) : process.cwd();
|
|
1520
1531
|
const abs = isAbsolute(assetPath) ? assetPath : resolve(cdkOutDir, assetPath);
|
|
1521
1532
|
if (!existsSync(abs) || !statSync(abs).isDirectory()) throw new LocalInvokeResolutionError(`Lambda '${logicalId}' asset directory '${abs}' does not exist or is not a directory. Re-synthesize the app and retry.`);
|
|
@@ -1571,7 +1582,7 @@ function resolveLambdaLayers(stack, logicalId, props) {
|
|
|
1571
1582
|
const entry = layers[i];
|
|
1572
1583
|
if (typeof entry === "string") {
|
|
1573
1584
|
const parsed = parseLayerVersionArn(entry);
|
|
1574
|
-
if (!parsed) throw new LocalInvokeResolutionError(`Lambda '${logicalId}' has a Layers entry [${i}]
|
|
1585
|
+
if (!parsed) throw new LocalInvokeResolutionError(`Lambda '${logicalId}' has a Layers entry [${i}] ${getEmbedConfig().productName} cannot resolve locally: literal string '${entry}'. Expected a same-stack Ref / Fn::GetAtt to an AWS::Lambda::LayerVersion OR a literal layer-version ARN of the form arn:aws:lambda:<region>:<account>:layer:<name>:<version>.`);
|
|
1575
1586
|
out.push({
|
|
1576
1587
|
kind: "arn",
|
|
1577
1588
|
logicalId: parsed.arn,
|
|
@@ -1580,7 +1591,7 @@ function resolveLambdaLayers(stack, logicalId, props) {
|
|
|
1580
1591
|
continue;
|
|
1581
1592
|
}
|
|
1582
1593
|
const layerLogicalId = pickLayerLogicalId(entry);
|
|
1583
|
-
if (!layerLogicalId) throw new LocalInvokeResolutionError(`Lambda '${logicalId}' has a Layers entry [${i}]
|
|
1594
|
+
if (!layerLogicalId) throw new LocalInvokeResolutionError(`Lambda '${logicalId}' has a Layers entry [${i}] ${getEmbedConfig().productName} cannot resolve locally: ${describeLayerEntry(entry)}. Expected a same-stack Ref / Fn::GetAtt to an AWS::Lambda::LayerVersion OR a literal layer-version ARN of the form arn:aws:lambda:<region>:<account>:layer:<name>:<version>.`);
|
|
1584
1595
|
const layerResource = resources[layerLogicalId];
|
|
1585
1596
|
if (!layerResource) throw new LocalInvokeResolutionError(`Lambda '${logicalId}' Layers entry [${i}] references '${layerLogicalId}', but no resource with that logical ID exists in stack '${stack.stackName}'.`);
|
|
1586
1597
|
if (layerResource.Type !== "AWS::Lambda::LayerVersion") throw new LocalInvokeResolutionError(`Lambda '${logicalId}' Layers entry [${i}] references '${layerLogicalId}' (${layerResource.Type}), which is not an AWS::Lambda::LayerVersion.`);
|
|
@@ -1716,7 +1727,7 @@ async function materializeLayerFromArn(layer, options = {}) {
|
|
|
1716
1727
|
} catch (err) {
|
|
1717
1728
|
throw new LayerMaterializationError(`Layer ${layer.arn}: failed to download layer ZIP from the presigned URL: ${errMsg(err)}.`);
|
|
1718
1729
|
}
|
|
1719
|
-
const dir = await mkdtemp(join(tmpdir(),
|
|
1730
|
+
const dir = await mkdtemp(join(tmpdir(), `${getEmbedConfig().resourceNamePrefix}-arn-layer-${layer.name}-${layer.version}-`));
|
|
1720
1731
|
try {
|
|
1721
1732
|
await unzipBufferToDirectory(zipBytes, dir);
|
|
1722
1733
|
} catch (err) {
|
|
@@ -1782,7 +1793,7 @@ async function buildAssumeRoleCommand(roleArn) {
|
|
|
1782
1793
|
const { AssumeRoleCommand } = await import("@aws-sdk/client-sts");
|
|
1783
1794
|
return new AssumeRoleCommand({
|
|
1784
1795
|
RoleArn: roleArn,
|
|
1785
|
-
RoleSessionName:
|
|
1796
|
+
RoleSessionName: `${getEmbedConfig().resourceNamePrefix}-layer-${Date.now()}`,
|
|
1786
1797
|
DurationSeconds: 3600
|
|
1787
1798
|
});
|
|
1788
1799
|
}
|
|
@@ -2841,7 +2852,7 @@ function resolveEcsTaskTarget(target, stacks, context) {
|
|
|
2841
2852
|
logicalId = parsed.pathOrId;
|
|
2842
2853
|
}
|
|
2843
2854
|
if (!logicalId || !resource) throw notFoundError$1(target, stack, resources);
|
|
2844
|
-
if (resource.Type === "AWS::Lambda::Function") throw new EcsTaskResolutionError(`Resource '${logicalId}' in ${stack.stackName} is a Lambda function, not an ECS task definition. Use
|
|
2855
|
+
if (resource.Type === "AWS::Lambda::Function") throw new EcsTaskResolutionError(`Resource '${logicalId}' in ${stack.stackName} is a Lambda function, not an ECS task definition. Use \`${getEmbedConfig().cliName} invoke\` for Lambda; \`${getEmbedConfig().cliName} run-task\` is ECS only.`);
|
|
2845
2856
|
if (resource.Type !== "AWS::ECS::TaskDefinition") throw new EcsTaskResolutionError(`Resource '${logicalId}' in ${stack.stackName} is ${resource.Type}, not an AWS::ECS::TaskDefinition.`);
|
|
2846
2857
|
return extractTaskDefinitionProperties(stack, logicalId, resource, context);
|
|
2847
2858
|
}
|
|
@@ -2864,7 +2875,7 @@ function extractTaskDefinitionProperties(stack, logicalId, resource, context) {
|
|
|
2864
2875
|
if (rawNetworkMode === "bridge" || rawNetworkMode === "awsvpc" || rawNetworkMode === "host" || rawNetworkMode === "none") networkMode = rawNetworkMode;
|
|
2865
2876
|
else throw new EcsTaskResolutionError(`Task definition '${logicalId}' has unsupported NetworkMode '${rawNetworkMode}'. Supported values: bridge / awsvpc / host / none.`);
|
|
2866
2877
|
if (networkMode === "awsvpc") warnings.push(`NetworkMode 'awsvpc' on '${logicalId}' is mapped to docker bridge locally — docker cannot emulate ENI-per-task. AWS SDK calls still reach public endpoints via the developer network.`);
|
|
2867
|
-
if (props["ProxyConfiguration"]) warnings.push(`Task definition '${logicalId}' declares 'ProxyConfiguration' (custom Envoy / App Mesh bootstrap), which is NOT honored by '
|
|
2878
|
+
if (props["ProxyConfiguration"]) warnings.push(`Task definition '${logicalId}' declares 'ProxyConfiguration' (custom Envoy / App Mesh bootstrap), which is NOT honored by '${getEmbedConfig().cliName} start-service' / '${getEmbedConfig().cliName} run-task' in v1. Local execution will run without the configured proxy. See design doc § 2 for the rationale.`);
|
|
2868
2879
|
const resources = stack.template.Resources ?? {};
|
|
2869
2880
|
const subContext = buildSubstitutionContextFromImageContext(context);
|
|
2870
2881
|
const taskRoleArn = resolveRoleArn(props["TaskRoleArn"], resources, subContext);
|
|
@@ -3122,10 +3133,10 @@ function parseContainerImage(raw, containerName, taskLogicalId, resources, _stac
|
|
|
3122
3133
|
if (getAttImage) return classifyResolvedImage(getAttImage);
|
|
3123
3134
|
const joinResolved = tryResolveImageFnJoin(raw, resources, context);
|
|
3124
3135
|
if (joinResolved.kind === "resolved") return classifyResolvedImage(joinResolved.uri);
|
|
3125
|
-
if (joinResolved.kind === "needs-state") throw new EcsTaskResolutionError(`Container '${containerName}' in task '${taskLogicalId}' references same-stack ECR repository '${joinResolved.repoLogicalId}' via Fn::Join.
|
|
3126
|
-
if (joinResolved.kind === "unsupported-join") throw new EcsTaskResolutionError(`Container '${containerName}' in task '${taskLogicalId}' has an unsupported Fn::Join Image shape: ${joinResolved.reason}.
|
|
3136
|
+
if (joinResolved.kind === "needs-state") throw new EcsTaskResolutionError(`Container '${containerName}' in task '${taskLogicalId}' references same-stack ECR repository '${joinResolved.repoLogicalId}' via Fn::Join. ${getEmbedConfig().cliName} run-task cannot resolve the repository URI without state — pass --from-state (the stack must have been deployed via cdkd deploy), build via ContainerImage.fromAsset, or pin a public image.`);
|
|
3137
|
+
if (joinResolved.kind === "unsupported-join") throw new EcsTaskResolutionError(`Container '${containerName}' in task '${taskLogicalId}' has an unsupported Fn::Join Image shape: ${joinResolved.reason}. ${getEmbedConfig().cliName} run-task recognizes the canonical CDK 2.x ContainerImage.fromEcrRepository Fn::Join shape (delimiter "" with nested Fn::Select/Fn::Split over an ECR Repository Arn GetAtt + Ref to the repo).`);
|
|
3127
3138
|
const flat = extractImageString(raw);
|
|
3128
|
-
if (!flat) throw new EcsTaskResolutionError(`Container '${containerName}' in task '${taskLogicalId}' has an unparseable Image property.
|
|
3139
|
+
if (!flat) throw new EcsTaskResolutionError(`Container '${containerName}' in task '${taskLogicalId}' has an unparseable Image property. ${getEmbedConfig().cliName} run-task v1 supports flat string images, single-key Fn::Sub bodies, and CDK-asset Image references.`);
|
|
3129
3140
|
if (flat.includes("cdk-hnb659fds-container-assets-")) {
|
|
3130
3141
|
const hashMatch = /:([a-f0-9]{8,})$/.exec(flat);
|
|
3131
3142
|
const out = { kind: "cdk-asset" };
|
|
@@ -3135,9 +3146,9 @@ function parseContainerImage(raw, containerName, taskLogicalId, resources, _stac
|
|
|
3135
3146
|
const substituted = substituteImagePlaceholders(flat, resources, context);
|
|
3136
3147
|
if (substituted.includes("${")) {
|
|
3137
3148
|
const unresolvedRepoRef = findUnresolvedEcrRepositoryRef(substituted, resources);
|
|
3138
|
-
if (unresolvedRepoRef) throw new EcsTaskResolutionError(`Container '${containerName}' in task '${taskLogicalId}' references same-stack ECR repository '${unresolvedRepoRef}'.
|
|
3139
|
-
if (substituted.includes("AWS::")) throw new EcsTaskResolutionError(`Container '${containerName}' in task '${taskLogicalId}' has an Image that references AWS pseudo parameters (${substituted}).
|
|
3140
|
-
throw new EcsTaskResolutionError(`Container '${containerName}' in task '${taskLogicalId}' has an Image with unresolved \${...} placeholders (${substituted}).
|
|
3149
|
+
if (unresolvedRepoRef) throw new EcsTaskResolutionError(`Container '${containerName}' in task '${taskLogicalId}' references same-stack ECR repository '${unresolvedRepoRef}'. ${getEmbedConfig().cliName} run-task v1 cannot resolve the repository URI without state — pass --from-state (the stack must have been deployed via cdkd deploy), build via ContainerImage.fromAsset, or pin a public image.`);
|
|
3150
|
+
if (substituted.includes("AWS::")) throw new EcsTaskResolutionError(`Container '${containerName}' in task '${taskLogicalId}' has an Image that references AWS pseudo parameters (${substituted}). ${getEmbedConfig().productName} could not resolve them: confirm AWS credentials are configured so STS GetCallerIdentity succeeds, and that --region / AWS_REGION names the target region. Workaround: build the image locally (ContainerImage.fromAsset) or pin a public image.`);
|
|
3151
|
+
throw new EcsTaskResolutionError(`Container '${containerName}' in task '${taskLogicalId}' has an Image with unresolved \${...} placeholders (${substituted}). ${getEmbedConfig().cliName} run-task v1 only resolves AWS pseudo parameters and same-stack AWS::ECR::Repository refs.`);
|
|
3141
3152
|
}
|
|
3142
3153
|
const ecrMatch = /^(\d{12})\.dkr\.ecr\.([^.]+)\.amazonaws\.com(?:\.cn)?\//.exec(substituted);
|
|
3143
3154
|
if (ecrMatch) return {
|
|
@@ -3242,8 +3253,8 @@ function parseVolume(raw, idx, taskLogicalId, subContext) {
|
|
|
3242
3253
|
const v = raw;
|
|
3243
3254
|
const name = pickString(v["Name"]);
|
|
3244
3255
|
if (!name) throw new EcsTaskResolutionError(`Task '${taskLogicalId}' Volumes[${idx}] has no Name.`);
|
|
3245
|
-
if (v["EFSVolumeConfiguration"]) throw new EcsTaskResolutionError(`Task '${taskLogicalId}' Volumes[${idx}] '${name}' uses EFSVolumeConfiguration, which
|
|
3246
|
-
if (v["FSxWindowsFileServerVolumeConfiguration"]) throw new EcsTaskResolutionError(`Task '${taskLogicalId}' Volumes[${idx}] '${name}' uses FSxWindowsFileServerVolumeConfiguration, which
|
|
3256
|
+
if (v["EFSVolumeConfiguration"]) throw new EcsTaskResolutionError(`Task '${taskLogicalId}' Volumes[${idx}] '${name}' uses EFSVolumeConfiguration, which ${getEmbedConfig().cliName} run-task cannot proxy locally. Workaround: bind-mount a local directory at the same containerPath via Host: { SourcePath: '<local-path>' }, or override at runtime via --env-vars semantics for a Phase 2 follow-up.`);
|
|
3257
|
+
if (v["FSxWindowsFileServerVolumeConfiguration"]) throw new EcsTaskResolutionError(`Task '${taskLogicalId}' Volumes[${idx}] '${name}' uses FSxWindowsFileServerVolumeConfiguration, which ${getEmbedConfig().cliName} run-task cannot proxy locally.`);
|
|
3247
3258
|
const dockerCfg = v["DockerVolumeConfiguration"];
|
|
3248
3259
|
if (dockerCfg && typeof dockerCfg === "object") {
|
|
3249
3260
|
const d = dockerCfg;
|
|
@@ -3487,6 +3498,43 @@ async function applyCrossStackResolverToTask(task, context) {
|
|
|
3487
3498
|
|
|
3488
3499
|
//#endregion
|
|
3489
3500
|
//#region src/local/runtime-image.ts
|
|
3501
|
+
/**
|
|
3502
|
+
* Map a CloudFormation `Runtime` string to the AWS Lambda base image that
|
|
3503
|
+
* bundles the matching runtime + the Lambda Runtime Interface Emulator (RIE),
|
|
3504
|
+
* plus the source-file extension for inline-code materialization.
|
|
3505
|
+
*
|
|
3506
|
+
* Per D1 in the issue, cdk-local uses the **full** base image
|
|
3507
|
+
* (`public.ecr.aws/lambda/<lang>:<version>`, ~600MB) over SAM's lighter
|
|
3508
|
+
* `public.ecr.aws/sam/emulation-<lang>` (~150MB). The size cost is one-time
|
|
3509
|
+
* per machine; in exchange the local runtime is the same artifact AWS runs
|
|
3510
|
+
* for container Lambdas, so a "works locally, breaks in AWS" mismatch is
|
|
3511
|
+
* almost always a config issue rather than an image divergence.
|
|
3512
|
+
*
|
|
3513
|
+
* Supports every current AWS Lambda runtime — Node.js, Python, Ruby,
|
|
3514
|
+
* Java, .NET, and the OS-only `provided.al2` / `provided.al2023` (the
|
|
3515
|
+
* canonical hosts for Go via a `bootstrap` binary, Rust, C/C++, or any
|
|
3516
|
+
* other compiled native runtime). The deprecated `go1.x` runtime is
|
|
3517
|
+
* explicitly rejected with a migration pointer to `provided.al2023`.
|
|
3518
|
+
*
|
|
3519
|
+
* Truly unknown runtime strings (e.g. typos like `nodejs99.x`, or
|
|
3520
|
+
* back-revs AWS retired well before cdk-local existed) fall through to a
|
|
3521
|
+
* generic error that lists every supported runtime.
|
|
3522
|
+
*
|
|
3523
|
+
* Ruby uses the same `<file>.<func>` handler grammar as Node.js and Python,
|
|
3524
|
+
* so the inline-code materializer's `lastIndexOf('.')` parse works
|
|
3525
|
+
* unchanged; only the file extension (`.rb`) differs.
|
|
3526
|
+
*
|
|
3527
|
+
* Java, .NET, and `provided.*` are **asset-backed only** — the Handler
|
|
3528
|
+
* value names a compiled artifact (`package.Class::method` for Java's
|
|
3529
|
+
* JVM class on the classpath; `Assembly::Namespace.Class::Method` for
|
|
3530
|
+
* .NET's CLR assembly / DLL; an arbitrary identifier per the user's
|
|
3531
|
+
* `bootstrap` binary for `provided.*`), which can only be supplied as a
|
|
3532
|
+
* compiled artifact directory packaged via `lambda.Code.fromAsset(...)`.
|
|
3533
|
+
* Inline `Code.ZipFile` has no meaning for any of them, so
|
|
3534
|
+
* `fileExtension` is `null` for every Java / .NET / `provided.*` entry
|
|
3535
|
+
* and `resolveRuntimeFileExtension` throws a runtime-specific message
|
|
3536
|
+
* routing the user to `Code.fromAsset(...)`.
|
|
3537
|
+
*/
|
|
3490
3538
|
const SUPPORTED_RUNTIMES = {
|
|
3491
3539
|
"nodejs18.x": {
|
|
3492
3540
|
image: "public.ecr.aws/lambda/nodejs:18",
|
|
@@ -3608,7 +3656,7 @@ function resolveRuntimeSpec(runtime) {
|
|
|
3608
3656
|
const spec = SUPPORTED_RUNTIMES[runtime];
|
|
3609
3657
|
if (spec) return spec;
|
|
3610
3658
|
if (runtime === "go1.x") throw new UnsupportedRuntimeError(runtime, "Runtime 'go1.x' was deprecated by AWS Lambda on 2024-01-08 and is no longer available. Migrate to the OS-only runtime: build your Go program as a `bootstrap` binary and set the CDK runtime to `lambda.Runtime.PROVIDED_AL2023` (or `lambda.Runtime.PROVIDED_AL2`). See https://docs.aws.amazon.com/lambda/latest/dg/lambda-golang.html");
|
|
3611
|
-
throw new UnsupportedRuntimeError(runtime, `Unknown runtime '${runtime}'.
|
|
3659
|
+
throw new UnsupportedRuntimeError(runtime, `Unknown runtime '${runtime}'. ${getEmbedConfig().cliName} invoke supports nodejs18.x / nodejs20.x / nodejs22.x / nodejs24.x / python3.11 / python3.12 / python3.13 / python3.14 / ruby3.2 / ruby3.3 / java8.al2 / java11 / java17 / java21 / dotnet6 / dotnet8 / provided.al2 / provided.al2023.`);
|
|
3612
3660
|
}
|
|
3613
3661
|
/**
|
|
3614
3662
|
* In-container path where cdk-local should bind-mount the function's
|
|
@@ -3794,7 +3842,7 @@ async function ensureDockerAvailable() {
|
|
|
3794
3842
|
]);
|
|
3795
3843
|
} catch (error) {
|
|
3796
3844
|
const err = error;
|
|
3797
|
-
if (err.code === "ENOENT") throw new DockerRunnerError(`${cmd} is not installed or not on PATH.
|
|
3845
|
+
if (err.code === "ENOENT") throw new DockerRunnerError(`${cmd} is not installed or not on PATH. ${getEmbedConfig().cliName} invoke needs Docker (or a compatible CLI specified via CDK_DOCKER) — install it and retry.`);
|
|
3798
3846
|
throw new DockerRunnerError(`${cmd} daemon is not reachable: ${err.stderr?.trim() || err.message || String(error)}. Start Docker Desktop / the docker daemon and retry.`);
|
|
3799
3847
|
}
|
|
3800
3848
|
}
|
|
@@ -4041,7 +4089,7 @@ function isCredentialFresh(creds) {
|
|
|
4041
4089
|
async function pullEcrImage(imageUri, options) {
|
|
4042
4090
|
const logger = getLogger().child("ecr-puller");
|
|
4043
4091
|
const parsed = parseEcrUri(imageUri);
|
|
4044
|
-
if (!parsed) throw new LocalInvokeBuildError(`Image URI '${imageUri}' is not an ECR URI.
|
|
4092
|
+
if (!parsed) throw new LocalInvokeBuildError(`Image URI '${imageUri}' is not an ECR URI. ${getEmbedConfig().cliName} invoke v1 only authenticates against ECR for the deployed-image fallback path.`);
|
|
4045
4093
|
const callerRegion = options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"];
|
|
4046
4094
|
if (options.skipPull) {
|
|
4047
4095
|
logger.info(`Skipping ECR pull (--no-pull). Verifying ${imageUri} is in local cache...`);
|
|
@@ -4107,7 +4155,7 @@ async function assumeRoleForEcr(roleArn, callerRegion, logger) {
|
|
|
4107
4155
|
try {
|
|
4108
4156
|
const creds = (await sts.send(new AssumeRoleCommand({
|
|
4109
4157
|
RoleArn: roleArn,
|
|
4110
|
-
RoleSessionName:
|
|
4158
|
+
RoleSessionName: `${getEmbedConfig().resourceNamePrefix}-ecr-${Date.now()}`,
|
|
4111
4159
|
DurationSeconds: 3600
|
|
4112
4160
|
}))).Credentials;
|
|
4113
4161
|
if (!creds || !creds.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken) throw new LocalInvokeBuildError(`AssumeRole(${roleArn}) returned no usable credentials. Verify the role's trust policy allows your identity to assume it.`);
|
|
@@ -4162,7 +4210,7 @@ async function verifyImageInLocalCache(imageUri) {
|
|
|
4162
4210
|
imageUri
|
|
4163
4211
|
]);
|
|
4164
4212
|
} catch {
|
|
4165
|
-
throw new LocalInvokeBuildError(`Image '${imageUri}' is not in the local docker cache and --no-pull was set. Either remove --no-pull (
|
|
4213
|
+
throw new LocalInvokeBuildError(`Image '${imageUri}' is not in the local docker cache and --no-pull was set. Either remove --no-pull (${getEmbedConfig().productName} will pull from ECR) or pre-pull the image manually with \`docker pull\`.`);
|
|
4166
4214
|
}
|
|
4167
4215
|
}
|
|
4168
4216
|
/**
|
|
@@ -4265,7 +4313,7 @@ function computeLocalTag(source) {
|
|
|
4265
4313
|
pushField(hash, "dockerOutputs", (source.dockerOutputs ?? []).join(""));
|
|
4266
4314
|
pushField(hash, "cacheFrom", (source.cacheFrom ?? []).map((o) => JSON.stringify(o)).join(""));
|
|
4267
4315
|
pushField(hash, "cacheTo", source.cacheTo ? JSON.stringify(source.cacheTo) : "");
|
|
4268
|
-
return
|
|
4316
|
+
return `${getEmbedConfig().resourceNamePrefix}-invoke-${hash.digest("hex").slice(0, 16)}`;
|
|
4269
4317
|
}
|
|
4270
4318
|
function pushField(hash, name, value) {
|
|
4271
4319
|
hash.update(name);
|
|
@@ -4852,16 +4900,19 @@ function singleFlight(fn, onError) {
|
|
|
4852
4900
|
//#region src/cli/commands/local-profile-credentials-file.ts
|
|
4853
4901
|
/**
|
|
4854
4902
|
* Path inside the container where the credentials file is mounted. Fixed
|
|
4855
|
-
* (not user-configurable) so the env-var injection is stable.
|
|
4856
|
-
* `/cdk-local-aws/` is outside `/var/task` (the Lambda code mount)
|
|
4857
|
-
* outside `/root/` (which the user's handler may bind-mount or
|
|
4858
|
-
* so there is no collision risk with the user's payload.
|
|
4903
|
+
* per run (not user-configurable) so the env-var injection is stable. The
|
|
4904
|
+
* default `/cdk-local-aws/` is outside `/var/task` (the Lambda code mount)
|
|
4905
|
+
* and outside `/root/` (which the user's handler may bind-mount or
|
|
4906
|
+
* modify), so there is no collision risk with the user's payload. The
|
|
4907
|
+
* `${awsBindMountPath}` segment honors the active embed config.
|
|
4859
4908
|
*/
|
|
4860
|
-
|
|
4909
|
+
function getContainerAwsCredentialsPath() {
|
|
4910
|
+
return `${getEmbedConfig().awsBindMountPath}/credentials`;
|
|
4911
|
+
}
|
|
4861
4912
|
/**
|
|
4862
4913
|
* Write a temporary AWS shared-credentials file containing the resolved
|
|
4863
4914
|
* `--profile <name>` credentials, ready to bind-mount into a Lambda
|
|
4864
|
-
* container at {@link
|
|
4915
|
+
* container at {@link getContainerAwsCredentialsPath}.
|
|
4865
4916
|
*
|
|
4866
4917
|
* The file content is the standard `[profile-name]` INI shape:
|
|
4867
4918
|
*
|
|
@@ -4882,7 +4933,7 @@ const CONTAINER_AWS_CREDENTIALS_PATH = "/cdk-local-aws/credentials";
|
|
|
4882
4933
|
async function writeProfileCredentialsFile(profileName, creds) {
|
|
4883
4934
|
if (profileName === "") throw new Error("writeProfileCredentialsFile: profile name must not be empty.");
|
|
4884
4935
|
if (/[\r\n[\]]/.test(profileName)) throw new Error(`writeProfileCredentialsFile: profile name '${profileName}' contains a forbidden character (any of CR, LF, '[', ']' would corrupt the INI file or the docker -e env var).`);
|
|
4885
|
-
const dir = await mkdtemp(path.join(tmpdir(),
|
|
4936
|
+
const dir = await mkdtemp(path.join(tmpdir(), `${getEmbedConfig().productName}-profile-creds-`));
|
|
4886
4937
|
const hostPath = path.join(dir, "credentials");
|
|
4887
4938
|
const lines = [
|
|
4888
4939
|
`[${profileName}]`,
|
|
@@ -4893,7 +4944,7 @@ async function writeProfileCredentialsFile(profileName, creds) {
|
|
|
4893
4944
|
await writeFile(hostPath, lines.join("\n") + "\n", { mode: 384 });
|
|
4894
4945
|
return {
|
|
4895
4946
|
hostPath,
|
|
4896
|
-
containerPath:
|
|
4947
|
+
containerPath: getContainerAwsCredentialsPath(),
|
|
4897
4948
|
profileName,
|
|
4898
4949
|
dispose: async () => {
|
|
4899
4950
|
await rm(dir, {
|
|
@@ -4977,7 +5028,7 @@ async function localInvokeCommand(target, options, extraStateProviders) {
|
|
|
4977
5028
|
const profileCredentials = options.profile ? await resolveProfileCredentials$1(options.profile) : void 0;
|
|
4978
5029
|
if (options.profile && profileCredentials) profileCredsFile = await writeProfileCredentialsFile(options.profile, profileCredentials);
|
|
4979
5030
|
const appCmd = resolveApp(options.app);
|
|
4980
|
-
if (!appCmd) throw new Error(
|
|
5031
|
+
if (!appCmd) throw new Error(`No CDK app specified. Pass --app, set ${getEmbedConfig().envPrefix}_APP, or add "app" to cdk.json.`);
|
|
4981
5032
|
logger.info("Synthesizing CDK app...");
|
|
4982
5033
|
const synthesizer = new Synthesizer();
|
|
4983
5034
|
const context = parseContextOptions(options.context);
|
|
@@ -5198,7 +5249,7 @@ function materializeLambdaLayers$1(layers) {
|
|
|
5198
5249
|
containerPath: "/opt",
|
|
5199
5250
|
readOnly: true
|
|
5200
5251
|
} };
|
|
5201
|
-
const tmpDir = mkdtempSync(path.join(tmpdir(),
|
|
5252
|
+
const tmpDir = mkdtempSync(path.join(tmpdir(), `${getEmbedConfig().resourceNamePrefix}-invoke-layers-`));
|
|
5202
5253
|
for (const layer of layers) cpSync(layer.assetPath, tmpDir, {
|
|
5203
5254
|
recursive: true,
|
|
5204
5255
|
force: true
|
|
@@ -5222,7 +5273,7 @@ async function resolveContainerImagePlan(lambda, options) {
|
|
|
5222
5273
|
noBuild: options.build === false
|
|
5223
5274
|
});
|
|
5224
5275
|
else {
|
|
5225
|
-
if (!parseEcrUri(lambda.imageUri)) throw new Error(`Container Lambda '${lambda.logicalId}' has no matching asset in cdk.out, and Code.ImageUri '${lambda.imageUri}' is not an ECR URI
|
|
5276
|
+
if (!parseEcrUri(lambda.imageUri)) throw new Error(`Container Lambda '${lambda.logicalId}' has no matching asset in cdk.out, and Code.ImageUri '${lambda.imageUri}' is not an ECR URI ${getEmbedConfig().binaryName} can authenticate against. Re-synthesize the CDK app (so cdk.out includes the build context) or deploy the image to ECR first.`);
|
|
5226
5277
|
logger.info(`No matching cdk.out asset for ${lambda.imageUri}; falling back to ECR pull (same-acct/region only)...`);
|
|
5227
5278
|
imageRef = await pullEcrImage(lambda.imageUri, {
|
|
5228
5279
|
skipPull: options.pull === false,
|
|
@@ -5276,7 +5327,7 @@ function envHasCrossStackIntrinsic(templateEnv) {
|
|
|
5276
5327
|
async function resolvePseudoParametersForInvoke(stackRegion, options) {
|
|
5277
5328
|
const logger = getLogger();
|
|
5278
5329
|
const region = options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? stackRegion;
|
|
5279
|
-
if (!region) logger.warn(
|
|
5330
|
+
if (!region) logger.warn(`Resolver references \${AWS::Region} but ${getEmbedConfig().binaryName} could not determine the target region. Pass --region, set AWS_REGION, or declare env.region on the CDK stack.`);
|
|
5280
5331
|
let accountId;
|
|
5281
5332
|
try {
|
|
5282
5333
|
const { STSClient, GetCallerIdentityCommand } = await import("@aws-sdk/client-sts");
|
|
@@ -5348,7 +5399,7 @@ async function assumeLambdaExecutionRole$1(roleArn, region) {
|
|
|
5348
5399
|
try {
|
|
5349
5400
|
const creds = (await sts.send(new AssumeRoleCommand({
|
|
5350
5401
|
RoleArn: roleArn,
|
|
5351
|
-
RoleSessionName:
|
|
5402
|
+
RoleSessionName: `${getEmbedConfig().resourceNamePrefix}-invoke-${Date.now()}`,
|
|
5352
5403
|
DurationSeconds: 3600
|
|
5353
5404
|
}))).Credentials;
|
|
5354
5405
|
if (!creds?.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken) throw new Error(`AssumeRole(${roleArn}) returned no usable credentials.`);
|
|
@@ -5405,7 +5456,7 @@ function materializeInlineCode$1(handler, source, fileExtension) {
|
|
|
5405
5456
|
const lastDot = handler.lastIndexOf(".");
|
|
5406
5457
|
if (lastDot <= 0) throw new Error(`Handler '${handler}' is malformed: expected '<modulePath>.<exportName>'.`);
|
|
5407
5458
|
const modulePath = handler.substring(0, lastDot);
|
|
5408
|
-
const dir = mkdtempSync(path.join(tmpdir(),
|
|
5459
|
+
const dir = mkdtempSync(path.join(tmpdir(), `${getEmbedConfig().resourceNamePrefix}-invoke-`));
|
|
5409
5460
|
const filePath = path.join(dir, `${modulePath}${fileExtension}`);
|
|
5410
5461
|
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
5411
5462
|
writeFileSync(filePath, source, "utf-8");
|
|
@@ -5438,12 +5489,13 @@ function pickReferencedLogicalId(intrinsic) {
|
|
|
5438
5489
|
}
|
|
5439
5490
|
}
|
|
5440
5491
|
function createLocalInvokeCommand(opts = {}) {
|
|
5492
|
+
setEmbedConfig(opts.embedConfig);
|
|
5441
5493
|
const invoke = new Command("invoke").description("Run a Lambda function locally in a Docker container (RIE-backed). Target accepts a CDK display path (MyStack/MyApi/Handler) or stack-qualified logical ID (MyStack:MyApiHandler1234ABCD). Single-stack apps may omit the stack prefix.").argument("<target>", "CDK display path or stack-qualified logical ID of the Lambda to invoke").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("--no-pull", "Skip docker pull (use cached image) — no-op for IMAGE local-build path; `docker build` does not pull base layers by default")).addOption(new Option("--no-build", "Skip docker build on the IMAGE local-build path (use the previously-built tag). Requires the deterministic tag to already be in the local registry; errors with an actionable message when missing. No-op for ZIP Lambdas and the IMAGE ECR-pull path. Compatible with --no-pull.")).addOption(new Option("--debug-port <port>", "Node --inspect-brk port (default: off)")).addOption(new Option("--container-host <host>", "Host to bind the RIE port to").default("127.0.0.1")).addOption(new Option("--assume-role [arn]", "Assume the Lambda's deployed execution role and forward STS-issued temp credentials to the container so the handler runs with the deployed function's narrow permissions. Three forms: (1) `--assume-role <arn>` assumes the explicit ARN; (2) `--assume-role` (bare) auto-resolves the function's execution role ARN from state (requires an active state source); (3) `--no-assume-role` explicitly opts out. Off by default — when omitted, the developer's shell credentials are forwarded unchanged (SAM-compatible default). STS failures degrade to a warn + dev-creds fallback.")).addOption(new Option("--layer-role-arn <arn>", "Role to sts:AssumeRole before calling lambda:GetLayerVersion on every literal-ARN entry in Properties.Layers. Use only when the dev credentials cannot read the layer — typically cross-account layers. AWS-published public layers (e.g. Lambda Powertools) are readable from every account and need no role.")).addOption(new Option("--ecr-role-arn <arn>", "Role ARN to assume before authenticating against ECR for cross-account / centralized registries. Issues sts:AssumeRole via the default credential chain and uses the temporary credentials for ecr:GetAuthorizationToken + docker pull. Required when the caller does not have direct cross-account access to the target repository. 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 DescribeStackResources and substitute Ref / Fn::ImportValue in env vars with the deployed physical IDs / exports. Use for CDK apps deployed via the upstream CDK CLI (`cdk deploy`). Bare form uses the resolved stack name; pass an explicit value when CFn stack name differs. Fn::GetAtt is warn-and-dropped in v1 (CFn DescribeStackResources 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.")).action(withErrorHandling(async (target, options) => {
|
|
5442
5494
|
await localInvokeCommand(target, options, opts.extraStateProviders);
|
|
5443
5495
|
}));
|
|
5444
5496
|
[
|
|
5445
|
-
...commonOptions,
|
|
5446
|
-
...appOptions,
|
|
5497
|
+
...commonOptions(),
|
|
5498
|
+
...appOptions(),
|
|
5447
5499
|
...contextOptions
|
|
5448
5500
|
].forEach((option) => invoke.addOption(option));
|
|
5449
5501
|
invoke.addOption(deprecatedRegionOption);
|
|
@@ -5815,7 +5867,7 @@ async function dispatchSfnStopExecution(params, region) {
|
|
|
5815
5867
|
return okJson(await client.send(new mod.StopExecutionCommand(input)));
|
|
5816
5868
|
}
|
|
5817
5869
|
async function dispatchAppConfigGetConfiguration(params, region) {
|
|
5818
|
-
return errorResponse(501,
|
|
5870
|
+
return errorResponse(501, `AppConfig-GetConfiguration is recognized but ${getEmbedConfig().productName} does not yet bundle @aws-sdk/client-appconfig. Use the deployed API for this subtype, or open an issue if you need local emulation.`);
|
|
5819
5871
|
}
|
|
5820
5872
|
function requireParams(params, required) {
|
|
5821
5873
|
const missing = required.filter((k) => !params[k] || params[k].trim() === "");
|
|
@@ -6044,7 +6096,7 @@ function discoverRoutes(stacks) {
|
|
|
6044
6096
|
errors.push(err instanceof Error ? err.message : String(err));
|
|
6045
6097
|
}
|
|
6046
6098
|
}
|
|
6047
|
-
if (errors.length > 0) throw new RouteDiscoveryError(
|
|
6099
|
+
if (errors.length > 0) throw new RouteDiscoveryError(`${getEmbedConfig().cliName} start-api: ${errors.length} malformed route(s) in the synthesized template:\n` + errors.map((e) => ` - ${e}`).join("\n"));
|
|
6048
6100
|
return routes;
|
|
6049
6101
|
}
|
|
6050
6102
|
/**
|
|
@@ -6177,7 +6229,7 @@ function discoverRestV1Method(logicalId, resource, template, stackName) {
|
|
|
6177
6229
|
method: httpMethod,
|
|
6178
6230
|
pathPattern: path,
|
|
6179
6231
|
lambdaLogicalId: "",
|
|
6180
|
-
unsupported: { reason: `${stackName}/${logicalId}.Integration.Uri: ${arnOutcome.detail} (got ${shortJson$1(integrationUri)}). Lambda Arn intrinsics on cross-stack / imported references are not resolvable locally; deploy the producer stack and use
|
|
6232
|
+
unsupported: { reason: `${stackName}/${logicalId}.Integration.Uri: ${arnOutcome.detail} (got ${shortJson$1(integrationUri)}). Lambda Arn intrinsics on cross-stack / imported references are not resolvable locally; deploy the producer stack and use \`${getEmbedConfig().cliName} invoke --from-state\` shapes if you need it.` }
|
|
6181
6233
|
}];
|
|
6182
6234
|
return [{
|
|
6183
6235
|
...baseRoute,
|
|
@@ -6271,7 +6323,7 @@ function buildHttpProxyIntegrationConfig(integration, stackName, logicalId) {
|
|
|
6271
6323
|
const uri = integration["Uri"];
|
|
6272
6324
|
if (typeof uri !== "string" || uri.length === 0) return {
|
|
6273
6325
|
kind: "unsupported",
|
|
6274
|
-
reason: `${stackName}/${logicalId}: HTTP_PROXY Integration.Uri must be a literal string in v1 (
|
|
6326
|
+
reason: `${stackName}/${logicalId}: HTTP_PROXY Integration.Uri must be a literal string in v1 (${getEmbedConfig().cliName} start-api does not resolve Fn::Sub / Fn::Join in HTTP_PROXY Uris); got ${shortJson$1(uri)}.`
|
|
6275
6327
|
};
|
|
6276
6328
|
const integrationHttpMethod = pickStringField(integration, "IntegrationHttpMethod");
|
|
6277
6329
|
const requestParameters = pickStringRecord(integration["RequestParameters"]);
|
|
@@ -6295,7 +6347,7 @@ function buildHttpIntegrationConfig(integration, stackName, logicalId) {
|
|
|
6295
6347
|
const uri = integration["Uri"];
|
|
6296
6348
|
if (typeof uri !== "string" || uri.length === 0) return {
|
|
6297
6349
|
kind: "unsupported",
|
|
6298
|
-
reason: `${stackName}/${logicalId}: HTTP Integration.Uri must be a literal string in v1 (
|
|
6350
|
+
reason: `${stackName}/${logicalId}: HTTP Integration.Uri must be a literal string in v1 (${getEmbedConfig().cliName} start-api does not resolve Fn::Sub / Fn::Join in HTTP Uris); got ${shortJson$1(uri)}.`
|
|
6299
6351
|
};
|
|
6300
6352
|
const integrationHttpMethod = pickStringField(integration, "IntegrationHttpMethod");
|
|
6301
6353
|
const requestParameters = pickStringRecord(integration["RequestParameters"]);
|
|
@@ -6329,7 +6381,7 @@ function buildAwsIntegrationConfig(integration, stackName, logicalId) {
|
|
|
6329
6381
|
const uri = integration["Uri"];
|
|
6330
6382
|
if (!uriContainsLambdaMarker(uri)) return {
|
|
6331
6383
|
kind: "unsupported",
|
|
6332
|
-
reason: `${stackName}/${logicalId}: REST v1 AWS integration targeting a non-Lambda service (Uri ${shortJson$1(uri)}) is not emulated locally in
|
|
6384
|
+
reason: `${stackName}/${logicalId}: REST v1 AWS integration targeting a non-Lambda service (Uri ${shortJson$1(uri)}) is not emulated locally in ${getEmbedConfig().binaryName} v1. Lambda non-proxy AWS integrations are supported; direct AWS service integrations (S3 / SQS / SNS / DynamoDB) require deploying to AWS. See https://github.com/go-to-k/cdkd/blob/main/docs/local-emulation.md.`
|
|
6333
6385
|
};
|
|
6334
6386
|
const arnOutcome = resolveLambdaArnOutcome(uri);
|
|
6335
6387
|
if (arnOutcome.kind === "unsupported") return {
|
|
@@ -6552,7 +6604,7 @@ function classifyServiceIntegrationRoute(baseRoute, integrationProps, stackName,
|
|
|
6552
6604
|
if (!isSupportedSubtype(subtypeRaw)) return {
|
|
6553
6605
|
...baseRoute,
|
|
6554
6606
|
lambdaLogicalId: "",
|
|
6555
|
-
unsupported: { reason: `${declaredAt}: HTTP API v2 service integration subtype '${stringifyValue(subtypeRaw)}' is not supported by
|
|
6607
|
+
unsupported: { reason: `${declaredAt}: HTTP API v2 service integration subtype '${stringifyValue(subtypeRaw)}' is not supported by ${getEmbedConfig().cliName} start-api (see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-aws-services-reference.html for the supported list).` }
|
|
6556
6608
|
};
|
|
6557
6609
|
const requestParameters = integrationProps["RequestParameters"];
|
|
6558
6610
|
if (!requestParameters || typeof requestParameters !== "object" || Array.isArray(requestParameters)) return {
|
|
@@ -6823,7 +6875,7 @@ function discoverOneApi(logicalId, resource, template, stackName) {
|
|
|
6823
6875
|
const routes = collectRoutesForApi(logicalId, template, stackName);
|
|
6824
6876
|
if (routes.length === 0) throw new Error(`${declaredAt}: WebSocket API has no AWS::ApiGatewayV2::Route children — at least one route (typically '$connect') is required to dispatch.`);
|
|
6825
6877
|
const authRoutes = collectAuthRoutesForApi(logicalId, template, stackName);
|
|
6826
|
-
const unsupported = authRoutes.length > 0 ? { reason: `WebSocket API requires authorizer support, which
|
|
6878
|
+
const unsupported = authRoutes.length > 0 ? { reason: `WebSocket API requires authorizer support, which ${getEmbedConfig().binaryName} v1 does not emulate. Affected route(s): ${authRoutes.map((r) => `${r.routeKey} [AuthorizationType=${r.authorizationType}]`).join(", ")}. The API will be discovered but no upgrade requests will be accepted on this server.` } : void 0;
|
|
6827
6879
|
return {
|
|
6828
6880
|
apiLogicalId: logicalId,
|
|
6829
6881
|
apiStackName: stackName,
|
|
@@ -6879,7 +6931,7 @@ function collectAuthRoutesForApi(apiLogicalId, template, _stackName) {
|
|
|
6879
6931
|
* fuller JSONPath / VTL evaluator and are out of scope for v1.
|
|
6880
6932
|
*/
|
|
6881
6933
|
function assertSupportedSelectionExpression(expr, declaredAt) {
|
|
6882
|
-
if (!/^\$request\.body(?:\.[A-Za-z_][A-Za-z0-9_]*)+$/.test(expr)) throw new Error(`${declaredAt}: RouteSelectionExpression '${expr}' is not supported in
|
|
6934
|
+
if (!/^\$request\.body(?:\.[A-Za-z_][A-Za-z0-9_]*)+$/.test(expr)) throw new Error(`${declaredAt}: RouteSelectionExpression '${expr}' is not supported in ${getEmbedConfig().cliName} start-api v1 — only '$request.body.<key>' shapes (optionally nested via dots) are recognized. File a follow-up issue if you need '$request.header.X' / '$context.X' / array-index access.`);
|
|
6883
6935
|
}
|
|
6884
6936
|
/**
|
|
6885
6937
|
* Parse a `$request.body.x.y` selection expression into the JSON-path
|
|
@@ -6933,7 +6985,7 @@ function collectRoutesForApi(apiLogicalId, template, stackName) {
|
|
|
6933
6985
|
if (!integration || integration.Type !== "AWS::ApiGatewayV2::Integration") throw new Error(`${declaredAt}: Target points at '${targetLogicalId}' which is not an AWS::ApiGatewayV2::Integration.`);
|
|
6934
6986
|
const integrationProps = integration.Properties ?? {};
|
|
6935
6987
|
const integrationType = integrationProps["IntegrationType"];
|
|
6936
|
-
if (integrationType !== "AWS_PROXY") throw new Error(`${declaredAt}: WebSocket route IntegrationType '${String(integrationType)}' is not supported in
|
|
6988
|
+
if (integrationType !== "AWS_PROXY") throw new Error(`${declaredAt}: WebSocket route IntegrationType '${String(integrationType)}' is not supported in ${getEmbedConfig().cliName} start-api v1 — only AWS_PROXY (Lambda) integrations are emulated.`);
|
|
6937
6989
|
const arnOutcome = resolveLambdaArnIntrinsic(integrationProps["IntegrationUri"]);
|
|
6938
6990
|
if (arnOutcome.kind === "unsupported") throw new Error(`${stackName}/${targetLogicalId}.IntegrationUri: ${arnOutcome.detail} — WebSocket routes must point at a same-template Lambda.`);
|
|
6939
6991
|
result.push({
|
|
@@ -7920,6 +7972,85 @@ async function probeHostGatewaySupport() {
|
|
|
7920
7972
|
|
|
7921
7973
|
//#endregion
|
|
7922
7974
|
//#region src/local/vtl-engine.ts
|
|
7975
|
+
/**
|
|
7976
|
+
* AWS API Gateway VTL (Velocity Template Language) evaluator — hand-rolled
|
|
7977
|
+
* minimal subset for `cdkl start-api`'s REST v1 non-AWS_PROXY
|
|
7978
|
+
* integrations (#457).
|
|
7979
|
+
*
|
|
7980
|
+
* Background
|
|
7981
|
+
* ----------
|
|
7982
|
+
*
|
|
7983
|
+
* AWS API Gateway uses VTL to map between HTTP request / response shapes
|
|
7984
|
+
* and integration backend (Lambda non-proxy / HTTP / MOCK / AWS service)
|
|
7985
|
+
* request / response shapes. The full VTL spec is large; AWS API Gateway
|
|
7986
|
+
* exposes a SUBSET plus three AWS-specific built-in variables (`$input`,
|
|
7987
|
+
* `$context`, `$util`). cdk-local implements the SUBSET that real CDK apps
|
|
7988
|
+
* use in practice — anything outside it surfaces a clear error rather
|
|
7989
|
+
* than silently producing wrong output. See the
|
|
7990
|
+
* `VtlEvaluationError.message` field for the exact name when something
|
|
7991
|
+
* is unsupported.
|
|
7992
|
+
*
|
|
7993
|
+
* Supported VTL features
|
|
7994
|
+
* ----------------------
|
|
7995
|
+
*
|
|
7996
|
+
* - Variable references: `$var` / `${var}` / `$obj.field` /
|
|
7997
|
+
* `$obj.field.subField` (Velocity property notation).
|
|
7998
|
+
* - Method calls on built-ins: `$input.body` / `$input.json('$.path')` /
|
|
7999
|
+
* `$input.path('$.path')` / `$input.params()` / `$input.params('name')` /
|
|
8000
|
+
* `$input.params('header')` / `$context.*` / `$util.escapeJavaScript(x)` /
|
|
8001
|
+
* `$util.base64Encode(x)` / `$util.base64Decode(x)` / `$util.urlEncode(x)` /
|
|
8002
|
+
* `$util.urlDecode(x)` / `$util.parseJson(x)`.
|
|
8003
|
+
* - `#set($var = expr)` directives.
|
|
8004
|
+
* - `#if(cond) ... #elseif(cond) ... #else ... #end` blocks.
|
|
8005
|
+
* - `#foreach($x in $list) ... #end` loops.
|
|
8006
|
+
* - String / number / boolean / null literals: `"text"`, `'text'`, `42`,
|
|
8007
|
+
* `3.14`, `true`, `false`, `null`.
|
|
8008
|
+
* - Logical operators: `&&`, `||`, `!`.
|
|
8009
|
+
* - Comparison operators: `==`, `!=`, `<`, `<=`, `>`, `>=`.
|
|
8010
|
+
* - Implicit string concatenation (literal text + interpolated `$var`).
|
|
8011
|
+
*
|
|
8012
|
+
* NOT supported (intentionally)
|
|
8013
|
+
* -----------------------------
|
|
8014
|
+
*
|
|
8015
|
+
* - User-defined macros (`#macro`).
|
|
8016
|
+
* - `#parse` / `#include`.
|
|
8017
|
+
* - Velocity's arithmetic operators (`+ - * /`) outside literal concat.
|
|
8018
|
+
* - Range operator (`[1..5]`).
|
|
8019
|
+
* - `$velocityCount` and other Velocity context built-ins.
|
|
8020
|
+
*
|
|
8021
|
+
* AWS API Gateway-specific bindings
|
|
8022
|
+
* ---------------------------------
|
|
8023
|
+
*
|
|
8024
|
+
* `$input.body` — the raw request body as a string.
|
|
8025
|
+
* `$input.json('$.path.to.field')` — JSONPath against the body,
|
|
8026
|
+
* returned as a JSON-stringified value (so primitives are JSON-quoted).
|
|
8027
|
+
* `$input.path('$.path.to.field')` — JSONPath against the body, returned
|
|
8028
|
+
* as the native value (primitives unquoted).
|
|
8029
|
+
* `$input.params()` — `{header: {...}, querystring: {...}, path: {...}}`.
|
|
8030
|
+
* `$input.params('name')` — order: path > query > header (deployed
|
|
8031
|
+
* behavior; AWS docs).
|
|
8032
|
+
* `$input.params('header').<name>` — header lookup.
|
|
8033
|
+
* `$input.params('querystring').<name>` — querystring lookup.
|
|
8034
|
+
* `$input.params('path').<name>` — path-parameter lookup.
|
|
8035
|
+
*
|
|
8036
|
+
* `$context.requestId` — synthesized per request.
|
|
8037
|
+
* `$context.identity.sourceIp` — request client IP.
|
|
8038
|
+
* `$context.identity.userAgent` — request user agent.
|
|
8039
|
+
* `$context.httpMethod` — request method.
|
|
8040
|
+
* `$context.resourcePath` — the route's pathPattern.
|
|
8041
|
+
* `$context.stage` — the route's stage name (`$default` if no stage).
|
|
8042
|
+
*
|
|
8043
|
+
* `$util.escapeJavaScript(s)` — escape `\`, `'`, `"`, `\n`, `\r`, `\t`
|
|
8044
|
+
* for embedding inside a JavaScript string literal.
|
|
8045
|
+
* `$util.base64Encode(s)` — base64 (no padding stripped).
|
|
8046
|
+
* `$util.base64Decode(s)` — UTF-8 decode of base64.
|
|
8047
|
+
* `$util.urlEncode(s)` / `$util.urlDecode(s)` — RFC 3986 encoding.
|
|
8048
|
+
* `$util.parseJson(s)` — `JSON.parse(s)`.
|
|
8049
|
+
*
|
|
8050
|
+
* Mismatches between cdk-local's evaluator and AWS-deployed VTL surface as
|
|
8051
|
+
* `VtlEvaluationError`. Spurious whitespace / formatting differences are
|
|
8052
|
+
* acceptable and not in the contract.
|
|
8053
|
+
*/
|
|
7923
8054
|
/** Error thrown when a template references an unsupported VTL feature. */
|
|
7924
8055
|
var VtlEvaluationError = class VtlEvaluationError extends Error {
|
|
7925
8056
|
constructor(message) {
|
|
@@ -8063,7 +8194,7 @@ var VtlEvaluator = class {
|
|
|
8063
8194
|
case "else":
|
|
8064
8195
|
case "elseif":
|
|
8065
8196
|
case "end": throw new VtlEvaluationError(`Unexpected #${name} outside of a #if / #foreach block`);
|
|
8066
|
-
default: throw new VtlEvaluationError(`Unsupported VTL directive #${name} (
|
|
8197
|
+
default: throw new VtlEvaluationError(`Unsupported VTL directive #${name} (${getEmbedConfig().cliName} start-api supports #set / #if / #elseif / #else / #foreach / #end / ##)`);
|
|
8067
8198
|
}
|
|
8068
8199
|
}
|
|
8069
8200
|
/**
|
|
@@ -8335,7 +8466,7 @@ var VtlEvaluator = class {
|
|
|
8335
8466
|
* value must be a function or a special-cased built-in.
|
|
8336
8467
|
*/
|
|
8337
8468
|
callValueAsMethod(value, argsRaw, refPath) {
|
|
8338
|
-
if (typeof value !== "function") throw new VtlEvaluationError(`Reference '$${refPath}' is not callable (got ${typeof value}).
|
|
8469
|
+
if (typeof value !== "function") throw new VtlEvaluationError(`Reference '$${refPath}' is not callable (got ${typeof value}). ${getEmbedConfig().productName} supports calling $input / $util / $context method-style references only.`);
|
|
8339
8470
|
return value(...this.parseArgList(argsRaw));
|
|
8340
8471
|
}
|
|
8341
8472
|
/**
|
|
@@ -8654,7 +8785,7 @@ function applyJsonPath(root, expr) {
|
|
|
8654
8785
|
if (c === ".") {
|
|
8655
8786
|
i++;
|
|
8656
8787
|
const m = /^[a-zA-Z_][a-zA-Z_0-9]*/.exec(trimmed.slice(i));
|
|
8657
|
-
if (!m) throw new VtlEvaluationError(`Unsupported JSONPath syntax at position ${i}: '${trimmed}' (
|
|
8788
|
+
if (!m) throw new VtlEvaluationError(`Unsupported JSONPath syntax at position ${i}: '${trimmed}' (${getEmbedConfig().productName} supports $, $.field, $.field.sub, $.array[index] only).`);
|
|
8658
8789
|
cursor = lookupField(cursor, m[0]);
|
|
8659
8790
|
i += m[0].length;
|
|
8660
8791
|
continue;
|
|
@@ -8668,7 +8799,7 @@ function applyJsonPath(root, expr) {
|
|
|
8668
8799
|
if (Array.isArray(cursor)) cursor = cursor[idx];
|
|
8669
8800
|
else cursor = null;
|
|
8670
8801
|
} else if (inside.startsWith("\"") && inside.endsWith("\"") || inside.startsWith("'") && inside.endsWith("'")) cursor = lookupField(cursor, inside.slice(1, -1));
|
|
8671
|
-
else throw new VtlEvaluationError(`Unsupported JSONPath bracket expression: '${inside}' (
|
|
8802
|
+
else throw new VtlEvaluationError(`Unsupported JSONPath bracket expression: '${inside}' (${getEmbedConfig().productName} supports integer indices and quoted string keys only).`);
|
|
8672
8803
|
i = close + 1;
|
|
8673
8804
|
continue;
|
|
8674
8805
|
}
|
|
@@ -8802,7 +8933,7 @@ function evaluateResponseParameters(responseParameters, opts = {}) {
|
|
|
8802
8933
|
for (const [key, value] of Object.entries(responseParameters)) {
|
|
8803
8934
|
const headerMatch = /^method\.response\.header\.(.+)$/.exec(key);
|
|
8804
8935
|
if (!headerMatch) {
|
|
8805
|
-
opts.onUnsupported?.(key, value, `Only method.response.header.<name> keys are supported on REST v1 ResponseParameters;
|
|
8936
|
+
opts.onUnsupported?.(key, value, `Only method.response.header.<name> keys are supported on REST v1 ResponseParameters; ${getEmbedConfig().productName} cannot map ${key}.`);
|
|
8806
8937
|
continue;
|
|
8807
8938
|
}
|
|
8808
8939
|
const headerName = headerMatch[1].toLowerCase();
|
|
@@ -8814,7 +8945,7 @@ function evaluateResponseParameters(responseParameters, opts = {}) {
|
|
|
8814
8945
|
out[headerName] = value.slice(1, -1);
|
|
8815
8946
|
continue;
|
|
8816
8947
|
}
|
|
8817
|
-
opts.onUnsupported?.(key, value, `ResponseParameter value '${value}' is a mapping expression (integration.response.* / context.*) which
|
|
8948
|
+
opts.onUnsupported?.(key, value, `ResponseParameter value '${value}' is a mapping expression (integration.response.* / context.*) which ${getEmbedConfig().cliName} start-api does not emulate. Only single-quoted literals are honored.`);
|
|
8818
8949
|
}
|
|
8819
8950
|
return out;
|
|
8820
8951
|
}
|
|
@@ -9270,7 +9401,7 @@ function warnSsrfRiskyUri(uri, routeLabel, warn) {
|
|
|
9270
9401
|
return;
|
|
9271
9402
|
}
|
|
9272
9403
|
const classification = classifyInternalHost(host);
|
|
9273
|
-
if (classification !== void 0) warn(`Integration URI for ${routeLabel} points at ${host} — ${classification}.
|
|
9404
|
+
if (classification !== void 0) warn(`Integration URI for ${routeLabel} points at ${host} — ${classification}. ${getEmbedConfig().productName} does NOT block this; ensure the upstream is intentional.`);
|
|
9274
9405
|
}
|
|
9275
9406
|
function safeJsonParse(s) {
|
|
9276
9407
|
try {
|
|
@@ -9318,8 +9449,8 @@ function applyRequestParameters(requestParameters, req, out) {
|
|
|
9318
9449
|
const queryMatch = /^integration\.request\.querystring\.(.+)$/.exec(key);
|
|
9319
9450
|
const pathMatch = /^integration\.request\.path\.(.+)$/.exec(key);
|
|
9320
9451
|
if (headerMatch) out.headers[headerMatch[1].toLowerCase()] = resolved;
|
|
9321
|
-
else if (queryMatch) logger.warn(`RequestParameter '${key}' (querystring rewrite) is recognized but
|
|
9322
|
-
else if (pathMatch) logger.warn(`RequestParameter '${key}' (path rewrite) is recognized but
|
|
9452
|
+
else if (queryMatch) logger.warn(`RequestParameter '${key}' (querystring rewrite) is recognized but ${getEmbedConfig().productName} applies querystring rewrites only via URI placeholder substitution; ignoring.`);
|
|
9453
|
+
else if (pathMatch) logger.warn(`RequestParameter '${key}' (path rewrite) is recognized but ${getEmbedConfig().productName} substitutes path placeholders via {param} in the URI; ignoring.`);
|
|
9323
9454
|
else logger.warn(`Unsupported RequestParameter key '${key}'; skipping.`);
|
|
9324
9455
|
}
|
|
9325
9456
|
}
|
|
@@ -9431,7 +9562,7 @@ function createContainerPool(specs, options) {
|
|
|
9431
9562
|
*/
|
|
9432
9563
|
async function startOne(spec) {
|
|
9433
9564
|
const hostPort = await pickFreePort();
|
|
9434
|
-
const name =
|
|
9565
|
+
const name = `${getEmbedConfig().resourceNamePrefix}-${spec.lambda.logicalId}-${process.pid}-${Math.floor(Math.random() * 1e6)}`;
|
|
9435
9566
|
logger.debug(`Starting container ${name} for ${spec.lambda.logicalId} (kind=${spec.kind}) on ${spec.containerHost}:${hostPort}`);
|
|
9436
9567
|
let containerId;
|
|
9437
9568
|
if (spec.kind === "zip") {
|
|
@@ -9660,7 +9791,7 @@ function createContainerPool(specs, options) {
|
|
|
9660
9791
|
let anyTimedOut = false;
|
|
9661
9792
|
for (const r of drainResults) if (r.timedOut) {
|
|
9662
9793
|
anyTimedOut = true;
|
|
9663
|
-
logger.warn(`Container pool dispose timed out waiting for ${r.entry.inUse.size} in-flight handle(s) on ${r.entry.logicalId} after ${drainTimeoutMs}ms; tearing down anyway. The verify.sh \`docker rm -f
|
|
9794
|
+
logger.warn(`Container pool dispose timed out waiting for ${r.entry.inUse.size} in-flight handle(s) on ${r.entry.logicalId} after ${drainTimeoutMs}ms; tearing down anyway. The verify.sh \`docker rm -f ${getEmbedConfig().resourceNamePrefix}-*\` sweep is the safety net.`);
|
|
9664
9795
|
}
|
|
9665
9796
|
if (!anyTimedOut) logger.debug(`In-flight drain completed in ${Date.now() - drainStart}ms`);
|
|
9666
9797
|
}
|
|
@@ -10515,6 +10646,72 @@ function buildCorsConfigFromCloudFrontChain(template) {
|
|
|
10515
10646
|
return out;
|
|
10516
10647
|
}
|
|
10517
10648
|
/**
|
|
10649
|
+
* Determine whether a Function URL (`AWS::Lambda::Url`, identified by its
|
|
10650
|
+
* logical id) is fronted by a CloudFront Distribution origin that uses
|
|
10651
|
+
* Origin Access Control (OAC) to SIGN origin requests.
|
|
10652
|
+
*
|
|
10653
|
+
* Production-correct CDK pattern (`FunctionUrlOrigin.withOriginAccessControl`):
|
|
10654
|
+
* the Function URL declares `AuthType: AWS_IAM`, but the END client never
|
|
10655
|
+
* signs as the IAM principal — CloudFront re-signs the origin request with
|
|
10656
|
+
* its own SigV4 credentials (service `lambda`) via the OAC, and the Function
|
|
10657
|
+
* URL's auto-generated resource policy trusts `cloudfront.amazonaws.com`.
|
|
10658
|
+
* Locally there is no CloudFront in the path, so no client signature can
|
|
10659
|
+
* reproduce CloudFront's. Callers use this to relax SigV4 verification
|
|
10660
|
+
* (warn-and-pass) for these Function URLs without forcing
|
|
10661
|
+
* `--allow-unverified-sigv4`.
|
|
10662
|
+
*
|
|
10663
|
+
* Detection: a CloudFront origin whose `DomainName` matches the canonical
|
|
10664
|
+
* `Fn::GetAtt[<fnUrlLogicalId>, 'FunctionUrl']` chain (see
|
|
10665
|
+
* {@link pickFnUrlLogicalIdFromOriginDomainName}) AND carries an
|
|
10666
|
+
* `OriginAccessControlId`. When that id resolves to an
|
|
10667
|
+
* `AWS::CloudFront::OriginAccessControl` whose `SigningBehavior` is
|
|
10668
|
+
* explicitly `never`, CloudFront does NOT sign — so we do NOT relax (the
|
|
10669
|
+
* AWS_IAM + never-sign combination is non-functional in production too).
|
|
10670
|
+
* Any other signing behavior (`always` — the CDK default — or `no-override`)
|
|
10671
|
+
* counts as OAC-fronted. An `OriginAccessControlId` that can't be resolved
|
|
10672
|
+
* to a local resource (imported literal id) also counts — its presence on a
|
|
10673
|
+
* Function URL origin is the signal.
|
|
10674
|
+
*/
|
|
10675
|
+
function isFunctionUrlOacFronted(template, fnUrlLogicalId) {
|
|
10676
|
+
const resources = template.Resources ?? {};
|
|
10677
|
+
for (const [, resource] of Object.entries(resources)) {
|
|
10678
|
+
if (resource.Type !== "AWS::CloudFront::Distribution") continue;
|
|
10679
|
+
const distConfig = (resource.Properties ?? {})["DistributionConfig"];
|
|
10680
|
+
if (!distConfig || typeof distConfig !== "object") continue;
|
|
10681
|
+
const origins = Array.isArray(distConfig["Origins"]) ? distConfig["Origins"] : [];
|
|
10682
|
+
for (const origin of origins) {
|
|
10683
|
+
if (!origin || typeof origin !== "object") continue;
|
|
10684
|
+
const o = origin;
|
|
10685
|
+
if (pickFnUrlLogicalIdFromOriginDomainName(o["DomainName"]) !== fnUrlLogicalId) continue;
|
|
10686
|
+
const oacRef = o["OriginAccessControlId"];
|
|
10687
|
+
if (oacRef === void 0 || oacRef === "") continue;
|
|
10688
|
+
const oacLogicalId = pickOacRefLogicalId(oacRef);
|
|
10689
|
+
if (!oacLogicalId) return true;
|
|
10690
|
+
const oac = resources[oacLogicalId];
|
|
10691
|
+
if (!oac || oac.Type !== "AWS::CloudFront::OriginAccessControl") return true;
|
|
10692
|
+
const oacConfig = (oac.Properties ?? {})["OriginAccessControlConfig"];
|
|
10693
|
+
if ((oacConfig && typeof oacConfig === "object" ? oacConfig["SigningBehavior"] : void 0) === "never") continue;
|
|
10694
|
+
return true;
|
|
10695
|
+
}
|
|
10696
|
+
}
|
|
10697
|
+
return false;
|
|
10698
|
+
}
|
|
10699
|
+
/**
|
|
10700
|
+
* Unwrap an origin's `OriginAccessControlId` to the referenced
|
|
10701
|
+
* `AWS::CloudFront::OriginAccessControl` logical id. CDK synthesizes this
|
|
10702
|
+
* as `{ "Fn::GetAtt": [<id>, "Id"] }`; `{ Ref: <id> }` is also accepted.
|
|
10703
|
+
* Returns undefined for a literal id string (imported OAC) or any other
|
|
10704
|
+
* shape.
|
|
10705
|
+
*/
|
|
10706
|
+
function pickOacRefLogicalId(value) {
|
|
10707
|
+
if (!value || typeof value !== "object") return void 0;
|
|
10708
|
+
const obj = value;
|
|
10709
|
+
const ref = obj["Ref"];
|
|
10710
|
+
if (typeof ref === "string" && ref.length > 0) return ref;
|
|
10711
|
+
const getAtt = obj["Fn::GetAtt"];
|
|
10712
|
+
if (Array.isArray(getAtt) && getAtt.length === 2 && typeof getAtt[0] === "string") return getAtt[0];
|
|
10713
|
+
}
|
|
10714
|
+
/**
|
|
10518
10715
|
* Detect the canonical CDK 2.x `DomainName` shape that points a
|
|
10519
10716
|
* CloudFront Origin at a Function URL:
|
|
10520
10717
|
* {Fn::Select: [2, {Fn::Split: ['/', {Fn::GetAtt: [<id>, 'FunctionUrl']}]}]}
|
|
@@ -10812,7 +11009,7 @@ function resolveRestV1Authorizer(authorizerLogicalId, template, stackName, decla
|
|
|
10812
11009
|
declaredAt
|
|
10813
11010
|
};
|
|
10814
11011
|
}
|
|
10815
|
-
throw new RouteDiscoveryError(`${stackName}/${authorizerLogicalId}: AWS::ApiGateway::Authorizer.Type '${String(type)}' is not supported by
|
|
11012
|
+
throw new RouteDiscoveryError(`${stackName}/${authorizerLogicalId}: AWS::ApiGateway::Authorizer.Type '${String(type)}' is not supported by ${getEmbedConfig().cliName} start-api (only TOKEN / REQUEST / COGNITO_USER_POOLS are accepted at the Authorizer resource).`);
|
|
10816
11013
|
}
|
|
10817
11014
|
/**
|
|
10818
11015
|
* Resolve an `AWS::ApiGatewayV2::Authorizer`. HTTP v2 has only `REQUEST`
|
|
@@ -10853,7 +11050,7 @@ function resolveHttpApiAuthorizer(authorizerLogicalId, routeAuthorizationScopes,
|
|
|
10853
11050
|
declaredAt
|
|
10854
11051
|
};
|
|
10855
11052
|
}
|
|
10856
|
-
throw new RouteDiscoveryError(`${stackName}/${authorizerLogicalId}: AWS::ApiGatewayV2::Authorizer.AuthorizerType '${String(authType)}' is not supported by
|
|
11053
|
+
throw new RouteDiscoveryError(`${stackName}/${authorizerLogicalId}: AWS::ApiGatewayV2::Authorizer.AuthorizerType '${String(authType)}' is not supported by ${getEmbedConfig().cliName} start-api (only REQUEST / JWT).`);
|
|
10857
11054
|
}
|
|
10858
11055
|
/**
|
|
10859
11056
|
* Thrown by {@link resolveLambdaArn} when the authorizer's
|
|
@@ -11053,8 +11250,8 @@ function pickStringFromArn(value, location) {
|
|
|
11053
11250
|
const arg = obj["Fn::GetAtt"];
|
|
11054
11251
|
if (Array.isArray(arg) && arg.length === 2 && typeof arg[0] === "string" && arg[1] === "Arn") {
|
|
11055
11252
|
const logicalId = arg[0];
|
|
11056
|
-
getLogger().warn(`${location}: uses Fn::GetAtt against logical ID '${logicalId}'.
|
|
11057
|
-
return `arn:aws:cognito-idp:us-east-1:000000000000:userpool/us-east-
|
|
11253
|
+
getLogger().warn(`${location}: uses Fn::GetAtt against logical ID '${logicalId}'. ${getEmbedConfig().cliName} start-api cannot resolve the deployed user pool ARN — synthesizing an unreachable placeholder so JWKS pass-through admits every token. For real signature verification, set 'providerArns: [pool.userPoolArn]' explicitly on the CDK construct.`);
|
|
11254
|
+
return `arn:aws:cognito-idp:us-east-1:000000000000:userpool/us-east-1_${getEmbedConfig().binaryName}placeholder${logicalId}`;
|
|
11058
11255
|
}
|
|
11059
11256
|
}
|
|
11060
11257
|
}
|
|
@@ -11116,7 +11313,7 @@ function attachAuthorizers(stacks, routes) {
|
|
|
11116
11313
|
errors.push(err instanceof Error ? err.message : String(err));
|
|
11117
11314
|
}
|
|
11118
11315
|
}
|
|
11119
|
-
if (errors.length > 0) throw new RouteDiscoveryError(
|
|
11316
|
+
if (errors.length > 0) throw new RouteDiscoveryError(`${getEmbedConfig().cliName} start-api: ${errors.length} authorizer error(s):\n` + errors.map((e) => ` - ${e}`).join("\n"));
|
|
11120
11317
|
return out;
|
|
11121
11318
|
}
|
|
11122
11319
|
/**
|
|
@@ -11151,7 +11348,8 @@ function detectFunctionUrlAuthorizer(urlResource, urlLogicalId, stack) {
|
|
|
11151
11348
|
return {
|
|
11152
11349
|
kind: "iam",
|
|
11153
11350
|
logicalId: "AWS_IAM",
|
|
11154
|
-
declaredAt: `${stack.stackName}/${urlLogicalId}
|
|
11351
|
+
declaredAt: `${stack.stackName}/${urlLogicalId}`,
|
|
11352
|
+
...isFunctionUrlOacFronted(stack.template, urlLogicalId) && { oacFronted: true }
|
|
11155
11353
|
};
|
|
11156
11354
|
}
|
|
11157
11355
|
function detectRestV1Authorizer(methodResource, methodLogicalId, stack) {
|
|
@@ -11919,11 +12117,16 @@ function defaultCredentialsLoader() {
|
|
|
11919
12117
|
* - **Signature mismatch** under the dev's own credentials → `{allow: false}`.
|
|
11920
12118
|
* The http-server maps this to 403 (REST v1 `policy-deny`).
|
|
11921
12119
|
* - **Different `Credential` access-key-id than the dev has** →
|
|
11922
|
-
* `{allow:
|
|
11923
|
-
*
|
|
12120
|
+
* `{allow: false}` by default (we can't reproduce a signing key we
|
|
12121
|
+
* don't have). With `allowUnverified` (the `--allow-unverified-sigv4`
|
|
12122
|
+
* flag, or `oacFronted` routes) → `{allow: true}` plus a one-line warn
|
|
12123
|
+
* (warn-and-pass).
|
|
11924
12124
|
* - **Valid signature with the dev's credentials** → `{allow: true}`.
|
|
11925
12125
|
* The principal id surfaced to the handler is the parsed
|
|
11926
12126
|
* `Credential` access-key-id.
|
|
12127
|
+
* - **`oacFronted` route** → the caller forces `allowUnverified` on, so
|
|
12128
|
+
* foreign / no-creds requests pass through (CloudFront re-signs origin
|
|
12129
|
+
* requests in production) and the warn lines reference CloudFront OAC.
|
|
11927
12130
|
*/
|
|
11928
12131
|
async function verifySigV4(req, loadCredentials, opts = {}) {
|
|
11929
12132
|
const logger = getLogger();
|
|
@@ -11990,7 +12193,7 @@ async function verifySigV4(req, loadCredentials, opts = {}) {
|
|
|
11990
12193
|
identityHash: void 0
|
|
11991
12194
|
};
|
|
11992
12195
|
}
|
|
11993
|
-
logger.warn(`AWS_IAM authorizer: failed to resolve local AWS credentials (${reason}). --allow-unverified-sigv4 is set; passing through with unverified principalId 'unverified-no-creds'. Do NOT trust event.requestContext.identity.accessKey in handler code.`);
|
|
12196
|
+
logger.warn(opts.oacFronted ? `AWS_IAM authorizer: Function URL is fronted by CloudFront OAC (CloudFront re-signs origin requests in production), and local AWS credentials could not be resolved (${reason}). Passing through with unverified principalId 'unverified-no-creds'. Do NOT trust event.requestContext.identity.accessKey in handler code.` : `AWS_IAM authorizer: failed to resolve local AWS credentials (${reason}). --allow-unverified-sigv4 is set; passing through with unverified principalId 'unverified-no-creds'. Do NOT trust event.requestContext.identity.accessKey in handler code.`);
|
|
11994
12197
|
return {
|
|
11995
12198
|
allow: true,
|
|
11996
12199
|
principalId: "unverified-no-creds",
|
|
@@ -12011,7 +12214,7 @@ async function verifySigV4(req, loadCredentials, opts = {}) {
|
|
|
12011
12214
|
};
|
|
12012
12215
|
}
|
|
12013
12216
|
if (!warned || !warned.has(dedupKey)) {
|
|
12014
|
-
logger.warn(`AWS_IAM authorizer: request signed with foreign access-key-id '${parsed.credentialAccessKeyId}'. --allow-unverified-sigv4 is set; passing through with unverified principalId 'unverified-foreign-identity'. Do NOT trust event.requestContext.authorizer.principalId in handler code.`);
|
|
12217
|
+
logger.warn(opts.oacFronted ? `AWS_IAM authorizer: Function URL is fronted by CloudFront OAC — in production CloudFront re-signs the origin request, so the local client's signature (access-key-id '${parsed.credentialAccessKeyId}') cannot be verified. Passing through with unverified principalId 'unverified-foreign-identity'. Do NOT trust event.requestContext.authorizer.principalId in handler code.` : `AWS_IAM authorizer: request signed with foreign access-key-id '${parsed.credentialAccessKeyId}'. --allow-unverified-sigv4 is set; passing through with unverified principalId 'unverified-foreign-identity'. Do NOT trust event.requestContext.authorizer.principalId in handler code.`);
|
|
12015
12218
|
warned?.add(dedupKey);
|
|
12016
12219
|
}
|
|
12017
12220
|
return {
|
|
@@ -12974,6 +13177,7 @@ async function runAuthorizerPass(authorizer, snapshot, matchCtx, state, opts, re
|
|
|
12974
13177
|
denyKind: "policy-deny"
|
|
12975
13178
|
};
|
|
12976
13179
|
}
|
|
13180
|
+
const oacFronted = authorizer.oacFronted === true;
|
|
12977
13181
|
const sigResult = await verifySigV4({
|
|
12978
13182
|
method: snapshot.method,
|
|
12979
13183
|
rawUrl: snapshot.rawUrl,
|
|
@@ -12981,7 +13185,8 @@ async function runAuthorizerPass(authorizer, snapshot, matchCtx, state, opts, re
|
|
|
12981
13185
|
body: snapshot.body
|
|
12982
13186
|
}, opts.sigV4CredentialsLoader, {
|
|
12983
13187
|
...opts.sigV4WarnedForeignIds && { warnedForeignIds: opts.sigV4WarnedForeignIds },
|
|
12984
|
-
|
|
13188
|
+
allowUnverified: oacFronted || opts.sigV4AllowUnverified === true,
|
|
13189
|
+
...oacFronted && { oacFronted: true }
|
|
12985
13190
|
});
|
|
12986
13191
|
if (!sigResult.allow) return {
|
|
12987
13192
|
result: { allow: false },
|
|
@@ -13312,7 +13517,7 @@ function flattenHeadersForMapping(req) {
|
|
|
13312
13517
|
* absent values.
|
|
13313
13518
|
*/
|
|
13314
13519
|
function buildServiceIntegrationContextVars(req, route) {
|
|
13315
|
-
const requestId =
|
|
13520
|
+
const requestId = `${getEmbedConfig().binaryName}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
13316
13521
|
const sourceIp = req.socket.remoteAddress ?? "127.0.0.1";
|
|
13317
13522
|
return {
|
|
13318
13523
|
requestId,
|
|
@@ -13913,7 +14118,7 @@ async function localStartApiCommand(target, options, extraStateProviders) {
|
|
|
13913
14118
|
let apiFilter = target;
|
|
13914
14119
|
if (options.api !== void 0) {
|
|
13915
14120
|
if (target !== void 0) throw new Error(`Cannot specify both positional target ('${target}') and --api flag ('${options.api}'). Use one or the other. The positional form is preferred — '--api' is a deprecated alias.`);
|
|
13916
|
-
logger.warn(
|
|
14121
|
+
logger.warn(`[deprecated] --api <id> will be removed in a future major release. Use the positional argument instead: '${getEmbedConfig().cliName} start-api <id>'.`);
|
|
13917
14122
|
apiFilter = options.api;
|
|
13918
14123
|
}
|
|
13919
14124
|
warnIfDeprecatedRegion(options);
|
|
@@ -13923,7 +14128,7 @@ async function localStartApiCommand(target, options, extraStateProviders) {
|
|
|
13923
14128
|
});
|
|
13924
14129
|
await ensureDockerAvailable();
|
|
13925
14130
|
const appCmd = resolveApp(options.app);
|
|
13926
|
-
if (!appCmd) throw new Error(
|
|
14131
|
+
if (!appCmd) throw new Error(`No CDK app specified. Pass --app, set ${getEmbedConfig().envPrefix}_APP, or add "app" to cdk.json.`);
|
|
13927
14132
|
const overrides = readEnvOverridesFile$2(options.envVars);
|
|
13928
14133
|
const debugPortBase = options.debugPortBase ? parseDebugPort(options.debugPortBase) : void 0;
|
|
13929
14134
|
const perLambdaConcurrency = parsePerLambdaConcurrency(options.perLambdaConcurrency);
|
|
@@ -13966,13 +14171,13 @@ async function localStartApiCommand(target, options, extraStateProviders) {
|
|
|
13966
14171
|
if (targetStacks.length === 0) throw new Error("No stacks matched. Pass --stack <name> (or --from-cfn-stack <name>) or run from a single-stack app.");
|
|
13967
14172
|
const routedStackNames = targetStacks.map((s) => s.stackName);
|
|
13968
14173
|
tryEmitFromCfnRedundancyTipOnce(options.fromCfnStack, routedStackNames, fromCfnTipEmitted, (routedStackName) => {
|
|
13969
|
-
logger.info(`tip: --from-cfn-stack value matches the routed stack name (${routedStackName}); you can omit the value:
|
|
14174
|
+
logger.info(`tip: --from-cfn-stack value matches the routed stack name (${routedStackName}); you can omit the value: \`${getEmbedConfig().cliName} start-api ... --from-cfn-stack\` (bare flag) resolves to the same value.`);
|
|
13970
14175
|
});
|
|
13971
14176
|
const routes = discoverRoutes(targetStacks);
|
|
13972
14177
|
const wsDiscovery = discoverWebSocketApis(targetStacks);
|
|
13973
14178
|
if (wsDiscovery.errors.length > 0) for (const e of wsDiscovery.errors) logger.warn(`WebSocket discovery: ${e}`);
|
|
13974
14179
|
const webSocketApis = wsDiscovery.apis;
|
|
13975
|
-
if (routes.length === 0 && webSocketApis.length === 0) throw new Error(
|
|
14180
|
+
if (routes.length === 0 && webSocketApis.length === 0) throw new Error(`No supported API routes were discovered. ${getEmbedConfig().cliName} start-api supports AWS::ApiGateway::* (REST v1), AWS::ApiGatewayV2::* (HTTP + WebSocket), and AWS::Lambda::Url (Function URL) with AWS_PROXY integrations only.`);
|
|
13976
14181
|
const stageMap = /* @__PURE__ */ new Map();
|
|
13977
14182
|
for (const stack of targetStacks) {
|
|
13978
14183
|
const m = buildStageMap(stack.template, options.stage);
|
|
@@ -14135,7 +14340,7 @@ async function localStartApiCommand(target, options, extraStateProviders) {
|
|
|
14135
14340
|
warnUnsupportedWebSocketApis(initialWsApis, logger);
|
|
14136
14341
|
if (initialWsApis.filter((api) => !api.unsupported).length > 0) {
|
|
14137
14342
|
const probe = await probeHostGatewaySupport();
|
|
14138
|
-
if (!probe.supported) throw new Error(
|
|
14343
|
+
if (!probe.supported) throw new Error(`${getEmbedConfig().cliName} start-api requires Docker ${HOST_GATEWAY_MIN_VERSION.major}.${HOST_GATEWAY_MIN_VERSION.minor}+ for WebSocket API support (--add-host=host.docker.internal:host-gateway needs the 20.10 host-gateway alias). Detected server version: ${probe.rawVersion || "<empty — daemon unreachable or output stripped>"}. Upgrade Docker, or remove the WebSocket API from this app to fall back to HTTP-only start-api.`);
|
|
14139
14344
|
if (probe.parsed === null) logger.warn(`Docker server version "${probe.rawVersion}" did not match the canonical "<major>.<minor>" shape; assuming host-gateway support. If WebSocket containers fail to reach the local server, verify your Docker-compatible CLI honors --add-host=host.docker.internal:host-gateway.`);
|
|
14140
14345
|
}
|
|
14141
14346
|
for (const api of initialWsApis) {
|
|
@@ -14311,7 +14516,7 @@ async function localStartApiCommand(target, options, extraStateProviders) {
|
|
|
14311
14516
|
if (shutdownStarted) {
|
|
14312
14517
|
if (!forceExitArmed) {
|
|
14313
14518
|
forceExitArmed = true;
|
|
14314
|
-
logger.warn(`Received second ${signal}; force-exiting. Orphan containers may remain — run 'docker ps --filter name
|
|
14519
|
+
logger.warn(`Received second ${signal}; force-exiting. Orphan containers may remain — run 'docker ps --filter name=${getEmbedConfig().resourceNamePrefix}-' and 'docker rm -f' to clean up.`);
|
|
14315
14520
|
process.exit(130);
|
|
14316
14521
|
}
|
|
14317
14522
|
return;
|
|
@@ -14497,10 +14702,21 @@ function warnVpcConfigLambdas(routesWithAuth, stacks) {
|
|
|
14497
14702
|
function warnIamRoutes(routesWithAuth) {
|
|
14498
14703
|
const logger = getLogger();
|
|
14499
14704
|
const iamRoutes = [];
|
|
14500
|
-
|
|
14501
|
-
|
|
14502
|
-
|
|
14503
|
-
|
|
14705
|
+
const oacRoutes = [];
|
|
14706
|
+
for (const entry of routesWithAuth) {
|
|
14707
|
+
if (entry.authorizer?.kind !== "iam") continue;
|
|
14708
|
+
if (entry.authorizer.oacFronted === true) oacRoutes.push(entry.route.declaredAt);
|
|
14709
|
+
else iamRoutes.push(entry.route.declaredAt);
|
|
14710
|
+
}
|
|
14711
|
+
if (iamRoutes.length === 0 && oacRoutes.length === 0) return false;
|
|
14712
|
+
if (iamRoutes.length > 0) {
|
|
14713
|
+
logger.warn(`${iamRoutes.length} route(s) declare AuthorizationType: AWS_IAM — ${getEmbedConfig().cliName} start-api verifies SigV4 signatures against your local AWS credentials, but does NOT emulate IAM policy evaluation (resource / action / condition rules). Signature-verified callers reach the handler under their own identity; downstream authorization is the dev's responsibility.`);
|
|
14714
|
+
for (const declaredAt of iamRoutes) logger.warn(` - ${declaredAt}`);
|
|
14715
|
+
}
|
|
14716
|
+
if (oacRoutes.length > 0) {
|
|
14717
|
+
logger.warn(`${oacRoutes.length} Function URL route(s) with AuthType: AWS_IAM are fronted by a CloudFront Origin Access Control. In production CloudFront re-signs the origin request, so no local client signature can be verified — ${getEmbedConfig().cliName} start-api passes these through (warn-and-pass) WITHOUT requiring --allow-unverified-sigv4. Do NOT trust the request identity in handler code.`);
|
|
14718
|
+
for (const declaredAt of oacRoutes) logger.warn(` - ${declaredAt}`);
|
|
14719
|
+
}
|
|
14504
14720
|
return true;
|
|
14505
14721
|
}
|
|
14506
14722
|
/**
|
|
@@ -14625,7 +14841,7 @@ async function resolveContainerImageForStartApi(lambda, skipPull) {
|
|
|
14625
14841
|
const logger = getLogger();
|
|
14626
14842
|
const localBuild = await resolveLocalBuildPlan(lambda);
|
|
14627
14843
|
if (localBuild) return { imageRef: await buildContainerImage(localBuild.asset, localBuild.cdkOutDir, { architecture: lambda.architecture }) };
|
|
14628
|
-
if (!parseEcrUri(lambda.imageUri)) throw new Error(`Container Lambda '${lambda.logicalId}' has no matching asset in cdk.out, and Code.ImageUri '${lambda.imageUri}' is not an ECR URI
|
|
14844
|
+
if (!parseEcrUri(lambda.imageUri)) throw new Error(`Container Lambda '${lambda.logicalId}' has no matching asset in cdk.out, and Code.ImageUri '${lambda.imageUri}' is not an ECR URI ${getEmbedConfig().binaryName} can authenticate against. Re-synthesize the CDK app (so cdk.out includes the build context) or deploy the image to ECR first.`);
|
|
14629
14845
|
logger.info(`No matching cdk.out asset for ${lambda.imageUri}; falling back to ECR pull (same-acct/region only)...`);
|
|
14630
14846
|
return { imageRef: await pullEcrImage(lambda.imageUri, { skipPull }) };
|
|
14631
14847
|
}
|
|
@@ -14697,7 +14913,7 @@ async function materializeLambdaLayers(layers, layerTmpDirs, layerRoleArn) {
|
|
|
14697
14913
|
});
|
|
14698
14914
|
}
|
|
14699
14915
|
if (flat.length === 1) return flat[0].assetPath;
|
|
14700
|
-
const dir = mkdtempSync(path.join(tmpdir(),
|
|
14916
|
+
const dir = mkdtempSync(path.join(tmpdir(), `${getEmbedConfig().resourceNamePrefix}-start-api-layers-`));
|
|
14701
14917
|
for (const layer of flat) cpSync(layer.assetPath, dir, {
|
|
14702
14918
|
recursive: true,
|
|
14703
14919
|
force: true
|
|
@@ -14725,7 +14941,7 @@ function resolveLambdaByLogicalId(logicalId, stacks) {
|
|
|
14725
14941
|
});
|
|
14726
14942
|
const runtime = typeof props["Runtime"] === "string" ? props["Runtime"] : "";
|
|
14727
14943
|
const handler = typeof props["Handler"] === "string" ? props["Handler"] : "";
|
|
14728
|
-
if (!runtime) throw new Error(`Lambda '${logicalId}' has no Runtime property and no Code.ImageUri.
|
|
14944
|
+
if (!runtime) throw new Error(`Lambda '${logicalId}' has no Runtime property and no Code.ImageUri. ${getEmbedConfig().cliName} start-api cannot tell if this is a ZIP or a container Lambda.`);
|
|
14729
14945
|
if (!handler) throw new Error(`Lambda '${logicalId}' has no Handler property.`);
|
|
14730
14946
|
const inlineCode = typeof code["ZipFile"] === "string" ? code["ZipFile"] : void 0;
|
|
14731
14947
|
let codePath = null;
|
|
@@ -14785,9 +15001,10 @@ function extractImageUri(value, logicalId, stackName, resources, region) {
|
|
|
14785
15001
|
const pseudoParameters = derivePseudoParametersFromRegion(region);
|
|
14786
15002
|
const joinResolved = tryResolveImageFnJoin(value, resources, pseudoParameters ? { pseudoParameters } : void 0);
|
|
14787
15003
|
if (joinResolved.kind === "resolved") return joinResolved.uri;
|
|
14788
|
-
if (joinResolved.kind === "needs-state") throw new Error(`Lambda '${logicalId}' in ${stackName} references same-stack ECR repository '${joinResolved.repoLogicalId}' via Fn::Join.
|
|
14789
|
-
if (joinResolved.kind === "unsupported-join") throw new Error(`Lambda '${logicalId}' in ${stackName} has an unsupported Fn::Join Code.ImageUri shape: ${joinResolved.reason}.
|
|
14790
|
-
|
|
15004
|
+
if (joinResolved.kind === "needs-state") throw new Error(`Lambda '${logicalId}' in ${stackName} references same-stack ECR repository '${joinResolved.repoLogicalId}' via Fn::Join. ${getEmbedConfig().cliName} start-api cannot resolve the repository URI without state — deploy the stack first, rebuild via lambda.DockerImageCode.fromImageAsset, or pin a public image.`);
|
|
15005
|
+
if (joinResolved.kind === "unsupported-join") throw new Error(`Lambda '${logicalId}' in ${stackName} has an unsupported Fn::Join Code.ImageUri shape: ${joinResolved.reason}. ${getEmbedConfig().cliName} start-api recognizes the canonical CDK 2.x lambda.DockerImageCode.fromEcr Fn::Join shape (delimiter "" with nested Fn::Select/Fn::Split over an ECR Repository Arn GetAtt + Ref to the repo).`);
|
|
15006
|
+
const accountIdHint = pseudoParameters ? ` (likely \${AWS::AccountId}, which ${getEmbedConfig().binaryName} cannot derive without a state source or STS)` : ` (${getEmbedConfig().binaryName} could not derive AWS pseudo parameters because stack.region was undefined)`;
|
|
15007
|
+
throw new Error(`Lambda '${logicalId}' in ${stackName} has an Fn::Join Code.ImageUri that ${getEmbedConfig().cliName} start-api cannot resolve${accountIdHint}. Workarounds: deploy first and run with a state-source flag (e.g. --from-cfn-stack or a host-provided extension), or pin a fully-literal public image URI.`);
|
|
14791
15008
|
}
|
|
14792
15009
|
}
|
|
14793
15010
|
}
|
|
@@ -14810,7 +15027,7 @@ function resolveImageLambda(args) {
|
|
|
14810
15027
|
const first = arches[0];
|
|
14811
15028
|
if (first === "arm64") architecture = "arm64";
|
|
14812
15029
|
else if (first === "x86_64") architecture = "x86_64";
|
|
14813
|
-
else throw new Error(`Lambda '${logicalId}' has unsupported Architectures value '${String(first)}'.
|
|
15030
|
+
else throw new Error(`Lambda '${logicalId}' has unsupported Architectures value '${String(first)}'. ${getEmbedConfig().cliName} start-api supports x86_64 and arm64.`);
|
|
14814
15031
|
}
|
|
14815
15032
|
const ephemeralStorageMb = extractEphemeralStorageMb(props, logicalId);
|
|
14816
15033
|
return {
|
|
@@ -14834,7 +15051,7 @@ function resolveImageLambda(args) {
|
|
|
14834
15051
|
*/
|
|
14835
15052
|
function resolveAssetCodePath(stack, logicalId, resource) {
|
|
14836
15053
|
const assetPath = resource.Metadata?.["aws:asset:path"];
|
|
14837
|
-
if (typeof assetPath !== "string" || assetPath.length === 0) throw new Error(`Lambda '${logicalId}' has no Metadata['aws:asset:path'].
|
|
15054
|
+
if (typeof assetPath !== "string" || assetPath.length === 0) throw new Error(`Lambda '${logicalId}' has no Metadata['aws:asset:path']. ${getEmbedConfig().cliName} start-api needs this hint to find the local asset directory. Re-synthesize the app and retry.`);
|
|
14838
15055
|
const cdkOutDir = stack.assetManifestPath ? path.dirname(stack.assetManifestPath) : process.cwd();
|
|
14839
15056
|
return path.isAbsolute(assetPath) ? assetPath : path.resolve(cdkOutDir, assetPath);
|
|
14840
15057
|
}
|
|
@@ -14891,7 +15108,7 @@ function materializeInlineCode(handler, source, fileExtension, tmpDirsOut) {
|
|
|
14891
15108
|
const lastDot = handler.lastIndexOf(".");
|
|
14892
15109
|
if (lastDot <= 0) throw new Error(`Handler '${handler}' is malformed: expected '<modulePath>.<exportName>'.`);
|
|
14893
15110
|
const modulePath = handler.substring(0, lastDot);
|
|
14894
|
-
const dir = mkdtempSync(path.join(tmpdir(),
|
|
15111
|
+
const dir = mkdtempSync(path.join(tmpdir(), `${getEmbedConfig().resourceNamePrefix}-start-api-`));
|
|
14895
15112
|
tmpDirsOut.add(dir);
|
|
14896
15113
|
const filePath = path.join(dir, `${modulePath}${fileExtension}`);
|
|
14897
15114
|
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
@@ -15000,7 +15217,7 @@ async function assumeLambdaExecutionRole(roleArn, region) {
|
|
|
15000
15217
|
try {
|
|
15001
15218
|
const creds = (await sts.send(new AssumeRoleCommand({
|
|
15002
15219
|
RoleArn: roleArn,
|
|
15003
|
-
RoleSessionName:
|
|
15220
|
+
RoleSessionName: `${getEmbedConfig().resourceNamePrefix}-start-api-${Date.now()}`,
|
|
15004
15221
|
DurationSeconds: 3600
|
|
15005
15222
|
}))).Credentials;
|
|
15006
15223
|
if (!creds?.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken) throw new Error(`AssumeRole(${roleArn}) returned no usable credentials.`);
|
|
@@ -15146,7 +15363,7 @@ async function reloadAllServers(args) {
|
|
|
15146
15363
|
const newKeys = new Set(newByKey.keys());
|
|
15147
15364
|
const added = [...newKeys].filter((k) => !oldKeys.has(k));
|
|
15148
15365
|
const removed = [...oldKeys].filter((k) => !newKeys.has(k));
|
|
15149
|
-
if (added.length > 0) logger.warn(`Reload detected new API surface(s): ${added.join(", ")}. Restart '
|
|
15366
|
+
if (added.length > 0) logger.warn(`Reload detected new API surface(s): ${added.join(", ")}. Restart '${getEmbedConfig().cliName} start-api' to serve them.`);
|
|
15150
15367
|
if (removed.length > 0) logger.warn(`Reload detected removed API surface(s): ${removed.join(", ")}. Their servers will keep serving stale routes until restart.`);
|
|
15151
15368
|
for (const booted of servers) {
|
|
15152
15369
|
const group = newByKey.get(booted.group.serverKey);
|
|
@@ -15334,12 +15551,13 @@ function resolveMtlsConfig(options) {
|
|
|
15334
15551
|
* Builder for the `start-api` subcommand.
|
|
15335
15552
|
*/
|
|
15336
15553
|
function createLocalStartApiCommand(opts = {}) {
|
|
15337
|
-
|
|
15554
|
+
setEmbedConfig(opts.embedConfig);
|
|
15555
|
+
const startApi = new Command("start-api").description("Run a long-running local HTTP server that maps API Gateway routes (REST v1, HTTP API, Function URL) to Lambda invocations against the AWS Lambda Runtime Interface Emulator (Docker required). Supports Lambda TOKEN/REQUEST authorizers, Cognito User Pool / HTTP v2 JWT authorizers, and AWS_IAM auth (REST v1 `AuthorizationType: AWS_IAM` and Function URL `AuthType: AWS_IAM` — SigV4 signature verification only; IAM policy evaluation is NOT emulated). When JWKS is unreachable, JWT authorizers fall back to pass-through (every token accepted) with a warn line — local dev fallback. VPC-config Lambdas run locally and surface a warn line at startup; their containers do NOT get attached to the deployed VPC subnets, so calls to private RDS / ElastiCache will fail.").argument("[target]", `Optional API filter. Accepts the bare CDK logical id ('MyHttpApi'; single-stack apps only), stack-qualified logical id ('MyStack:MyHttpApi'), full CDK Construct path ('MyStack/MyHttpApi/Resource'), or an ancestor Construct path that prefix-matches ('MyStack/MyHttpApi'). When omitted, every discovered API gets its own server. Mirrors \`${getEmbedConfig().cliName} invoke\` / \`${getEmbedConfig().cliName} run-task\` target syntax.`).addOption(new Option("--port <port>", "HTTP server port (default: auto-allocate)").default("0")).addOption(new Option("--host <host>", "Bind address").default("127.0.0.1")).addOption(new Option("--stack <name>", "Stack to start (single-stack apps auto-detect)")).addOption(new Option("--warm", "Pre-start one container per Lambda at server boot").default(false)).addOption(new Option("--per-lambda-concurrency <n>", "Pool size cap per Lambda (default 2, max 4)").default("2")).addOption(new Option("--no-pull", "Skip docker pull (cached image)")).addOption(new Option("--container-host <host>", "IP the host uses to bind/probe the RIE port (must be a numeric IP — `docker run -p <ip>:<port>:8080` rejects hostnames). Defaults to 127.0.0.1.").default("127.0.0.1")).addOption(new Option("--debug-port-base <port>", "Reserve a contiguous --debug-port range (one per Lambda)")).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"LogicalId\":{\"KEY\":\"VALUE\"}, \"Parameters\": {...}})")).addOption(new Option("--assume-role <arn-or-pair>", "Assume the Lambda's execution role and forward STS-issued temp creds. Bare <arn> = global default; <LogicalId>=<arn> = per-Lambda override (repeatable). Per-Lambda > global > unset (developer creds passed through).").argParser((raw, prev) => parseAssumeRoleToken(raw, prev))).addOption(new Option("--watch", "Hot-reload: re-synth + re-discover routes when cdk.out/ or asset directories change. Off by default; the server keeps the previous version serving when synth fails mid-reload.").default(false)).addOption(new Option("--stage <name>", "Select an API Gateway Stage by its 'StageName'. Default: the first Stage attached to each API. Drives event.stageVariables for both REST v1 and HTTP API v2. NOTE: For HTTP API v2 routes, requestContext.stage is always '$default' regardless of this flag (AWS-side limitation — HTTP API only exposes one stage to the integration event); only event.stageVariables is affected for v2 routes. For REST v1 routes the selected StageName is also threaded into requestContext.stage.")).addOption(new Option("--api <id>", "DEPRECATED — use the positional <target> argument instead. Same accepted forms (bare logical id, stack-qualified, Construct path, ancestor prefix). Will be removed in a future major release.")).addOption(new Option("--layer-role-arn <arn>", "Role to sts:AssumeRole before calling lambda:GetLayerVersion on every literal-ARN entry in Properties.Layers (issue #448). Use only when the dev credentials cannot read the layer — typically cross-account layers. AWS-published public layers (e.g. Lambda Powertools) are readable from every account and need no role.")).addOption(new Option("--from-cfn-stack [cfn-stack-name]", "Read a deployed CloudFormation stack via DescribeStackResources and substitute Ref / Fn::ImportValue in Lambda env vars with the deployed physical IDs / exports. Use for CDK apps deployed via the upstream CDK CLI (`cdk deploy`). Bare form uses the resolved stack name per routed stack; pass an explicit value when a single CFn stack should serve every routed stack. Fn::GetAtt is warn-and-dropped in v1 (CFn DescribeStackResources 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.")).addOption(new Option("--mtls-truststore <path>", `PEM-encoded CA bundle for client-certificate verification (mutual TLS). When set, the local server switches from HTTP to HTTPS and the TLS handshake rejects clients whose certificate doesn't chain to one of these CAs. Verified certs are surfaced on the Lambda event under requestContext.identity.clientCert (REST v1) / requestContext.authentication.clientCert (HTTP API v2). Must be set together with --mtls-cert + --mtls-key; partial flag sets are rejected. Generate a CA + server + client cert for local dev: openssl req -x509 -newkey rsa:2048 -nodes -keyout ca-key.pem -out ca.pem -subj "/CN=${getEmbedConfig().resourceNamePrefix}-ca" -days 365; openssl req -newkey rsa:2048 -nodes -keyout server-key.pem -out server-csr.pem -subj "/CN=localhost"; openssl x509 -req -in server-csr.pem -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out server-cert.pem -days 365; openssl req -newkey rsa:2048 -nodes -keyout client-key.pem -out client-csr.pem -subj "/CN=client"; openssl x509 -req -in client-csr.pem -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out client-cert.pem -days 365; curl --cacert ca.pem --cert client-cert.pem --key client-key.pem https://localhost:<port>/...`)).addOption(new Option("--mtls-cert <path>", "PEM-encoded server certificate for mutual TLS. Self-signed is fine for local dev. Must be set together with --mtls-truststore + --mtls-key.")).addOption(new Option("--mtls-key <path>", "PEM-encoded server private key matching --mtls-cert. Must be set together with --mtls-truststore + --mtls-cert.")).addOption(new Option("--allow-unverified-sigv4", "Opt-in: allow AWS_IAM SigV4 requests that cannot be cryptographically verified (foreign access-key-id, OR no local AWS credentials configured) to pass through with a placeholder principalId. DEFAULT off — fail-closed so unauthenticated bypass is impossible against `event.requestContext.identity.accessKey`-trusting handler code. Use only in dev loops where you understand the risk.").default(false)).action(withErrorHandling(async (target, options) => {
|
|
15338
15556
|
await localStartApiCommand(target, options, opts.extraStateProviders);
|
|
15339
15557
|
}));
|
|
15340
15558
|
[
|
|
15341
|
-
...commonOptions,
|
|
15342
|
-
...appOptions,
|
|
15559
|
+
...commonOptions(),
|
|
15560
|
+
...appOptions(),
|
|
15343
15561
|
...contextOptions
|
|
15344
15562
|
].forEach((opt) => startApi.addOption(opt));
|
|
15345
15563
|
startApi.addOption(deprecatedRegionOption);
|
|
@@ -15419,7 +15637,7 @@ const SHARED_SVC_SUBNET_OCTET = 171;
|
|
|
15419
15637
|
* CLI tears down ONCE at the end of the run.
|
|
15420
15638
|
*/
|
|
15421
15639
|
async function createSharedSvcNetwork(options = {}) {
|
|
15422
|
-
const networkName = `${options.prefix ??
|
|
15640
|
+
const networkName = `${options.prefix ?? getEmbedConfig().resourceNamePrefix}-svc-${randomBytes(4).toString("hex")}`;
|
|
15423
15641
|
const { cidr, sidecarIp } = buildEndpointSubnet(171);
|
|
15424
15642
|
return {
|
|
15425
15643
|
networkName,
|
|
@@ -15459,7 +15677,7 @@ async function createNetworkAndSidecar(args) {
|
|
|
15459
15677
|
]);
|
|
15460
15678
|
} catch (err) {
|
|
15461
15679
|
const e = err;
|
|
15462
|
-
throw new DockerRunnerError(`docker network create failed: ${e.stderr?.trim() || e.message || String(err)}. Hint: another
|
|
15680
|
+
throw new DockerRunnerError(`docker network create failed: ${e.stderr?.trim() || e.message || String(err)}. Hint: another ${getEmbedConfig().productName} run may already own subnet ${cidr}; wait for it to finish, or remove the leftover network with \`docker network ls\` + \`docker network rm\`. \`${getEmbedConfig().cliName} start-service\` shares one network across every service in the run; bare \`${getEmbedConfig().cliName} run-task\` uses a per-task network so only one run can be active at a time.`);
|
|
15463
15681
|
}
|
|
15464
15682
|
const sidecarArgs = [
|
|
15465
15683
|
"run",
|
|
@@ -15498,7 +15716,7 @@ async function createNetworkAndSidecar(args) {
|
|
|
15498
15716
|
* lookup at container start doesn't race.
|
|
15499
15717
|
*/
|
|
15500
15718
|
async function createTaskNetwork(options = {}) {
|
|
15501
|
-
const networkName = `${options.prefix ??
|
|
15719
|
+
const networkName = `${options.prefix ?? getEmbedConfig().resourceNamePrefix}-task-${randomBytes(4).toString("hex")}`;
|
|
15502
15720
|
const { cidr, sidecarIp } = options.subnetOctet === void 0 ? {
|
|
15503
15721
|
cidr: DEFAULT_METADATA_ENDPOINT_SUBNET,
|
|
15504
15722
|
sidecarIp: METADATA_ENDPOINT_IP
|
|
@@ -16045,7 +16263,7 @@ async function prepareOneImage(task, container, options) {
|
|
|
16045
16263
|
if (image.assetHash && dockerImages[image.assetHash]) asset = dockerImages[image.assetHash];
|
|
16046
16264
|
else if (entries.length === 1) asset = entries[0][1];
|
|
16047
16265
|
if (!asset) throw new EcsTaskRunnerError(`Container '${container.name}' references a CDK asset image but no matching entry was found in cdk.out. Re-synthesize the CDK app and retry.`);
|
|
16048
|
-
const tag =
|
|
16266
|
+
const tag = `${getEmbedConfig().resourceNamePrefix}-run-task-${(image.assetHash ?? "single").slice(0, 16)}`;
|
|
16049
16267
|
const actualTag = await buildDockerImage(asset, cdkOutDir, {
|
|
16050
16268
|
tag,
|
|
16051
16269
|
...options.platformOverride !== void 0 && { platform: options.platformOverride },
|
|
@@ -16084,7 +16302,7 @@ async function realizeDockerVolumes(volumes, state) {
|
|
|
16084
16302
|
if (cfg?.driver) args.push("--driver", cfg.driver);
|
|
16085
16303
|
if (cfg?.driverOpts) for (const [k, val] of Object.entries(cfg.driverOpts)) args.push("--opt", `${k}=${val}`);
|
|
16086
16304
|
if (cfg?.labels) for (const [k, val] of Object.entries(cfg.labels)) args.push("--label", `${k}=${val}`);
|
|
16087
|
-
const dockerVolumeName =
|
|
16305
|
+
const dockerVolumeName = `${getEmbedConfig().resourceNamePrefix}-${v.name}-${randHex(4)}`;
|
|
16088
16306
|
args.push(dockerVolumeName);
|
|
16089
16307
|
try {
|
|
16090
16308
|
await execFileAsync$1(getDockerCmd(), args);
|
|
@@ -16124,7 +16342,7 @@ function groupSecretsByContainer(resolved) {
|
|
|
16124
16342
|
function buildDockerRunArgs(opts) {
|
|
16125
16343
|
const { task, container, image, network, volumeByName, secrets, containerHost, roleArn } = opts;
|
|
16126
16344
|
const args = ["run", "-d"];
|
|
16127
|
-
args.push("--name",
|
|
16345
|
+
args.push("--name", `${getEmbedConfig().resourceNamePrefix}-${task.family}-${container.name}-${randHex(3)}`);
|
|
16128
16346
|
args.push("--network", network);
|
|
16129
16347
|
args.push("--network-alias", container.name);
|
|
16130
16348
|
if (opts.networkAliases && opts.networkAliases.length > 0) {
|
|
@@ -16256,7 +16474,7 @@ async function localRunTaskCommand(target, options, extraStateProviders) {
|
|
|
16256
16474
|
});
|
|
16257
16475
|
await ensureDockerAvailable();
|
|
16258
16476
|
const appCmd = resolveApp(options.app);
|
|
16259
|
-
if (!appCmd) throw new Error(
|
|
16477
|
+
if (!appCmd) throw new Error(`No CDK app specified. Pass --app, set ${getEmbedConfig().envPrefix}_APP, or add "app" to cdk.json.`);
|
|
16260
16478
|
logger.info("Synthesizing CDK app...");
|
|
16261
16479
|
const synthesizer = new Synthesizer();
|
|
16262
16480
|
const context = parseContextOptions(options.context);
|
|
@@ -16297,7 +16515,7 @@ async function localRunTaskCommand(target, options, extraStateProviders) {
|
|
|
16297
16515
|
let assumedCredentials;
|
|
16298
16516
|
let resolvedRoleArn;
|
|
16299
16517
|
if (options.assumeTaskRole === true) {
|
|
16300
|
-
if (!task.taskRoleArn) throw new Error(
|
|
16518
|
+
if (!task.taskRoleArn) throw new Error(`--assume-task-role passed without an ARN but the task definition has no resolvable TaskRoleArn. Either the task definition does not set TaskRoleArn, or it points at a resource ${getEmbedConfig().binaryName} cannot resolve to an IAM Role at synth time. Pass the ARN explicitly: --assume-task-role <arn>`);
|
|
16301
16519
|
resolvedRoleArn = await resolvePlaceholderAccount$1(task.taskRoleArn, options.region);
|
|
16302
16520
|
assumedCredentials = await assumeTaskRole$1(resolvedRoleArn, options.region);
|
|
16303
16521
|
} else if (typeof options.assumeTaskRole === "string") {
|
|
@@ -16327,7 +16545,7 @@ async function localRunTaskCommand(target, options, extraStateProviders) {
|
|
|
16327
16545
|
};
|
|
16328
16546
|
const result = await runEcsTask(task, runOpts, state);
|
|
16329
16547
|
if (options.detach) {
|
|
16330
|
-
logger.info(
|
|
16548
|
+
logger.info(`Task containers started in detached mode; ${getEmbedConfig().binaryName} is exiting.`);
|
|
16331
16549
|
logger.info(`Use 'docker ps --filter network=${result.state.network?.networkName ?? "<network>"}' to inspect; tear down with 'docker rm -f' and 'docker network rm'.`);
|
|
16332
16550
|
sigintCount = 99;
|
|
16333
16551
|
return;
|
|
@@ -16366,7 +16584,7 @@ async function assumeTaskRole$1(roleArn, region) {
|
|
|
16366
16584
|
try {
|
|
16367
16585
|
const creds = (await sts.send(new AssumeRoleCommand({
|
|
16368
16586
|
RoleArn: roleArn,
|
|
16369
|
-
RoleSessionName:
|
|
16587
|
+
RoleSessionName: `${getEmbedConfig().resourceNamePrefix}-run-task-${Date.now()}`,
|
|
16370
16588
|
DurationSeconds: 3600
|
|
16371
16589
|
}))).Credentials;
|
|
16372
16590
|
if (!creds?.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken) throw new Error(`AssumeRole(${roleArn}) returned no usable credentials.`);
|
|
@@ -16393,7 +16611,7 @@ async function buildEcsImageResolutionContext$1(candidate, stateProvider, option
|
|
|
16393
16611
|
const wantsPseudoForEnvOrSecret = !!stateProvider && needs.needsEnvOrSecretSubstitution;
|
|
16394
16612
|
if (needs.needsPseudoParameters || wantsPseudoForEnvOrSecret) {
|
|
16395
16613
|
const region = options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? candidate.region;
|
|
16396
|
-
if (!region) logger.warn(
|
|
16614
|
+
if (!region) logger.warn(`Resolver references \${AWS::Region} but ${getEmbedConfig().binaryName} could not determine the target region. Pass --region, set AWS_REGION, or declare env.region on the CDK stack.`);
|
|
16397
16615
|
let accountId;
|
|
16398
16616
|
try {
|
|
16399
16617
|
accountId = await resolveCallerAccountId$1(region);
|
|
@@ -16481,12 +16699,13 @@ async function resolveSidecarCredentials(options, assumedCredentials) {
|
|
|
16481
16699
|
if (options.profile) return resolveProfileCredentials(options.profile);
|
|
16482
16700
|
}
|
|
16483
16701
|
function createLocalRunTaskCommand(opts = {}) {
|
|
16484
|
-
|
|
16702
|
+
setEmbedConfig(opts.embedConfig);
|
|
16703
|
+
const cmd = new Command("run-task").description("Run an AWS::ECS::TaskDefinition locally — pulls/builds images, sets up a per-task docker network with the AWS-published metadata-endpoints sidecar, and starts every container in dependsOn order. Target accepts a CDK display path (MyStack/MyService/TaskDef) or stack-qualified logical ID (MyStack:MyServiceTaskDefXYZ1234). Single-stack apps may omit the stack prefix.").argument("<target>", "CDK display path or stack-qualified logical ID of the AWS::ECS::TaskDefinition to run").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 function 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. Issues sts:AssumeRole via the default credential chain and uses the temporary credentials for ecr:GetAuthorizationToken + docker pull. Required when the caller does not have direct cross-account access to the target repository. Same-account / same-region pulls do not need this flag.")).addOption(new Option("--platform <platform>", "Force docker --platform (linux/amd64 or linux/arm64). Default: inferred from task RuntimePlatform.CpuArchitecture")).addOption(new Option("--keep-running", "Don't docker rm -f the user containers on task exit (network + sidecar are still torn down). Use when you want to docker exec into a stopped container for post-mortems.").default(false)).addOption(new Option("--detach", "Start the containers in the background and exit (skip log streaming + auto teardown). Useful in CI smoke tests; caller manages container lifecycle.").default(false)).addOption(new Option("--from-cfn-stack [cfn-stack-name]", `Read a deployed CloudFormation stack via DescribeStackResources 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 DescribeStackResources 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.")).action(withErrorHandling(async (target, options) => {
|
|
16485
16704
|
await localRunTaskCommand(target, options, opts.extraStateProviders);
|
|
16486
16705
|
}));
|
|
16487
16706
|
[
|
|
16488
|
-
...commonOptions,
|
|
16489
|
-
...appOptions,
|
|
16707
|
+
...commonOptions(),
|
|
16708
|
+
...appOptions(),
|
|
16490
16709
|
...contextOptions
|
|
16491
16710
|
].forEach((opt) => cmd.addOption(opt));
|
|
16492
16711
|
cmd.addOption(deprecatedRegionOption);
|
|
@@ -16530,7 +16749,7 @@ function resolveEcsServiceTarget(target, stacks, context) {
|
|
|
16530
16749
|
serviceLogicalId = parsed.pathOrId;
|
|
16531
16750
|
}
|
|
16532
16751
|
if (!serviceLogicalId || !serviceResource) throw notFoundError(target, stack, resources);
|
|
16533
|
-
if (serviceResource.Type === "AWS::ECS::TaskDefinition") throw new EcsTaskResolutionError(`Resource '${serviceLogicalId}' in ${stack.stackName} is an ECS TaskDefinition, not a Service. Use
|
|
16752
|
+
if (serviceResource.Type === "AWS::ECS::TaskDefinition") throw new EcsTaskResolutionError(`Resource '${serviceLogicalId}' in ${stack.stackName} is an ECS TaskDefinition, not a Service. Use \`${getEmbedConfig().cliName} run-task\` for one-shot tasks; \`${getEmbedConfig().cliName} start-service\` is Service-only.`);
|
|
16534
16753
|
if (serviceResource.Type !== "AWS::ECS::Service") throw new EcsTaskResolutionError(`Resource '${serviceLogicalId}' in ${stack.stackName} is ${serviceResource.Type}, not an AWS::ECS::Service.`);
|
|
16535
16754
|
return extractServiceProperties(stack, serviceLogicalId, serviceResource, stacks, context);
|
|
16536
16755
|
}
|
|
@@ -16589,7 +16808,7 @@ function extractServiceConnect(raw, task) {
|
|
|
16589
16808
|
const cfg = raw;
|
|
16590
16809
|
if (cfg["Enabled"] === false) return void 0;
|
|
16591
16810
|
const namespaceName = pickServiceConnectNamespace(cfg["Namespace"]);
|
|
16592
|
-
if (!namespaceName) throw new EcsTaskResolutionError(`ServiceConnectConfiguration.Namespace must be a literal string (the Cloud Map namespace name like '
|
|
16811
|
+
if (!namespaceName) throw new EcsTaskResolutionError(`ServiceConnectConfiguration.Namespace must be a literal string (the Cloud Map namespace name like '${getEmbedConfig().resourceNamePrefix}.local'); got ${JSON.stringify(cfg["Namespace"])}. Intrinsic / cross-stack namespace references are not supported in v1.`);
|
|
16593
16812
|
const rawServices = cfg["Services"];
|
|
16594
16813
|
if (!Array.isArray(rawServices) || rawServices.length === 0) return {
|
|
16595
16814
|
namespaceName,
|
|
@@ -16650,7 +16869,7 @@ function extractServiceRegistries(raw, serviceLogicalId, warnings) {
|
|
|
16650
16869
|
const registryArn = e["RegistryArn"];
|
|
16651
16870
|
let cloudMapServiceLogicalId;
|
|
16652
16871
|
if (typeof registryArn === "string") {
|
|
16653
|
-
warnings.push(`ECS Service '${serviceLogicalId}' ServiceRegistries[] entry has a literal-string RegistryArn ('${registryArn}');
|
|
16872
|
+
warnings.push(`ECS Service '${serviceLogicalId}' ServiceRegistries[] entry has a literal-string RegistryArn ('${registryArn}'); ${getEmbedConfig().productName} cannot resolve external Cloud Map services locally. Skipping this registration; peer services will not discover this endpoint through the in-process registry. Use Fn::GetAtt: [<CloudMapServiceLogicalId>, "Arn"] instead so ${getEmbedConfig().productName} can resolve the namespace + service name from the synthesized template.`);
|
|
16654
16873
|
continue;
|
|
16655
16874
|
}
|
|
16656
16875
|
if (registryArn && typeof registryArn === "object" && !Array.isArray(registryArn)) {
|
|
@@ -16691,7 +16910,7 @@ function resolveTaskDefinitionReference(taskDefRef, stack, serviceLogicalId) {
|
|
|
16691
16910
|
return refValue;
|
|
16692
16911
|
}
|
|
16693
16912
|
}
|
|
16694
|
-
throw new EcsTaskResolutionError(`ECS Service '${serviceLogicalId}' has an unsupported TaskDefinition reference shape: ${JSON.stringify(taskDefRef)}.
|
|
16913
|
+
throw new EcsTaskResolutionError(`ECS Service '${serviceLogicalId}' has an unsupported TaskDefinition reference shape: ${JSON.stringify(taskDefRef)}. ${getEmbedConfig().cliName} start-service v1 supports only Ref to a same-stack AWS::ECS::TaskDefinition; cross-stack TaskDefinitions are deferred.`);
|
|
16695
16914
|
}
|
|
16696
16915
|
function parseDesiredCount(raw, serviceLogicalId) {
|
|
16697
16916
|
if (raw === void 0 || raw === null) return 1;
|
|
@@ -17104,7 +17323,7 @@ async function publishReplicaToCloudMap(service, instance, discovery, ownerKeyPr
|
|
|
17104
17323
|
if (service.serviceRegistries.length > 0) {
|
|
17105
17324
|
const index = discovery.cloudMapIndexByStack.get(service.stack.stackName);
|
|
17106
17325
|
if (!index) {
|
|
17107
|
-
logger.warn(`ECS Service '${service.serviceLogicalId}' declares ServiceRegistries[] but
|
|
17326
|
+
logger.warn(`ECS Service '${service.serviceLogicalId}' declares ServiceRegistries[] but ${getEmbedConfig().productName} has no Cloud Map index for stack ${service.stack.stackName}. Skipping registration.`);
|
|
17108
17327
|
return;
|
|
17109
17328
|
}
|
|
17110
17329
|
let j = 0;
|
|
@@ -17207,7 +17426,7 @@ function pickEssentialContainerId(instance, service) {
|
|
|
17207
17426
|
const defaultWaitForExitImpl = async (containerId) => {
|
|
17208
17427
|
const { execFile } = await import("node:child_process");
|
|
17209
17428
|
const { promisify } = await import("node:util");
|
|
17210
|
-
const { getDockerCmd } = await import("./docker-cmd-
|
|
17429
|
+
const { getDockerCmd } = await import("./docker-cmd-o4ovyAhR.js").then((n) => n.t);
|
|
17211
17430
|
const { stdout } = await promisify(execFile)(getDockerCmd(), ["wait", containerId], { maxBuffer: 1024 * 1024 });
|
|
17212
17431
|
const code = parseInt(stdout.trim(), 10);
|
|
17213
17432
|
return Number.isFinite(code) ? code : -1;
|
|
@@ -17457,7 +17676,7 @@ function buildCloudMapIndex(stack) {
|
|
|
17457
17676
|
const warnings = [];
|
|
17458
17677
|
const resources = stack.template.Resources ?? {};
|
|
17459
17678
|
for (const [logicalId, resource] of Object.entries(resources)) {
|
|
17460
|
-
if (resource.Type === "AWS::ServiceDiscovery::PublicDnsNamespace") throw new EcsTaskResolutionError(`Stack ${stack.stackName}: AWS::ServiceDiscovery::PublicDnsNamespace '${logicalId}' is not supported by local emulation — public DNS defeats the "local" point. Use a PrivateDnsNamespace for
|
|
17679
|
+
if (resource.Type === "AWS::ServiceDiscovery::PublicDnsNamespace") throw new EcsTaskResolutionError(`Stack ${stack.stackName}: AWS::ServiceDiscovery::PublicDnsNamespace '${logicalId}' is not supported by local emulation — public DNS defeats the "local" point. Use a PrivateDnsNamespace for ${getEmbedConfig().cliName} start-service.`);
|
|
17461
17680
|
if (resource.Type === "AWS::ServiceDiscovery::HttpNamespace") throw new EcsTaskResolutionError(`Stack ${stack.stackName}: AWS::ServiceDiscovery::HttpNamespace '${logicalId}' is not supported by local emulation — HttpNamespace uses the AWS Cloud Map DiscoverInstances API directly (no DNS), which would require shimming the AWS SDK inside every container. Use a PrivateDnsNamespace + DnsConfig instead.`);
|
|
17462
17681
|
if (resource.Type !== "AWS::ServiceDiscovery::PrivateDnsNamespace") continue;
|
|
17463
17682
|
const props = resource.Properties ?? {};
|
|
@@ -17551,7 +17770,7 @@ async function localStartServiceCommand(targets, options, extraStateProviders) {
|
|
|
17551
17770
|
if (options.verbose) logger.setLevel("debug");
|
|
17552
17771
|
warnIfDeprecatedRegion(options);
|
|
17553
17772
|
const skipPull = options.pull === false;
|
|
17554
|
-
if (!targets || targets.length === 0) throw new LocalStartServiceError(
|
|
17773
|
+
if (!targets || targets.length === 0) throw new LocalStartServiceError(`${getEmbedConfig().cliName} start-service requires at least one <target>. Pass one or more service paths like 'Stack/Orders' 'Stack/Frontend'.`);
|
|
17555
17774
|
rejectExplicitCfnStackWithMultipleStacks(options, targets.length);
|
|
17556
17775
|
const perTarget = targets.map((t) => ({
|
|
17557
17776
|
target: t,
|
|
@@ -17593,7 +17812,7 @@ async function localStartServiceCommand(targets, options, extraStateProviders) {
|
|
|
17593
17812
|
});
|
|
17594
17813
|
await ensureDockerAvailable();
|
|
17595
17814
|
const appCmd = resolveApp(options.app);
|
|
17596
|
-
if (!appCmd) throw new Error(
|
|
17815
|
+
if (!appCmd) throw new Error(`No CDK app specified. Pass --app, set ${getEmbedConfig().envPrefix}_APP, or add "app" to cdk.json.`);
|
|
17597
17816
|
logger.info("Synthesizing CDK app...");
|
|
17598
17817
|
const synthesizer = new Synthesizer();
|
|
17599
17818
|
const context = parseContextOptions(options.context);
|
|
@@ -17741,7 +17960,7 @@ async function assumeTaskRole(roleArn, region) {
|
|
|
17741
17960
|
try {
|
|
17742
17961
|
const creds = (await sts.send(new AssumeRoleCommand({
|
|
17743
17962
|
RoleArn: roleArn,
|
|
17744
|
-
RoleSessionName:
|
|
17963
|
+
RoleSessionName: `${getEmbedConfig().resourceNamePrefix}-start-service-${Date.now()}`,
|
|
17745
17964
|
DurationSeconds: 3600
|
|
17746
17965
|
}))).Credentials;
|
|
17747
17966
|
if (!creds?.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken) throw new LocalStartServiceError(`AssumeRole(${roleArn}) returned no usable credentials.`);
|
|
@@ -17767,7 +17986,7 @@ async function buildEcsImageResolutionContext(target, stacks, options, stateProv
|
|
|
17767
17986
|
const wantsPseudoForEnvOrSecret = !!stateProvider && needs.needsEnvOrSecretSubstitution;
|
|
17768
17987
|
if (needs.needsPseudoParameters || wantsPseudoForEnvOrSecret) {
|
|
17769
17988
|
const region = options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? candidate.region;
|
|
17770
|
-
if (!region) logger.warn(
|
|
17989
|
+
if (!region) logger.warn(`Resolver references \${AWS::Region} but ${getEmbedConfig().binaryName} could not determine the target region. Pass --region, set AWS_REGION, or declare env.region on the CDK stack.`);
|
|
17771
17990
|
let accountId;
|
|
17772
17991
|
try {
|
|
17773
17992
|
accountId = await resolveCallerAccountId(region);
|
|
@@ -17874,12 +18093,13 @@ async function resolveSharedSidecarCredentials(options) {
|
|
|
17874
18093
|
if (options.profile) return resolveProfileCredentials(options.profile);
|
|
17875
18094
|
}
|
|
17876
18095
|
function createLocalStartServiceCommand(opts = {}) {
|
|
17877
|
-
|
|
18096
|
+
setEmbedConfig(opts.embedConfig);
|
|
18097
|
+
const cmd = 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.`).argument("<targets...>", "One or more CDK display paths or stack-qualified logical IDs of the AWS::ECS::Service resources to run").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 DescribeStackResources 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 DescribeStackResources 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.")).action(withErrorHandling(async (targets, options) => {
|
|
17878
18098
|
await localStartServiceCommand(targets, options, opts.extraStateProviders);
|
|
17879
18099
|
}));
|
|
17880
18100
|
[
|
|
17881
|
-
...commonOptions,
|
|
17882
|
-
...appOptions,
|
|
18101
|
+
...commonOptions(),
|
|
18102
|
+
...appOptions(),
|
|
17883
18103
|
...contextOptions
|
|
17884
18104
|
].forEach((opt) => cmd.addOption(opt));
|
|
17885
18105
|
cmd.addOption(deprecatedRegionOption);
|
|
@@ -17888,4 +18108,4 @@ function createLocalStartServiceCommand(opts = {}) {
|
|
|
17888
18108
|
|
|
17889
18109
|
//#endregion
|
|
17890
18110
|
export { LocalStateSourceError as a, rejectExplicitCfnStackWithMultipleStacks as c, CfnLocalStateProvider as d, createLocalInvokeCommand as i, resolveCfnRegion as l, createLocalRunTaskCommand as n, createLocalStateProvider as o, createLocalStartApiCommand as r, isCfnFlagPresent as s, createLocalStartServiceCommand as t, resolveCfnStackName as u };
|
|
17891
|
-
//# sourceMappingURL=local-start-service-
|
|
18111
|
+
//# sourceMappingURL=local-start-service-DquKcP26.js.map
|