@go-to-k/cdkd 0.121.0 → 0.123.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -3
- package/dist/cli.js +505 -67
- package/dist/cli.js.map +1 -1
- package/dist/{deploy-engine-B2RZT3ai.js → deploy-engine-DWpeb9wT.js} +339 -20
- package/dist/deploy-engine-DWpeb9wT.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/deploy-engine-B2RZT3ai.js.map +0 -1
|
@@ -6,7 +6,7 @@ import { CloudControlClient, CreateResourceCommand, DeleteResourceCommand, GetRe
|
|
|
6
6
|
import { AttachRolePolicyCommand, CreateRoleCommand, DeleteRoleCommand, DeleteRolePermissionsBoundaryCommand, DeleteRolePolicyCommand, DetachRolePolicyCommand, GetRoleCommand, GetRolePolicyCommand, IAMClient, ListAttachedRolePoliciesCommand, ListInstanceProfilesForRoleCommand, ListRolePoliciesCommand, ListRoleTagsCommand, ListRolesCommand, NoSuchEntityException, PutRolePermissionsBoundaryCommand, PutRolePolicyCommand, RemoveRoleFromInstanceProfileCommand, TagRoleCommand, UntagRoleCommand, UpdateAssumeRolePolicyCommand, UpdateRoleCommand } from "@aws-sdk/client-iam";
|
|
7
7
|
import { PublishCommand, SNSClient } from "@aws-sdk/client-sns";
|
|
8
8
|
import { GetFunctionUrlConfigCommand, InvokeCommand, LambdaClient, waitUntilFunctionActiveV2, waitUntilFunctionUpdatedV2 } from "@aws-sdk/client-lambda";
|
|
9
|
-
import { GetCallerIdentityCommand, STSClient } from "@aws-sdk/client-sts";
|
|
9
|
+
import { AssumeRoleCommand, GetCallerIdentityCommand, STSClient } from "@aws-sdk/client-sts";
|
|
10
10
|
import { DescribeAvailabilityZonesCommand, DescribeImagesCommand, DescribeLaunchTemplatesCommand, DescribeRouteTablesCommand, DescribeSecurityGroupsCommand, DescribeSubnetsCommand, DescribeVpcsCommand, DescribeVpnGatewaysCommand, EC2Client } from "@aws-sdk/client-ec2";
|
|
11
11
|
import { DescribeTableCommand } from "@aws-sdk/client-dynamodb";
|
|
12
12
|
import { GetRestApiCommand } from "@aws-sdk/client-api-gateway";
|
|
@@ -1294,6 +1294,45 @@ async function resolveBucketRegion(bucketName, opts = {}) {
|
|
|
1294
1294
|
function clearBucketRegionCache() {
|
|
1295
1295
|
cache.clear();
|
|
1296
1296
|
}
|
|
1297
|
+
/**
|
|
1298
|
+
* Resolve the cdkd state bucket name + region for a sibling AWS account.
|
|
1299
|
+
*
|
|
1300
|
+
* Used by cross-account `Fn::GetStackOutput`: once the consumer's resolver
|
|
1301
|
+
* has assumed the producer's role, it needs to know which bucket the
|
|
1302
|
+
* producer's `cdkd deploy` wrote state to. cdkd's canonical bucket name
|
|
1303
|
+
* (since v0.7.0) is `cdkd-state-{accountId}` — region-free because S3
|
|
1304
|
+
* names are globally unique. The bucket's actual region is then looked
|
|
1305
|
+
* up via `GetBucketLocation` using the supplied (assumed) credentials.
|
|
1306
|
+
*
|
|
1307
|
+
* Why not reuse the consumer-side bucket-name resolution path: that path
|
|
1308
|
+
* supports legacy region-suffixed names (`cdkd-state-{accountId}-{region}`)
|
|
1309
|
+
* and an "empty-new-bucket" fallback, both of which require listing the
|
|
1310
|
+
* bucket contents to disambiguate. For cross-account reads we accept the
|
|
1311
|
+
* narrower scope — the producer must be on the canonical region-free
|
|
1312
|
+
* bucket layout (PR #60+, v0.10.0+) — because supporting the legacy
|
|
1313
|
+
* layout cross-account would require account-wide `s3:ListAllMyBuckets`
|
|
1314
|
+
* permission in the assumed role for no real-world benefit (legacy
|
|
1315
|
+
* accounts are nearing 5 years old; cross-account features land
|
|
1316
|
+
* post-legacy).
|
|
1317
|
+
*
|
|
1318
|
+
* @param accountId 12-digit AWS account ID of the producer (extracted
|
|
1319
|
+
* from the role ARN via `parseIamRoleArn`).
|
|
1320
|
+
* @param credentials Assumed credentials produced by
|
|
1321
|
+
* `assumeRoleForCrossAccountStateRead`. Threaded into the
|
|
1322
|
+
* `GetBucketLocation` call so the producer's bucket
|
|
1323
|
+
* policy can authorize the read against the assumed
|
|
1324
|
+
* principal (not the consumer's default credentials).
|
|
1325
|
+
*
|
|
1326
|
+
* @returns `{ bucket, region }` — the producer's canonical state bucket
|
|
1327
|
+
* name and its actual region.
|
|
1328
|
+
*/
|
|
1329
|
+
async function resolveCrossAccountStateBucket(accountId, credentials) {
|
|
1330
|
+
const bucket = `cdkd-state-${accountId}`;
|
|
1331
|
+
return {
|
|
1332
|
+
bucket,
|
|
1333
|
+
region: await resolveBucketRegion(bucket, { credentials })
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1297
1336
|
|
|
1298
1337
|
//#endregion
|
|
1299
1338
|
//#region src/synthesis/app-executor.ts
|
|
@@ -3591,6 +3630,18 @@ var S3StateBackend = class {
|
|
|
3591
3630
|
this.clientOpts = clientOpts;
|
|
3592
3631
|
}
|
|
3593
3632
|
/**
|
|
3633
|
+
* Read-only accessor for the S3 key prefix this backend writes under
|
|
3634
|
+
* (defaults to `cdkd`). Used by the cross-account `Fn::GetStackOutput`
|
|
3635
|
+
* resolver when it constructs an ephemeral state backend pointed at
|
|
3636
|
+
* the producer account's bucket — the producer's prefix should match
|
|
3637
|
+
* the consumer's prefix (both sides almost always default to `cdkd`,
|
|
3638
|
+
* but `--state-prefix` overrides at the consumer side propagate
|
|
3639
|
+
* cleanly).
|
|
3640
|
+
*/
|
|
3641
|
+
get prefix() {
|
|
3642
|
+
return this.config.prefix;
|
|
3643
|
+
}
|
|
3644
|
+
/**
|
|
3594
3645
|
* Get the new (region-scoped) S3 key for a stack's state file.
|
|
3595
3646
|
*/
|
|
3596
3647
|
getStateKey(stackName, region) {
|
|
@@ -5389,6 +5440,200 @@ var DiffCalculator = class DiffCalculator {
|
|
|
5389
5440
|
}
|
|
5390
5441
|
};
|
|
5391
5442
|
|
|
5443
|
+
//#endregion
|
|
5444
|
+
//#region src/utils/role-arn.ts
|
|
5445
|
+
/**
|
|
5446
|
+
* Process-lifetime cache of assumed credentials keyed by RoleArn.
|
|
5447
|
+
*
|
|
5448
|
+
* Storing the in-flight `Promise` (rather than the resolved value) collapses
|
|
5449
|
+
* concurrent first-time callers into a single `sts:AssumeRole` request. After
|
|
5450
|
+
* the promise resolves we keep the same entry for subsequent callers so a
|
|
5451
|
+
* stack that references the same producer N times via `Fn::GetStackOutput`
|
|
5452
|
+
* only pays the STS hop once.
|
|
5453
|
+
*
|
|
5454
|
+
* The cache is keyed by RoleArn alone (not RoleArn + region) because STS
|
|
5455
|
+
* credentials are global — assumed credentials work against any region's
|
|
5456
|
+
* service endpoint. The downstream S3 client built from these credentials
|
|
5457
|
+
* picks its own region via `GetBucketLocation`.
|
|
5458
|
+
*
|
|
5459
|
+
* **Expiration handling**: on every cache hit, the cached credentials'
|
|
5460
|
+
* `expiration` is compared against `Date.now()` with a 60-second safety
|
|
5461
|
+
* buffer to avoid the "STS expires 1-2s early due to clock skew" race.
|
|
5462
|
+
* Expired entries are evicted and a fresh AssumeRole is issued.
|
|
5463
|
+
*
|
|
5464
|
+
* **Rejection handling**: when the in-flight promise rejects (e.g.
|
|
5465
|
+
* transient STS throttle, AccessDenied), the cache entry is evicted so a
|
|
5466
|
+
* subsequent caller will retry. Without this, a single transient failure
|
|
5467
|
+
* would pin the rest of the deploy to the same error.
|
|
5468
|
+
*/
|
|
5469
|
+
const crossAccountCredentialsCache = /* @__PURE__ */ new Map();
|
|
5470
|
+
/**
|
|
5471
|
+
* Safety buffer applied when checking whether cached credentials are still
|
|
5472
|
+
* valid. STS occasionally reports `Expiration` 1-2 seconds AFTER the moment
|
|
5473
|
+
* the token actually stops working (clock skew between AWS's auth plane and
|
|
5474
|
+
* the local machine), so we evict the cache entry one minute BEFORE the
|
|
5475
|
+
* recorded expiration to keep long-running deploys safe.
|
|
5476
|
+
*/
|
|
5477
|
+
const CRED_EXPIRY_SAFETY_MS = 6e4;
|
|
5478
|
+
/**
|
|
5479
|
+
* Regex for an IAM role ARN. Accepts every published AWS partition
|
|
5480
|
+
* (`aws`, `aws-us-gov`, `aws-cn`, `aws-iso`, `aws-iso-b`, etc. — matched
|
|
5481
|
+
* loosely as `aws[a-z0-9-]*`) and any role-name shape including
|
|
5482
|
+
* service-linked roles with a `/path/` prefix
|
|
5483
|
+
* (e.g. `arn:aws:iam::111122223333:role/aws-service-role/.../AWSServiceRoleForX`).
|
|
5484
|
+
*
|
|
5485
|
+
* Capture group 1 is the partition, group 2 is the 12-digit account ID.
|
|
5486
|
+
*/
|
|
5487
|
+
const IAM_ROLE_ARN_RE = /^arn:(aws[a-z0-9-]*):iam::(\d{12}):role\/[\w+=,.@-]+(?:\/[\w+=,.@-]+)*$/;
|
|
5488
|
+
/**
|
|
5489
|
+
* Parse an IAM role ARN into its component parts.
|
|
5490
|
+
*
|
|
5491
|
+
* @param roleArn The full role ARN to parse.
|
|
5492
|
+
* @returns `{ partition, accountId }` on success, `null` on a
|
|
5493
|
+
* structurally-invalid input. The caller is responsible for
|
|
5494
|
+
* surfacing a clear error message when this returns `null`.
|
|
5495
|
+
*/
|
|
5496
|
+
function parseIamRoleArn(roleArn) {
|
|
5497
|
+
const match = IAM_ROLE_ARN_RE.exec(roleArn);
|
|
5498
|
+
if (!match || !match[1] || !match[2]) return null;
|
|
5499
|
+
return {
|
|
5500
|
+
partition: match[1],
|
|
5501
|
+
accountId: match[2]
|
|
5502
|
+
};
|
|
5503
|
+
}
|
|
5504
|
+
/**
|
|
5505
|
+
* Assume an IAM role across accounts and return temporary credentials for
|
|
5506
|
+
* reading the producer account's cdkd state bucket.
|
|
5507
|
+
*
|
|
5508
|
+
* **Why a dedicated helper (instead of reusing `applyRoleArnIfSet`).** The
|
|
5509
|
+
* `--role-arn` flag writes assumed credentials into the process's `AWS_*`
|
|
5510
|
+
* env vars so EVERY subsequent SDK client picks them up. That is the right
|
|
5511
|
+
* behavior for the CLI-wide flag, but the wrong behavior for cross-account
|
|
5512
|
+
* `Fn::GetStackOutput`: the producer's role should authorize ONLY the S3
|
|
5513
|
+
* state read, not the consumer's provisioning calls (which still run under
|
|
5514
|
+
* the consumer account's normal credentials). Threading the credentials
|
|
5515
|
+
* through a fresh `S3Client` via this helper keeps the scope narrow.
|
|
5516
|
+
*
|
|
5517
|
+
* **Why cache per-RoleArn for the process lifetime.** A multi-resource
|
|
5518
|
+
* stack typically references `Fn::GetStackOutput` from many template sites
|
|
5519
|
+
* (every IAM policy / Lambda env / ALB listener that pulls a shared VPC ID
|
|
5520
|
+
* from a platform stack). Assuming the role once per deploy is sufficient;
|
|
5521
|
+
* the cached credentials are valid for the STS session lifetime (default
|
|
5522
|
+
* 1 hour) which dwarfs the typical deploy duration.
|
|
5523
|
+
*
|
|
5524
|
+
* **Cache miss / refresh paths.** On every call we look up the cached
|
|
5525
|
+
* entry. If it exists AND its `Expiration` is still further in the
|
|
5526
|
+
* future than {@link CRED_EXPIRY_SAFETY_MS}, we return it. Otherwise the
|
|
5527
|
+
* entry is evicted and a fresh AssumeRole hop runs — important for
|
|
5528
|
+
* deploys longer than the 1-hour STS session window (multi-stack
|
|
5529
|
+
* `--all` runs, big Custom-Resource trees, etc.).
|
|
5530
|
+
*
|
|
5531
|
+
* **Rejection handling.** When the underlying STS call throws (e.g.
|
|
5532
|
+
* transient throttle, AccessDenied, trust policy mismatch), the cache
|
|
5533
|
+
* entry is evicted INSIDE the IIFE before the error propagates, so a
|
|
5534
|
+
* subsequent caller will retry the AssumeRole hop rather than getting
|
|
5535
|
+
* pinned to the same rejection. Concurrent first-time callers still
|
|
5536
|
+
* share the SAME in-flight promise (so a single failure surfaces
|
|
5537
|
+
* uniformly), but the next caller after rejection gets a clean slate.
|
|
5538
|
+
*/
|
|
5539
|
+
async function assumeRoleForCrossAccountStateRead(roleArn) {
|
|
5540
|
+
const cached = crossAccountCredentialsCache.get(roleArn);
|
|
5541
|
+
if (cached) {
|
|
5542
|
+
const cachedCreds = await cached;
|
|
5543
|
+
if (!cachedCreds.expiration || Date.now() < cachedCreds.expiration.getTime() - CRED_EXPIRY_SAFETY_MS) return cachedCreds;
|
|
5544
|
+
crossAccountCredentialsCache.delete(roleArn);
|
|
5545
|
+
}
|
|
5546
|
+
const promise = (async () => {
|
|
5547
|
+
const logger = getLogger().child("role-arn");
|
|
5548
|
+
logger.debug(`Assuming role for cross-account state read: ${roleArn}`);
|
|
5549
|
+
const sts = new STSClient({});
|
|
5550
|
+
try {
|
|
5551
|
+
let response;
|
|
5552
|
+
try {
|
|
5553
|
+
response = await sts.send(new AssumeRoleCommand({
|
|
5554
|
+
RoleArn: roleArn,
|
|
5555
|
+
RoleSessionName: `cdkd-xacc-${Date.now()}`,
|
|
5556
|
+
DurationSeconds: 3600
|
|
5557
|
+
}));
|
|
5558
|
+
} catch (err) {
|
|
5559
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
5560
|
+
throw new Error(`AssumeRole into ${roleArn} failed: ${message}. If this is a trust-policy issue, the producer's role must allow sts:AssumeRole from the consumer's principal. See docs/cross-stack-references.md for the trust-policy template.`, { cause: err instanceof Error ? err : void 0 });
|
|
5561
|
+
}
|
|
5562
|
+
if (!response.Credentials) throw new Error(`AssumeRole for cross-account Fn::GetStackOutput returned no credentials (RoleArn=${roleArn})`);
|
|
5563
|
+
const { AccessKeyId, SecretAccessKey, SessionToken, Expiration } = response.Credentials;
|
|
5564
|
+
if (!AccessKeyId || !SecretAccessKey || !SessionToken) throw new Error(`AssumeRole response missing required credentials fields for cross-account state read (RoleArn=${roleArn})`);
|
|
5565
|
+
logger.info(`Assumed role for cross-account state read: ${roleArn} (session expires ${Expiration?.toISOString() ?? "unknown"})`);
|
|
5566
|
+
return {
|
|
5567
|
+
accessKeyId: AccessKeyId,
|
|
5568
|
+
secretAccessKey: SecretAccessKey,
|
|
5569
|
+
sessionToken: SessionToken,
|
|
5570
|
+
...Expiration && { expiration: Expiration }
|
|
5571
|
+
};
|
|
5572
|
+
} finally {
|
|
5573
|
+
sts.destroy();
|
|
5574
|
+
}
|
|
5575
|
+
})().catch((err) => {
|
|
5576
|
+
if (crossAccountCredentialsCache.get(roleArn) === promise) crossAccountCredentialsCache.delete(roleArn);
|
|
5577
|
+
throw err;
|
|
5578
|
+
});
|
|
5579
|
+
crossAccountCredentialsCache.set(roleArn, promise);
|
|
5580
|
+
return promise;
|
|
5581
|
+
}
|
|
5582
|
+
/**
|
|
5583
|
+
* Resolve the role-arn argument (CLI flag or `CDKD_ROLE_ARN` env var) and,
|
|
5584
|
+
* when set, assume the role and write the resulting temporary credentials
|
|
5585
|
+
* into `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` / `AWS_SESSION_TOKEN`
|
|
5586
|
+
* for the rest of the process.
|
|
5587
|
+
*
|
|
5588
|
+
* **Why env vars, not threaded credentials.** cdkd constructs ~13
|
|
5589
|
+
* independent `AwsClients` instances across deploy / destroy / state /
|
|
5590
|
+
* import / etc. paths (each with its own region, sometimes — e.g. the
|
|
5591
|
+
* state-bucket client lives in a different region from the provisioning
|
|
5592
|
+
* clients). Threading a `credentials` object through every site is high
|
|
5593
|
+
* churn for an opt-in flag. AWS SDK v3 reads the standard `AWS_*` env
|
|
5594
|
+
* vars at the top of its default credentials chain, so writing into them
|
|
5595
|
+
* once at the command's entry makes every later `new XxxClient()` pick
|
|
5596
|
+
* up the assumed-role credentials automatically without touching the
|
|
5597
|
+
* client construction sites.
|
|
5598
|
+
*
|
|
5599
|
+
* **Why cdkd needs admin-equivalent on the assumed role.** Unlike `cdk
|
|
5600
|
+
* deploy`, cdkd does NOT route through CloudFormation. There is no
|
|
5601
|
+
* cfn-exec-role to delegate to. Every IAM / EC2 / Lambda / etc. API
|
|
5602
|
+
* call is issued from the cdkd process directly. The role you pass to
|
|
5603
|
+
* `--role-arn` (or set in `CDKD_ROLE_ARN`) MUST therefore have
|
|
5604
|
+
* admin-equivalent permissions on the resources being deployed; CDK
|
|
5605
|
+
* CLI's `cdk-hnb659fds-deploy-role-*` is NOT sufficient — that role
|
|
5606
|
+
* only carries CFn + asset-publish permissions.
|
|
5607
|
+
*
|
|
5608
|
+
* Default session duration is 1 hour. For longer-running deploys, the
|
|
5609
|
+
* caller should re-issue the cdkd command (the in-flight credentials
|
|
5610
|
+
* stay valid until expiry, but a re-run is the simplest recovery for
|
|
5611
|
+
* the rare case where a deploy outlives them).
|
|
5612
|
+
*/
|
|
5613
|
+
async function applyRoleArnIfSet(opts) {
|
|
5614
|
+
const roleArn = opts.roleArn || process.env["CDKD_ROLE_ARN"];
|
|
5615
|
+
if (!roleArn) return;
|
|
5616
|
+
const logger = getLogger().child("role-arn");
|
|
5617
|
+
logger.debug(`Assuming role ${roleArn}...`);
|
|
5618
|
+
const sts = new STSClient({ ...opts.region && { region: opts.region } });
|
|
5619
|
+
try {
|
|
5620
|
+
const response = await sts.send(new AssumeRoleCommand({
|
|
5621
|
+
RoleArn: roleArn,
|
|
5622
|
+
RoleSessionName: `cdkd-${Date.now()}`,
|
|
5623
|
+
DurationSeconds: 3600
|
|
5624
|
+
}));
|
|
5625
|
+
if (!response.Credentials) throw new Error(`AssumeRole returned no credentials for role ${roleArn}`);
|
|
5626
|
+
const { AccessKeyId, SecretAccessKey, SessionToken, Expiration } = response.Credentials;
|
|
5627
|
+
if (!AccessKeyId || !SecretAccessKey || !SessionToken) throw new Error(`AssumeRole response missing credentials fields for role ${roleArn}`);
|
|
5628
|
+
process.env["AWS_ACCESS_KEY_ID"] = AccessKeyId;
|
|
5629
|
+
process.env["AWS_SECRET_ACCESS_KEY"] = SecretAccessKey;
|
|
5630
|
+
process.env["AWS_SESSION_TOKEN"] = SessionToken;
|
|
5631
|
+
logger.info(`Assumed role ${roleArn} (session expires ${Expiration?.toISOString() ?? "unknown"})`);
|
|
5632
|
+
} finally {
|
|
5633
|
+
sts.destroy();
|
|
5634
|
+
}
|
|
5635
|
+
}
|
|
5636
|
+
|
|
5392
5637
|
//#endregion
|
|
5393
5638
|
//#region src/deployment/intrinsic-function-resolver.ts
|
|
5394
5639
|
/**
|
|
@@ -6122,7 +6367,8 @@ var IntrinsicFunctionResolver = class {
|
|
|
6122
6367
|
});
|
|
6123
6368
|
}
|
|
6124
6369
|
/**
|
|
6125
|
-
* Resolve Fn::GetStackOutput (cross-stack / cross-region
|
|
6370
|
+
* Resolve Fn::GetStackOutput (cross-stack / cross-region / cross-account
|
|
6371
|
+
* output reference).
|
|
6126
6372
|
*
|
|
6127
6373
|
* Shape: { "Fn::GetStackOutput": { "StackName": "...", "OutputName": "...",
|
|
6128
6374
|
* "Region": "...", "RoleArn": "..." } }
|
|
@@ -6133,11 +6379,20 @@ var IntrinsicFunctionResolver = class {
|
|
|
6133
6379
|
* `s3://{bucket}/cdkd/{StackName}/{Region}/state.json`. When `Region` is
|
|
6134
6380
|
* omitted, the consumer's deploy region is used.
|
|
6135
6381
|
*
|
|
6136
|
-
* RoleArn (cross-account)
|
|
6137
|
-
*
|
|
6138
|
-
*
|
|
6139
|
-
*
|
|
6140
|
-
*
|
|
6382
|
+
* **RoleArn (cross-account)**: when set, cdkd issues `sts:AssumeRole`
|
|
6383
|
+
* against the supplied role and reads the PRODUCER ACCOUNT's separate
|
|
6384
|
+
* cdkd state bucket (`cdkd-state-{producerAccountId}`) — bucket name
|
|
6385
|
+
* derived from the role ARN's account ID and the canonical
|
|
6386
|
+
* region-free bucket convention. The assumed credentials are cached
|
|
6387
|
+
* per-RoleArn for the deploy lifetime so a stack that references the
|
|
6388
|
+
* same producer multiple times only pays one STS hop. **The inline
|
|
6389
|
+
* `RoleArn` argument is constrained to literal strings only** — no
|
|
6390
|
+
* `Ref` / `Fn::GetAtt` / `Fn::Sub` chains — because the resolver
|
|
6391
|
+
* context isn't guaranteed to have the producer-account info available
|
|
6392
|
+
* at intrinsic-resolution time and a typo'd role lookup is far worse
|
|
6393
|
+
* than a clear "literal-string required" error at template-author
|
|
6394
|
+
* time. Same-account references (no RoleArn) take the original
|
|
6395
|
+
* shared-state-backend path.
|
|
6141
6396
|
*/
|
|
6142
6397
|
async resolveGetStackOutput(arg, context) {
|
|
6143
6398
|
if (!arg || typeof arg !== "object" || Array.isArray(arg)) throw new Error(`Fn::GetStackOutput: argument must be an object with StackName/OutputName/Region/RoleArn, got ${arg === null ? "null" : Array.isArray(arg) ? "array" : typeof arg}`);
|
|
@@ -6156,26 +6411,90 @@ var IntrinsicFunctionResolver = class {
|
|
|
6156
6411
|
}
|
|
6157
6412
|
let roleArn;
|
|
6158
6413
|
if ("RoleArn" in args && args["RoleArn"] !== void 0 && args["RoleArn"] !== null) {
|
|
6159
|
-
const
|
|
6160
|
-
if (typeof
|
|
6161
|
-
roleArn =
|
|
6162
|
-
}
|
|
6163
|
-
if (roleArn) throw new Error(`Fn::GetStackOutput:
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
|
|
6167
|
-
const stateData = await context.stateBackend.getState(stackName, region);
|
|
6168
|
-
if (!stateData) throw new Error(`Fn::GetStackOutput: stack '${stackName}' not found in region '${region}'. Make sure the producer stack has been deployed via cdkd.`);
|
|
6414
|
+
const raw = args["RoleArn"];
|
|
6415
|
+
if (typeof raw !== "string" || raw === "") throw new Error(`Fn::GetStackOutput: RoleArn must be a literal string in the template (no Ref / Fn::GetAtt / Fn::Sub allowed for cross-account references). Got ${raw === null ? "null" : Array.isArray(raw) ? "array" : typeof raw}${typeof raw === "object" ? ` (intrinsic shape: ${JSON.stringify(raw).slice(0, 80)})` : ""}.`);
|
|
6416
|
+
roleArn = raw;
|
|
6417
|
+
}
|
|
6418
|
+
if (!roleArn && context.stackName && context.stackName === stackName && region === this.resolverRegion) throw new Error(`Fn::GetStackOutput: cannot reference own stack '${stackName}' in the same region '${region}'`);
|
|
6419
|
+
this.logger.debug(`Resolving Fn::GetStackOutput: StackName=${stackName}, Region=${region}, OutputName=${outputName}${roleArn ? `, RoleArn=${roleArn}` : ""}`);
|
|
6420
|
+
const stateData = roleArn ? await this.getCrossAccountStackState(roleArn, stackName, region, context) : await this.getSameAccountStackState(stackName, region, context);
|
|
6421
|
+
if (!stateData) throw new Error(`Fn::GetStackOutput: stack '${stackName}' not found in region '${region}'${roleArn ? ` (cross-account via ${roleArn})` : ""}. Make sure the producer stack has been deployed via cdkd.`);
|
|
6169
6422
|
const outputs = stateData.state.outputs ?? {};
|
|
6170
6423
|
if (!(outputName in outputs)) {
|
|
6171
6424
|
const available = Object.keys(outputs).join(", ") || "(none)";
|
|
6172
6425
|
throw new Error(`Fn::GetStackOutput: output '${outputName}' not found in stack '${stackName}' (${region}). Available outputs: ${available}`);
|
|
6173
6426
|
}
|
|
6174
6427
|
const value = outputs[outputName];
|
|
6175
|
-
this.logger.info(`Resolved Fn::GetStackOutput: StackName=${stackName}, Region=${region}, OutputName=${outputName} -> ${JSON.stringify(value)}`);
|
|
6428
|
+
this.logger.info(`Resolved Fn::GetStackOutput: StackName=${stackName}, Region=${region}, OutputName=${outputName}${roleArn ? `, RoleArn=${roleArn}` : ""} -> ${JSON.stringify(value)}`);
|
|
6176
6429
|
return value;
|
|
6177
6430
|
}
|
|
6178
6431
|
/**
|
|
6432
|
+
* Read the producer's state from the SAME AWS account (no RoleArn).
|
|
6433
|
+
*
|
|
6434
|
+
* Uses the consumer's shared `context.stateBackend` — the same backend
|
|
6435
|
+
* the consumer used to read / write its own state. The same-account
|
|
6436
|
+
* path covers cross-region cleanly because the bucket name is
|
|
6437
|
+
* account-scoped (not region-scoped).
|
|
6438
|
+
*/
|
|
6439
|
+
async getSameAccountStackState(stackName, region, context) {
|
|
6440
|
+
if (!context.stateBackend) throw new Error("Fn::GetStackOutput: state backend is required for cross-stack references");
|
|
6441
|
+
return context.stateBackend.getState(stackName, region);
|
|
6442
|
+
}
|
|
6443
|
+
/**
|
|
6444
|
+
* Read the producer's state from a DIFFERENT AWS account (RoleArn set).
|
|
6445
|
+
*
|
|
6446
|
+
* Pipeline:
|
|
6447
|
+
* 1. Parse `roleArn` for the producer's account id (rejects malformed
|
|
6448
|
+
* ARNs up front with a clear message — no opaque STS error later).
|
|
6449
|
+
* 2. `sts:AssumeRole` against `roleArn`, cached per role for the
|
|
6450
|
+
* deploy lifetime (typical: 1 STS hop covering many `Fn::GetStackOutput`
|
|
6451
|
+
* sites in the same deploy).
|
|
6452
|
+
* 3. Derive the producer's canonical state bucket
|
|
6453
|
+
* (`cdkd-state-{producerAccountId}`) and auto-detect its region
|
|
6454
|
+
* via `GetBucketLocation` with the assumed credentials.
|
|
6455
|
+
* 4. Build a fresh, narrowly-scoped `S3StateBackend` against that
|
|
6456
|
+
* bucket with the assumed credentials and call `getState` —
|
|
6457
|
+
* reuses the entire state-parsing + schema-version-tolerance
|
|
6458
|
+
* machinery (legacy `version: 1` keys, migration warnings, etc.).
|
|
6459
|
+
*
|
|
6460
|
+
* The constructed `S3Client` and backend live only for the duration of
|
|
6461
|
+
* this call. cdkd does NOT mutate the process's `AWS_*` env vars (that
|
|
6462
|
+
* would leak the assumed credentials into every subsequent provisioning
|
|
6463
|
+
* client — opposite of what we want; provisioning still runs under the
|
|
6464
|
+
* consumer's normal credentials).
|
|
6465
|
+
*/
|
|
6466
|
+
async getCrossAccountStackState(roleArn, stackName, region, context) {
|
|
6467
|
+
const parsed = parseIamRoleArn(roleArn);
|
|
6468
|
+
if (!parsed) throw new Error(`Fn::GetStackOutput: RoleArn '${roleArn}' is not a valid IAM role ARN. Expected shape: arn:<partition>:iam::<12-digit-account-id>:role/<role-name> (e.g. arn:aws:iam::123456789012:role/MyRole, arn:aws-us-gov:iam::...).`);
|
|
6469
|
+
const credentials = await assumeRoleForCrossAccountStateRead(roleArn);
|
|
6470
|
+
const { bucket, region: bucketRegion } = await resolveCrossAccountStateBucket(parsed.accountId, credentials);
|
|
6471
|
+
const prefix = context.stateBackend?.prefix ?? "cdkd";
|
|
6472
|
+
return new S3StateBackend(new S3Client({
|
|
6473
|
+
region: bucketRegion,
|
|
6474
|
+
credentials: {
|
|
6475
|
+
accessKeyId: credentials.accessKeyId,
|
|
6476
|
+
secretAccessKey: credentials.secretAccessKey,
|
|
6477
|
+
sessionToken: credentials.sessionToken
|
|
6478
|
+
},
|
|
6479
|
+
logger: {
|
|
6480
|
+
debug: () => {},
|
|
6481
|
+
info: () => {},
|
|
6482
|
+
warn: () => {},
|
|
6483
|
+
error: () => {}
|
|
6484
|
+
}
|
|
6485
|
+
}), {
|
|
6486
|
+
bucket,
|
|
6487
|
+
prefix
|
|
6488
|
+
}, {
|
|
6489
|
+
region: bucketRegion,
|
|
6490
|
+
credentials: {
|
|
6491
|
+
accessKeyId: credentials.accessKeyId,
|
|
6492
|
+
secretAccessKey: credentials.secretAccessKey,
|
|
6493
|
+
sessionToken: credentials.sessionToken
|
|
6494
|
+
}
|
|
6495
|
+
}).getState(stackName, region);
|
|
6496
|
+
}
|
|
6497
|
+
/**
|
|
6179
6498
|
* Resolve Fn::FindInMap intrinsic function
|
|
6180
6499
|
*
|
|
6181
6500
|
* Fn::FindInMap: [MapName, TopLevelKey, SecondLevelKey]
|
|
@@ -9856,5 +10175,5 @@ var DeployEngine = class {
|
|
|
9856
10175
|
};
|
|
9857
10176
|
|
|
9858
10177
|
//#endregion
|
|
9859
|
-
export {
|
|
9860
|
-
//# sourceMappingURL=deploy-engine-
|
|
10178
|
+
export { DependencyError as $, AssetPublisher as A, getLegacyStateBucketName as B, applyRoleArnIfSet as C, generateResourceNameWithFallback as Ct, LockManager as D, TemplateParser as E, getDockerCmd as F, resolveStateBucketWithDefaultAndSource as G, resolveCaptureObservedState as H, runDockerForeground as I, clearBucketRegionCache as J, warnDeprecatedNoPrefixCliFlag as K, runDockerStreaming as L, WorkGraph as M, buildDockerImage as N, S3StateBackend as O, formatDockerLoginError as P, ConfigError as Q, Synthesizer as R, IntrinsicFunctionResolver as S, generateResourceName as St, DagBuilder as T, withStackName as Tt, resolveSkipPrefix as U, resolveApp as V, resolveStateBucketWithDefault as W, AssetError as X, resolveBucketRegion as Y, CdkdError as Z, normalizeAwsTagsToCfn as _, setLogger as _t, withRetry as a, ResourceUpdateNotSupportedError as at, CloudControlProvider as b, PATTERN_B_NAME_PROPERTIES as bt, cyan as c, StackTerminationProtectionError as ct, red as d, formatError as dt, LocalInvokeBuildError as et, yellow as f, isCdkdError as ft, matchesCdkPath as g, getLogger as gt, CDK_PATH_TAG as h, ConsoleLogger as ht, withResourceDeadline as i, ResourceTimeoutError as it, stringifyValue as j, shouldRetainResource as k, gray as l, StateError as lt, collectInlinePolicyNamesManagedBySiblings as m, withErrorHandling as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, PartialFailureError as nt, IMPLICIT_DELETE_DEPENDENCIES as o, RouteDiscoveryError as ot, IAMRoleProvider as p, normalizeAwsError as pt, AssemblyReader as q, DeployEngine as r, ProvisioningError as rt, bold as s, StackHasActiveImportsError as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, LockError as tt, green as u, SynthesisError as ut, resolveExplicitPhysicalId as v, runStackBuffered as vt, DiffCalculator as w, withSkipPrefix as wt, assertRegionMatch as x, PATTERN_B_RESOURCE_TYPES as xt, ProviderRegistry as y, getLiveRenderer as yt, getDefaultStateBucketName as z };
|
|
10179
|
+
//# sourceMappingURL=deploy-engine-DWpeb9wT.js.map
|