@go-to-k/cdkd 0.117.1 → 0.118.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 +196 -55
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -7901,11 +7901,12 @@ var DynamoDBTableProvider = class {
|
|
|
7901
7901
|
* is surfaced for BOTH the LOCAL replica AND cross-region replicas
|
|
7902
7902
|
* via per-region SDK clients (cached in `regionalClientCache` for
|
|
7903
7903
|
* the deploy run). Issue #389 lifted the v1 LOCAL-only limitation.
|
|
7904
|
-
* - Cross-region replica Tags propagation (Issue #389):
|
|
7905
|
-
*
|
|
7906
|
-
*
|
|
7907
|
-
*
|
|
7908
|
-
*
|
|
7904
|
+
* - Cross-region replica Tags propagation (Issue #389 / #441):
|
|
7905
|
+
* BOTH `create()` and `update()` resolve each non-local replica's
|
|
7906
|
+
* table ARN by swapping the region segment of the local ARN and
|
|
7907
|
+
* issue `TagResource` / `UntagResource` against a per-region
|
|
7908
|
+
* client. The shared helper `applyCrossRegionReplicaTagsDiff`
|
|
7909
|
+
* centralizes the diff + best-effort WARN-on-failure contract.
|
|
7909
7910
|
*/
|
|
7910
7911
|
var DynamoDBGlobalTableProvider = class {
|
|
7911
7912
|
dynamoDBClient;
|
|
@@ -8114,6 +8115,13 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
8114
8115
|
if (!region || region === currentRegion) continue;
|
|
8115
8116
|
await this.addReplica(tableName, replica, region, logicalId);
|
|
8116
8117
|
}
|
|
8118
|
+
for (const replica of replicas) {
|
|
8119
|
+
const region = replica["Region"];
|
|
8120
|
+
if (!region || region === currentRegion) continue;
|
|
8121
|
+
const replicaTags = replica["Tags"];
|
|
8122
|
+
if (!replicaTags || replicaTags.length === 0) continue;
|
|
8123
|
+
await this.applyCrossRegionReplicaTagsDiff(tableInfo.tableArn, region, void 0, replicaTags, tableName);
|
|
8124
|
+
}
|
|
8117
8125
|
if (properties["TimeToLiveSpecification"]) {
|
|
8118
8126
|
const ttl = properties["TimeToLiveSpecification"];
|
|
8119
8127
|
const attributeName = ttl["AttributeName"];
|
|
@@ -8299,6 +8307,8 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
8299
8307
|
const region = replica["Region"];
|
|
8300
8308
|
if (!region || region === currentRegion) continue;
|
|
8301
8309
|
await this.addReplica(physicalId, replica, region, logicalId);
|
|
8310
|
+
const newReplicaTags = replica["Tags"];
|
|
8311
|
+
await this.applyCrossRegionReplicaTagsDiff(tableArn, region, void 0, newReplicaTags, physicalId);
|
|
8302
8312
|
const newReadAutoScaling = (replica["ReadProvisionedThroughputSettings"] ?? {})["ReadCapacityAutoScalingSettings"];
|
|
8303
8313
|
if (newBilling === "PROVISIONED" && newReadAutoScaling) {
|
|
8304
8314
|
const regionalAutoScalingClient = this.getRegionalAutoScalingClient(region);
|
|
@@ -8311,16 +8321,7 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
8311
8321
|
const oldReplica = (previousProperties["Replicas"] ?? []).find((r) => r["Region"] === region);
|
|
8312
8322
|
const oldReplicaTags = oldReplica?.["Tags"];
|
|
8313
8323
|
const newReplicaTags = replica["Tags"];
|
|
8314
|
-
|
|
8315
|
-
const replicaArn = this.replicaArnForRegion(tableArn, region);
|
|
8316
|
-
if (replicaArn) try {
|
|
8317
|
-
const regionalClient = this.getRegionalClient(region);
|
|
8318
|
-
await this.applyTagDiffOnClient(regionalClient, replicaArn, oldReplicaTags, newReplicaTags);
|
|
8319
|
-
} catch (tagErr) {
|
|
8320
|
-
this.logger.warn(`Could not apply Tags diff to cross-region replica ${region} of ${physicalId}: ${tagErr instanceof Error ? tagErr.message : String(tagErr)}. The replica's Tags state will surface as drift until the next successful deploy.`);
|
|
8321
|
-
}
|
|
8322
|
-
else this.logger.warn(`Could not derive replica ARN for region ${region} from ${tableArn} — skipping Tags propagation for ${physicalId}`);
|
|
8323
|
-
} else this.logger.warn(`Local DescribeTable returned no TableArn — cannot propagate Tags to cross-region replica ${region} of ${physicalId}`);
|
|
8324
|
+
await this.applyCrossRegionReplicaTagsDiff(tableArn, region, oldReplicaTags, newReplicaTags, physicalId);
|
|
8324
8325
|
const oldReadAutoScaling = (oldReplica?.["ReadProvisionedThroughputSettings"] ?? {})["ReadCapacityAutoScalingSettings"];
|
|
8325
8326
|
const newReadAutoScaling = (replica["ReadProvisionedThroughputSettings"] ?? {})["ReadCapacityAutoScalingSettings"];
|
|
8326
8327
|
const effectiveNewReadAutoScaling = newBilling === "PAY_PER_REQUEST" ? void 0 : newReadAutoScaling;
|
|
@@ -8439,10 +8440,49 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
8439
8440
|
await this.applyTagDiffOnClient(this.dynamoDBClient, tableArn, oldTagsRaw, newTagsRaw);
|
|
8440
8441
|
}
|
|
8441
8442
|
/**
|
|
8443
|
+
* Propagate a per-replica Tags diff to ONE cross-region replica via a
|
|
8444
|
+
* per-region client (Issue #389 / #441 — closes the create-side gap).
|
|
8445
|
+
* Centralizes the common shape used by BOTH `create()` (`oldTags`
|
|
8446
|
+
* undefined → every new tag is an add) and `update()` (per-replica
|
|
8447
|
+
* modify path's old-vs-new diff). Best-effort: a failure here logs at
|
|
8448
|
+
* WARN naming the offending region + ARN + reason and the deploy
|
|
8449
|
+
* continues — the cross-region Tags state will surface as drift on
|
|
8450
|
+
* the next run (or `cdkd drift --revert`) rather than aborting the
|
|
8451
|
+
* deploy mid-flight. Mirrors the autoscaling diff's failure contract
|
|
8452
|
+
* (PR #393).
|
|
8453
|
+
*
|
|
8454
|
+
* `tableArn` is the LOCAL replica's table ARN (returned by the
|
|
8455
|
+
* post-create `waitForTableActive` or the inline `DescribeTable` in
|
|
8456
|
+
* `update()`); the helper swaps the region segment via
|
|
8457
|
+
* `replicaArnForRegion` before issuing `TagResource` / `UntagResource`
|
|
8458
|
+
* against the per-region client.
|
|
8459
|
+
*
|
|
8460
|
+
* No-op when `oldTags` deep-equals `newTags` — the caller is allowed
|
|
8461
|
+
* to invoke unconditionally without first diffing.
|
|
8462
|
+
*/
|
|
8463
|
+
async applyCrossRegionReplicaTagsDiff(tableArn, region, oldTags, newTags, physicalIdForLogs) {
|
|
8464
|
+
if (deepEqual$1(oldTags, newTags)) return;
|
|
8465
|
+
if (!tableArn) {
|
|
8466
|
+
this.logger.warn(`Local DescribeTable returned no TableArn — cannot propagate Tags to cross-region replica ${region} of ${physicalIdForLogs}`);
|
|
8467
|
+
return;
|
|
8468
|
+
}
|
|
8469
|
+
const replicaArn = this.replicaArnForRegion(tableArn, region);
|
|
8470
|
+
if (!replicaArn) {
|
|
8471
|
+
this.logger.warn(`Could not derive replica ARN for region ${region} from ${tableArn} — skipping Tags propagation for ${physicalIdForLogs}`);
|
|
8472
|
+
return;
|
|
8473
|
+
}
|
|
8474
|
+
try {
|
|
8475
|
+
const regionalClient = this.getRegionalClient(region);
|
|
8476
|
+
await this.applyTagDiffOnClient(regionalClient, replicaArn, oldTags, newTags);
|
|
8477
|
+
} catch (tagErr) {
|
|
8478
|
+
this.logger.warn(`Could not apply Tags diff to cross-region replica ${region} of ${physicalIdForLogs}: ${tagErr instanceof Error ? tagErr.message : String(tagErr)}. The replica's Tags state will surface as drift until the next successful deploy.`);
|
|
8479
|
+
}
|
|
8480
|
+
}
|
|
8481
|
+
/**
|
|
8442
8482
|
* Apply a Tags diff against the given `DynamoDBClient` (which may be
|
|
8443
8483
|
* the local client or a per-region client returned by
|
|
8444
8484
|
* `getRegionalClient`). Used by the local-replica path AND the
|
|
8445
|
-
* cross-region replica Tags propagation path (Issue #389).
|
|
8485
|
+
* cross-region replica Tags propagation path (Issue #389 / #441).
|
|
8446
8486
|
*/
|
|
8447
8487
|
async applyTagDiffOnClient(client, tableArn, oldTagsRaw, newTagsRaw) {
|
|
8448
8488
|
const toMap = (tags) => {
|
|
@@ -8732,9 +8772,12 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
8732
8772
|
*
|
|
8733
8773
|
* Per-replica sub-specifications (`ContributorInsightsSpecification` /
|
|
8734
8774
|
* `PointInTimeRecoverySpecification` / `KinesisStreamSpecification`)
|
|
8735
|
-
* are surfaced
|
|
8736
|
-
*
|
|
8737
|
-
*
|
|
8775
|
+
* are surfaced for BOTH the LOCAL replica AND cross-region replicas
|
|
8776
|
+
* via per-region SDK clients cached in `regionalClientCache` (Issue
|
|
8777
|
+
* #389 lifted the v1 LOCAL-only limitation; the per-replica reads
|
|
8778
|
+
* happen in `readReplicaSubSpecs` below). Each cross-region call is
|
|
8779
|
+
* best-effort — a permissions gap in one region omits the offending
|
|
8780
|
+
* key rather than aborting the whole drift read.
|
|
8738
8781
|
*/
|
|
8739
8782
|
async readCurrentState(physicalId, _logicalId, _resourceType) {
|
|
8740
8783
|
try {
|
|
@@ -37254,8 +37297,11 @@ function buildSubstitutionContextFromImageContext(context) {
|
|
|
37254
37297
|
* `physicalId`, and `Fn::GetAtt: [<Repo>, 'RepositoryUri']` shapes
|
|
37255
37298
|
* are resolved via the same state record.
|
|
37256
37299
|
*
|
|
37257
|
-
*
|
|
37258
|
-
*
|
|
37300
|
+
* Cross-account / cross-region pull (#455): `pullEcrImage` auto-detects
|
|
37301
|
+
* cross-account from `sts:GetCallerIdentity` and authenticates against
|
|
37302
|
+
* the URI's region directly. Pass `--ecr-role-arn <arn>` when the caller
|
|
37303
|
+
* does not already have cross-account `ecr:GetAuthorizationToken` /
|
|
37304
|
+
* `ecr:BatchGetImage` access on the target repository.
|
|
37259
37305
|
*/
|
|
37260
37306
|
function parseContainerImage(raw, containerName, taskLogicalId, resources, _stack, context) {
|
|
37261
37307
|
const getAttImage = tryResolveImageGetAtt(raw, resources, context);
|
|
@@ -37926,16 +37972,26 @@ function redactAwsCredentialsInArgs(args) {
|
|
|
37926
37972
|
//#endregion
|
|
37927
37973
|
//#region src/local/ecr-puller.ts
|
|
37928
37974
|
/**
|
|
37929
|
-
* ECR pull fallback for `cdkd local invoke`
|
|
37930
|
-
*
|
|
37975
|
+
* ECR pull fallback for `cdkd local invoke` / `cdkd local start-api` /
|
|
37976
|
+
* `cdkd local run-task`. When the image URI resolves to an ECR repo but
|
|
37931
37977
|
* doesn't match any cdk.out asset (typical when invoking a stack
|
|
37932
|
-
* deployed elsewhere
|
|
37933
|
-
*
|
|
37934
|
-
*
|
|
37935
|
-
* **
|
|
37936
|
-
* -
|
|
37937
|
-
*
|
|
37938
|
-
* -
|
|
37978
|
+
* deployed elsewhere or sharing a centralized registry), cdkd
|
|
37979
|
+
* authenticates against the target registry and runs `docker pull`.
|
|
37980
|
+
*
|
|
37981
|
+
* **Cross-account / cross-region** (#455):
|
|
37982
|
+
* - Same-account, same-region: fast path. No STS hop. The default
|
|
37983
|
+
* credential chain is used directly for `ecr:GetAuthorizationToken`.
|
|
37984
|
+
* - `ecrRoleArn` is provided: `sts:AssumeRole` is issued via the
|
|
37985
|
+
* default credential chain to obtain temporary credentials for the
|
|
37986
|
+
* target account. The resulting credentials authenticate the ECR
|
|
37987
|
+
* client (regardless of region — the ECR client is built for the
|
|
37988
|
+
* URI's region, which can differ from the caller's profile region).
|
|
37989
|
+
* - Cross-account, NO `ecrRoleArn`: cdkd falls through to the
|
|
37990
|
+
* default credential chain. This works when the caller has been
|
|
37991
|
+
* granted cross-account `ecr:GetAuthorizationToken` +
|
|
37992
|
+
* `ecr:BatchGetImage` permissions on the target repository via an
|
|
37993
|
+
* IAM policy; otherwise AWS rejects the call with `AccessDenied`
|
|
37994
|
+
* and the user is pointed at `--ecr-role-arn`.
|
|
37939
37995
|
*
|
|
37940
37996
|
* The `--no-pull` semantics (C3 in the design doc):
|
|
37941
37997
|
* - When NOT set: `ecrLogin` + `docker pull <uri>`.
|
|
@@ -37962,34 +38018,87 @@ function parseEcrUri(imageUri) {
|
|
|
37962
38018
|
};
|
|
37963
38019
|
}
|
|
37964
38020
|
/**
|
|
37965
|
-
*
|
|
38021
|
+
* Module-level cache for STS-issued AssumeRole credentials, keyed by
|
|
38022
|
+
* `(ecrRoleArn, callerRegion)`. Closes the reviewer's MAJOR finding: ECS
|
|
38023
|
+
* run-task with N containers under one `--ecr-role-arn` would otherwise issue
|
|
38024
|
+
* N× `AssumeRole` and N× `GetCallerIdentity` for identical credentials valid
|
|
38025
|
+
* for 3600s. The cache keeps a 5-minute safety margin against the recorded
|
|
38026
|
+
* `Expiration` so STS-side / local-clock skew never lets a stale entry through.
|
|
37966
38027
|
*
|
|
37967
|
-
*
|
|
37968
|
-
*
|
|
37969
|
-
*
|
|
38028
|
+
* Cache key is intentionally `(roleArn, region)` rather than full caller
|
|
38029
|
+
* identity — STS issues per-region session creds, and a switch of `--region`
|
|
38030
|
+
* between two `local invoke` calls in the same process must re-issue.
|
|
38031
|
+
*
|
|
38032
|
+
* NOT cleared on process exit — Node's module scope evaporates with the
|
|
38033
|
+
* process, and no inter-process sharing is desired (each `cdkd local invoke`
|
|
38034
|
+
* is its own isolated runtime).
|
|
38035
|
+
*/
|
|
38036
|
+
const ASSUMED_ROLE_CACHE = /* @__PURE__ */ new Map();
|
|
38037
|
+
/**
|
|
38038
|
+
* Module-level cache for `STS:GetCallerIdentity`. The result is identity-only
|
|
38039
|
+
* (`Account`) and invariant for the lifetime of the process under one set of
|
|
38040
|
+
* default credentials. Keyed by `callerRegion` to avoid a cross-region leak
|
|
38041
|
+
* when the caller flips `AWS_REGION` mid-process (STS is global but the SDK
|
|
38042
|
+
* uses regional endpoints; the result is invariant in practice, but we key
|
|
38043
|
+
* on region for safety).
|
|
38044
|
+
*/
|
|
38045
|
+
const CALLER_IDENTITY_CACHE = /* @__PURE__ */ new Map();
|
|
38046
|
+
/** 5-minute safety margin against the recorded STS expiration timestamp. */
|
|
38047
|
+
const STS_CREDENTIAL_SAFETY_MARGIN_MS = 300 * 1e3;
|
|
38048
|
+
function isCredentialFresh(creds) {
|
|
38049
|
+
if (!creds.expiration) return false;
|
|
38050
|
+
return creds.expiration.getTime() - Date.now() > STS_CREDENTIAL_SAFETY_MARGIN_MS;
|
|
38051
|
+
}
|
|
38052
|
+
/**
|
|
38053
|
+
* Pull (or verify locally cached) a container image from ECR.
|
|
38054
|
+
*
|
|
38055
|
+
* Auto-detects cross-account from `STS:GetCallerIdentity` and assumes
|
|
38056
|
+
* the supplied role when set. Returns the image URI the caller should
|
|
38057
|
+
* pass to `docker run` (same as the input — no rewriting).
|
|
37970
38058
|
*/
|
|
37971
38059
|
async function pullEcrImage(imageUri, options) {
|
|
37972
38060
|
const logger = getLogger().child("ecr-puller");
|
|
37973
38061
|
const parsed = parseEcrUri(imageUri);
|
|
37974
38062
|
if (!parsed) throw new LocalInvokeBuildError(`Image URI '${imageUri}' is not an ECR URI. cdkd local invoke v1 only authenticates against ECR for the deployed-image fallback path.`);
|
|
37975
|
-
const sts = new STSClient({ region: parsed.region });
|
|
37976
|
-
let callerAccount;
|
|
37977
|
-
try {
|
|
37978
|
-
const identity = await sts.send(new GetCallerIdentityCommand({}));
|
|
37979
|
-
if (!identity.Account) throw new LocalInvokeBuildError("STS GetCallerIdentity returned no Account. Verify your AWS credentials.");
|
|
37980
|
-
callerAccount = identity.Account;
|
|
37981
|
-
} finally {
|
|
37982
|
-
sts.destroy();
|
|
37983
|
-
}
|
|
37984
|
-
if (callerAccount !== parsed.accountId) throw new LocalInvokeBuildError(`Image URI '${imageUri}' is in account ${parsed.accountId}, but the caller is ${callerAccount}. Cross-account ECR pull is not supported in cdkd local invoke v1 — deferred to a follow-up PR. Workaround: assume a role in the target account before invoking, or build the image locally with \`cdkd local invoke -a cdk.out\` (no ECR pull).`);
|
|
37985
38063
|
const callerRegion = options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"];
|
|
37986
|
-
if (callerRegion && callerRegion !== parsed.region) throw new LocalInvokeBuildError(`Image URI '${imageUri}' is in region ${parsed.region}, but the caller's region is ${callerRegion}. Cross-region ECR pull is not supported in cdkd local invoke v1 — deferred to a follow-up PR. Workaround: re-run with AWS_REGION=${parsed.region} set, or build the image locally with -a cdk.out.`);
|
|
37987
38064
|
if (options.skipPull) {
|
|
37988
38065
|
logger.info(`Skipping ECR pull (--no-pull). Verifying ${imageUri} is in local cache...`);
|
|
37989
38066
|
await verifyImageInLocalCache(imageUri);
|
|
37990
38067
|
return imageUri;
|
|
37991
38068
|
}
|
|
37992
|
-
const
|
|
38069
|
+
const callerIdentityKey = callerRegion ?? "_unset";
|
|
38070
|
+
let callerAccount = CALLER_IDENTITY_CACHE.get(callerIdentityKey);
|
|
38071
|
+
if (callerAccount === void 0) {
|
|
38072
|
+
const sts = new STSClient({ ...callerRegion && { region: callerRegion } });
|
|
38073
|
+
try {
|
|
38074
|
+
const identity = await sts.send(new GetCallerIdentityCommand({}));
|
|
38075
|
+
if (!identity.Account) throw new LocalInvokeBuildError("STS GetCallerIdentity returned no Account. Verify your AWS credentials.");
|
|
38076
|
+
callerAccount = identity.Account;
|
|
38077
|
+
CALLER_IDENTITY_CACHE.set(callerIdentityKey, callerAccount);
|
|
38078
|
+
} finally {
|
|
38079
|
+
sts.destroy();
|
|
38080
|
+
}
|
|
38081
|
+
}
|
|
38082
|
+
const crossAccount = callerAccount !== parsed.accountId;
|
|
38083
|
+
const crossRegion = callerRegion !== void 0 && callerRegion !== parsed.region;
|
|
38084
|
+
let assumed;
|
|
38085
|
+
if (options.ecrRoleArn) {
|
|
38086
|
+
const cacheKey = `${options.ecrRoleArn}|${callerRegion ?? "_unset"}`;
|
|
38087
|
+
const cached = ASSUMED_ROLE_CACHE.get(cacheKey);
|
|
38088
|
+
if (cached && isCredentialFresh(cached)) {
|
|
38089
|
+
assumed = cached;
|
|
38090
|
+
logger.debug(`Reusing cached AssumeRole credentials for ${options.ecrRoleArn}`);
|
|
38091
|
+
} else {
|
|
38092
|
+
assumed = await assumeRoleForEcr(options.ecrRoleArn, callerRegion, logger);
|
|
38093
|
+
ASSUMED_ROLE_CACHE.set(cacheKey, assumed);
|
|
38094
|
+
logger.info(`Assumed role ${options.ecrRoleArn} for ECR pull (account=${parsed.accountId}, region=${parsed.region})`);
|
|
38095
|
+
}
|
|
38096
|
+
} else if (crossAccount) logger.info(`Cross-account ECR pull: image account ${parsed.accountId} != caller ${callerAccount}. Using the caller's credentials; pass --ecr-role-arn <arn> if AWS rejects with AccessDenied.`);
|
|
38097
|
+
if (crossRegion) logger.info(`Cross-region ECR pull: image region ${parsed.region} != caller ${callerRegion ?? "(unset)"}. Authenticating against the image region directly.`);
|
|
38098
|
+
const ecr = new ECRClient({
|
|
38099
|
+
region: parsed.region,
|
|
38100
|
+
...assumed && { credentials: assumed }
|
|
38101
|
+
});
|
|
37993
38102
|
try {
|
|
37994
38103
|
await ecrLogin(ecr, parsed.accountId, parsed.region);
|
|
37995
38104
|
} finally {
|
|
@@ -38004,10 +38113,39 @@ async function pullEcrImage(imageUri, options) {
|
|
|
38004
38113
|
return imageUri;
|
|
38005
38114
|
}
|
|
38006
38115
|
/**
|
|
38007
|
-
*
|
|
38008
|
-
*
|
|
38009
|
-
*
|
|
38010
|
-
*
|
|
38116
|
+
* Assume the supplied role via the SDK default credential chain and
|
|
38117
|
+
* return the resulting temporary credentials. The STS client is built
|
|
38118
|
+
* with the caller's profile region (or unset) — STS is a global
|
|
38119
|
+
* service so the region is informational, but threading it through
|
|
38120
|
+
* mirrors the convention used by `src/utils/role-arn.ts`.
|
|
38121
|
+
*/
|
|
38122
|
+
async function assumeRoleForEcr(roleArn, callerRegion, logger) {
|
|
38123
|
+
logger.debug(`Assuming role ${roleArn} for ECR pull...`);
|
|
38124
|
+
const sts = new STSClient({ ...callerRegion && { region: callerRegion } });
|
|
38125
|
+
try {
|
|
38126
|
+
const creds = (await sts.send(new AssumeRoleCommand({
|
|
38127
|
+
RoleArn: roleArn,
|
|
38128
|
+
RoleSessionName: `cdkd-local-ecr-${Date.now()}`,
|
|
38129
|
+
DurationSeconds: 3600
|
|
38130
|
+
}))).Credentials;
|
|
38131
|
+
if (!creds || !creds.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken) throw new LocalInvokeBuildError(`AssumeRole(${roleArn}) returned no usable credentials. Verify the role's trust policy allows your identity to assume it.`);
|
|
38132
|
+
return {
|
|
38133
|
+
accessKeyId: creds.AccessKeyId,
|
|
38134
|
+
secretAccessKey: creds.SecretAccessKey,
|
|
38135
|
+
sessionToken: creds.SessionToken,
|
|
38136
|
+
...creds.Expiration && { expiration: creds.Expiration }
|
|
38137
|
+
};
|
|
38138
|
+
} catch (err) {
|
|
38139
|
+
if (err instanceof LocalInvokeBuildError) throw err;
|
|
38140
|
+
throw new LocalInvokeBuildError(`Failed to assume role ${roleArn} for ECR pull: ${err instanceof Error ? err.message : String(err)}. Verify the role exists and its trust policy permits the caller's identity to assume it.`);
|
|
38141
|
+
} finally {
|
|
38142
|
+
sts.destroy();
|
|
38143
|
+
}
|
|
38144
|
+
}
|
|
38145
|
+
/**
|
|
38146
|
+
* Authenticate the local docker daemon against the target ECR registry.
|
|
38147
|
+
* Mirrors `DockerAssetPublisher.ecrLogin` but stays in this module so the
|
|
38148
|
+
* local-invoke path doesn't depend on the publisher's larger surface area.
|
|
38011
38149
|
*/
|
|
38012
38150
|
async function ecrLogin(client, accountId, region) {
|
|
38013
38151
|
getLogger().child("ecr-puller").debug(`ECR login (account=${accountId}, region=${region})`);
|
|
@@ -43702,7 +43840,8 @@ async function prepareOneImage(task, container, options) {
|
|
|
43702
43840
|
return image.uri;
|
|
43703
43841
|
case "ecr": return pullEcrImage(image.uri, {
|
|
43704
43842
|
skipPull: options.skipPull,
|
|
43705
|
-
...options.region !== void 0 && { region: options.region }
|
|
43843
|
+
...options.region !== void 0 && { region: options.region },
|
|
43844
|
+
...options.ecrRoleArn !== void 0 && { ecrRoleArn: options.ecrRoleArn }
|
|
43706
43845
|
});
|
|
43707
43846
|
case "cdk-asset": {
|
|
43708
43847
|
const cdkOutDir = task.stack.assetManifestPath ? dirname(task.stack.assetManifestPath) : void 0;
|
|
@@ -43956,6 +44095,7 @@ async function localRunTaskCommand(target, options) {
|
|
|
43956
44095
|
if (resolvedRoleArn) runOpts.taskRoleArn = resolvedRoleArn;
|
|
43957
44096
|
if (options.platform) runOpts.platformOverride = options.platform;
|
|
43958
44097
|
if (options.region) runOpts.region = options.region;
|
|
44098
|
+
if (options.ecrRoleArn) runOpts.ecrRoleArn = options.ecrRoleArn;
|
|
43959
44099
|
const result = await runEcsTask(task, runOpts, state);
|
|
43960
44100
|
if (options.detach) {
|
|
43961
44101
|
logger.info("Task containers started in detached mode; cdkd is exiting.");
|
|
@@ -44105,7 +44245,7 @@ function readEnvOverridesFile$1(filePath) {
|
|
|
44105
44245
|
return parsed;
|
|
44106
44246
|
}
|
|
44107
44247
|
function createLocalRunTaskCommand() {
|
|
44108
|
-
const cmd = new Command("run-task").description("Run an AWS::ECS::TaskDefinition locally — pulls/builds images, sets up a per-task docker network with the AWS-published metadata-endpoints sidecar, and starts every container in dependsOn order. Target accepts a CDK display path (MyStack/MyService/TaskDef) or stack-qualified logical ID (MyStack:MyServiceTaskDefXYZ1234). Single-stack apps may omit the stack prefix.").argument("<target>", "CDK display path or stack-qualified logical ID of the AWS::ECS::TaskDefinition to run").addOption(new Option("--cluster <name>", "Cluster name surfaced to ECS_CONTAINER_METADATA_URI_V4 and used as the docker network prefix").default("cdkd-local")).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"ContainerName\":{\"KEY\":\"VALUE\"}, \"Parameters\":{}})")).addOption(new Option("--container-host <ip>", "Host IP to bind published container ports to. Must be a numeric IP (Docker rejects hostnames here)").default("127.0.0.1")).addOption(new Option("--assume-task-role [arn]", "Assume the task definition's TaskRoleArn (or the supplied ARN) and forward STS-issued temp credentials via the metadata sidecar so containers run with the deployed function role. Bare flag uses the template's TaskRoleArn; pass an explicit ARN to override.")).addOption(new Option("--no-pull", "Skip docker pull for every container image and the metadata sidecar")).addOption(new Option("--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-state", "Read cdkd S3 state for the target stack and substitute Fn::Sub / Fn::GetAtt references to same-stack AWS::ECR::Repository resources with the deployed URI. Off by default — only the AWS pseudo-parameter tier (${AWS::AccountId} / ${AWS::Region}) is resolved without this flag.").default(false)).addOption(new Option("--stack-region <region>", "Region of the cdkd state record to read (used with --from-state when the same stack name has state in multiple regions).")).action(withErrorHandling(localRunTaskCommand));
|
|
44248
|
+
const cmd = new Command("run-task").description("Run an AWS::ECS::TaskDefinition locally — pulls/builds images, sets up a per-task docker network with the AWS-published metadata-endpoints sidecar, and starts every container in dependsOn order. Target accepts a CDK display path (MyStack/MyService/TaskDef) or stack-qualified logical ID (MyStack:MyServiceTaskDefXYZ1234). Single-stack apps may omit the stack prefix.").argument("<target>", "CDK display path or stack-qualified logical ID of the AWS::ECS::TaskDefinition to run").addOption(new Option("--cluster <name>", "Cluster name surfaced to ECS_CONTAINER_METADATA_URI_V4 and used as the docker network prefix").default("cdkd-local")).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"ContainerName\":{\"KEY\":\"VALUE\"}, \"Parameters\":{}})")).addOption(new Option("--container-host <ip>", "Host IP to bind published container ports to. Must be a numeric IP (Docker rejects hostnames here)").default("127.0.0.1")).addOption(new Option("--assume-task-role [arn]", "Assume the task definition's TaskRoleArn (or the supplied ARN) and forward STS-issued temp credentials via the metadata sidecar so containers run with the deployed function role. Bare flag uses the template's TaskRoleArn; pass an explicit ARN to override.")).addOption(new Option("--no-pull", "Skip docker pull for every container image and the metadata sidecar")).addOption(new Option("--ecr-role-arn <arn>", "Role ARN to assume before authenticating against ECR for cross-account / centralized registries (#455). 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-state", "Read cdkd S3 state for the target stack and substitute Fn::Sub / Fn::GetAtt references to same-stack AWS::ECR::Repository resources with the deployed URI. Off by default — only the AWS pseudo-parameter tier (${AWS::AccountId} / ${AWS::Region}) is resolved without this flag.").default(false)).addOption(new Option("--stack-region <region>", "Region of the cdkd state record to read (used with --from-state when the same stack name has state in multiple regions).")).action(withErrorHandling(localRunTaskCommand));
|
|
44109
44249
|
[
|
|
44110
44250
|
...commonOptions,
|
|
44111
44251
|
...appOptions,
|
|
@@ -44449,7 +44589,8 @@ async function resolveContainerImagePlan(lambda, options) {
|
|
|
44449
44589
|
logger.info(`No matching cdk.out asset for ${lambda.imageUri}; falling back to ECR pull (same-acct/region only)...`);
|
|
44450
44590
|
imageRef = await pullEcrImage(lambda.imageUri, {
|
|
44451
44591
|
skipPull: options.pull === false,
|
|
44452
|
-
...options.region !== void 0 && { region: options.region }
|
|
44592
|
+
...options.region !== void 0 && { region: options.region },
|
|
44593
|
+
...options.ecrRoleArn !== void 0 && { ecrRoleArn: options.ecrRoleArn }
|
|
44453
44594
|
});
|
|
44454
44595
|
}
|
|
44455
44596
|
const tmpfs = resolveTmpfsForLambda(lambda);
|
|
@@ -44743,7 +44884,7 @@ function pickReferencedLogicalId(intrinsic) {
|
|
|
44743
44884
|
*/
|
|
44744
44885
|
function createLocalCommand() {
|
|
44745
44886
|
const local = new Command("local").description("Local execution of Lambda functions (RIE) and ECS task definitions (Docker required)");
|
|
44746
|
-
const invoke = new Command("invoke").description("Run a Lambda function locally in a Docker container (RIE-backed). Target accepts a CDK display path (MyStack/MyApi/Handler) or stack-qualified logical ID (MyStack:MyApiHandler1234ABCD). Single-stack apps may omit the stack prefix.").argument("<target>", "CDK display path or stack-qualified logical ID of the Lambda to invoke").addOption(new Option("-e, --event <file>", "JSON event payload file (default: {})")).addOption(new Option("--event-stdin", "Read event JSON from stdin").default(false)).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"LogicalId\":{\"KEY\":\"VALUE\"}})")).addOption(new Option("--no-pull", "Skip docker pull (use cached image) — no-op for IMAGE local-build path; `docker build` does not pull base layers by default")).addOption(new Option("--no-build", "Skip docker build on the IMAGE local-build path (use the previously-built tag). Requires the deterministic tag to already be in the local registry; errors with an actionable message when missing. No-op for ZIP Lambdas and the IMAGE ECR-pull path. Compatible with --no-pull.")).addOption(new Option("--debug-port <port>", "Node --inspect-brk port (default: off)")).addOption(new Option("--container-host <host>", "Host to bind the RIE port to").default("127.0.0.1")).addOption(new Option("--assume-role [arn]", "Assume the Lambda's deployed execution role and forward STS-issued temp credentials to the container so the handler runs with the deployed function's narrow permissions (closes the \"developer admin / function narrow\" skew). Three forms: (1) `--assume-role <arn>` assumes the explicit ARN; (2) `--assume-role` (bare) auto-resolves the function's execution role ARN from cdkd state (requires --from-state); (3) `--no-assume-role` explicitly opts out (forces dev creds even with --from-state). 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("--from-state", "Read cdkd S3 state for the target stack and substitute Ref / Fn::GetAtt / Fn::Sub in env vars with the deployed physical IDs / attributes. Off by default — keep PR 1 warn-and-drop semantics; turn on for stacks already deployed via cdkd deploy.").default(false)).addOption(new Option("--stack-region <region>", "Region of the cdkd state record to read (used with --from-state when the same stack name has state in multiple regions).")).action(withErrorHandling(localInvokeCommand));
|
|
44887
|
+
const invoke = new Command("invoke").description("Run a Lambda function locally in a Docker container (RIE-backed). Target accepts a CDK display path (MyStack/MyApi/Handler) or stack-qualified logical ID (MyStack:MyApiHandler1234ABCD). Single-stack apps may omit the stack prefix.").argument("<target>", "CDK display path or stack-qualified logical ID of the Lambda to invoke").addOption(new Option("-e, --event <file>", "JSON event payload file (default: {})")).addOption(new Option("--event-stdin", "Read event JSON from stdin").default(false)).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"LogicalId\":{\"KEY\":\"VALUE\"}})")).addOption(new Option("--no-pull", "Skip docker pull (use cached image) — no-op for IMAGE local-build path; `docker build` does not pull base layers by default")).addOption(new Option("--no-build", "Skip docker build on the IMAGE local-build path (use the previously-built tag). Requires the deterministic tag to already be in the local registry; errors with an actionable message when missing. No-op for ZIP Lambdas and the IMAGE ECR-pull path. Compatible with --no-pull.")).addOption(new Option("--debug-port <port>", "Node --inspect-brk port (default: off)")).addOption(new Option("--container-host <host>", "Host to bind the RIE port to").default("127.0.0.1")).addOption(new Option("--assume-role [arn]", "Assume the Lambda's deployed execution role and forward STS-issued temp credentials to the container so the handler runs with the deployed function's narrow permissions (closes the \"developer admin / function narrow\" skew). Three forms: (1) `--assume-role <arn>` assumes the explicit ARN; (2) `--assume-role` (bare) auto-resolves the function's execution role ARN from cdkd state (requires --from-state); (3) `--no-assume-role` explicitly opts out (forces dev creds even with --from-state). 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("--ecr-role-arn <arn>", "Role ARN to assume before authenticating against ECR for cross-account / centralized registries (#455). 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-state", "Read cdkd S3 state for the target stack and substitute Ref / Fn::GetAtt / Fn::Sub in env vars with the deployed physical IDs / attributes. Off by default — keep PR 1 warn-and-drop semantics; turn on for stacks already deployed via cdkd deploy.").default(false)).addOption(new Option("--stack-region <region>", "Region of the cdkd state record to read (used with --from-state when the same stack name has state in multiple regions).")).action(withErrorHandling(localInvokeCommand));
|
|
44747
44888
|
[
|
|
44748
44889
|
...commonOptions,
|
|
44749
44890
|
...appOptions,
|
|
@@ -46031,7 +46172,7 @@ function reorderArgs(argv) {
|
|
|
46031
46172
|
*/
|
|
46032
46173
|
async function main() {
|
|
46033
46174
|
const program = new Command();
|
|
46034
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.
|
|
46175
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.118.0");
|
|
46035
46176
|
program.addCommand(createBootstrapCommand());
|
|
46036
46177
|
program.addCommand(createSynthCommand());
|
|
46037
46178
|
program.addCommand(createListCommand());
|