cdk-local 0.73.6 → 0.75.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/dist/cli.js +2 -2
- package/dist/{docker-cmd-voNPrcRh.js → docker-cmd-Dqx2YENO.js} +47 -15
- package/dist/docker-cmd-Dqx2YENO.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/internal.d.ts +2 -2
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +2 -2
- package/dist/{local-list-DgwUotOK.js → local-list-CFINvNxo.js} +662 -475
- package/dist/local-list-CFINvNxo.js.map +1 -0
- package/dist/{local-list-ChlobAQL.d.ts → local-list-cmhJbUBN.d.ts} +10 -2
- package/dist/{local-list-ChlobAQL.d.ts.map → local-list-cmhJbUBN.d.ts.map} +1 -1
- package/package.json +1 -1
- package/dist/docker-cmd-voNPrcRh.js.map +0 -1
- package/dist/local-list-DgwUotOK.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as runDockerStreaming, c as getEmbedConfig, i as runDockerForeground, n as formatDockerLoginError, o as spawnStreaming, r as getDockerCmd, s as getLogger, u as setEmbedConfig } from "./docker-cmd-
|
|
1
|
+
import { a as runDockerStreaming, c as getEmbedConfig, i as runDockerForeground, n as formatDockerLoginError, o as spawnStreaming, r as getDockerCmd, s as getLogger, u as setEmbedConfig } from "./docker-cmd-Dqx2YENO.js";
|
|
2
2
|
import { cpSync, createWriteStream, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { homedir, tmpdir } from "node:os";
|
|
4
4
|
import * as path$1 from "node:path";
|
|
@@ -113,6 +113,35 @@ function parseAssumeRoleToken(raw, previous) {
|
|
|
113
113
|
return acc;
|
|
114
114
|
}
|
|
115
115
|
/**
|
|
116
|
+
* Compose `--assume-role` (value-form accumulator) and the separate
|
|
117
|
+
* `--assume-role-auto` boolean flag into the in-process
|
|
118
|
+
* representation `localStartApiCommand` consumes downstream.
|
|
119
|
+
*
|
|
120
|
+
* - both unset -> `undefined` (pass dev creds through)
|
|
121
|
+
* - only `--assume-role-auto` -> `{ perLambda: {}, bareAutoResolve: true }`
|
|
122
|
+
* (per-Lambda auto-resolve for every routed Lambda)
|
|
123
|
+
* - only `--assume-role` -> the value-form accumulator (global ARN
|
|
124
|
+
* and/or per-Lambda map) as-is
|
|
125
|
+
* - both -> accumulator with `bareAutoResolve = true` overlaid;
|
|
126
|
+
* the per-Lambda map still wins for named Lambdas, auto-resolve
|
|
127
|
+
* fills in for every Lambda the map does NOT name
|
|
128
|
+
*
|
|
129
|
+
* Enforces the mutual-exclusion guard: `--assume-role-auto` and a
|
|
130
|
+
* global ARN (`--assume-role <arn>`) both occupy the "default for
|
|
131
|
+
* every other Lambda" slot, so the combination is ambiguous and
|
|
132
|
+
* rejected at boot with a clear error. The per-Lambda map IS
|
|
133
|
+
* compatible with either side.
|
|
134
|
+
*/
|
|
135
|
+
function normalizeStartApiAssumeRole(raw, autoResolve) {
|
|
136
|
+
if (raw === void 0) return autoResolve ? {
|
|
137
|
+
perLambda: {},
|
|
138
|
+
bareAutoResolve: true
|
|
139
|
+
} : void 0;
|
|
140
|
+
if (autoResolve && raw.globalArn) throw new Error(`--assume-role-auto auto-resolves EACH routed Lambda's own execution role, but --assume-role ${raw.globalArn} also names a single global default. These are mutually exclusive on the global slot. Either drop the global ARN to keep --assume-role-auto for every Lambda, or drop --assume-role-auto to keep the global default. Per-Lambda overrides (--assume-role <LogicalId>=<arn>) are compatible with either side.`);
|
|
141
|
+
if (autoResolve) raw.bareAutoResolve = true;
|
|
142
|
+
return raw;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
116
145
|
* Resolve the effective IAM role ARN for a given Lambda. Per-Lambda
|
|
117
146
|
* override wins; otherwise the global default; otherwise `undefined`
|
|
118
147
|
* (no role to assume — pass developer creds through).
|
|
@@ -6498,7 +6527,10 @@ async function pullImage(image, skipPull) {
|
|
|
6498
6527
|
}
|
|
6499
6528
|
logger.debug(`Pulling ${image} (silent — pass --verbose to stream progress)`);
|
|
6500
6529
|
try {
|
|
6501
|
-
await runDockerStreaming(["pull", image], {
|
|
6530
|
+
await runDockerStreaming(["pull", image], {
|
|
6531
|
+
streamLive: false,
|
|
6532
|
+
progressLabel: `Pulling ${image}`
|
|
6533
|
+
});
|
|
6502
6534
|
} catch (err) {
|
|
6503
6535
|
const e = err;
|
|
6504
6536
|
if (e.exitCode === void 0 || e.exitCode === null) throw new DockerRunnerError(`docker pull ${image} failed: ${e.message ?? String(err)}`);
|
|
@@ -6773,7 +6805,8 @@ async function buildDockerImage(asset, cdkOutDir, options) {
|
|
|
6773
6805
|
try {
|
|
6774
6806
|
await runDockerStreaming(buildArgs, {
|
|
6775
6807
|
cwd: contextDir,
|
|
6776
|
-
env: { BUILDX_NO_DEFAULT_ATTESTATIONS: "1" }
|
|
6808
|
+
env: { BUILDX_NO_DEFAULT_ATTESTATIONS: "1" },
|
|
6809
|
+
...options.progressLabel !== void 0 && { progressLabel: options.progressLabel }
|
|
6777
6810
|
});
|
|
6778
6811
|
} catch (err) {
|
|
6779
6812
|
const e = err;
|
|
@@ -7080,7 +7113,8 @@ async function buildContainerImage(asset, cdkOutDir, options) {
|
|
|
7080
7113
|
const actualTag = await buildDockerImage(asset, cdkOutDir, {
|
|
7081
7114
|
tag,
|
|
7082
7115
|
platform,
|
|
7083
|
-
wrapError: (stderr) => new LocalInvokeBuildError(`docker build failed for container Lambda asset (${asset.source.directory ?? asset.source.executable?.join(" ")}): ${stderr}`)
|
|
7116
|
+
wrapError: (stderr) => new LocalInvokeBuildError(`docker build failed for container Lambda asset (${asset.source.directory ?? asset.source.executable?.join(" ")}): ${stderr}`),
|
|
7117
|
+
progressLabel: `Building container image (platform=${platform})`
|
|
7084
7118
|
});
|
|
7085
7119
|
if (actualTag !== tag) {
|
|
7086
7120
|
logger.debug(`Re-tagging executable-built image '${actualTag}' → '${tag}'`);
|
|
@@ -15260,6 +15294,9 @@ async function localStartApiCommand(targets, options, extraStateProviders) {
|
|
|
15260
15294
|
const interactivePicked = { value: false };
|
|
15261
15295
|
const allStacksConflictList = allStacksConflicts(options, targets, apiFilters);
|
|
15262
15296
|
if (allStacksConflictList.length > 0) throw new Error(`--all-stacks serves every stack's API and cannot be combined with a target subset selector (${allStacksConflictList.join(", ")}). Drop --all-stacks to target specific APIs, or drop the selector to serve them all. The bare --from-cfn-stack flag (no value) IS compatible with --all-stacks.`);
|
|
15297
|
+
const normalizedAssumeRole = normalizeStartApiAssumeRole(options.assumeRole, options.assumeRoleAuto ?? false);
|
|
15298
|
+
if (normalizedAssumeRole === void 0) delete options.assumeRole;
|
|
15299
|
+
else options.assumeRole = normalizedAssumeRole;
|
|
15263
15300
|
await applyRoleArnIfSet({
|
|
15264
15301
|
roleArn: options.roleArn,
|
|
15265
15302
|
region: options.region,
|
|
@@ -15269,7 +15306,8 @@ async function localStartApiCommand(targets, options, extraStateProviders) {
|
|
|
15269
15306
|
const appCmd = resolveApp(options.app);
|
|
15270
15307
|
if (!appCmd) throw new Error(`No CDK app specified. Pass --app, set ${getEmbedConfig().envPrefix}_APP, or add "app" to cdk.json.`);
|
|
15271
15308
|
const overrides = readEnvOverridesFile$4(options.envVars);
|
|
15272
|
-
const
|
|
15309
|
+
const debugPortRaw = options.debugPortBase ?? options.debugPort;
|
|
15310
|
+
const debugPortBase = debugPortRaw ? parseDebugPort(debugPortRaw) : void 0;
|
|
15273
15311
|
const perLambdaConcurrency = parsePerLambdaConcurrency(options.perLambdaConcurrency);
|
|
15274
15312
|
const inlineTmpDirs = /* @__PURE__ */ new Set();
|
|
15275
15313
|
const layerTmpDirs = /* @__PURE__ */ new Set();
|
|
@@ -15370,7 +15408,7 @@ async function localStartApiCommand(targets, options, extraStateProviders) {
|
|
|
15370
15408
|
logicalId,
|
|
15371
15409
|
stacks: targetStacks,
|
|
15372
15410
|
overrides,
|
|
15373
|
-
assumeRole:
|
|
15411
|
+
assumeRole: normalizedAssumeRole,
|
|
15374
15412
|
containerHost: options.containerHost,
|
|
15375
15413
|
...debugPortBase !== void 0 && { debugPort: debugPortBase + i },
|
|
15376
15414
|
stsRegion: options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"],
|
|
@@ -15378,7 +15416,9 @@ async function localStartApiCommand(targets, options, extraStateProviders) {
|
|
|
15378
15416
|
layerTmpDirs,
|
|
15379
15417
|
stateByStack,
|
|
15380
15418
|
skipPull: options.pull === false,
|
|
15419
|
+
skipBuild: options.build === false,
|
|
15381
15420
|
...options.profile !== void 0 && { profile: options.profile },
|
|
15421
|
+
...options.ecrRoleArn !== void 0 && { ecrRoleArn: options.ecrRoleArn },
|
|
15382
15422
|
...options.layerRoleArn !== void 0 && { layerRoleArn: options.layerRoleArn },
|
|
15383
15423
|
...profileCredentials && { profileCredentials },
|
|
15384
15424
|
...profileCredsFile && { profileCredsFile },
|
|
@@ -16033,6 +16073,59 @@ function warnIamRoutes(routesWithAuth) {
|
|
|
16033
16073
|
return true;
|
|
16034
16074
|
}
|
|
16035
16075
|
/**
|
|
16076
|
+
* Resolve the effective IAM role ARN to assume for a single routed
|
|
16077
|
+
* Lambda, honoring the four `--assume-role` forms of issue #256
|
|
16078
|
+
* Option 1 (non-breaking step):
|
|
16079
|
+
*
|
|
16080
|
+
* - per-Lambda override (`<LogicalId>=<arn>`) — wins over every other
|
|
16081
|
+
* form for the named Lambda.
|
|
16082
|
+
* - global default (bare ARN) — used when the per-Lambda map misses.
|
|
16083
|
+
* - bare-auto-resolve (`--assume-role` with no value) — when neither
|
|
16084
|
+
* the per-Lambda map nor a global default applies, resolves THIS
|
|
16085
|
+
* Lambda's own `Role` (its `AWS::Lambda::Function`'s `Role`
|
|
16086
|
+
* property -> role ARN). Resolution sources, in order:
|
|
16087
|
+
* 1. literal-ARN form of `Properties.Role` in the SYNTHESIZED
|
|
16088
|
+
* template (rare in CDK output but possible);
|
|
16089
|
+
* 2. deployed state (`stateBundle.state.resources`) when
|
|
16090
|
+
* `--from-cfn-stack` (or a host-provided extension) is
|
|
16091
|
+
* active, via the same shared helper
|
|
16092
|
+
* `resolveExecutionRoleArnFromState` `cdkl invoke`'s bare-
|
|
16093
|
+
* form uses.
|
|
16094
|
+
* - flag absent / `--no-assume-role-auto` — returns `undefined`
|
|
16095
|
+
* (per-Lambda map and global default already filtered out by the
|
|
16096
|
+
* null-check at the top; bare-auto-resolve = false short-circuits
|
|
16097
|
+
* before the state lookup).
|
|
16098
|
+
*
|
|
16099
|
+
* The shape-level mutual exclusion (bare-auto-resolve + global ARN
|
|
16100
|
+
* together) is caught at boot by
|
|
16101
|
+
* {@link normalizeStartApiAssumeRole}; this resolver only sees a
|
|
16102
|
+
* well-formed `AssumeRoleOption`.
|
|
16103
|
+
*
|
|
16104
|
+
* Bare-auto-resolve misses (no literal-ARN, no state, state did not
|
|
16105
|
+
* carry the role attribute) warn-and-fall-through to the
|
|
16106
|
+
* dev-creds-passed-through branch — same trade-off `cdkl invoke
|
|
16107
|
+
* --assume-role` makes when bare auto-resolve cannot find an ARN.
|
|
16108
|
+
*
|
|
16109
|
+
* Exported for unit testing.
|
|
16110
|
+
*/
|
|
16111
|
+
function resolveStartApiAssumeRoleArn(args) {
|
|
16112
|
+
const { logicalId, assumeRole, lambdaResource, stateBundle } = args;
|
|
16113
|
+
if (!assumeRole) return void 0;
|
|
16114
|
+
const explicit = effectiveAssumeRoleArn(logicalId, assumeRole);
|
|
16115
|
+
if (explicit) return explicit;
|
|
16116
|
+
if (!assumeRole.bareAutoResolve) return void 0;
|
|
16117
|
+
const roleProp = (lambdaResource.Properties ?? {})["Role"];
|
|
16118
|
+
if (typeof roleProp === "string" && roleProp.startsWith("arn:")) return roleProp;
|
|
16119
|
+
if (stateBundle) {
|
|
16120
|
+
const fromState = resolveExecutionRoleArnFromState(stateBundle.state, logicalId);
|
|
16121
|
+
if (fromState) {
|
|
16122
|
+
getLogger().info(`--assume-role: auto-resolved execution role for '${logicalId}' from state: ${fromState}`);
|
|
16123
|
+
return fromState;
|
|
16124
|
+
}
|
|
16125
|
+
}
|
|
16126
|
+
getLogger().warn(`--assume-role: could not auto-resolve the execution role ARN for '${logicalId}'. Pair --assume-role with --from-cfn-stack (or a host-provided state-source extension) so the deployed Role's ARN can be looked up, OR pin the ARN explicitly with --assume-role ${logicalId}=<arn>. Falling back to the developer's shell credentials for this Lambda.`);
|
|
16127
|
+
}
|
|
16128
|
+
/**
|
|
16036
16129
|
* Build the per-Lambda container spec — code dir, env vars (template +
|
|
16037
16130
|
* --env-vars overlay), STS-issued creds when --assume-role names this
|
|
16038
16131
|
* Lambda, optional --debug-port reservation. Errors out with a clear
|
|
@@ -16050,7 +16143,11 @@ async function buildContainerSpec(args) {
|
|
|
16050
16143
|
codeDir = lambda.codePath ?? materializeInlineCode$2(lambda.handler, lambda.inlineCode ?? "", resolveRuntimeFileExtension(lambda.runtime), inlineTmpDirs);
|
|
16051
16144
|
optDir = await materializeLambdaLayers$1(lambda.layers, layerTmpDirs, layerRoleArn);
|
|
16052
16145
|
} else {
|
|
16053
|
-
imageRef = (await resolveContainerImageForStartApi(lambda, skipPull, args.profile
|
|
16146
|
+
imageRef = (await resolveContainerImageForStartApi(lambda, skipPull, args.profile, {
|
|
16147
|
+
...args.skipBuild !== void 0 && { noBuild: args.skipBuild },
|
|
16148
|
+
...args.ecrRoleArn !== void 0 && { ecrRoleArn: args.ecrRoleArn },
|
|
16149
|
+
...args.stsRegion !== void 0 && { region: args.stsRegion }
|
|
16150
|
+
})).imageRef;
|
|
16054
16151
|
platform = architectureToPlatform(lambda.architecture);
|
|
16055
16152
|
}
|
|
16056
16153
|
let templateEnv = getTemplateEnv$1(lambda.resource);
|
|
@@ -16099,7 +16196,12 @@ async function buildContainerSpec(args) {
|
|
|
16099
16196
|
AWS_LAMBDA_LOG_STREAM_NAME: "local",
|
|
16100
16197
|
...envResult.resolved
|
|
16101
16198
|
};
|
|
16102
|
-
const roleArn =
|
|
16199
|
+
const roleArn = resolveStartApiAssumeRoleArn({
|
|
16200
|
+
logicalId,
|
|
16201
|
+
assumeRole,
|
|
16202
|
+
lambdaResource: lambda.resource,
|
|
16203
|
+
stateBundle
|
|
16204
|
+
});
|
|
16103
16205
|
if (roleArn) {
|
|
16104
16206
|
const creds = await assumeLambdaExecutionRole$1(roleArn, stsRegion, profile);
|
|
16105
16207
|
dockerEnv["AWS_ACCESS_KEY_ID"] = creds.accessKeyId;
|
|
@@ -16171,23 +16273,30 @@ async function buildContainerSpec(args) {
|
|
|
16171
16273
|
* Resolve a container Lambda's local docker image — local build from
|
|
16172
16274
|
* `cdk.out` asset manifest first, ECR-pull fallback when the asset
|
|
16173
16275
|
* manifest has no matching entry. Mirrors `cdkl invoke`'s
|
|
16174
|
-
* `resolveContainerImagePlan` shape
|
|
16175
|
-
* need the no-build flag (deterministic-tag cache reuse is automatic
|
|
16176
|
-
* across reloads because the per-Lambda tag is content-addressed).
|
|
16276
|
+
* `resolveContainerImagePlan` shape.
|
|
16177
16277
|
*
|
|
16178
|
-
*
|
|
16179
|
-
* `cdkl invoke`
|
|
16180
|
-
*
|
|
16278
|
+
* Issue #249 / C7 + C8 — `noBuild` and `ecrRoleArn` are accepted for
|
|
16279
|
+
* parity with `cdkl invoke` / `cdkl run-task`. `noBuild` short-circuits
|
|
16280
|
+
* the local-asset build path (the deterministic content-addressed tag
|
|
16281
|
+
* must already be in the local registry). `ecrRoleArn` lets the
|
|
16282
|
+
* ECR-pull fallback authenticate against cross-account / centralized
|
|
16283
|
+
* registries via STS AssumeRole — same-account / same-region pulls do
|
|
16284
|
+
* not need it.
|
|
16181
16285
|
*/
|
|
16182
|
-
async function resolveContainerImageForStartApi(lambda, skipPull, profile) {
|
|
16286
|
+
async function resolveContainerImageForStartApi(lambda, skipPull, profile, options) {
|
|
16183
16287
|
const logger = getLogger();
|
|
16184
16288
|
const localBuild = await resolveLocalBuildPlan$1(lambda);
|
|
16185
|
-
if (localBuild) return { imageRef: await buildContainerImage(localBuild.asset, localBuild.cdkOutDir, {
|
|
16289
|
+
if (localBuild) return { imageRef: await buildContainerImage(localBuild.asset, localBuild.cdkOutDir, {
|
|
16290
|
+
architecture: lambda.architecture,
|
|
16291
|
+
...options?.noBuild !== void 0 && { noBuild: options.noBuild }
|
|
16292
|
+
}) };
|
|
16186
16293
|
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.`);
|
|
16187
|
-
logger.info(`No matching cdk.out asset for ${lambda.imageUri}; falling back to ECR pull
|
|
16294
|
+
logger.info(`No matching cdk.out asset for ${lambda.imageUri}; falling back to ECR pull...`);
|
|
16188
16295
|
return { imageRef: await pullEcrImage(lambda.imageUri, {
|
|
16189
16296
|
skipPull,
|
|
16190
|
-
...profile !== void 0 && { profile }
|
|
16297
|
+
...profile !== void 0 && { profile },
|
|
16298
|
+
...options?.ecrRoleArn !== void 0 && { ecrRoleArn: options.ecrRoleArn },
|
|
16299
|
+
...options?.region !== void 0 && { region: options.region }
|
|
16191
16300
|
}) };
|
|
16192
16301
|
}
|
|
16193
16302
|
/**
|
|
@@ -16925,7 +17034,7 @@ function createLocalStartApiCommand(opts = {}) {
|
|
|
16925
17034
|
* `--help` clusters. Chainable: returns `cmd`.
|
|
16926
17035
|
*/
|
|
16927
17036
|
function addStartApiSpecificOptions(cmd) {
|
|
16928
|
-
return cmd.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("--all-stacks", "Serve every stack's API in a multi-stack app (each API on its own port) instead of erroring out. Mutually exclusive with a positional target subset, --stack, and an explicit --from-cfn-stack <name>; the bare --from-cfn-stack flag stays compatible (binds each routed stack to its own CFn stack).").default(false)).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.
|
|
17037
|
+
return cmd.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("--all-stacks", "Serve every stack's API in a multi-stack app (each API on its own port) instead of erroring out. Mutually exclusive with a positional target subset, --stack, and an explicit --from-cfn-stack <name>; the bare --from-cfn-stack flag stays compatible (binds each routed stack to its own CFn stack).").default(false)).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("--no-build", "Skip docker build on every container Lambda local-asset build (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("--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("--debug-port <port>", "Alias of --debug-port-base for parity with `cdkl invoke --debug-port`. Reserves a contiguous --debug-port range (one per Lambda) starting at <port>.")).addOption(new Option("--ecr-role-arn <arn>", "Role ARN to assume before authenticating against ECR for cross-account / centralized registries on the container-Lambda ECR-pull fallback. 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("--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. Two forms: (1) `--assume-role <arn>` sets a single global default ARN used for every routed Lambda; (2) `--assume-role <LogicalId>=<arn>` (repeatable) is a per-Lambda override (wins over the global default and over --assume-role-auto for the named Lambda). Pair with --assume-role-auto to auto-resolve every other Lambda. Precedence: per-Lambda > (--assume-role-auto OR global default) > unset (developer creds passed through).").argParser((raw, prev) => parseAssumeRoleToken(raw, prev))).addOption(new Option("--assume-role-auto", "Auto-resolve EACH routed Lambda's own execution role per-Lambda (literal ARN / Fn::GetAtt against template / --from-cfn-stack state). Slower boot — one STS call per Lambda — but the right shape when each Lambda's deployed role differs. Mutually exclusive with `--assume-role <arn>` (global default); errors at boot. Compatible with `--assume-role <LogicalId>=<arn>` per-Lambda overrides — the map wins for named Lambdas, auto-resolve handles the rest.").default(false)).addOption(new Option("--watch", "Hot-reload: re-synth + re-discover routes when the CDK app's source changes (honors cdk.json watch.include/exclude; cdk.out, node_modules, .git are always excluded). 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 <targets...> argument instead. Accepts a SINGLE identifier; for a subset pass multiple positional targets. 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 ListStackResources 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 ListStackResources does not return per-attribute values).")).addOption(new Option("--stack-region <region>", "Region of the state record to read. Used with --from-cfn-stack as the CFn client region.")).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("--strict-sigv4", "Opt-in: DENY AWS_IAM SigV4 requests that cannot be cryptographically verified (foreign access-key-id — e.g. a federated / Cognito Identity Pool / cross-account signer — OR no local AWS credentials configured) instead of the default warn-and-pass. DEFAULT off: cdk-local warn-and-passes unverifiable IAM requests with a placeholder principalId so local dev exercises app logic without reproducing an auth boundary it cannot fully emulate. OAC-fronted Function URLs always warn-and-pass regardless.").default(false));
|
|
16929
17038
|
}
|
|
16930
17039
|
|
|
16931
17040
|
//#endregion
|
|
@@ -17554,7 +17663,7 @@ function createLocalInvokeCommand(opts = {}) {
|
|
|
17554
17663
|
* `--help` clusters. Chainable: returns `cmd`.
|
|
17555
17664
|
*/
|
|
17556
17665
|
function addInvokeSpecificOptions(cmd) {
|
|
17557
|
-
return cmd.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. Semantics differ by code path: ZIP Lambdas skip pulling the public Lambda base image; Container Lambdas on the local-build path are a no-op (docker build does not refresh the FROM cache by default); Container Lambdas on the ECR-pull fallback skip docker pull AND error if the image is not in the local cache (re-run without --no-pull or pre-pull manually).")).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 ListStackResources 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 ListStackResources does not return per-attribute values).")).addOption(new Option("--stack-region <region>", "Region of the state record to read. Used with --from-cfn-stack as the CFn client region."));
|
|
17666
|
+
return cmd.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\"}, \"Parameters\": {...}})")).addOption(new Option("--no-pull", "Skip docker pull. Semantics differ by code path: ZIP Lambdas skip pulling the public Lambda base image; Container Lambdas on the local-build path are a no-op (docker build does not refresh the FROM cache by default); Container Lambdas on the ECR-pull fallback skip docker pull AND error if the image is not in the local cache (re-run without --no-pull or pre-pull manually).")).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 IP the host uses to bind the RIE port to. Must be a numeric IP (Docker rejects hostnames here). Defaults to 127.0.0.1.").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 ListStackResources 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 ListStackResources does not return per-attribute values).")).addOption(new Option("--stack-region <region>", "Region of the state record to read. Used with --from-cfn-stack as the CFn client region."));
|
|
17558
17667
|
}
|
|
17559
17668
|
|
|
17560
17669
|
//#endregion
|
|
@@ -17616,7 +17725,7 @@ async function buildAgentCoreCodeImage(options) {
|
|
|
17616
17725
|
"--file",
|
|
17617
17726
|
dockerfilePath,
|
|
17618
17727
|
options.sourceDir
|
|
17619
|
-
]);
|
|
17728
|
+
], { progressLabel: `Building agent image (runtime=${options.runtime})` });
|
|
17620
17729
|
} catch (err) {
|
|
17621
17730
|
const stderr = err.stderr?.trim();
|
|
17622
17731
|
throw new LocalInvokeBuildError(`docker build failed for AgentCore code artifact (${options.sourceDir})${stderr ? `: ${stderr}` : ""}`);
|
|
@@ -19219,7 +19328,7 @@ function createLocalInvokeAgentCoreCommand(opts = {}) {
|
|
|
19219
19328
|
* in three `--help` clusters. Chainable: returns `cmd`.
|
|
19220
19329
|
*/
|
|
19221
19330
|
function addInvokeAgentCoreSpecificOptions(cmd) {
|
|
19222
|
-
return cmd.addOption(new Option("-e, --event <file>", "JSON event payload file (default: {})")).addOption(new Option("--event-stdin", "Read event JSON from stdin").default(false)).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"LogicalId\":{\"KEY\":\"VALUE\"}})")).addOption(new Option("--session-id <id>", "AgentCore runtime session id header value (default: a random UUID)")).addOption(new Option("--ws", "Stream over the HTTP-protocol agent's bidirectional /ws WebSocket endpoint (on 8080) instead of POST /invocations: send --event as the first frame and print every received frame to stdout until the agent closes. Ignored for an MCP runtime.").default(false)).addOption(new Option("--ws-interactive", "REPL mode for --ws: after the initial --event frame, read additional frames from stdin (one frame per line, trailing newline stripped) and send each as a text frame until stdin EOFs (Ctrl-D) or the agent closes. Only meaningful with --ws.").default(false)).addOption(new Option("--bearer-token <jwt>", "Bearer JWT
|
|
19331
|
+
return cmd.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\"}, \"Parameters\": {...}})")).addOption(new Option("--session-id <id>", "AgentCore runtime session id header value (default: a random UUID)")).addOption(new Option("--ws", "Stream over the HTTP-protocol agent's bidirectional /ws WebSocket endpoint (on 8080) instead of POST /invocations: send --event as the first frame and print every received frame to stdout until the agent closes. Ignored for an MCP runtime.").default(false)).addOption(new Option("--ws-interactive", "REPL mode for --ws: after the initial --event frame, read additional frames from stdin (one frame per line, trailing newline stripped) and send each as a text frame until stdin EOFs (Ctrl-D) or the agent closes. Only meaningful with --ws.").default(false)).addOption(new Option("--bearer-token <jwt>", "Bearer JWT this command SUPPLIES (the supplier role) when the runtime declares a customJwtAuthorizer — `cdkl invoke-agentcore` is the local-dev client making the outbound call, so it always presents this token to its own invocation. Verified against the runtime's OIDC discovery URL (signature / issuer / expiry / audience) before the container starts, then forwarded to /invocations as Authorization: Bearer <jwt>. Contrast with `cdkl start-alb --bearer-token`, where the role is reversed — the ALB front-door RECEIVES inbound requests and injects this token only as a default when an inbound request has none (default-when-missing).")).addOption(new Option("--no-verify-auth", "Skip inbound JWT verification even when the runtime declares a customJwtAuthorizer (local-dev escape hatch). A --bearer-token, if given, is still forwarded.")).addOption(new Option("--sigv4", "Sign the /invocations POST with AWS SigV4 (service bedrock-agentcore) using the resolved credentials, matching the cloud default when the runtime declares no customJwtAuthorizer. Opt-in: default unsigned. Mutually exclusive with --bearer-token; ignored on a JWT-protected runtime.").default(false)).addOption(new Option("--platform <platform>", "docker --platform for the agent container (linux/amd64 or linux/arm64). Defaults to linux/arm64 because the cloud AgentCore Runtime requires arm64. Override to linux/amd64 only when iterating against an amd64 dev container locally; note the image will not run on the cloud runtime as-is.").choices(["linux/amd64", "linux/arm64"]).default("linux/arm64")).addOption(new Option("--no-pull", "Skip docker pull (use cached image) — no-op for the local-build path")).addOption(new Option("--no-build", "Skip docker build on the local-asset path (use the previously-built tag). No-op for the ECR / registry pull paths.")).addOption(new Option("--container-host <host>", "Host IP the host uses to bind the agent port to. Must be a numeric IP (Docker rejects hostnames here). Defaults to 127.0.0.1.").default("127.0.0.1")).addOption(new Option("--timeout <ms>", "Per-request timeout in milliseconds. Applied to POST /invocations, POST /mcp, and the /ws open-to-close window. Raise this for long-running agent calls that exceed the default.").default(12e4).argParser(parseTimeoutMs)).addOption(new Option("--assume-role [arn]", "Assume the runtime's execution role and forward STS-issued temp credentials to the container so the agent runs with the deployed role. Three forms: (1) `--assume-role <arn>` assumes the explicit ARN; (2) `--assume-role` (bare) uses the runtime's RoleArn when it is a literal ARN; (3) `--no-assume-role` opts out. Off by default — the developer's shell credentials are forwarded unchanged.")).addOption(new Option("--ecr-role-arn <arn>", "Role ARN to assume before authenticating against ECR for cross-account / centralized registries. Same-account / same-region pulls do not need this flag.")).addOption(new Option("--from-cfn-stack [cfn-stack-name]", "Read a deployed CloudFormation stack via ListStackResources and substitute Ref / Fn::ImportValue in env vars with the deployed physical IDs / exports. Bare form uses the resolved stack name; pass an explicit value when the CFn stack name differs.")).addOption(new Option("--stack-region <region>", "Region of the state record to read. Used with --from-cfn-stack as the CFn client region."));
|
|
19223
19332
|
}
|
|
19224
19333
|
|
|
19225
19334
|
//#endregion
|
|
@@ -20130,10 +20239,19 @@ async function prepareOneImage(task, container, options) {
|
|
|
20130
20239
|
else if (entries.length === 1) asset = entries[0][1];
|
|
20131
20240
|
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.`);
|
|
20132
20241
|
const tag = `${getEmbedConfig().resourceNamePrefix}-run-task-${(image.assetHash ?? "single").slice(0, 16)}`;
|
|
20242
|
+
if (options.skipBuild === true) {
|
|
20243
|
+
const logger = getLogger().child("ecs-runner");
|
|
20244
|
+
if (image.assetHash === void 0) throw new LocalInvokeBuildError(`Cannot honor --no-build for ECS container '${container.name}': the asset has no stable assetHash (synthetic tag '${tag}' may collide with unrelated prior runs). Drop --no-build, or re-synthesize with an explicit assetHash on the ContainerImage.fromAsset call.`);
|
|
20245
|
+
logger.info(`Skipping docker build (--no-build) for container '${container.name}'; verifying ${tag} is in local registry...`);
|
|
20246
|
+
if (!await isImageInLocalCache(tag)) throw new LocalInvokeBuildError(`image '${tag}' not in local registry and --no-build is set (ECS container '${container.name}'); remove --no-build or run \`docker build\` manually first.`);
|
|
20247
|
+
logger.debug(`Local tag ${tag} is cached; skipping build for container '${container.name}'.`);
|
|
20248
|
+
return tag;
|
|
20249
|
+
}
|
|
20133
20250
|
const actualTag = await buildDockerImage(asset, cdkOutDir, {
|
|
20134
20251
|
tag,
|
|
20135
20252
|
...options.platformOverride !== void 0 && { platform: options.platformOverride },
|
|
20136
|
-
wrapError: (stderr) => new LocalInvokeBuildError(`docker build failed for ECS container '${container.name}' (${asset.source.directory ?? asset.source.executable?.join(" ")}): ${stderr}`)
|
|
20253
|
+
wrapError: (stderr) => new LocalInvokeBuildError(`docker build failed for ECS container '${container.name}' (${asset.source.directory ?? asset.source.executable?.join(" ")}): ${stderr}`),
|
|
20254
|
+
progressLabel: `Building container image for '${container.name}'`
|
|
20137
20255
|
});
|
|
20138
20256
|
if (actualTag !== tag) try {
|
|
20139
20257
|
await runDockerStreaming([
|
|
@@ -20356,419 +20474,85 @@ function shellJoin(parts) {
|
|
|
20356
20474
|
}
|
|
20357
20475
|
|
|
20358
20476
|
//#endregion
|
|
20359
|
-
//#region src/
|
|
20360
|
-
|
|
20361
|
-
|
|
20362
|
-
|
|
20363
|
-
|
|
20364
|
-
|
|
20365
|
-
|
|
20366
|
-
|
|
20367
|
-
|
|
20368
|
-
|
|
20369
|
-
|
|
20370
|
-
|
|
20371
|
-
|
|
20372
|
-
|
|
20373
|
-
|
|
20374
|
-
|
|
20375
|
-
|
|
20376
|
-
|
|
20377
|
-
|
|
20378
|
-
if (!cleanupPromise) cleanupPromise = (async () => {
|
|
20379
|
-
try {
|
|
20380
|
-
await cleanupEcsRun(state, { keepRunning: options.keepRunning });
|
|
20381
|
-
} catch (err) {
|
|
20382
|
-
getLogger().debug(`cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
20383
|
-
}
|
|
20384
|
-
if (profileCredsFile) try {
|
|
20385
|
-
await profileCredsFile.dispose();
|
|
20386
|
-
} catch (err) {
|
|
20387
|
-
getLogger().debug(`Failed to remove profile credentials tmpdir ${profileCredsFile.hostPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
20388
|
-
}
|
|
20389
|
-
})();
|
|
20390
|
-
await cleanupPromise;
|
|
20391
|
-
};
|
|
20392
|
-
try {
|
|
20393
|
-
await applyRoleArnIfSet({
|
|
20394
|
-
roleArn: options.roleArn,
|
|
20395
|
-
region: options.region,
|
|
20396
|
-
profile: options.profile
|
|
20397
|
-
});
|
|
20398
|
-
await ensureDockerAvailable();
|
|
20399
|
-
const appCmd = resolveApp(options.app);
|
|
20400
|
-
if (!appCmd) throw new Error(`No CDK app specified. Pass --app, set ${getEmbedConfig().envPrefix}_APP, or add "app" to cdk.json.`);
|
|
20401
|
-
logger.info("Synthesizing CDK app...");
|
|
20402
|
-
const synthesizer = new Synthesizer();
|
|
20403
|
-
const context = parseContextOptions(options.context);
|
|
20404
|
-
const synthOpts = {
|
|
20405
|
-
app: appCmd,
|
|
20406
|
-
output: options.output,
|
|
20407
|
-
...options.region && { region: options.region },
|
|
20408
|
-
...options.profile && { profile: options.profile },
|
|
20409
|
-
...Object.keys(context).length > 0 && { context }
|
|
20410
|
-
};
|
|
20411
|
-
const { stacks } = await synthesizer.synthesize(synthOpts);
|
|
20412
|
-
const resolvedTarget = await resolveSingleTarget(target, {
|
|
20413
|
-
entries: listTargets(stacks).ecsTaskDefinitions,
|
|
20414
|
-
message: "Select an ECS task definition to run",
|
|
20415
|
-
noun: "ECS task definitions",
|
|
20416
|
-
onMissing: () => new CdkLocalError(`${getEmbedConfig().cliName} run-task requires a <target> (an ECS task definition display path or logical ID). Run \`${getEmbedConfig().cliName} list\` to see them, or run it in a TTY to pick interactively.`, "LOCAL_RUN_TASK_TARGET_REQUIRED")
|
|
20417
|
-
});
|
|
20418
|
-
const candidate = pickCandidateStack$1(parseEcsTarget(resolvedTarget).stackPattern, stacks);
|
|
20419
|
-
stateProvider = createLocalStateProvider(options, candidate?.stackName ?? "", await resolveCfnFallbackRegion(options, candidate?.region), extraStateProviders);
|
|
20420
|
-
const imageContext = await buildEcsImageResolutionContext$1(candidate, stateProvider, options);
|
|
20421
|
-
const task = resolveEcsTaskTarget(resolvedTarget, stacks, imageContext);
|
|
20422
|
-
logger.info(`Target: ${task.stack.stackName}/${task.taskDefinitionLogicalId} (family=${task.family}, containers=${task.containers.length})`);
|
|
20423
|
-
const taskNeeds = detectEcsImageResolutionNeeds(stacks.find((s) => s.stackName === task.stack.stackName) ?? task.stack);
|
|
20424
|
-
if (stateProvider && taskNeeds.needsCrossStackResolver) {
|
|
20425
|
-
const consumerRegion = options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? task.stack.region ?? "us-east-1";
|
|
20426
|
-
const resolver = await stateProvider.buildCrossStackResolver(consumerRegion);
|
|
20427
|
-
if (resolver) await applyCrossStackResolverToTask(task, {
|
|
20428
|
-
resources: imageContext?.stateResources ?? {},
|
|
20429
|
-
...imageContext?.pseudoParameters && { pseudoParameters: imageContext.pseudoParameters },
|
|
20430
|
-
...imageContext?.stateParameters && { parameters: imageContext.stateParameters },
|
|
20431
|
-
...imageContext?.stateSensitiveParameters?.length && { sensitiveParameters: new Set(imageContext.stateSensitiveParameters) },
|
|
20432
|
-
consumerRegion,
|
|
20433
|
-
crossStackResolver: resolver
|
|
20434
|
-
});
|
|
20435
|
-
} else if (!stateProvider && taskNeeds.needsCrossStackResolver) logger.warn("Container Environment / Secrets entries contain Fn::ImportValue / Fn::GetStackOutput intrinsics. Pass a state-source flag (e.g. --from-cfn-stack or a host-provided extension) to substitute them against deployed state.");
|
|
20436
|
-
sigintHandler = () => {
|
|
20437
|
-
sigintCount += 1;
|
|
20438
|
-
if (sigintCount >= 2) {
|
|
20439
|
-
process.stderr.write("Force-exit on second ^C; container cleanup skipped.\n");
|
|
20440
|
-
process.exit(130);
|
|
20441
|
-
}
|
|
20442
|
-
logger.info("Stopping task...");
|
|
20443
|
-
cleanup().then(() => process.exit(130));
|
|
20444
|
-
};
|
|
20445
|
-
process.on("SIGINT", sigintHandler);
|
|
20446
|
-
let assumedCredentials;
|
|
20447
|
-
let resolvedRoleArn;
|
|
20448
|
-
if (options.assumeTaskRole === true) {
|
|
20449
|
-
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>`);
|
|
20450
|
-
resolvedRoleArn = await resolvePlaceholderAccount$1(task.taskRoleArn, options.region, options.profile);
|
|
20451
|
-
assumedCredentials = await assumeTaskRole$1(resolvedRoleArn, options.region, options.profile);
|
|
20452
|
-
} else if (typeof options.assumeTaskRole === "string") {
|
|
20453
|
-
resolvedRoleArn = options.assumeTaskRole;
|
|
20454
|
-
assumedCredentials = await assumeTaskRole$1(resolvedRoleArn, options.region, options.profile);
|
|
20455
|
-
}
|
|
20456
|
-
const sidecarCredentials = await resolveSidecarCredentials(options, assumedCredentials);
|
|
20457
|
-
if (options.profile && sidecarCredentials && !assumedCredentials) profileCredsFile = await writeProfileCredentialsFile(options.profile, sidecarCredentials);
|
|
20458
|
-
const envOverrides = readEnvOverridesFile$1(options.envVars);
|
|
20459
|
-
const runOpts = {
|
|
20460
|
-
cluster: options.cluster,
|
|
20461
|
-
containerHost: options.containerHost,
|
|
20462
|
-
skipPull: options.pull === false,
|
|
20463
|
-
keepRunning: options.keepRunning,
|
|
20464
|
-
detach: options.detach
|
|
20465
|
-
};
|
|
20466
|
-
if (envOverrides) runOpts.envOverrides = envOverrides;
|
|
20467
|
-
if (sidecarCredentials) runOpts.taskCredentials = sidecarCredentials;
|
|
20468
|
-
if (resolvedRoleArn) runOpts.taskRoleArn = resolvedRoleArn;
|
|
20469
|
-
if (options.platform) runOpts.platformOverride = options.platform;
|
|
20470
|
-
if (options.region) runOpts.region = options.region;
|
|
20471
|
-
if (options.ecrRoleArn) runOpts.ecrRoleArn = options.ecrRoleArn;
|
|
20472
|
-
if (options.profile) runOpts.profile = options.profile;
|
|
20473
|
-
const hostPortOverrides = parseHostPortOverrides(options.hostPort);
|
|
20474
|
-
if (Object.keys(hostPortOverrides).length > 0) runOpts.hostPortOverrides = hostPortOverrides;
|
|
20475
|
-
if (profileCredsFile) runOpts.profileCredentialsFile = {
|
|
20476
|
-
hostPath: profileCredsFile.hostPath,
|
|
20477
|
-
containerPath: profileCredsFile.containerPath,
|
|
20478
|
-
profileName: profileCredsFile.profileName
|
|
20479
|
-
};
|
|
20480
|
-
const result = await runEcsTask(task, runOpts, state);
|
|
20481
|
-
if (options.detach) {
|
|
20482
|
-
logger.info(`Task containers started in detached mode; ${getEmbedConfig().binaryName} is exiting.`);
|
|
20483
|
-
logger.info(`Use 'docker ps --filter network=${result.state.network?.networkName ?? "<network>"}' to inspect; tear down with 'docker rm -f' and 'docker network rm'.`);
|
|
20484
|
-
sigintCount = 99;
|
|
20485
|
-
return;
|
|
20486
|
-
}
|
|
20487
|
-
if (result.essentialContainerName) logger.info(`Essential container '${result.essentialContainerName}' exited with code ${result.exitCode}.`);
|
|
20488
|
-
if (result.exitCode !== 0) process.exitCode = result.exitCode;
|
|
20489
|
-
} finally {
|
|
20490
|
-
if (sigintHandler) process.off("SIGINT", sigintHandler);
|
|
20491
|
-
if (stateProvider) stateProvider.dispose();
|
|
20492
|
-
if (!options.detach) await cleanup();
|
|
20477
|
+
//#region src/local/ecs-service-resolver.ts
|
|
20478
|
+
function resolveEcsServiceTarget(target, stacks, context, options) {
|
|
20479
|
+
if (stacks.length === 0) throw new EcsTaskResolutionError("No stacks found in the synthesized assembly.");
|
|
20480
|
+
const parsed = parseEcsTarget(target);
|
|
20481
|
+
const stack = pickStack$1(parsed, stacks);
|
|
20482
|
+
const resources = stack.template.Resources ?? {};
|
|
20483
|
+
let serviceLogicalId;
|
|
20484
|
+
let serviceResource;
|
|
20485
|
+
if (parsed.isPath) {
|
|
20486
|
+
const index = buildCdkPathIndex(stack.template);
|
|
20487
|
+
const services = resolveCdkPathToLogicalIds(parsed.pathOrId, index).filter(({ logicalId: l }) => resources[l]?.Type === "AWS::ECS::Service");
|
|
20488
|
+
if (services.length === 0) throw notFoundError(target, stack, resources, parsed);
|
|
20489
|
+
if (services.length > 1) throw new EcsTaskResolutionError(`Target '${target}' matches ${services.length} ECS services in ${stack.stackName}: ` + services.map((s) => s.logicalId).join(", ") + ". Refine the path or use the stack:LogicalId form.");
|
|
20490
|
+
serviceLogicalId = services[0].logicalId;
|
|
20491
|
+
serviceResource = resources[serviceLogicalId];
|
|
20492
|
+
} else {
|
|
20493
|
+
serviceResource = resources[parsed.pathOrId];
|
|
20494
|
+
if (!serviceResource) throw notFoundError(target, stack, resources, parsed);
|
|
20495
|
+
serviceLogicalId = parsed.pathOrId;
|
|
20493
20496
|
}
|
|
20497
|
+
if (!serviceLogicalId || !serviceResource) throw notFoundError(target, stack, resources, parsed);
|
|
20498
|
+
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.`);
|
|
20499
|
+
if (serviceResource.Type !== "AWS::ECS::Service") throw new EcsTaskResolutionError(`Resource '${serviceLogicalId}' in ${stack.stackName} is ${serviceResource.Type}, not an AWS::ECS::Service.`);
|
|
20500
|
+
return extractServiceProperties(stack, serviceLogicalId, serviceResource, stacks, context, options);
|
|
20494
20501
|
}
|
|
20495
20502
|
/**
|
|
20496
|
-
*
|
|
20497
|
-
*
|
|
20498
|
-
*
|
|
20503
|
+
* Pure-functional extraction from the synth resource. Exposed for unit
|
|
20504
|
+
* testing the per-field resolution rules (DesiredCount default, missing
|
|
20505
|
+
* TaskDefinition, intrinsic shapes).
|
|
20499
20506
|
*/
|
|
20500
|
-
|
|
20501
|
-
|
|
20502
|
-
const
|
|
20503
|
-
const
|
|
20504
|
-
|
|
20505
|
-
|
|
20506
|
-
})
|
|
20507
|
-
|
|
20508
|
-
|
|
20509
|
-
|
|
20510
|
-
|
|
20511
|
-
|
|
20512
|
-
|
|
20507
|
+
function extractServiceProperties(stack, serviceLogicalId, resource, stacks, context, options) {
|
|
20508
|
+
const props = resource.Properties ?? {};
|
|
20509
|
+
const warnings = [];
|
|
20510
|
+
const taskDefRef = props["TaskDefinition"];
|
|
20511
|
+
if (taskDefRef === void 0 || taskDefRef === null) throw new EcsTaskResolutionError(`ECS Service '${serviceLogicalId}' in ${stack.stackName} has no TaskDefinition property.`);
|
|
20512
|
+
const taskDefLogicalId = resolveTaskDefinitionReference(taskDefRef, stack, serviceLogicalId);
|
|
20513
|
+
const task = resolveEcsTaskTarget(`${stack.stackName}:${taskDefLogicalId}`, stacks, context);
|
|
20514
|
+
const desiredCount = parseDesiredCount(props["DesiredCount"], serviceLogicalId);
|
|
20515
|
+
const healthCheckGracePeriodSeconds = parseHealthCheckGrace(props["HealthCheckGracePeriodSeconds"], serviceLogicalId);
|
|
20516
|
+
const serviceName = parseServiceName(props["ServiceName"], serviceLogicalId);
|
|
20517
|
+
const serviceDisplayName = deriveServiceDisplayName(props["ServiceName"], serviceLogicalId, resource.Metadata);
|
|
20518
|
+
if (!options?.suppressLoadBalancerWarning && Array.isArray(props["LoadBalancers"]) && props["LoadBalancers"].length > 0) {
|
|
20519
|
+
const { cliName } = getEmbedConfig();
|
|
20520
|
+
warnings.push(`ECS Service '${serviceLogicalId}' declares LoadBalancers, but \`${cliName} start-service\` runs the replicas only; no local listener fronts them. Reach the containers via their published ports, or run \`${cliName} start-alb <Stack>/<Alb>\` to boot the same replicas behind a local front-door that round-robins the listener rules.`);
|
|
20513
20521
|
}
|
|
20522
|
+
const serviceConnect = extractServiceConnect(props["ServiceConnectConfiguration"], task);
|
|
20523
|
+
const out = {
|
|
20524
|
+
stack,
|
|
20525
|
+
serviceLogicalId,
|
|
20526
|
+
resource,
|
|
20527
|
+
serviceName,
|
|
20528
|
+
serviceDisplayName,
|
|
20529
|
+
desiredCount,
|
|
20530
|
+
healthCheckGracePeriodSeconds,
|
|
20531
|
+
task,
|
|
20532
|
+
serviceRegistries: extractServiceRegistries(props["ServiceRegistries"], serviceLogicalId, warnings),
|
|
20533
|
+
warnings
|
|
20534
|
+
};
|
|
20535
|
+
if (serviceConnect) out.serviceConnect = serviceConnect;
|
|
20536
|
+
return out;
|
|
20514
20537
|
}
|
|
20515
20538
|
/**
|
|
20516
|
-
*
|
|
20517
|
-
|
|
20518
|
-
|
|
20519
|
-
|
|
20520
|
-
|
|
20521
|
-
|
|
20522
|
-
|
|
20523
|
-
|
|
20524
|
-
|
|
20525
|
-
|
|
20526
|
-
|
|
20527
|
-
|
|
20528
|
-
|
|
20529
|
-
|
|
20530
|
-
|
|
20531
|
-
|
|
20532
|
-
|
|
20533
|
-
secretAccessKey: creds.SecretAccessKey,
|
|
20534
|
-
sessionToken: creds.SessionToken
|
|
20535
|
-
};
|
|
20536
|
-
} finally {
|
|
20537
|
-
sts.destroy();
|
|
20538
|
-
}
|
|
20539
|
-
}
|
|
20540
|
-
/**
|
|
20541
|
-
* Build the substitution context the ECS task resolver consumes.
|
|
20542
|
-
* Returns `undefined` when no container's `Image` field needs
|
|
20543
|
-
* substitution — the resolver behaves as before in that case.
|
|
20544
|
-
*/
|
|
20545
|
-
async function buildEcsImageResolutionContext$1(candidate, stateProvider, options) {
|
|
20546
|
-
const logger = getLogger();
|
|
20547
|
-
if (!candidate) return void 0;
|
|
20548
|
-
const needs = detectEcsImageResolutionNeeds(candidate);
|
|
20549
|
-
if (!needs.needsPseudoParameters && !needs.needsStateResources && !needs.needsEnvOrSecretSubstitution) return;
|
|
20550
|
-
const ctx = {};
|
|
20551
|
-
const wantsPseudoForEnvOrSecret = !!stateProvider && needs.needsEnvOrSecretSubstitution;
|
|
20552
|
-
if (needs.needsPseudoParameters || wantsPseudoForEnvOrSecret) {
|
|
20553
|
-
const region = options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? candidate.region;
|
|
20554
|
-
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.`);
|
|
20555
|
-
let accountId;
|
|
20556
|
-
try {
|
|
20557
|
-
accountId = await resolveCallerAccountId$1(region, options.profile);
|
|
20558
|
-
} catch (err) {
|
|
20559
|
-
logger.warn(`Resolver needs \${AWS::AccountId} but STS GetCallerIdentity failed: ${err instanceof Error ? err.message : String(err)}. Substitution will be skipped; affected env / secret entries will be dropped with per-key warnings.`);
|
|
20560
|
-
}
|
|
20561
|
-
const partitionAndSuffix = region ? derivePartitionAndUrlSuffix(region) : void 0;
|
|
20562
|
-
ctx.pseudoParameters = {
|
|
20563
|
-
...accountId !== void 0 && { accountId },
|
|
20564
|
-
...region !== void 0 && { region },
|
|
20565
|
-
...partitionAndSuffix && {
|
|
20566
|
-
partition: partitionAndSuffix.partition,
|
|
20567
|
-
urlSuffix: partitionAndSuffix.urlSuffix
|
|
20568
|
-
}
|
|
20569
|
-
};
|
|
20570
|
-
}
|
|
20571
|
-
const wantsState = needs.needsStateResources || needs.needsEnvOrSecretSubstitution;
|
|
20572
|
-
if (stateProvider && wantsState) {
|
|
20573
|
-
const loaded = await stateProvider.load(candidate.stackName, candidate.region);
|
|
20574
|
-
if (loaded) ctx.stateResources = loaded.resources;
|
|
20575
|
-
else {
|
|
20576
|
-
const loadError = stateProvider.getLastLoadError?.();
|
|
20577
|
-
if (loadError) ctx.stateLoadFailureMessage = loadError;
|
|
20578
|
-
}
|
|
20579
|
-
if (needs.needsEnvOrSecretSubstitution && stateProvider.resolveTemplateSsmParameters) {
|
|
20580
|
-
const ssmParameters = await stateProvider.resolveTemplateSsmParameters(candidate.template);
|
|
20581
|
-
if (Object.keys(ssmParameters.values).length > 0) ctx.stateParameters = ssmParameters.values;
|
|
20582
|
-
if (ssmParameters.secureStringLogicalIds.length > 0) ctx.stateSensitiveParameters = ssmParameters.secureStringLogicalIds;
|
|
20583
|
-
}
|
|
20584
|
-
} else if (!stateProvider && needs.needsStateResources) logger.warn("Container Image references a same-stack AWS::ECR::Repository. Pass a state-source flag (e.g. --from-cfn-stack or a host-provided extension) to substitute the deployed repository URI. Otherwise the resolver will surface its existing error.");
|
|
20585
|
-
else if (!stateProvider && needs.needsEnvOrSecretSubstitution) logger.warn("Container Environment / Secrets entries contain CloudFormation intrinsics (Ref / Fn::GetAtt / Fn::Sub / Fn::Join). Pass a state-source flag (e.g. --from-cfn-stack or a host-provided extension) to substitute them against deployed state. Without a state source these entries are dropped (per-key warnings will follow).");
|
|
20586
|
-
return ctx;
|
|
20587
|
-
}
|
|
20588
|
-
function pickCandidateStack$1(stackPattern, stacks) {
|
|
20589
|
-
if (stackPattern === null) {
|
|
20590
|
-
if (stacks.length === 1) return stacks[0];
|
|
20591
|
-
return;
|
|
20592
|
-
}
|
|
20593
|
-
const matched = matchStacks(stacks, [stackPattern]);
|
|
20594
|
-
if (matched.length === 1) return matched[0];
|
|
20595
|
-
}
|
|
20596
|
-
async function resolveCallerAccountId$1(region, profile) {
|
|
20597
|
-
const { STSClient, GetCallerIdentityCommand } = await import("@aws-sdk/client-sts");
|
|
20598
|
-
const sts = new STSClient(buildStsClientConfig({
|
|
20599
|
-
region,
|
|
20600
|
-
profile
|
|
20601
|
-
}));
|
|
20602
|
-
try {
|
|
20603
|
-
return (await sts.send(new GetCallerIdentityCommand({}))).Account;
|
|
20604
|
-
} finally {
|
|
20605
|
-
sts.destroy();
|
|
20606
|
-
}
|
|
20607
|
-
}
|
|
20608
|
-
/**
|
|
20609
|
-
* Read the `--env-vars` JSON file using the same SAM-style shape as
|
|
20610
|
-
* `cdkl invoke --env-vars`: top-level keys are container names, with
|
|
20611
|
-
* `Parameters` reserved for global entries.
|
|
20612
|
-
*/
|
|
20613
|
-
function readEnvOverridesFile$1(filePath) {
|
|
20614
|
-
if (!filePath) return void 0;
|
|
20615
|
-
let raw;
|
|
20616
|
-
try {
|
|
20617
|
-
raw = readFileSync(filePath, "utf-8");
|
|
20618
|
-
} catch (err) {
|
|
20619
|
-
throw new Error(`Failed to read --env-vars file '${filePath}': ${err instanceof Error ? err.message : String(err)}`);
|
|
20620
|
-
}
|
|
20621
|
-
let parsed;
|
|
20622
|
-
try {
|
|
20623
|
-
parsed = JSON.parse(raw);
|
|
20624
|
-
} catch (err) {
|
|
20625
|
-
throw new Error(`Failed to parse --env-vars file '${filePath}' as JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
20626
|
-
}
|
|
20627
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`--env-vars file '${filePath}' must contain a JSON object at the top level.`);
|
|
20628
|
-
return parsed;
|
|
20629
|
-
}
|
|
20630
|
-
/**
|
|
20631
|
-
* Pick the credentials forwarded to the AWS-published
|
|
20632
|
-
* `amazon-ecs-local-container-endpoints` sidecar. Precedence:
|
|
20633
|
-
* 1. `--assume-task-role <arn>` (or bare `--assume-task-role` against
|
|
20634
|
-
* a resolvable `TaskRoleArn`) → STS-assumed temp creds. Highest
|
|
20635
|
-
* priority — when the user opted in to IAM emulation, those creds
|
|
20636
|
-
* drive the sidecar regardless of `--profile`.
|
|
20637
|
-
* 2. `--profile <p>` → resolved via {@link resolveProfileCredentials}
|
|
20638
|
-
* (the SDK's default credential provider chain — SSO / IAM
|
|
20639
|
-
* Identity Center / fromIni / role-assumption). NEW in this PR.
|
|
20640
|
-
* 3. Neither set → `undefined`; the sidecar runs with its own
|
|
20641
|
-
* default credential chain (typically empty inside a fresh
|
|
20642
|
-
* container — user containers will get 4xx from the credentials
|
|
20643
|
-
* endpoint, mimicking IAM-misconfigured prod).
|
|
20644
|
-
*
|
|
20645
|
-
* Extracted as an exported helper so a unit test can exercise every
|
|
20646
|
-
* branch without having to mock the full Synth + Docker + AWS pipeline
|
|
20647
|
-
* (the strategy used for the Lambda container path).
|
|
20648
|
-
*/
|
|
20649
|
-
async function resolveSidecarCredentials(options, assumedCredentials) {
|
|
20650
|
-
if (assumedCredentials) return assumedCredentials;
|
|
20651
|
-
if (options.profile) return resolveProfileCredentials(options.profile);
|
|
20652
|
-
}
|
|
20653
|
-
function createLocalRunTaskCommand(opts = {}) {
|
|
20654
|
-
setEmbedConfig(opts.embedConfig);
|
|
20655
|
-
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. Omit <target> in an interactive terminal to pick the task definition from a list.").argument("[target]", "CDK display path or stack-qualified logical ID of the AWS::ECS::TaskDefinition to run (omit to pick interactively in a TTY)").action(withErrorHandling(async (target, options) => {
|
|
20656
|
-
await localRunTaskCommand(target, options, opts.extraStateProviders);
|
|
20657
|
-
}));
|
|
20658
|
-
addRunTaskSpecificOptions(cmd);
|
|
20659
|
-
[
|
|
20660
|
-
...commonOptions(),
|
|
20661
|
-
...appOptions(),
|
|
20662
|
-
...contextOptions
|
|
20663
|
-
].forEach((opt) => cmd.addOption(opt));
|
|
20664
|
-
cmd.addOption(regionOption);
|
|
20665
|
-
return cmd;
|
|
20666
|
-
}
|
|
20667
|
-
/**
|
|
20668
|
-
* Register the option block that `cdkl run-task` adds on top of the shared
|
|
20669
|
-
* common / app / context option helpers. Shared between `cdkl run-task` and
|
|
20670
|
-
* any host CLI (e.g. cdkd's `local run-task`) that wraps the single-task
|
|
20671
|
-
* ECS local runner, so adding or renaming a `run-task`-only flag here
|
|
20672
|
-
* propagates to every embedder without duplicate `.addOption(...)` blocks.
|
|
20673
|
-
*
|
|
20674
|
-
* Calling order only affects `--help` presentation (Commander parses
|
|
20675
|
-
* insertion-order-independent). The host-CLI convention is host-specific
|
|
20676
|
-
* options first, then this helper, then the shared common / app / context
|
|
20677
|
-
* options — host flags / run-task flags / common flags grouped in three
|
|
20678
|
-
* `--help` clusters. Chainable: returns `cmd`.
|
|
20679
|
-
*
|
|
20680
|
-
* NOTE: `run-task` does NOT compose with {@link addCommonEcsServiceOptions}
|
|
20681
|
-
* even though many flags overlap. The two ECS surfaces (single-task vs
|
|
20682
|
-
* multi-replica service) have intentionally divergent defaults
|
|
20683
|
-
* (`run-task` has no `--max-tasks` / `--restart-policy`; `start-service`
|
|
20684
|
-
* / `start-alb` have no `--host-port` / `--keep-running` / `--detach`),
|
|
20685
|
-
* and folding `run-task` into the service common block would mutate the
|
|
20686
|
-
* surface non-trivially. Each command keeps its own helper.
|
|
20687
|
-
*/
|
|
20688
|
-
function addRunTaskSpecificOptions(cmd) {
|
|
20689
|
-
return cmd.addOption(new Option("--cluster <name>", "Cluster name surfaced to ECS_CONTAINER_METADATA_URI_V4 and used as the docker network prefix").default(getEmbedConfig().resourceNamePrefix)).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"ContainerName\":{\"KEY\":\"VALUE\"}, \"Parameters\":{}})")).addOption(new Option("--container-host <ip>", "Host IP to bind published container ports to. Must be a numeric IP (Docker rejects hostnames here)").default("127.0.0.1")).addOption(new Option("--host-port <containerPort=hostPort...>", "Publish a container port on a specific host port (e.g. 80=8080); repeatable. Default: host port == container port. Use this on macOS to map a privileged container port (< 1024) to a non-privileged host port and avoid the Docker Desktop admin-password prompt.")).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 ListStackResources and substitute Ref / Fn::ImportValue in container env vars / secrets / image URIs with the deployed physical IDs / exports. Use for CDK apps deployed via the upstream CDK CLI (\`cdk deploy\`). Bare form uses the ${getEmbedConfig().binaryName} stack name; pass an explicit value when the CFn stack name differs. Fn::GetAtt in container Environment[].Value is warn-and-dropped: CFn ListStackResources does not return per-attribute values, and unlike Lambda (where \`cdkl invoke --from-cfn-stack\` recovers Fn::GetAtt from the deployed function via lambda:GetFunctionConfiguration), no ECS-side equivalent resolves attributes off a deployed task / service.`)).addOption(new Option("--stack-region <region>", "Region of the state record to read. Used with --from-cfn-stack as the CFn client region."));
|
|
20690
|
-
}
|
|
20691
|
-
|
|
20692
|
-
//#endregion
|
|
20693
|
-
//#region src/local/ecs-service-resolver.ts
|
|
20694
|
-
function resolveEcsServiceTarget(target, stacks, context, options) {
|
|
20695
|
-
if (stacks.length === 0) throw new EcsTaskResolutionError("No stacks found in the synthesized assembly.");
|
|
20696
|
-
const parsed = parseEcsTarget(target);
|
|
20697
|
-
const stack = pickStack$1(parsed, stacks);
|
|
20698
|
-
const resources = stack.template.Resources ?? {};
|
|
20699
|
-
let serviceLogicalId;
|
|
20700
|
-
let serviceResource;
|
|
20701
|
-
if (parsed.isPath) {
|
|
20702
|
-
const index = buildCdkPathIndex(stack.template);
|
|
20703
|
-
const services = resolveCdkPathToLogicalIds(parsed.pathOrId, index).filter(({ logicalId: l }) => resources[l]?.Type === "AWS::ECS::Service");
|
|
20704
|
-
if (services.length === 0) throw notFoundError(target, stack, resources, parsed);
|
|
20705
|
-
if (services.length > 1) throw new EcsTaskResolutionError(`Target '${target}' matches ${services.length} ECS services in ${stack.stackName}: ` + services.map((s) => s.logicalId).join(", ") + ". Refine the path or use the stack:LogicalId form.");
|
|
20706
|
-
serviceLogicalId = services[0].logicalId;
|
|
20707
|
-
serviceResource = resources[serviceLogicalId];
|
|
20708
|
-
} else {
|
|
20709
|
-
serviceResource = resources[parsed.pathOrId];
|
|
20710
|
-
if (!serviceResource) throw notFoundError(target, stack, resources, parsed);
|
|
20711
|
-
serviceLogicalId = parsed.pathOrId;
|
|
20712
|
-
}
|
|
20713
|
-
if (!serviceLogicalId || !serviceResource) throw notFoundError(target, stack, resources, parsed);
|
|
20714
|
-
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.`);
|
|
20715
|
-
if (serviceResource.Type !== "AWS::ECS::Service") throw new EcsTaskResolutionError(`Resource '${serviceLogicalId}' in ${stack.stackName} is ${serviceResource.Type}, not an AWS::ECS::Service.`);
|
|
20716
|
-
return extractServiceProperties(stack, serviceLogicalId, serviceResource, stacks, context, options);
|
|
20717
|
-
}
|
|
20718
|
-
/**
|
|
20719
|
-
* Pure-functional extraction from the synth resource. Exposed for unit
|
|
20720
|
-
* testing the per-field resolution rules (DesiredCount default, missing
|
|
20721
|
-
* TaskDefinition, intrinsic shapes).
|
|
20722
|
-
*/
|
|
20723
|
-
function extractServiceProperties(stack, serviceLogicalId, resource, stacks, context, options) {
|
|
20724
|
-
const props = resource.Properties ?? {};
|
|
20725
|
-
const warnings = [];
|
|
20726
|
-
const taskDefRef = props["TaskDefinition"];
|
|
20727
|
-
if (taskDefRef === void 0 || taskDefRef === null) throw new EcsTaskResolutionError(`ECS Service '${serviceLogicalId}' in ${stack.stackName} has no TaskDefinition property.`);
|
|
20728
|
-
const taskDefLogicalId = resolveTaskDefinitionReference(taskDefRef, stack, serviceLogicalId);
|
|
20729
|
-
const task = resolveEcsTaskTarget(`${stack.stackName}:${taskDefLogicalId}`, stacks, context);
|
|
20730
|
-
const desiredCount = parseDesiredCount(props["DesiredCount"], serviceLogicalId);
|
|
20731
|
-
const healthCheckGracePeriodSeconds = parseHealthCheckGrace(props["HealthCheckGracePeriodSeconds"], serviceLogicalId);
|
|
20732
|
-
const serviceName = parseServiceName(props["ServiceName"], serviceLogicalId);
|
|
20733
|
-
const serviceDisplayName = deriveServiceDisplayName(props["ServiceName"], serviceLogicalId, resource.Metadata);
|
|
20734
|
-
if (!options?.suppressLoadBalancerWarning && Array.isArray(props["LoadBalancers"]) && props["LoadBalancers"].length > 0) {
|
|
20735
|
-
const { cliName } = getEmbedConfig();
|
|
20736
|
-
warnings.push(`ECS Service '${serviceLogicalId}' declares LoadBalancers, but \`${cliName} start-service\` runs the replicas only; no local listener fronts them. Reach the containers via their published ports, or run \`${cliName} start-alb <Stack>/<Alb>\` to boot the same replicas behind a local front-door that round-robins the listener rules.`);
|
|
20737
|
-
}
|
|
20738
|
-
const serviceConnect = extractServiceConnect(props["ServiceConnectConfiguration"], task);
|
|
20739
|
-
const out = {
|
|
20740
|
-
stack,
|
|
20741
|
-
serviceLogicalId,
|
|
20742
|
-
resource,
|
|
20743
|
-
serviceName,
|
|
20744
|
-
serviceDisplayName,
|
|
20745
|
-
desiredCount,
|
|
20746
|
-
healthCheckGracePeriodSeconds,
|
|
20747
|
-
task,
|
|
20748
|
-
serviceRegistries: extractServiceRegistries(props["ServiceRegistries"], serviceLogicalId, warnings),
|
|
20749
|
-
warnings
|
|
20750
|
-
};
|
|
20751
|
-
if (serviceConnect) out.serviceConnect = serviceConnect;
|
|
20752
|
-
return out;
|
|
20753
|
-
}
|
|
20754
|
-
/**
|
|
20755
|
-
* Parse `ServiceConnectConfiguration` against the producer TaskDef.
|
|
20756
|
-
* Returns `undefined` when the block is missing OR `Enabled: false`.
|
|
20757
|
-
*
|
|
20758
|
-
* Reject conditions (surface as resolver-time errors so the user sees
|
|
20759
|
-
* them BEFORE the docker network is created):
|
|
20760
|
-
* - `Namespace` is not a literal string. CDK 2.x always emits a
|
|
20761
|
-
* literal string here (verified 2026-05-22); cross-stack /
|
|
20762
|
-
* intrinsic shapes are out of scope.
|
|
20763
|
-
* - `Services[].PortName` doesn't match any of the TaskDef's
|
|
20764
|
-
* `ContainerDefinitions[].PortMappings[].Name` entries.
|
|
20765
|
-
*
|
|
20766
|
-
* Note on `clientAliases[]` shape: each ClientAlias can declare a
|
|
20767
|
-
* `DnsName` (the bare short-name peers connect to, e.g. `orders`) AND
|
|
20768
|
-
* a `Port` (the listening port the alias maps to inside the consumer).
|
|
20769
|
-
* cdk-local surfaces both verbatim; the registry / `--add-host` overlay
|
|
20770
|
-
* publishes each `DnsName` as a bare alias pointing at the same IP as
|
|
20771
|
-
* the canonical fqdn.
|
|
20539
|
+
* Parse `ServiceConnectConfiguration` against the producer TaskDef.
|
|
20540
|
+
* Returns `undefined` when the block is missing OR `Enabled: false`.
|
|
20541
|
+
*
|
|
20542
|
+
* Reject conditions (surface as resolver-time errors so the user sees
|
|
20543
|
+
* them BEFORE the docker network is created):
|
|
20544
|
+
* - `Namespace` is not a literal string. CDK 2.x always emits a
|
|
20545
|
+
* literal string here (verified 2026-05-22); cross-stack /
|
|
20546
|
+
* intrinsic shapes are out of scope.
|
|
20547
|
+
* - `Services[].PortName` doesn't match any of the TaskDef's
|
|
20548
|
+
* `ContainerDefinitions[].PortMappings[].Name` entries.
|
|
20549
|
+
*
|
|
20550
|
+
* Note on `clientAliases[]` shape: each ClientAlias can declare a
|
|
20551
|
+
* `DnsName` (the bare short-name peers connect to, e.g. `orders`) AND
|
|
20552
|
+
* a `Port` (the listening port the alias maps to inside the consumer).
|
|
20553
|
+
* cdk-local surfaces both verbatim; the registry / `--add-host` overlay
|
|
20554
|
+
* publishes each `DnsName` as a bare alias pointing at the same IP as
|
|
20555
|
+
* the canonical fqdn.
|
|
20772
20556
|
*/
|
|
20773
20557
|
function extractServiceConnect(raw, task) {
|
|
20774
20558
|
if (!raw || typeof raw !== "object") return void 0;
|
|
@@ -21846,7 +21630,7 @@ async function softReloadReplica(args) {
|
|
|
21846
21630
|
const defaultDockerInspectWorkdirImpl = async (containerId) => {
|
|
21847
21631
|
const { execFile } = await import("node:child_process");
|
|
21848
21632
|
const { promisify } = await import("node:util");
|
|
21849
|
-
const { getDockerCmd } = await import("./docker-cmd-
|
|
21633
|
+
const { getDockerCmd } = await import("./docker-cmd-Dqx2YENO.js").then((n) => n.t);
|
|
21850
21634
|
const { stdout } = await promisify(execFile)(getDockerCmd(), [
|
|
21851
21635
|
"inspect",
|
|
21852
21636
|
"--format",
|
|
@@ -21865,7 +21649,7 @@ let dockerInspectWorkdirImpl = defaultDockerInspectWorkdirImpl;
|
|
|
21865
21649
|
const defaultDockerCpImpl = async (src, dst) => {
|
|
21866
21650
|
const { execFile } = await import("node:child_process");
|
|
21867
21651
|
const { promisify } = await import("node:util");
|
|
21868
|
-
const { getDockerCmd } = await import("./docker-cmd-
|
|
21652
|
+
const { getDockerCmd } = await import("./docker-cmd-Dqx2YENO.js").then((n) => n.t);
|
|
21869
21653
|
await promisify(execFile)(getDockerCmd(), [
|
|
21870
21654
|
"cp",
|
|
21871
21655
|
src,
|
|
@@ -21880,7 +21664,7 @@ let dockerCpImpl = defaultDockerCpImpl;
|
|
|
21880
21664
|
const defaultDockerRestartImpl = async (containerId) => {
|
|
21881
21665
|
const { execFile } = await import("node:child_process");
|
|
21882
21666
|
const { promisify } = await import("node:util");
|
|
21883
|
-
const { getDockerCmd } = await import("./docker-cmd-
|
|
21667
|
+
const { getDockerCmd } = await import("./docker-cmd-Dqx2YENO.js").then((n) => n.t);
|
|
21884
21668
|
await promisify(execFile)(getDockerCmd(), ["restart", containerId]);
|
|
21885
21669
|
};
|
|
21886
21670
|
let dockerRestartImpl = defaultDockerRestartImpl;
|
|
@@ -21920,7 +21704,7 @@ async function disconnectOldFromSharedNetwork(oldInstance) {
|
|
|
21920
21704
|
const defaultDockerNetworkDisconnectImpl = async (networkName, containerId) => {
|
|
21921
21705
|
const { execFile } = await import("node:child_process");
|
|
21922
21706
|
const { promisify } = await import("node:util");
|
|
21923
|
-
const { getDockerCmd } = await import("./docker-cmd-
|
|
21707
|
+
const { getDockerCmd } = await import("./docker-cmd-Dqx2YENO.js").then((n) => n.t);
|
|
21924
21708
|
await promisify(execFile)(getDockerCmd(), [
|
|
21925
21709
|
"network",
|
|
21926
21710
|
"disconnect",
|
|
@@ -22097,7 +21881,7 @@ function pickEssentialContainerId(instance, service) {
|
|
|
22097
21881
|
const defaultWaitForExitImpl = async (containerId) => {
|
|
22098
21882
|
const { execFile } = await import("node:child_process");
|
|
22099
21883
|
const { promisify } = await import("node:util");
|
|
22100
|
-
const { getDockerCmd } = await import("./docker-cmd-
|
|
21884
|
+
const { getDockerCmd } = await import("./docker-cmd-Dqx2YENO.js").then((n) => n.t);
|
|
22101
21885
|
const { stdout } = await promisify(execFile)(getDockerCmd(), ["wait", containerId], { maxBuffer: 1024 * 1024 });
|
|
22102
21886
|
const code = parseInt(stdout.trim(), 10);
|
|
22103
21887
|
return Number.isFinite(code) ? code : -1;
|
|
@@ -22117,7 +21901,7 @@ const EXIT_LOG_TAIL_LINES = 50;
|
|
|
22117
21901
|
const defaultReadContainerLogsImpl = async (containerId) => {
|
|
22118
21902
|
const { execFile } = await import("node:child_process");
|
|
22119
21903
|
const { promisify } = await import("node:util");
|
|
22120
|
-
const { getDockerCmd } = await import("./docker-cmd-
|
|
21904
|
+
const { getDockerCmd } = await import("./docker-cmd-Dqx2YENO.js").then((n) => n.t);
|
|
22121
21905
|
const { stdout, stderr } = await promisify(execFile)(getDockerCmd(), [
|
|
22122
21906
|
"logs",
|
|
22123
21907
|
"--tail",
|
|
@@ -24965,7 +24749,8 @@ async function runImageOverrideBuilds(overrides) {
|
|
|
24965
24749
|
try {
|
|
24966
24750
|
await runDockerStreaming(args, {
|
|
24967
24751
|
cwd: entry.contextDir,
|
|
24968
|
-
env: { BUILDX_NO_DEFAULT_ATTESTATIONS: "1" }
|
|
24752
|
+
env: { BUILDX_NO_DEFAULT_ATTESTATIONS: "1" },
|
|
24753
|
+
progressLabel: `Building override image for '${target}'`
|
|
24969
24754
|
});
|
|
24970
24755
|
} catch (err) {
|
|
24971
24756
|
const e = err;
|
|
@@ -25401,7 +25186,7 @@ async function reloadAllServices(args) {
|
|
|
25401
25186
|
async function loadAssetContextForTarget(args) {
|
|
25402
25187
|
const { target, controller, stacks, cdkOutDir, assetLoader, logger } = args;
|
|
25403
25188
|
const parsed = parseEcsTarget(target);
|
|
25404
|
-
const candidate = pickCandidateStack(parsed.stackPattern, stacks);
|
|
25189
|
+
const candidate = pickCandidateStack$1(parsed.stackPattern, stacks);
|
|
25405
25190
|
if (!candidate) {
|
|
25406
25191
|
logger.debug(`loadAssetContext: stack pattern '${parsed.stackPattern}' from target '${target}' not in the assembly. Classifier will see no asset context (rebuild).`);
|
|
25407
25192
|
return;
|
|
@@ -25465,7 +25250,7 @@ async function loadAssetContextForTarget(args) {
|
|
|
25465
25250
|
*/
|
|
25466
25251
|
async function rollOneTarget(args) {
|
|
25467
25252
|
const { controller, newBoot, stacks, options, discovery, skipPull, extraStateProviders, profileCredsFile, frontDoorPools, suppressLoadBalancerWarning, verdict, imageOverrideTag, logger } = args;
|
|
25468
|
-
const candidate = pickCandidateStack(parseEcsTarget(newBoot.target).stackPattern, stacks);
|
|
25253
|
+
const candidate = pickCandidateStack$1(parseEcsTarget(newBoot.target).stackPattern, stacks);
|
|
25469
25254
|
const stateProvider = createLocalStateProvider(options, candidate?.stackName ?? "", await resolveCfnFallbackRegion(options, candidate?.region), extraStateProviders);
|
|
25470
25255
|
try {
|
|
25471
25256
|
let resolved;
|
|
@@ -25517,7 +25302,7 @@ async function rollOneTarget(args) {
|
|
|
25517
25302
|
}
|
|
25518
25303
|
}
|
|
25519
25304
|
async function bootOneTarget(boot, runState, stacks, options, discovery, skipPull, extraStateProviders, profileCredsFile, frontDoorPools, suppressLoadBalancerWarning, imageOverrideTag) {
|
|
25520
|
-
const candidate = pickCandidateStack(parseEcsTarget(boot.target).stackPattern, stacks);
|
|
25305
|
+
const candidate = pickCandidateStack$1(parseEcsTarget(boot.target).stackPattern, stacks);
|
|
25521
25306
|
const stateProvider = createLocalStateProvider(options, candidate?.stackName ?? "", await resolveCfnFallbackRegion(options, candidate?.region), extraStateProviders);
|
|
25522
25307
|
try {
|
|
25523
25308
|
return await runOneTarget(boot, runState, stacks, options, discovery, skipPull, stateProvider, profileCredsFile, frontDoorPools, suppressLoadBalancerWarning, imageOverrideTag);
|
|
@@ -25556,7 +25341,7 @@ async function resolveServiceAndRunnerOpts(boot, stacks, options, discovery, ski
|
|
|
25556
25341
|
const logger = getLogger();
|
|
25557
25342
|
const target = boot.target;
|
|
25558
25343
|
const quiet = opts.quiet === true;
|
|
25559
|
-
const imageContext = await buildEcsImageResolutionContext(target, stacks, options, stateProvider);
|
|
25344
|
+
const imageContext = await buildEcsImageResolutionContext$1(target, stacks, options, stateProvider);
|
|
25560
25345
|
const service = resolveEcsServiceTarget(target, stacks, imageContext, { suppressLoadBalancerWarning });
|
|
25561
25346
|
if (!quiet) logger.info(`Target: ${service.stack.stackName}/${service.serviceLogicalId} (service=${service.serviceName}, desiredCount=${service.desiredCount}, task=${service.task.taskDefinitionLogicalId})`);
|
|
25562
25347
|
if (!quiet && service.serviceConnect) logger.info(`Service Connect: namespace='${service.serviceConnect.namespaceName}', ${service.serviceConnect.services.length} service(s) registered for peer discovery.`);
|
|
@@ -25577,21 +25362,23 @@ async function resolveServiceAndRunnerOpts(boot, stacks, options, discovery, ski
|
|
|
25577
25362
|
await applyCrossStackResolverToTask(service.task, subContext);
|
|
25578
25363
|
}
|
|
25579
25364
|
} else if (!stateProvider && taskNeeds.needsCrossStackResolver) logger.warn("Container Environment / Secrets entries contain Fn::ImportValue / Fn::GetStackOutput intrinsics. Pass a state-source flag (e.g. --from-cfn-stack or a host-provided extension) to substitute them against deployed state.");
|
|
25365
|
+
const effectiveAssumeRole = resolveEcsAssumeRoleOption(options);
|
|
25580
25366
|
let assumedCredentials;
|
|
25581
25367
|
let resolvedRoleArn;
|
|
25582
|
-
if (
|
|
25583
|
-
if (!service.task.taskRoleArn) throw new LocalStartServiceError(`--assume-
|
|
25584
|
-
resolvedRoleArn = await resolvePlaceholderAccount(service.task.taskRoleArn, options.region, options.profile);
|
|
25585
|
-
assumedCredentials = await assumeTaskRole(resolvedRoleArn, options.region, options.profile);
|
|
25586
|
-
} else if (typeof
|
|
25587
|
-
resolvedRoleArn =
|
|
25588
|
-
assumedCredentials = await assumeTaskRole(resolvedRoleArn, options.region, options.profile);
|
|
25589
|
-
}
|
|
25590
|
-
const envOverrides = readEnvOverridesFile(options.envVars);
|
|
25368
|
+
if (effectiveAssumeRole === true) {
|
|
25369
|
+
if (!service.task.taskRoleArn) throw new LocalStartServiceError(`--assume-role passed without an ARN but service '${service.serviceLogicalId}' has no resolvable TaskRoleArn. Pass the ARN explicitly: --assume-role <arn>`);
|
|
25370
|
+
resolvedRoleArn = await resolvePlaceholderAccount$1(service.task.taskRoleArn, options.region, options.profile);
|
|
25371
|
+
assumedCredentials = await assumeTaskRole$1(resolvedRoleArn, options.region, options.profile);
|
|
25372
|
+
} else if (typeof effectiveAssumeRole === "string") {
|
|
25373
|
+
resolvedRoleArn = effectiveAssumeRole;
|
|
25374
|
+
assumedCredentials = await assumeTaskRole$1(resolvedRoleArn, options.region, options.profile);
|
|
25375
|
+
}
|
|
25376
|
+
const envOverrides = readEnvOverridesFile$1(options.envVars);
|
|
25591
25377
|
const taskOpts = {
|
|
25592
25378
|
cluster: options.cluster,
|
|
25593
25379
|
containerHost: options.containerHost,
|
|
25594
25380
|
skipPull,
|
|
25381
|
+
skipBuild: options.build === false,
|
|
25595
25382
|
keepRunning: false,
|
|
25596
25383
|
detach: true
|
|
25597
25384
|
};
|
|
@@ -25882,7 +25669,7 @@ function describeRuleActionForSummary(action) {
|
|
|
25882
25669
|
function describeForwardTargetForSummary(t) {
|
|
25883
25670
|
return t.kind === "lambda" ? `<Lambda: ${t.lambda.logicalId}>` : `<ECS: ${t.serviceTarget}>`;
|
|
25884
25671
|
}
|
|
25885
|
-
async function resolvePlaceholderAccount(arn, region, profile) {
|
|
25672
|
+
async function resolvePlaceholderAccount$1(arn, region, profile) {
|
|
25886
25673
|
if (!arn.includes("${AWS::AccountId}")) return arn;
|
|
25887
25674
|
const { STSClient, GetCallerIdentityCommand } = await import("@aws-sdk/client-sts");
|
|
25888
25675
|
const sts = new STSClient(buildStsClientConfig({
|
|
@@ -25897,7 +25684,7 @@ async function resolvePlaceholderAccount(arn, region, profile) {
|
|
|
25897
25684
|
sts.destroy();
|
|
25898
25685
|
}
|
|
25899
25686
|
}
|
|
25900
|
-
async function assumeTaskRole(roleArn, region, profile) {
|
|
25687
|
+
async function assumeTaskRole$1(roleArn, region, profile) {
|
|
25901
25688
|
const { STSClient, AssumeRoleCommand } = await import("@aws-sdk/client-sts");
|
|
25902
25689
|
const sts = new STSClient(buildStsClientConfig({
|
|
25903
25690
|
region,
|
|
@@ -25924,9 +25711,9 @@ async function assumeTaskRole(roleArn, region, profile) {
|
|
|
25924
25711
|
* site-level binding test that locks the `--from-cfn-stack` SSM-parameter
|
|
25925
25712
|
* resolution call (issue #94).
|
|
25926
25713
|
*/
|
|
25927
|
-
async function buildEcsImageResolutionContext(target, stacks, options, stateProvider) {
|
|
25714
|
+
async function buildEcsImageResolutionContext$1(target, stacks, options, stateProvider) {
|
|
25928
25715
|
const logger = getLogger();
|
|
25929
|
-
const candidate = pickCandidateStack(parseEcsTarget(target).stackPattern, stacks);
|
|
25716
|
+
const candidate = pickCandidateStack$1(parseEcsTarget(target).stackPattern, stacks);
|
|
25930
25717
|
if (!candidate) return void 0;
|
|
25931
25718
|
const needs = detectEcsImageResolutionNeeds(candidate);
|
|
25932
25719
|
if (!needs.needsPseudoParameters && !needs.needsStateResources && !needs.needsEnvOrSecretSubstitution) return;
|
|
@@ -25937,7 +25724,7 @@ async function buildEcsImageResolutionContext(target, stacks, options, stateProv
|
|
|
25937
25724
|
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.`);
|
|
25938
25725
|
let accountId;
|
|
25939
25726
|
try {
|
|
25940
|
-
accountId = await resolveCallerAccountId(region, options.profile);
|
|
25727
|
+
accountId = await resolveCallerAccountId$1(region, options.profile);
|
|
25941
25728
|
} catch (err) {
|
|
25942
25729
|
logger.warn(`Resolver needs \${AWS::AccountId} but STS GetCallerIdentity failed: ${err instanceof Error ? err.message : String(err)}. Substitution will be skipped; affected env / secret entries will be dropped with per-key warnings.`);
|
|
25943
25730
|
}
|
|
@@ -25968,7 +25755,7 @@ async function buildEcsImageResolutionContext(target, stacks, options, stateProv
|
|
|
25968
25755
|
else if (!stateProvider && needs.needsEnvOrSecretSubstitution) logger.warn("Container Environment / Secrets entries contain CloudFormation intrinsics. Pass a state-source flag (e.g. --from-cfn-stack or a host-provided extension) to substitute them against the deployed state.");
|
|
25969
25756
|
return ctx;
|
|
25970
25757
|
}
|
|
25971
|
-
function pickCandidateStack(stackPattern, stacks) {
|
|
25758
|
+
function pickCandidateStack$1(stackPattern, stacks) {
|
|
25972
25759
|
if (stackPattern === null) {
|
|
25973
25760
|
if (stacks.length === 1) return stacks[0];
|
|
25974
25761
|
return;
|
|
@@ -25976,7 +25763,7 @@ function pickCandidateStack(stackPattern, stacks) {
|
|
|
25976
25763
|
const matched = matchStacks(stacks, [stackPattern]);
|
|
25977
25764
|
if (matched.length === 1) return matched[0];
|
|
25978
25765
|
}
|
|
25979
|
-
async function resolveCallerAccountId(region, profile) {
|
|
25766
|
+
async function resolveCallerAccountId$1(region, profile) {
|
|
25980
25767
|
const { STSClient, GetCallerIdentityCommand } = await import("@aws-sdk/client-sts");
|
|
25981
25768
|
const sts = new STSClient(buildStsClientConfig({
|
|
25982
25769
|
region,
|
|
@@ -25988,7 +25775,7 @@ async function resolveCallerAccountId(region, profile) {
|
|
|
25988
25775
|
sts.destroy();
|
|
25989
25776
|
}
|
|
25990
25777
|
}
|
|
25991
|
-
function readEnvOverridesFile(filePath) {
|
|
25778
|
+
function readEnvOverridesFile$1(filePath) {
|
|
25992
25779
|
if (!filePath) return void 0;
|
|
25993
25780
|
let raw;
|
|
25994
25781
|
try {
|
|
@@ -26133,12 +25920,12 @@ async function resolveAndBuildImageOverrides(args) {
|
|
|
26133
25920
|
const { perTarget, stacks, options, extraStateProviders, logger } = args;
|
|
26134
25921
|
const resolvedForPeek = [];
|
|
26135
25922
|
for (const target of perTarget) {
|
|
26136
|
-
const candidate = pickCandidateStack(parseEcsTarget(target).stackPattern, stacks);
|
|
25923
|
+
const candidate = pickCandidateStack$1(parseEcsTarget(target).stackPattern, stacks);
|
|
26137
25924
|
const stateProvider = createLocalStateProvider(options, candidate?.stackName ?? "", await resolveCfnFallbackRegion(options, candidate?.region), extraStateProviders);
|
|
26138
25925
|
let imageContext;
|
|
26139
25926
|
let service;
|
|
26140
25927
|
try {
|
|
26141
|
-
imageContext = await buildEcsImageResolutionContext(target, stacks, options, stateProvider);
|
|
25928
|
+
imageContext = await buildEcsImageResolutionContext$1(target, stacks, options, stateProvider);
|
|
26142
25929
|
service = resolveEcsServiceTarget(target, stacks, imageContext, { suppressLoadBalancerWarning: true });
|
|
26143
25930
|
} catch (err) {
|
|
26144
25931
|
logger.debug(`--image-override peek failed for '${target}': ${err instanceof Error ? err.message : String(err)}.`);
|
|
@@ -26197,20 +25984,81 @@ function enforceStrictOverrides(strict, uncoveredPinnedTargets) {
|
|
|
26197
25984
|
throw new LocalStartServiceError(`--strict-overrides set, but ${uncoveredPinnedTargets.length} pinned target(s) remain uncovered: ${uncoveredPinnedTargets.join(", ")}. Pass --image-override <service>=<dockerfile> for each, drop --strict-overrides, or drop --from-cfn-stack to iterate on local CDK assets.`);
|
|
26198
25985
|
}
|
|
26199
25986
|
/**
|
|
26200
|
-
*
|
|
26201
|
-
*
|
|
26202
|
-
*
|
|
26203
|
-
*
|
|
25987
|
+
* Build the canonical `--cluster <name>` Option for every ECS surface.
|
|
25988
|
+
*
|
|
25989
|
+
* Shared between `cdkl run-task` and the `start-service` / `start-alb`
|
|
25990
|
+
* pair so the default (the active embed config's `resourceNamePrefix`)
|
|
25991
|
+
* is defined exactly once — issue #249 / C12. Building it as a fresh
|
|
25992
|
+
* Option per call (not a module-level const) is load-bearing: the
|
|
25993
|
+
* embed config is host-installed at command-factory time, and a
|
|
25994
|
+
* module-level Option would freeze the host-default prefix at
|
|
25995
|
+
* import time.
|
|
26204
25996
|
*/
|
|
26205
|
-
function
|
|
26206
|
-
|
|
26207
|
-
|
|
26208
|
-
|
|
26209
|
-
|
|
26210
|
-
|
|
26211
|
-
|
|
26212
|
-
|
|
26213
|
-
|
|
25997
|
+
function ecsClusterOption() {
|
|
25998
|
+
return new Option("--cluster <name>", "Cluster name surfaced to ECS_CONTAINER_METADATA_URI_V4 and used as the docker network prefix").default(getEmbedConfig().resourceNamePrefix);
|
|
25999
|
+
}
|
|
26000
|
+
/**
|
|
26001
|
+
* Add the `--assume-task-role` flag plus its `--assume-role` alias to
|
|
26002
|
+
* an ECS command. Issue #249 / C6 — every non-ECS command calls the
|
|
26003
|
+
* same concept `--assume-role`, so the ECS surface accepts the
|
|
26004
|
+
* cross-command name as a NON-BREAKING alias. Precedence when both
|
|
26005
|
+
* are passed: `--assume-role` (new) wins. {@link resolveEcsAssumeRoleOption}
|
|
26006
|
+
* computes the effective value and fires a one-time deprecation warn
|
|
26007
|
+
* when only the legacy `--assume-task-role` is set.
|
|
26008
|
+
*
|
|
26009
|
+
* Breaking unification of all three parsers (`invoke`, `start-api`,
|
|
26010
|
+
* ECS) is tracked separately under issue #256.
|
|
26011
|
+
*/
|
|
26012
|
+
function addEcsAssumeRoleOptions(cmd) {
|
|
26013
|
+
return cmd.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. DEPRECATED alias of `--assume-role`; both forms work, but `--assume-role` is the cross-command name and will become the only form in a future release.")).addOption(new Option("--assume-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. Cross-command alias matching the same flag on `invoke` / `invoke-agentcore` / `start-api`; supersedes the older `--assume-task-role` (which still works)."));
|
|
26014
|
+
}
|
|
26015
|
+
/**
|
|
26016
|
+
* Module-level latch for the `--assume-task-role` deprecation warn.
|
|
26017
|
+
* Per-process single-fire, so a `--watch` reload that re-enters the
|
|
26018
|
+
* resolver on every source-change firing does not spam the user.
|
|
26019
|
+
*
|
|
26020
|
+
* Test-only `__resetAssumeTaskRoleDeprecationLatch` lets the unit suite
|
|
26021
|
+
* drive the latch through repeated invocations.
|
|
26022
|
+
*/
|
|
26023
|
+
let assumeTaskRoleDeprecationWarned = false;
|
|
26024
|
+
/**
|
|
26025
|
+
* Collapse `--assume-task-role` (legacy) and `--assume-role` (new) into
|
|
26026
|
+
* a single effective value. When BOTH are set, `--assume-role` wins
|
|
26027
|
+
* (matches Commander's "later wins" default for repeatable flags, and
|
|
26028
|
+
* gives the cross-command name priority). When ONLY `--assume-task-role`
|
|
26029
|
+
* is set, fires a **per-process one-time** deprecation warn naming the
|
|
26030
|
+
* replacement — the latch above prevents `--watch` reloads from
|
|
26031
|
+
* re-emitting the warn on every source-change firing.
|
|
26032
|
+
*
|
|
26033
|
+
* Issue #249 / C6.
|
|
26034
|
+
*/
|
|
26035
|
+
function resolveEcsAssumeRoleOption(options) {
|
|
26036
|
+
if (options.assumeRole !== void 0) return options.assumeRole;
|
|
26037
|
+
if (options.assumeTaskRole !== void 0) {
|
|
26038
|
+
if (!assumeTaskRoleDeprecationWarned) {
|
|
26039
|
+
assumeTaskRoleDeprecationWarned = true;
|
|
26040
|
+
getLogger().warn("--assume-task-role is deprecated; use --assume-role instead. Both forms continue to work, but --assume-role is the cross-command name.");
|
|
26041
|
+
}
|
|
26042
|
+
return options.assumeTaskRole;
|
|
26043
|
+
}
|
|
26044
|
+
}
|
|
26045
|
+
/**
|
|
26046
|
+
* Add the CLI options shared by both ECS-service commands (`start-service` and
|
|
26047
|
+
* `start-alb`) to a command. The command-specific argument / description and
|
|
26048
|
+
* the one unique option (`--host-port` vs `--lb-port`) are added by each
|
|
26049
|
+
* factory.
|
|
26050
|
+
*/
|
|
26051
|
+
function addCommonEcsServiceOptions(cmd) {
|
|
26052
|
+
cmd.addOption(ecsClusterOption()).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"));
|
|
26053
|
+
addEcsAssumeRoleOptions(cmd);
|
|
26054
|
+
cmd.addOption(new Option("--no-pull", "Skip docker pull for every container image and the metadata sidecar")).addOption(new Option("--no-build", "Skip docker build on the local CDK-asset path for every container (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 ECR-pull / public-registry images. Per-container --image-override mappings take precedence (the override tag is used as-is, --no-build does not apply). Compatible with --no-pull.")).addOption(new Option("--ecr-role-arn <arn>", "Role ARN to assume before authenticating against ECR for cross-account / centralized registries.")).addOption(new Option("--platform <platform>", "Force docker --platform (linux/amd64 or linux/arm64). Default: inferred from task RuntimePlatform.CpuArchitecture")).addOption(new Option("--max-tasks <n>", `Hard cap on local replica count. Caps the template DesiredCount so local dev machines don't run an unbounded number of containers. Cannot exceed ${83} due to the per-replica link-local /24 subnet allocator's range.`).default(3).argParser(parseMaxTasks)).addOption(new Option("--restart-policy <policy>", "How to react when an essential container exits. 'on-failure' (default) restarts only on non-zero exit; 'always' restarts on every exit; 'none' shuts the replica down and runs the service degraded.").default("on-failure").argParser(parseRestartPolicy)).addOption(new Option("--from-cfn-stack [cfn-stack-name]", `Read a deployed CloudFormation stack via ListStackResources and substitute Ref / Fn::ImportValue in container env vars / secrets / image URIs with the deployed physical IDs / exports. Use for CDK apps deployed via the upstream CDK CLI (\`cdk deploy\`). Bare form uses the ${getEmbedConfig().binaryName} stack name; pass an explicit value when the CFn stack name differs. Fn::GetAtt in container Environment[].Value is warn-and-dropped: CFn ListStackResources does not return per-attribute values, and unlike Lambda (where \`cdkl invoke --from-cfn-stack\` recovers Fn::GetAtt from the deployed function via lambda:GetFunctionConfiguration), no ECS-side equivalent resolves attributes off a deployed task / service.`)).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("--no-logs", "Disable foreground streaming of each replica container stdout/stderr. By default every booted replica streams its docker logs to the host terminal with a [svc=<service> r=<replica-index> c=<container>] prefix (parity with `run-task`). Pass --no-logs for multi-replica / multi-service runs whose interleaved log volume is unreadable; `docker logs -f <id>` in a separate terminal stays available.")).addOption(new Option("--shadow-ready-timeout <ms>", `Issue #265 — milliseconds the \`--watch\` rolling primitive waits for a shadow replica's first essential-container port to accept a TCP connection before swapping Cloud Map + front-door registrations off the old replica. Defaults to 60000 (60s) which covers realistic prod-shaped Node app cold-starts (TS->JS compile, full node_modules graph, framework boot, DB pool init). Bump higher when the reload log surfaces "TCP probe <ip>:<port> did not accept within <N>ms" — typical for Java / heavy ORM init / \`--inspect-brk\` attach pauses. Honored by --watch on \`${getEmbedConfig().binaryName} start-service\` and \`${getEmbedConfig().binaryName} start-alb\`. Env var: \`${getEmbedConfig().envPrefix}_SHADOW_READY_TIMEOUT_MS\` (flag wins over env).`).argParser(parseShadowReadyTimeout));
|
|
26055
|
+
[
|
|
26056
|
+
...commonOptions(),
|
|
26057
|
+
...appOptions(),
|
|
26058
|
+
...contextOptions
|
|
26059
|
+
].forEach((opt) => cmd.addOption(opt));
|
|
26060
|
+
cmd.addOption(regionOption);
|
|
26061
|
+
return cmd;
|
|
26214
26062
|
}
|
|
26215
26063
|
/**
|
|
26216
26064
|
* Issue #238 — register the `--image-override` flag family shared by
|
|
@@ -26232,6 +26080,345 @@ function addImageOverrideOptions(cmd) {
|
|
|
26232
26080
|
return cmd.addOption(new Option("--image-override <service=dockerfile or dockerfile...>", "Replace a service target's deployed-registry image with a local `docker build` of the supplied Dockerfile (repeatable). Two forms: <service>=<dockerfile> binds the service explicitly; a bare <dockerfile> opens a multi-select picker against the still-uncovered pinned targets (one Dockerfile across N services). Mix freely. Lets `--from-cfn-stack` still reach real AWS state while you iterate locally on the application container.")).addOption(new Option("--image-build-arg <KEY=VAL...>", "Global `docker build --build-arg KEY=VAL` pair applied to every --image-override build (repeatable). Lets a CDK Dockerfile that branches on an ARG (env target, base image tag, package mirror) honor that ARG locally without editing the Dockerfile.")).addOption(new Option("--image-build-secret <id=src...>", "Global `docker build --secret id=<id>,src=<src>` entry applied to every --image-override build (repeatable). Enables `RUN --mount=type=secret,id=<id>` in the Dockerfile — the standard private-registry / npm-token recipe (e.g. `--image-build-secret npmrc=./.npmrc`).")).addOption(new Option("--image-target <stage or svc=stage...>", "Repeatable. Bare `<stage>` is a global --target for every --image-override build; `<service>=<stage>` (issue #240) scopes the --target to one overridden target so a monorepo with different multi-stage Dockerfiles per service can stop each at its own intermediate stage. Per-service form overrides the global on the named target.")).addOption(new Option("--no-interactive-overrides", "Suppress the interactive boot prompt that walks each pinned target asking for a Dockerfile path, and the multi-select prompt fired by `--image-override <dockerfile>` (picker form). Useful for CI / scripted invocations.")).addOption(new Option("--strict-overrides", "Fail fast at boot when any pinned target remains uncovered after `--image-override` + the boot prompt resolve. Off by default; the boot WARN per uncovered pinned target still fires regardless.").default(false));
|
|
26233
26081
|
}
|
|
26234
26082
|
|
|
26083
|
+
//#endregion
|
|
26084
|
+
//#region src/cli/commands/local-run-task.ts
|
|
26085
|
+
/**
|
|
26086
|
+
* `cdkl run-task <target>` — Phase 1 of the ECS local-execution
|
|
26087
|
+
* trilogy. Synthesizes the CDK app, locates the target
|
|
26088
|
+
* `AWS::ECS::TaskDefinition`, stands up a per-task docker network with
|
|
26089
|
+
* the AWS-published `amazon-ecs-local-container-endpoints` sidecar, and
|
|
26090
|
+
* starts every container in `dependsOn` order. The essential
|
|
26091
|
+
* container's exit code drives the CLI's exit.
|
|
26092
|
+
*/
|
|
26093
|
+
async function localRunTaskCommand(target, options, extraStateProviders) {
|
|
26094
|
+
const logger = getLogger();
|
|
26095
|
+
if (options.verbose) logger.setLevel("debug");
|
|
26096
|
+
const state = createEcsRunState();
|
|
26097
|
+
let sigintHandler;
|
|
26098
|
+
let sigintCount = 0;
|
|
26099
|
+
let stateProvider;
|
|
26100
|
+
let profileCredsFile;
|
|
26101
|
+
let cleanupPromise;
|
|
26102
|
+
const cleanup = async () => {
|
|
26103
|
+
if (!cleanupPromise) cleanupPromise = (async () => {
|
|
26104
|
+
try {
|
|
26105
|
+
await cleanupEcsRun(state, { keepRunning: options.keepRunning });
|
|
26106
|
+
} catch (err) {
|
|
26107
|
+
getLogger().debug(`cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
26108
|
+
}
|
|
26109
|
+
if (profileCredsFile) try {
|
|
26110
|
+
await profileCredsFile.dispose();
|
|
26111
|
+
} catch (err) {
|
|
26112
|
+
getLogger().debug(`Failed to remove profile credentials tmpdir ${profileCredsFile.hostPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
26113
|
+
}
|
|
26114
|
+
})();
|
|
26115
|
+
await cleanupPromise;
|
|
26116
|
+
};
|
|
26117
|
+
try {
|
|
26118
|
+
await applyRoleArnIfSet({
|
|
26119
|
+
roleArn: options.roleArn,
|
|
26120
|
+
region: options.region,
|
|
26121
|
+
profile: options.profile
|
|
26122
|
+
});
|
|
26123
|
+
await ensureDockerAvailable();
|
|
26124
|
+
const appCmd = resolveApp(options.app);
|
|
26125
|
+
if (!appCmd) throw new Error(`No CDK app specified. Pass --app, set ${getEmbedConfig().envPrefix}_APP, or add "app" to cdk.json.`);
|
|
26126
|
+
logger.info("Synthesizing CDK app...");
|
|
26127
|
+
const synthesizer = new Synthesizer();
|
|
26128
|
+
const context = parseContextOptions(options.context);
|
|
26129
|
+
const synthOpts = {
|
|
26130
|
+
app: appCmd,
|
|
26131
|
+
output: options.output,
|
|
26132
|
+
...options.region && { region: options.region },
|
|
26133
|
+
...options.profile && { profile: options.profile },
|
|
26134
|
+
...Object.keys(context).length > 0 && { context }
|
|
26135
|
+
};
|
|
26136
|
+
const { stacks } = await synthesizer.synthesize(synthOpts);
|
|
26137
|
+
const resolvedTarget = await resolveSingleTarget(target, {
|
|
26138
|
+
entries: listTargets(stacks).ecsTaskDefinitions,
|
|
26139
|
+
message: "Select an ECS task definition to run",
|
|
26140
|
+
noun: "ECS task definitions",
|
|
26141
|
+
onMissing: () => new CdkLocalError(`${getEmbedConfig().cliName} run-task requires a <target> (an ECS task definition display path or logical ID). Run \`${getEmbedConfig().cliName} list\` to see them, or run it in a TTY to pick interactively.`, "LOCAL_RUN_TASK_TARGET_REQUIRED")
|
|
26142
|
+
});
|
|
26143
|
+
const candidate = pickCandidateStack(parseEcsTarget(resolvedTarget).stackPattern, stacks);
|
|
26144
|
+
stateProvider = createLocalStateProvider(options, candidate?.stackName ?? "", await resolveCfnFallbackRegion(options, candidate?.region), extraStateProviders);
|
|
26145
|
+
const imageContext = await buildEcsImageResolutionContext(candidate, stateProvider, options);
|
|
26146
|
+
const task = resolveEcsTaskTarget(resolvedTarget, stacks, imageContext);
|
|
26147
|
+
logger.info(`Target: ${task.stack.stackName}/${task.taskDefinitionLogicalId} (family=${task.family}, containers=${task.containers.length})`);
|
|
26148
|
+
const taskNeeds = detectEcsImageResolutionNeeds(stacks.find((s) => s.stackName === task.stack.stackName) ?? task.stack);
|
|
26149
|
+
if (stateProvider && taskNeeds.needsCrossStackResolver) {
|
|
26150
|
+
const consumerRegion = options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? task.stack.region ?? "us-east-1";
|
|
26151
|
+
const resolver = await stateProvider.buildCrossStackResolver(consumerRegion);
|
|
26152
|
+
if (resolver) await applyCrossStackResolverToTask(task, {
|
|
26153
|
+
resources: imageContext?.stateResources ?? {},
|
|
26154
|
+
...imageContext?.pseudoParameters && { pseudoParameters: imageContext.pseudoParameters },
|
|
26155
|
+
...imageContext?.stateParameters && { parameters: imageContext.stateParameters },
|
|
26156
|
+
...imageContext?.stateSensitiveParameters?.length && { sensitiveParameters: new Set(imageContext.stateSensitiveParameters) },
|
|
26157
|
+
consumerRegion,
|
|
26158
|
+
crossStackResolver: resolver
|
|
26159
|
+
});
|
|
26160
|
+
} else if (!stateProvider && taskNeeds.needsCrossStackResolver) logger.warn("Container Environment / Secrets entries contain Fn::ImportValue / Fn::GetStackOutput intrinsics. Pass a state-source flag (e.g. --from-cfn-stack or a host-provided extension) to substitute them against deployed state.");
|
|
26161
|
+
sigintHandler = () => {
|
|
26162
|
+
sigintCount += 1;
|
|
26163
|
+
if (sigintCount >= 2) {
|
|
26164
|
+
process.stderr.write("Force-exit on second ^C; container cleanup skipped.\n");
|
|
26165
|
+
process.exit(130);
|
|
26166
|
+
}
|
|
26167
|
+
logger.info("Stopping task...");
|
|
26168
|
+
cleanup().then(() => process.exit(130));
|
|
26169
|
+
};
|
|
26170
|
+
process.on("SIGINT", sigintHandler);
|
|
26171
|
+
const effectiveAssumeRole = resolveEcsAssumeRoleOption(options);
|
|
26172
|
+
let assumedCredentials;
|
|
26173
|
+
let resolvedRoleArn;
|
|
26174
|
+
if (effectiveAssumeRole === true) {
|
|
26175
|
+
if (!task.taskRoleArn) throw new Error(`--assume-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-role <arn>`);
|
|
26176
|
+
resolvedRoleArn = await resolvePlaceholderAccount(task.taskRoleArn, options.region, options.profile);
|
|
26177
|
+
assumedCredentials = await assumeTaskRole(resolvedRoleArn, options.region, options.profile);
|
|
26178
|
+
} else if (typeof effectiveAssumeRole === "string") {
|
|
26179
|
+
resolvedRoleArn = effectiveAssumeRole;
|
|
26180
|
+
assumedCredentials = await assumeTaskRole(resolvedRoleArn, options.region, options.profile);
|
|
26181
|
+
}
|
|
26182
|
+
const sidecarCredentials = await resolveSidecarCredentials(options, assumedCredentials);
|
|
26183
|
+
if (options.profile && sidecarCredentials && !assumedCredentials) profileCredsFile = await writeProfileCredentialsFile(options.profile, sidecarCredentials);
|
|
26184
|
+
const envOverrides = readEnvOverridesFile(options.envVars);
|
|
26185
|
+
const runOpts = {
|
|
26186
|
+
cluster: options.cluster,
|
|
26187
|
+
containerHost: options.containerHost,
|
|
26188
|
+
skipPull: options.pull === false,
|
|
26189
|
+
skipBuild: options.build === false,
|
|
26190
|
+
keepRunning: options.keepRunning,
|
|
26191
|
+
detach: options.detach
|
|
26192
|
+
};
|
|
26193
|
+
if (envOverrides) runOpts.envOverrides = envOverrides;
|
|
26194
|
+
if (sidecarCredentials) runOpts.taskCredentials = sidecarCredentials;
|
|
26195
|
+
if (resolvedRoleArn) runOpts.taskRoleArn = resolvedRoleArn;
|
|
26196
|
+
if (options.platform) runOpts.platformOverride = options.platform;
|
|
26197
|
+
if (options.region) runOpts.region = options.region;
|
|
26198
|
+
if (options.ecrRoleArn) runOpts.ecrRoleArn = options.ecrRoleArn;
|
|
26199
|
+
if (options.profile) runOpts.profile = options.profile;
|
|
26200
|
+
const hostPortOverrides = parseHostPortOverrides(options.hostPort);
|
|
26201
|
+
if (Object.keys(hostPortOverrides).length > 0) runOpts.hostPortOverrides = hostPortOverrides;
|
|
26202
|
+
if (profileCredsFile) runOpts.profileCredentialsFile = {
|
|
26203
|
+
hostPath: profileCredsFile.hostPath,
|
|
26204
|
+
containerPath: profileCredsFile.containerPath,
|
|
26205
|
+
profileName: profileCredsFile.profileName
|
|
26206
|
+
};
|
|
26207
|
+
const result = await runEcsTask(task, runOpts, state);
|
|
26208
|
+
if (options.detach) {
|
|
26209
|
+
logger.info(`Task containers started in detached mode; ${getEmbedConfig().binaryName} is exiting.`);
|
|
26210
|
+
logger.info(`Use 'docker ps --filter network=${result.state.network?.networkName ?? "<network>"}' to inspect; tear down with 'docker rm -f' and 'docker network rm'.`);
|
|
26211
|
+
sigintCount = 99;
|
|
26212
|
+
return;
|
|
26213
|
+
}
|
|
26214
|
+
if (result.essentialContainerName) logger.info(`Essential container '${result.essentialContainerName}' exited with code ${result.exitCode}.`);
|
|
26215
|
+
if (result.exitCode !== 0) process.exitCode = result.exitCode;
|
|
26216
|
+
} finally {
|
|
26217
|
+
if (sigintHandler) process.off("SIGINT", sigintHandler);
|
|
26218
|
+
if (stateProvider) stateProvider.dispose();
|
|
26219
|
+
if (!options.detach) await cleanup();
|
|
26220
|
+
}
|
|
26221
|
+
}
|
|
26222
|
+
/**
|
|
26223
|
+
* If `arn` contains the `${AWS::AccountId}` placeholder emitted by the
|
|
26224
|
+
* resolver for inline same-stack IAM Roles, substitute the live caller
|
|
26225
|
+
* account via STS `GetCallerIdentity`. Otherwise pass through unchanged.
|
|
26226
|
+
*/
|
|
26227
|
+
async function resolvePlaceholderAccount(arn, region, profile) {
|
|
26228
|
+
if (!arn.includes("${AWS::AccountId}")) return arn;
|
|
26229
|
+
const { STSClient, GetCallerIdentityCommand } = await import("@aws-sdk/client-sts");
|
|
26230
|
+
const sts = new STSClient(buildStsClientConfig({
|
|
26231
|
+
region,
|
|
26232
|
+
profile
|
|
26233
|
+
}));
|
|
26234
|
+
try {
|
|
26235
|
+
const account = (await sts.send(new GetCallerIdentityCommand({}))).Account;
|
|
26236
|
+
if (!account) throw new Error(`--assume-task-role: GetCallerIdentity returned no Account; cannot resolve placeholder ARN '${arn}'. Pass the ARN explicitly: --assume-task-role <arn>`);
|
|
26237
|
+
return arn.split(TASK_ROLE_ACCOUNT_PLACEHOLDER).join(account);
|
|
26238
|
+
} finally {
|
|
26239
|
+
sts.destroy();
|
|
26240
|
+
}
|
|
26241
|
+
}
|
|
26242
|
+
/**
|
|
26243
|
+
* Assume `roleArn` and return temp credentials.
|
|
26244
|
+
*/
|
|
26245
|
+
async function assumeTaskRole(roleArn, region, profile) {
|
|
26246
|
+
const { STSClient, AssumeRoleCommand } = await import("@aws-sdk/client-sts");
|
|
26247
|
+
const sts = new STSClient(buildStsClientConfig({
|
|
26248
|
+
region,
|
|
26249
|
+
profile
|
|
26250
|
+
}));
|
|
26251
|
+
try {
|
|
26252
|
+
const creds = (await sts.send(new AssumeRoleCommand({
|
|
26253
|
+
RoleArn: roleArn,
|
|
26254
|
+
RoleSessionName: `${getEmbedConfig().resourceNamePrefix}-run-task-${Date.now()}`,
|
|
26255
|
+
DurationSeconds: 3600
|
|
26256
|
+
}))).Credentials;
|
|
26257
|
+
if (!creds?.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken) throw new Error(`AssumeRole(${roleArn}) returned no usable credentials.`);
|
|
26258
|
+
return {
|
|
26259
|
+
accessKeyId: creds.AccessKeyId,
|
|
26260
|
+
secretAccessKey: creds.SecretAccessKey,
|
|
26261
|
+
sessionToken: creds.SessionToken
|
|
26262
|
+
};
|
|
26263
|
+
} finally {
|
|
26264
|
+
sts.destroy();
|
|
26265
|
+
}
|
|
26266
|
+
}
|
|
26267
|
+
/**
|
|
26268
|
+
* Build the substitution context the ECS task resolver consumes.
|
|
26269
|
+
* Returns `undefined` when no container's `Image` field needs
|
|
26270
|
+
* substitution — the resolver behaves as before in that case.
|
|
26271
|
+
*/
|
|
26272
|
+
async function buildEcsImageResolutionContext(candidate, stateProvider, options) {
|
|
26273
|
+
const logger = getLogger();
|
|
26274
|
+
if (!candidate) return void 0;
|
|
26275
|
+
const needs = detectEcsImageResolutionNeeds(candidate);
|
|
26276
|
+
if (!needs.needsPseudoParameters && !needs.needsStateResources && !needs.needsEnvOrSecretSubstitution) return;
|
|
26277
|
+
const ctx = {};
|
|
26278
|
+
const wantsPseudoForEnvOrSecret = !!stateProvider && needs.needsEnvOrSecretSubstitution;
|
|
26279
|
+
if (needs.needsPseudoParameters || wantsPseudoForEnvOrSecret) {
|
|
26280
|
+
const region = options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? candidate.region;
|
|
26281
|
+
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.`);
|
|
26282
|
+
let accountId;
|
|
26283
|
+
try {
|
|
26284
|
+
accountId = await resolveCallerAccountId(region, options.profile);
|
|
26285
|
+
} catch (err) {
|
|
26286
|
+
logger.warn(`Resolver needs \${AWS::AccountId} but STS GetCallerIdentity failed: ${err instanceof Error ? err.message : String(err)}. Substitution will be skipped; affected env / secret entries will be dropped with per-key warnings.`);
|
|
26287
|
+
}
|
|
26288
|
+
const partitionAndSuffix = region ? derivePartitionAndUrlSuffix(region) : void 0;
|
|
26289
|
+
ctx.pseudoParameters = {
|
|
26290
|
+
...accountId !== void 0 && { accountId },
|
|
26291
|
+
...region !== void 0 && { region },
|
|
26292
|
+
...partitionAndSuffix && {
|
|
26293
|
+
partition: partitionAndSuffix.partition,
|
|
26294
|
+
urlSuffix: partitionAndSuffix.urlSuffix
|
|
26295
|
+
}
|
|
26296
|
+
};
|
|
26297
|
+
}
|
|
26298
|
+
const wantsState = needs.needsStateResources || needs.needsEnvOrSecretSubstitution;
|
|
26299
|
+
if (stateProvider && wantsState) {
|
|
26300
|
+
const loaded = await stateProvider.load(candidate.stackName, candidate.region);
|
|
26301
|
+
if (loaded) ctx.stateResources = loaded.resources;
|
|
26302
|
+
else {
|
|
26303
|
+
const loadError = stateProvider.getLastLoadError?.();
|
|
26304
|
+
if (loadError) ctx.stateLoadFailureMessage = loadError;
|
|
26305
|
+
}
|
|
26306
|
+
if (needs.needsEnvOrSecretSubstitution && stateProvider.resolveTemplateSsmParameters) {
|
|
26307
|
+
const ssmParameters = await stateProvider.resolveTemplateSsmParameters(candidate.template);
|
|
26308
|
+
if (Object.keys(ssmParameters.values).length > 0) ctx.stateParameters = ssmParameters.values;
|
|
26309
|
+
if (ssmParameters.secureStringLogicalIds.length > 0) ctx.stateSensitiveParameters = ssmParameters.secureStringLogicalIds;
|
|
26310
|
+
}
|
|
26311
|
+
} else if (!stateProvider && needs.needsStateResources) logger.warn("Container Image references a same-stack AWS::ECR::Repository. Pass a state-source flag (e.g. --from-cfn-stack or a host-provided extension) to substitute the deployed repository URI. Otherwise the resolver will surface its existing error.");
|
|
26312
|
+
else if (!stateProvider && needs.needsEnvOrSecretSubstitution) logger.warn("Container Environment / Secrets entries contain CloudFormation intrinsics (Ref / Fn::GetAtt / Fn::Sub / Fn::Join). Pass a state-source flag (e.g. --from-cfn-stack or a host-provided extension) to substitute them against deployed state. Without a state source these entries are dropped (per-key warnings will follow).");
|
|
26313
|
+
return ctx;
|
|
26314
|
+
}
|
|
26315
|
+
function pickCandidateStack(stackPattern, stacks) {
|
|
26316
|
+
if (stackPattern === null) {
|
|
26317
|
+
if (stacks.length === 1) return stacks[0];
|
|
26318
|
+
return;
|
|
26319
|
+
}
|
|
26320
|
+
const matched = matchStacks(stacks, [stackPattern]);
|
|
26321
|
+
if (matched.length === 1) return matched[0];
|
|
26322
|
+
}
|
|
26323
|
+
async function resolveCallerAccountId(region, profile) {
|
|
26324
|
+
const { STSClient, GetCallerIdentityCommand } = await import("@aws-sdk/client-sts");
|
|
26325
|
+
const sts = new STSClient(buildStsClientConfig({
|
|
26326
|
+
region,
|
|
26327
|
+
profile
|
|
26328
|
+
}));
|
|
26329
|
+
try {
|
|
26330
|
+
return (await sts.send(new GetCallerIdentityCommand({}))).Account;
|
|
26331
|
+
} finally {
|
|
26332
|
+
sts.destroy();
|
|
26333
|
+
}
|
|
26334
|
+
}
|
|
26335
|
+
/**
|
|
26336
|
+
* Read the `--env-vars` JSON file using the same SAM-style shape as
|
|
26337
|
+
* `cdkl invoke --env-vars`: top-level keys are container names, with
|
|
26338
|
+
* `Parameters` reserved for global entries.
|
|
26339
|
+
*/
|
|
26340
|
+
function readEnvOverridesFile(filePath) {
|
|
26341
|
+
if (!filePath) return void 0;
|
|
26342
|
+
let raw;
|
|
26343
|
+
try {
|
|
26344
|
+
raw = readFileSync(filePath, "utf-8");
|
|
26345
|
+
} catch (err) {
|
|
26346
|
+
throw new Error(`Failed to read --env-vars file '${filePath}': ${err instanceof Error ? err.message : String(err)}`);
|
|
26347
|
+
}
|
|
26348
|
+
let parsed;
|
|
26349
|
+
try {
|
|
26350
|
+
parsed = JSON.parse(raw);
|
|
26351
|
+
} catch (err) {
|
|
26352
|
+
throw new Error(`Failed to parse --env-vars file '${filePath}' as JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
26353
|
+
}
|
|
26354
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`--env-vars file '${filePath}' must contain a JSON object at the top level.`);
|
|
26355
|
+
return parsed;
|
|
26356
|
+
}
|
|
26357
|
+
/**
|
|
26358
|
+
* Pick the credentials forwarded to the AWS-published
|
|
26359
|
+
* `amazon-ecs-local-container-endpoints` sidecar. Precedence:
|
|
26360
|
+
* 1. `--assume-task-role <arn>` (or bare `--assume-task-role` against
|
|
26361
|
+
* a resolvable `TaskRoleArn`) → STS-assumed temp creds. Highest
|
|
26362
|
+
* priority — when the user opted in to IAM emulation, those creds
|
|
26363
|
+
* drive the sidecar regardless of `--profile`.
|
|
26364
|
+
* 2. `--profile <p>` → resolved via {@link resolveProfileCredentials}
|
|
26365
|
+
* (the SDK's default credential provider chain — SSO / IAM
|
|
26366
|
+
* Identity Center / fromIni / role-assumption). NEW in this PR.
|
|
26367
|
+
* 3. Neither set → `undefined`; the sidecar runs with its own
|
|
26368
|
+
* default credential chain (typically empty inside a fresh
|
|
26369
|
+
* container — user containers will get 4xx from the credentials
|
|
26370
|
+
* endpoint, mimicking IAM-misconfigured prod).
|
|
26371
|
+
*
|
|
26372
|
+
* Extracted as an exported helper so a unit test can exercise every
|
|
26373
|
+
* branch without having to mock the full Synth + Docker + AWS pipeline
|
|
26374
|
+
* (the strategy used for the Lambda container path).
|
|
26375
|
+
*/
|
|
26376
|
+
async function resolveSidecarCredentials(options, assumedCredentials) {
|
|
26377
|
+
if (assumedCredentials) return assumedCredentials;
|
|
26378
|
+
if (options.profile) return resolveProfileCredentials(options.profile);
|
|
26379
|
+
}
|
|
26380
|
+
function createLocalRunTaskCommand(opts = {}) {
|
|
26381
|
+
setEmbedConfig(opts.embedConfig);
|
|
26382
|
+
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. Omit <target> in an interactive terminal to pick the task definition from a list.").argument("[target]", "CDK display path or stack-qualified logical ID of the AWS::ECS::TaskDefinition to run (omit to pick interactively in a TTY)").action(withErrorHandling(async (target, options) => {
|
|
26383
|
+
await localRunTaskCommand(target, options, opts.extraStateProviders);
|
|
26384
|
+
}));
|
|
26385
|
+
addRunTaskSpecificOptions(cmd);
|
|
26386
|
+
[
|
|
26387
|
+
...commonOptions(),
|
|
26388
|
+
...appOptions(),
|
|
26389
|
+
...contextOptions
|
|
26390
|
+
].forEach((opt) => cmd.addOption(opt));
|
|
26391
|
+
cmd.addOption(regionOption);
|
|
26392
|
+
return cmd;
|
|
26393
|
+
}
|
|
26394
|
+
/**
|
|
26395
|
+
* Register the option block that `cdkl run-task` adds on top of the shared
|
|
26396
|
+
* common / app / context option helpers. Shared between `cdkl run-task` and
|
|
26397
|
+
* any host CLI (e.g. cdkd's `local run-task`) that wraps the single-task
|
|
26398
|
+
* ECS local runner, so adding or renaming a `run-task`-only flag here
|
|
26399
|
+
* propagates to every embedder without duplicate `.addOption(...)` blocks.
|
|
26400
|
+
*
|
|
26401
|
+
* Calling order only affects `--help` presentation (Commander parses
|
|
26402
|
+
* insertion-order-independent). The host-CLI convention is host-specific
|
|
26403
|
+
* options first, then this helper, then the shared common / app / context
|
|
26404
|
+
* options — host flags / run-task flags / common flags grouped in three
|
|
26405
|
+
* `--help` clusters. Chainable: returns `cmd`.
|
|
26406
|
+
*
|
|
26407
|
+
* NOTE: `run-task` does NOT compose with {@link addCommonEcsServiceOptions}
|
|
26408
|
+
* even though many flags overlap. The two ECS surfaces (single-task vs
|
|
26409
|
+
* multi-replica service) have intentionally divergent defaults
|
|
26410
|
+
* (`run-task` has no `--max-tasks` / `--restart-policy`; `start-service`
|
|
26411
|
+
* / `start-alb` have no `--host-port` / `--keep-running` / `--detach`),
|
|
26412
|
+
* and folding `run-task` into the service common block would mutate the
|
|
26413
|
+
* surface non-trivially. Each command keeps its own helper.
|
|
26414
|
+
*/
|
|
26415
|
+
function addRunTaskSpecificOptions(cmd) {
|
|
26416
|
+
cmd.addOption(ecsClusterOption()).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("--host-port <containerPort=hostPort...>", "Publish a container port on a specific host port (e.g. 80=8080); repeatable. Default: host port == container port. Use this on macOS to map a privileged container port (< 1024) to a non-privileged host port and avoid the Docker Desktop admin-password prompt."));
|
|
26417
|
+
addEcsAssumeRoleOptions(cmd);
|
|
26418
|
+
cmd.addOption(new Option("--no-pull", "Skip docker pull for every container image and the metadata sidecar")).addOption(new Option("--no-build", "Skip docker build on every CDK-asset container (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 ECR-pull / public-registry containers. Compatible with --no-pull.")).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 ListStackResources and substitute Ref / Fn::ImportValue in container env vars / secrets / image URIs with the deployed physical IDs / exports. Use for CDK apps deployed via the upstream CDK CLI (\`cdk deploy\`). Bare form uses the ${getEmbedConfig().binaryName} stack name; pass an explicit value when the CFn stack name differs. Fn::GetAtt in container Environment[].Value is warn-and-dropped: CFn ListStackResources does not return per-attribute values, and unlike Lambda (where \`cdkl invoke --from-cfn-stack\` recovers Fn::GetAtt from the deployed function via lambda:GetFunctionConfiguration), no ECS-side equivalent resolves attributes off a deployed task / service.`)).addOption(new Option("--stack-region <region>", "Region of the state record to read. Used with --from-cfn-stack as the CFn client region."));
|
|
26419
|
+
return cmd;
|
|
26420
|
+
}
|
|
26421
|
+
|
|
26235
26422
|
//#endregion
|
|
26236
26423
|
//#region src/cli/commands/local-start-service.ts
|
|
26237
26424
|
/**
|
|
@@ -27156,7 +27343,7 @@ function createLocalStartAlbCommand(opts = {}) {
|
|
|
27156
27343
|
*/
|
|
27157
27344
|
function addAlbSpecificOptions(cmd) {
|
|
27158
27345
|
addImageOverrideOptions(cmd);
|
|
27159
|
-
return cmd.addOption(new Option("--lb-port <listenerPort=hostPort...>", "Bind the local front-door on a specific host port (e.g. 80=8080); repeatable. Default: host port == ALB listener port. Use this on macOS to remap a privileged listener port (< 1024) to a non-privileged host port.")).addOption(new Option("--tls", "Terminate TLS locally for cloud-HTTPS listeners. Default: a cloud-HTTPS listener is served over plain HTTP locally (X-Forwarded-Proto: https is preserved so the upstream app still sees the deployed listener protocol). Implied by --tls-cert / --tls-key. Use this when local-dev cookies need Secure / SameSite=None, when the upstream app inspects TLS metadata, or for mTLS / SNI testing — otherwise plain HTTP is friendlier (no self-signed cert warnings in curl / browser).")).addOption(new Option("--tls-cert <path>", "PEM-encoded server certificate for HTTPS front-door listeners. Implies --tls. Must be set together with --tls-key. Pass --tls alone (without --tls-cert / --tls-key) to auto-generate a self-signed cert (cached under $XDG_CACHE_HOME/cdk-local/alb-https/, default ~/.cache/cdk-local/alb-https/); requires openssl on PATH. The deployed Listener Certificates[] are NOT fetched (ACM private keys are not retrievable by design). The auto-generated cert lists DNS:localhost,IP:127.0.0.1 as SubjectAltName, so a client validating a non-loopback --container-host will fail the SAN check — pass --tls-cert / --tls-key with a SAN covering that host instead.")).addOption(new Option("--tls-key <path>", "PEM-encoded server private key matching --tls-cert. Implies --tls. Must be set together with --tls-cert.")).addOption(new Option("--no-verify-auth", "Disable local enforcement of authenticate-cognito / authenticate-oidc actions. Every request is served as if the auth check passed. Useful for local dev where you do not want to mint a Bearer token at all.")).addOption(new Option("--bearer-token <jwt>", "Default Bearer JWT
|
|
27346
|
+
return cmd.addOption(new Option("--lb-port <listenerPort=hostPort...>", "Bind the local front-door on a specific host port (e.g. 80=8080); repeatable. Default: host port == ALB listener port. Use this on macOS to remap a privileged listener port (< 1024) to a non-privileged host port.")).addOption(new Option("--tls", "Terminate TLS locally for cloud-HTTPS listeners. Default: a cloud-HTTPS listener is served over plain HTTP locally (X-Forwarded-Proto: https is preserved so the upstream app still sees the deployed listener protocol). Implied by --tls-cert / --tls-key. Use this when local-dev cookies need Secure / SameSite=None, when the upstream app inspects TLS metadata, or for mTLS / SNI testing — otherwise plain HTTP is friendlier (no self-signed cert warnings in curl / browser).")).addOption(new Option("--tls-cert <path>", "PEM-encoded server certificate for HTTPS front-door listeners. Implies --tls. Must be set together with --tls-key. Pass --tls alone (without --tls-cert / --tls-key) to auto-generate a self-signed cert (cached under $XDG_CACHE_HOME/cdk-local/alb-https/, default ~/.cache/cdk-local/alb-https/); requires openssl on PATH. The deployed Listener Certificates[] are NOT fetched (ACM private keys are not retrievable by design). The auto-generated cert lists DNS:localhost,IP:127.0.0.1 as SubjectAltName, so a client validating a non-loopback --container-host will fail the SAN check — pass --tls-cert / --tls-key with a SAN covering that host instead.")).addOption(new Option("--tls-key <path>", "PEM-encoded server private key matching --tls-cert. Implies --tls. Must be set together with --tls-cert.")).addOption(new Option("--no-verify-auth", "Disable local enforcement of authenticate-cognito / authenticate-oidc actions. Every request is served as if the auth check passed. Useful for local dev where you do not want to mint a Bearer token at all.")).addOption(new Option("--bearer-token <jwt>", "Default Bearer JWT this command INJECTS only when an inbound request has none (the default-when-missing role) — `cdkl start-alb` is the local ALB front-door RECEIVING outside-in requests, so this token is the fallback the front-door slots in as Authorization: Bearer <jwt> if the caller did not already supply one. Verified against the same JWKS / OIDC discovery URL the deployed ALB would (signature + iss + aud + exp). Cookie pass-through (AWSELBAuthSessionCookie-*) also bypasses the guard. Contrast with `cdkl invoke-agentcore --bearer-token`, where the role is reversed — that command is the outbound client and ALWAYS presents this token (the supplier).")).addOption(new Option("--watch", "Hot-reload: re-synth + per-replica reload of every ECS service behind the ALB when the CDK source changes (honors cdk.json watch.include/exclude; cdk.out, node_modules, .git are always excluded). A per-firing classifier picks the per-replica primitive: source-only edits on interpreted-language handlers (Node/Python/Ruby/shell) take a bind-mount FAST PATH (`docker cp` the new source into each replica + `docker restart`; no rebuild, front-door pool entry unchanged since the IP/port are preserved). Dockerfile / dependency manifest / compiled-language source / ambiguous edits fall through to the rebuild rolling primitive — boot a shadow under a bumped generation suffix, wait for its container port to accept a TCP connection, atomically register it in the front-door pool, then drop the old entry and retire the old container. Either path rolls one replica at a time, so a continuous external request stream against the listener port sees zero connection refusals across the reload. The host front-door (TLS, JWKS cache, Lambda-target containers, listener sockets) stays up across the reload. Lambda target groups behind the ALB are a no-op on reload (the warm RIE container keeps its boot-time image). Off by default; existing replica(s) keep serving when synth fails mid-reload.").default(false));
|
|
27160
27347
|
}
|
|
27161
27348
|
|
|
27162
27349
|
//#endregion
|
|
@@ -27252,5 +27439,5 @@ function addListSpecificOptions(cmd) {
|
|
|
27252
27439
|
}
|
|
27253
27440
|
|
|
27254
27441
|
//#endregion
|
|
27255
|
-
export {
|
|
27256
|
-
//# sourceMappingURL=local-list-
|
|
27442
|
+
export { MCP_PROTOCOL_VERSION as $, pickAgentCoreCandidateStack as $n, applyAuthorizerOverlay as $t, mergeForService as A, rejectExplicitCfnStackWithMultipleStacks as An, startApiServer as At, DEFAULT_SHADOW_READY_TIMEOUT_MS as B, listTargets as Bn, buildMethodArn as Bt, parseRestartPolicy as C, substituteAgainstState as Cn, materializeLayerFromArn as Ct, ImageOverrideError as D, LocalStateSourceError as Dn, filterRoutesByApiIdentifiers as Dt, runEcsServiceEmulator as E, substituteEnvVarsFromStateAsync as En, filterRoutesByApiIdentifier as Et, isLocalCdkAssetImage as F, collectSsmParameterRefs as Fn, buildJwksUrlFromIssuer as Ft, addInvokeAgentCoreSpecificOptions as G, pickRefLogicalId as Gn, attachAuthorizers as Gt, setShadowReadyTimeoutMs as H, discoverWebSocketApisOrThrow as Hn, evaluateCachedLambdaPolicy as Ht, listPinnedTargets as I, resolveSsmParameters as In, createJwksCache as It, A2A_CONTAINER_PORT as J, AGENTCORE_AGUI_PROTOCOL as Jn, buildCorsConfigFromCloudFrontChain as Jt, createLocalInvokeAgentCoreCommand as K, resolveLambdaArnIntrinsic as Kn, applyCorsResponseHeaders as Kt, buildCloudMapIndex as L, resolveWatchConfig as Ln, verifyCognitoJwt as Lt, resolveImageOverrides as M, resolveCfnRegion as Mn, resolveServiceIntegrationParameters as Mt, runImageOverrideBuilds as N, resolveCfnStackName as Nn, defaultCredentialsLoader as Nt, buildImageOverrideTag as O, createLocalStateProvider as On, groupRoutesByServer as Ot, describePinnedImageUri as P, CfnLocalStateProvider as Pn, buildCognitoJwksUrl as Pt, MCP_PATH as Q, AgentCoreResolutionError as Qn, translateLambdaResponse as Qt, CloudMapRegistry as R, resolveSingleTarget as Rn, verifyJwtAuthorizer as Rt, parseMaxTasks as S, EcsTaskResolutionError as Sn, buildStageMap as St, resolveSharedSidecarCredentials as T, substituteEnvVarsFromState as Tn, availableApiIdentifiers as Tt, getContainerNetworkIp as U, parseSelectionExpressionPath as Un, invokeRequestAuthorizer as Ut, SOFT_RELOAD_COMPLETION_LOG_SUFFIX as V, discoverWebSocketApis as Vn, computeRequestIdentityHash as Vt, attachContainerLogStreamer as W, discoverRoutes as Wn, invokeTokenAuthorizer as Wt, a2aInvokeOnce as X, AGENTCORE_MCP_PROTOCOL as Xn, matchPreflight as Xt, A2A_PATH as Y, AGENTCORE_HTTP_PROTOCOL as Yn, isFunctionUrlOacFronted as Yt, MCP_CONTAINER_PORT as Z, AGENTCORE_RUNTIME_TYPE as Zn, matchRoute as Zt, addCommonEcsServiceOptions as _, architectureToPlatform as _n, createWatchPredicates as _t, albStrategy as a, tryParseStatus as an, LocalInvokeBuildError as ar, invokeAgentCore as at, buildEcsImageResolutionContext$1 as b, resolveRuntimeFileExtension as bn, createFileWatcher as bt, resolveAlbTarget as c, probeHostGatewaySupport as cn, SUPPORTED_CODE_RUNTIMES as ct, addStartServiceSpecificOptions as d, buildMgmtEndpointEnvUrl as dn, renderCodeDockerfile as dt, buildHttpApiV2Event as en, resolveAgentCoreTarget as er, mcpInvokeOnce as et, createLocalStartServiceCommand as f, handleConnectionsRequest as fn, toCmdArgv as ft, MAX_TASKS_SUBNET_RANGE_CAP as g, buildMessageEvent as gn, createLocalStartApiCommand as gt, createLocalRunTaskCommand as h, buildDisconnectEvent as hn, addStartApiSpecificOptions as ht, addAlbSpecificOptions as i, selectIntegrationResponse as in, tryResolveImageFnJoin as ir, AGENTCORE_SESSION_ID_HEADER as it, parseImageOverrideFlags as j, resolveCfnFallbackRegion as jn, resolveSelectionExpression as jt, enforceImageOverrideOrphans as k, isCfnFlagPresent as kn, readMtlsMaterialsFromDisk as kt, isApplicationLoadBalancer as l, bufferToBody as ln, buildAgentCoreCodeImage as lt, addRunTaskSpecificOptions as m, buildConnectEvent as mn, createLocalInvokeCommand as mt, createLocalListCommand as n, evaluateResponseParameters as nn, formatStateRemedy as nr, AGENTCORE_SIGV4_SERVICE as nt, createLocalStartAlbCommand as o, VtlEvaluationError as on, buildStsClientConfig as or, waitForAgentCorePing as ot, serviceStrategy as p, parseConnectionsPath as pn, addInvokeSpecificOptions as pt, invokeAgentCoreWs as q, AGENTCORE_A2A_PROTOCOL as qn, buildCorsConfigByApiId as qt, formatTargetListing as r, pickResponseTemplate as rn, substituteImagePlaceholders as rr, signAgentCoreInvocation as rt, parseLbPortOverrides as s, HOST_GATEWAY_MIN_VERSION as sn, resolveProfileCredentials as sr, downloadAndExtractS3Bundle as st, addListSpecificOptions as t, buildRestV1Event as tn, derivePseudoParametersFromRegion as tr, parseSseForJsonRpc as tt, resolveAlbFrontDoor as u, ConnectionRegistry as un, computeCodeImageTag as ut, addEcsAssumeRoleOptions as v, buildContainerImage as vn, resolveApiTargetSubset as vt, resolveEcsAssumeRoleOption as w, substituteAgainstStateAsync as wn, resolveEnvVars as wt, ecsClusterOption as x, resolveRuntimeImage as xn, attachStageContext as xt, addImageOverrideOptions as y, resolveRuntimeCodeMountPath as yn, createAuthorizerCache as yt, classifySourceChange as z, countTargets as zn, verifyJwtViaDiscovery as zt };
|
|
27443
|
+
//# sourceMappingURL=local-list-CFINvNxo.js.map
|