@go-to-k/cdkd 0.284.3 → 0.284.5
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/{asg-provider-iHWEv4gN.js → asg-provider-C1NtROYt.js} +2 -2
- package/dist/{asg-provider-iHWEv4gN.js.map → asg-provider-C1NtROYt.js.map} +1 -1
- package/dist/cli.js +74 -15
- package/dist/cli.js.map +1 -1
- package/dist/{deploy-engine-DJ-CgH80.js → deploy-engine-BKSfYWps.js} +439 -56
- package/dist/deploy-engine-BKSfYWps.js.map +1 -0
- package/dist/index.d.ts +11 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/deploy-engine-DJ-CgH80.js.map +0 -1
|
@@ -1045,7 +1045,7 @@ var aws_clients_exports = /* @__PURE__ */ __exportAll({
|
|
|
1045
1045
|
/**
|
|
1046
1046
|
* AWS clients manager
|
|
1047
1047
|
*/
|
|
1048
|
-
var AwsClients = class {
|
|
1048
|
+
var AwsClients = class AwsClients {
|
|
1049
1049
|
s3Client;
|
|
1050
1050
|
cloudControlClient;
|
|
1051
1051
|
iamClient;
|
|
@@ -1080,6 +1080,87 @@ var AwsClients = class {
|
|
|
1080
1080
|
};
|
|
1081
1081
|
}
|
|
1082
1082
|
/**
|
|
1083
|
+
* The region this instance was EXPLICITLY configured with, or `undefined`.
|
|
1084
|
+
*
|
|
1085
|
+
* Deliberately reads only `config.region` and consults NO environment
|
|
1086
|
+
* variable, which is the opposite of what an earlier revision did and the
|
|
1087
|
+
* difference matters (issue #1957). An env read here is not merely
|
|
1088
|
+
* incomplete, it is UNSTABLE in the one direction that is dangerous: the SDK
|
|
1089
|
+
* memoizes a region-less client's region at its first resolution
|
|
1090
|
+
* (`@smithy/node-config-provider`'s `loadConfig` wraps the provider chain in
|
|
1091
|
+
* `memoize`), while `deploy.ts`'s `switchRegion` keeps mutating
|
|
1092
|
+
* `process.env.AWS_REGION` per stack and restores it in each stack's
|
|
1093
|
+
* `finally`. So an env-derived answer can report region X for a client that
|
|
1094
|
+
* long ago pinned itself to region P — letting a caller conclude "these
|
|
1095
|
+
* clients already point at my region" and use the WRONG ones, which is worse
|
|
1096
|
+
* than not knowing.
|
|
1097
|
+
*
|
|
1098
|
+
* When this returns `undefined` the region is not merely unknown to us, it is
|
|
1099
|
+
* NOT YET DECIDED — and that is the important part. {@link clientOptions}
|
|
1100
|
+
* omits `region` entirely in that case, so each service client resolves and
|
|
1101
|
+
* MEMOIZES its own region independently, at its own first construction, from
|
|
1102
|
+
* an environment `deploy.ts`'s `switchRegion` is actively mutating. The
|
|
1103
|
+
* members of one region-less bag can therefore disagree with each other:
|
|
1104
|
+
* `ssm` can pin `us-west-2` and `secretsManager` pin `us-east-1` a moment
|
|
1105
|
+
* later, because the getters are lazy and each samples a different instant.
|
|
1106
|
+
*
|
|
1107
|
+
* So there is deliberately NO method here that reports "the region of these
|
|
1108
|
+
* clients" for an unconfigured instance. An earlier revision had one — it
|
|
1109
|
+
* asked `this.ssm.config.region()` — and it was unsound for exactly this
|
|
1110
|
+
* reason: it measured ONE member of a bag whose members need not agree, and
|
|
1111
|
+
* the caller then reused the whole bag on the strength of it. A caller that
|
|
1112
|
+
* needs a region it can rely on must build a CONFIGURED bag
|
|
1113
|
+
* ({@link withRegion} always sets `region`, so every member of a derived bag
|
|
1114
|
+
* agrees by construction).
|
|
1115
|
+
*/
|
|
1116
|
+
get configuredRegion() {
|
|
1117
|
+
return this.config.region || void 0;
|
|
1118
|
+
}
|
|
1119
|
+
/**
|
|
1120
|
+
* The CREDENTIAL half of this instance's configuration — `profile` plus any
|
|
1121
|
+
* explicitly supplied `credentials` — deliberately WITHOUT `region`.
|
|
1122
|
+
*
|
|
1123
|
+
* This is the half that must survive a region override. Note what it is and
|
|
1124
|
+
* is NOT worth: `--profile` ALSO reaches a freshly constructed client through
|
|
1125
|
+
* the environment, because `src/cli/program.ts` sets `process.env.AWS_PROFILE`
|
|
1126
|
+
* in a `preAction` hook for every command — so carrying `profile` here is
|
|
1127
|
+
* belt-and-braces rather than the thing standing between a user and the wrong
|
|
1128
|
+
* account. What genuinely has no environment path is an explicit
|
|
1129
|
+
* `credentials` object: nothing exports it, so a sibling built without it
|
|
1130
|
+
* falls back to the default chain. The same is true of any library caller
|
|
1131
|
+
* that constructs `AwsClients` directly and therefore never runs the CLI's
|
|
1132
|
+
* `preAction` hook. `--role-arn` needs nothing carried at all —
|
|
1133
|
+
* `applyRoleArnIfSet` exports `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` /
|
|
1134
|
+
* `AWS_SESSION_TOKEN` into the process environment.
|
|
1135
|
+
*
|
|
1136
|
+
* The `credentials` object is CLONED rather than aliased: the returned bag is
|
|
1137
|
+
* handed to every derived sibling, and sharing one mutable object would let a
|
|
1138
|
+
* caller reach through it and rewrite the ambient instance's credentials.
|
|
1139
|
+
*/
|
|
1140
|
+
get credentialConfig() {
|
|
1141
|
+
return {
|
|
1142
|
+
...this.config.profile && { profile: this.config.profile },
|
|
1143
|
+
...this.config.credentials && { credentials: { ...this.config.credentials } }
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
/**
|
|
1147
|
+
* Derive a sibling bound to `region`, carrying this instance's credential
|
|
1148
|
+
* configuration (see {@link credentialConfig}) and overriding ONLY the region.
|
|
1149
|
+
*
|
|
1150
|
+
* Used by {@link IntrinsicFunctionResolver} to pin a dynamic-reference lookup
|
|
1151
|
+
* (`{{resolve:secretsmanager:...}}` / `{{resolve:ssm:...}}`) to the stack's own
|
|
1152
|
+
* region instead of whichever region the process-global singleton happens to
|
|
1153
|
+
* hold at that moment (issue #1957). The returned instance owns its own lazily
|
|
1154
|
+
* constructed clients and its own `destroy()`; it is NOT registered as the
|
|
1155
|
+
* global, so nothing else in the process can observe it.
|
|
1156
|
+
*/
|
|
1157
|
+
withRegion(region) {
|
|
1158
|
+
return new AwsClients({
|
|
1159
|
+
...this.credentialConfig,
|
|
1160
|
+
region
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
/**
|
|
1083
1164
|
* Get S3 client
|
|
1084
1165
|
*
|
|
1085
1166
|
* Note: If region and credentials are not provided, AWS SDK will use:
|
|
@@ -10578,6 +10659,29 @@ function maskSecretsInText(text, secrets) {
|
|
|
10578
10659
|
if (!regex) return text;
|
|
10579
10660
|
return text.replace(regex, "***");
|
|
10580
10661
|
}
|
|
10662
|
+
/**
|
|
10663
|
+
* Bind a {@link RecordedSecretValues} bag into a {@link SecretMasker} for a
|
|
10664
|
+
* caller to hand to a provider.
|
|
10665
|
+
*
|
|
10666
|
+
* The bag is captured BY REFERENCE and read on every call, and there is
|
|
10667
|
+
* deliberately NO `secrets.size === 0` short-circuit here: collapsing an empty
|
|
10668
|
+
* bag to the identity function at BIND time would go permanently blind to
|
|
10669
|
+
* everything added afterwards. {@link maskSecretsInText} makes that check at
|
|
10670
|
+
* CALL time, where it is correct and costs a `Map.size` read.
|
|
10671
|
+
*
|
|
10672
|
+
* Stated as a property rather than a live requirement, because it is worth
|
|
10673
|
+
* being exact about: every caller today FILLS its bag before binding — the
|
|
10674
|
+
* rollback executor's arms run `resolveReplayProps` first and only then build
|
|
10675
|
+
* the masker, and the deploy engine resolves before it calls the provider — so
|
|
10676
|
+
* a bind-time short-circuit would pass every existing integration. It is the
|
|
10677
|
+
* ORDER, not the reference capture, that makes them work now, and the order is
|
|
10678
|
+
* the kind of thing a later refactor reverses without noticing. The unit test
|
|
10679
|
+
* `masks values added to the bag AFTER the masker was built` is what holds the
|
|
10680
|
+
* property up on its own.
|
|
10681
|
+
*/
|
|
10682
|
+
function createSecretMasker(secrets) {
|
|
10683
|
+
return (text) => maskSecretsInText(text, secrets);
|
|
10684
|
+
}
|
|
10581
10685
|
|
|
10582
10686
|
//#endregion
|
|
10583
10687
|
//#region src/provisioning/config-shape.ts
|
|
@@ -12283,6 +12387,44 @@ const MAX_DYNAMIC_REFERENCE_THROTTLE_RETRIES = 4;
|
|
|
12283
12387
|
* without real waits (mirrors `describeTypeRetryDelays`).
|
|
12284
12388
|
*/
|
|
12285
12389
|
const dynamicReferenceRetryDelays = {};
|
|
12390
|
+
/**
|
|
12391
|
+
* Is `region` safe to build an AWS SDK client from?
|
|
12392
|
+
*
|
|
12393
|
+
* This is a SECURITY gate, not an AWS region registry, and the distinction
|
|
12394
|
+
* decides how strict it is. The SDK turns a region into a hostname by
|
|
12395
|
+
* substitution — `https://ssm.{region}.amazonaws.com` — so a value carrying a
|
|
12396
|
+
* host delimiter escapes the label and re-points the endpoint: the measured
|
|
12397
|
+
* case is `evil.example.com#`, which yields
|
|
12398
|
+
* `https://ssm.evil.example.com/#.amazonaws.com` and sends a SigV4-SIGNED
|
|
12399
|
+
* request (access key id + signature) to an attacker-controlled host.
|
|
12400
|
+
*
|
|
12401
|
+
* The reachable input is `Fn::GetAZs`, whose argument is TEMPLATE-DERIVED and
|
|
12402
|
+
* can arrive through an `Fn::ImportValue` or a parameter — i.e. it is not
|
|
12403
|
+
* necessarily written by whoever runs the deploy. Before issue #1957 that value
|
|
12404
|
+
* only fed the `region-name` FILTER of a `DescribeAvailabilityZones` call and
|
|
12405
|
+
* never built a client, so binding lookups to a region is exactly what made it
|
|
12406
|
+
* reachable; the gate ships with the binding.
|
|
12407
|
+
*
|
|
12408
|
+
* So the predicate is CHARSET-based rather than shape-based: lowercase
|
|
12409
|
+
* alphanumerics and hyphens only, which cannot express `.`, `/`, `:`, `@`, `?`
|
|
12410
|
+
* or `#` and therefore cannot leave the hostname label. It deliberately does
|
|
12411
|
+
* NOT try to enumerate real regions — AWS keeps adding them
|
|
12412
|
+
* (`ap-southeast-7`, `il-central-1`, `mx-central-1`, `eusc-de-east-1`), and a
|
|
12413
|
+
* pattern tight enough to reject `----` would also reject the next one. A
|
|
12414
|
+
* region-shaped-but-nonexistent value is not a security problem: it resolves to
|
|
12415
|
+
* a hostname that does not exist and the SDK fails loudly.
|
|
12416
|
+
*
|
|
12417
|
+
* Note the sibling pattern in `src/cli/commands/state-file-keys.ts` is NOT
|
|
12418
|
+
* reusable here: it requires `^[a-z]{2}(-[a-z]+)+-\d+$`, which rejects
|
|
12419
|
+
* `eusc-de-east-1` (the European Sovereign Cloud partition's four-letter
|
|
12420
|
+
* prefix).
|
|
12421
|
+
*
|
|
12422
|
+
* Callers must {@link canonicalizeRegion} first — `US-EAST-1` is a documented
|
|
12423
|
+
* input and is lowercase-canonical, not invalid.
|
|
12424
|
+
*/
|
|
12425
|
+
function isClientSafeRegion(region) {
|
|
12426
|
+
return /^[a-z0-9][a-z0-9-]{0,30}$/.test(region);
|
|
12427
|
+
}
|
|
12286
12428
|
/** Test seam for {@link FABRICATED_ACCOUNT_INFO_TTL_MS} expiry. */
|
|
12287
12429
|
const accountInfoClock = { now: () => Date.now() };
|
|
12288
12430
|
let fabricatedAccountIdentity = null;
|
|
@@ -12421,6 +12563,63 @@ function stringifyParameterForLog(paramDef, value) {
|
|
|
12421
12563
|
var IntrinsicFunctionResolver = class {
|
|
12422
12564
|
logger = getLogger().child("IntrinsicFunctionResolver");
|
|
12423
12565
|
resolverRegion;
|
|
12566
|
+
/**
|
|
12567
|
+
* The region the CONSTRUCTOR was given, or `undefined` when it was called
|
|
12568
|
+
* without one — unlike {@link resolverRegion}, which substitutes
|
|
12569
|
+
* `AWS_REGION` / `us-east-1` so every consumer has a string to work with.
|
|
12570
|
+
*
|
|
12571
|
+
* The distinction is load-bearing for {@link clientsForRegion} and for
|
|
12572
|
+
* nothing else: re-pointing an AWS lookup away from the ambient clients is
|
|
12573
|
+
* only safe when a caller SAID which region this resolver stands for.
|
|
12574
|
+
*
|
|
12575
|
+
* IN PRODUCTION THIS IS ALWAYS SET, and saying so matters more than the
|
|
12576
|
+
* guard it enables. Every construction site defaults the region BEFORE the
|
|
12577
|
+
* constructor and passes a `string` — `deploy.ts`, `scrub.ts`, `drift.ts`,
|
|
12578
|
+
* `import.ts`, `export.ts`, `diff-recursive.ts`, `rollback-executor.ts` —
|
|
12579
|
+
* so the `undefined` arm is reachable only through the no-argument
|
|
12580
|
+
* constructor, which nothing but tests uses. It is kept because the
|
|
12581
|
+
* parameter is optional and the arm must therefore exist, not because a
|
|
12582
|
+
* shipped path depends on it.
|
|
12583
|
+
*
|
|
12584
|
+
* That has a USER-VISIBLE consequence, deliberately accepted (issue #1957
|
|
12585
|
+
* review). For a REGION-AGNOSTIC stack (no `env.region`) run with neither
|
|
12586
|
+
* `--region` nor `AWS_REGION`, the region those callers compute is the
|
|
12587
|
+
* hard-coded `us-east-1` fallback, so the lookup now goes there — where
|
|
12588
|
+
* before it followed the ambient clients to whatever `~/.aws/config` said.
|
|
12589
|
+
* The new behaviour is the consistent one: `us-east-1` is already the region
|
|
12590
|
+
* cdkd keys that stack's state file, its lock and its export index under, so
|
|
12591
|
+
* the resolved value and the record that stores it now agree. Previously
|
|
12592
|
+
* they did not, which is the same class of defect this issue is about, one
|
|
12593
|
+
* layer up. A stack WITH an explicit `env.region` is unaffected — every
|
|
12594
|
+
* caller prefers it (`scrub.ts` does `stack.region || region`).
|
|
12595
|
+
*/
|
|
12596
|
+
explicitRegion;
|
|
12597
|
+
/**
|
|
12598
|
+
* AWS clients pinned to a region OTHER than the ambient singleton's, built
|
|
12599
|
+
* lazily on first mismatch and keyed by region (issue #1957).
|
|
12600
|
+
*
|
|
12601
|
+
* Lifetime is deliberately the resolver's own, matching {@link cfnClients}
|
|
12602
|
+
* two fields down: both are per-region SDK clients this instance builds for
|
|
12603
|
+
* itself, and neither is destroyed, because `IntrinsicFunctionResolver` has
|
|
12604
|
+
* no teardown hook and every construction site (`DeployEngine`, `scrub`,
|
|
12605
|
+
* `drift`, `import`, `diff-recursive`, `export`, `rollback-executor`) would
|
|
12606
|
+
* have to grow one. The bound on what that costs is small and worth stating:
|
|
12607
|
+
* an entry exists only when a resolver's region DIFFERS from the ambient
|
|
12608
|
+
* one — i.e. only on a genuinely cross-region run — and at most one per
|
|
12609
|
+
* foreign region per resolver, versus the ambient clients which are already
|
|
12610
|
+
* created and destroyed per stack by `deploy.ts`.
|
|
12611
|
+
*
|
|
12612
|
+
* KEYED BY REGION ALONE, which means an entry also pins the CREDENTIAL
|
|
12613
|
+
* configuration of whichever ambient instance was current at the first
|
|
12614
|
+
* mismatch for that region. That is sound today because the credential half
|
|
12615
|
+
* is process-wide rather than per-stack: `--profile` comes from one CLI
|
|
12616
|
+
* option and `--role-arn` lands in `process.env`, so every ambient instance
|
|
12617
|
+
* a run installs carries the same one. Widen the key to include
|
|
12618
|
+
* {@link AwsClients.credentialConfig} the moment that stops being true —
|
|
12619
|
+
* per-stack credentials would otherwise let one stack's lookups run under
|
|
12620
|
+
* another's identity, which is a worse bug than the one this cache serves.
|
|
12621
|
+
*/
|
|
12622
|
+
regionScopedClients = /* @__PURE__ */ new Map();
|
|
12424
12623
|
strictGetAtt;
|
|
12425
12624
|
cfnFallback;
|
|
12426
12625
|
/**
|
|
@@ -12511,24 +12710,17 @@ var IntrinsicFunctionResolver = class {
|
|
|
12511
12710
|
* nested stacks ship or that pass starts recording secrets; a resolver per
|
|
12512
12711
|
* node is the fix then, not a wider key here.
|
|
12513
12712
|
*
|
|
12514
|
-
*
|
|
12515
|
-
*
|
|
12516
|
-
*
|
|
12517
|
-
* (
|
|
12518
|
-
*
|
|
12519
|
-
*
|
|
12520
|
-
*
|
|
12521
|
-
*
|
|
12522
|
-
*
|
|
12523
|
-
*
|
|
12524
|
-
*
|
|
12525
|
-
* stacks in several regions. The split of ownership is therefore: THIS field
|
|
12526
|
-
* closes the cache as a cross-region / cross-stack carrier, while #1957 owns
|
|
12527
|
-
* the wrong-region READ — which is the outcome #1933's title names, so #1933
|
|
12528
|
-
* is not fully resolved until #1957 lands. Pinning the lookup to
|
|
12529
|
-
* {@link resolverRegion} means constructing region-scoped clients, which is a
|
|
12530
|
-
* credentials decision (a bare `new SSMClient({ region })` drops the ambient
|
|
12531
|
-
* profile / assume-role config) and belongs to #1957 rather than here.
|
|
12713
|
+
* The OTHER half of the same outcome — the lookups themselves reading the
|
|
12714
|
+
* process-ambient `getAwsClients()` singleton, whose region is whichever the
|
|
12715
|
+
* process installed last — was issue
|
|
12716
|
+
* [#1957](https://github.com/go-to-k/cdkd/issues/1957) and is now closed by
|
|
12717
|
+
* {@link clientsForRegion}: a resolver whose region differs from the ambient
|
|
12718
|
+
* one builds its own region-pinned clients (carrying the ambient profile /
|
|
12719
|
+
* credentials) instead of reading whatever the singleton currently holds.
|
|
12720
|
+
* The two halves remain SEPARATE mechanisms and both are needed — this field
|
|
12721
|
+
* stops a resolved value from travelling between regions or stacks, while
|
|
12722
|
+
* the scoped clients stop the FIRST resolution from reading the wrong region
|
|
12723
|
+
* (no cache involved, so nothing here could ever have prevented it).
|
|
12532
12724
|
*/
|
|
12533
12725
|
cachedDynamicReferences = /* @__PURE__ */ new Map();
|
|
12534
12726
|
/**
|
|
@@ -12554,6 +12746,7 @@ var IntrinsicFunctionResolver = class {
|
|
|
12554
12746
|
warnedUnrecognizedSsmTypes = /* @__PURE__ */ new Set();
|
|
12555
12747
|
constructor(region, options) {
|
|
12556
12748
|
this.resolverRegion = region || process.env["AWS_REGION"] || "us-east-1";
|
|
12749
|
+
this.explicitRegion = region || void 0;
|
|
12557
12750
|
this.strictGetAtt = options?.strictGetAtt ?? false;
|
|
12558
12751
|
this.cfnFallback = options?.cfnFallback ?? true;
|
|
12559
12752
|
}
|
|
@@ -12566,6 +12759,133 @@ var IntrinsicFunctionResolver = class {
|
|
|
12566
12759
|
this.physicalIdFallbackCount = 0;
|
|
12567
12760
|
}
|
|
12568
12761
|
/**
|
|
12762
|
+
* AWS clients for a REGION-SENSITIVE lookup, pinned to `targetRegion`
|
|
12763
|
+
* (issue [#1957](https://github.com/go-to-k/cdkd/issues/1957)).
|
|
12764
|
+
*
|
|
12765
|
+
* Every lookup in this class used to read `getAwsClients()` — the
|
|
12766
|
+
* PROCESS-GLOBAL singleton, whose region is whichever one the process
|
|
12767
|
+
* installed last. That is not the same thing as the region this resolver
|
|
12768
|
+
* stands for, and the gap is reachable on main:
|
|
12769
|
+
*
|
|
12770
|
+
* - `cdkd deploy` defaults to `--stack-concurrency 4` and re-points the
|
|
12771
|
+
* singleton per stack, so two stacks in different regions race for one
|
|
12772
|
+
* mutable global and stack B's `GetSecretValue` / `GetParameter` can run
|
|
12773
|
+
* against stack A's client. The resolved value is redacted on its way into
|
|
12774
|
+
* state, so nothing downstream records which region answered.
|
|
12775
|
+
* - `cdkd scrub --all` installs the clients ONCE while resolving per-stack
|
|
12776
|
+
* regions, so a region-B `SecureString` whose region-A namesake is a plain
|
|
12777
|
+
* `String` is classified PUBLIC and left in PLAINTEXT in state.json — the
|
|
12778
|
+
* same disclosure class as GHSA-p5qg-v9gv-hc7w, not merely a wrong value.
|
|
12779
|
+
* - `cdkd drift --revert` WRITES the resolved value to a live resource, so
|
|
12780
|
+
* there the wrong region is a wrong write rather than a wrong report.
|
|
12781
|
+
*
|
|
12782
|
+
* Fixing it here rather than at the ~10 `setAwsClients` call sites is what
|
|
12783
|
+
* makes it one mechanism instead of a per-command patch: this class already
|
|
12784
|
+
* knows its own region, and every construction site already passes the
|
|
12785
|
+
* per-stack one.
|
|
12786
|
+
*
|
|
12787
|
+
* REUSING THE AMBIENT CLIENTS REQUIRES PROOF THAT THEY ALREADY POINT AT
|
|
12788
|
+
* `targetRegion`, and the direction of that test is the whole correctness
|
|
12789
|
+
* argument. CloudFormation semantics say a stack's dynamic references resolve
|
|
12790
|
+
* in the STACK's region, and every construction site passes exactly that — so
|
|
12791
|
+
* once a region has been named, sending the lookup there is not an
|
|
12792
|
+
* optimisation to be justified, it is the requirement. Whether the ambient
|
|
12793
|
+
* singleton happens to agree only decides whether an object allocation can be
|
|
12794
|
+
* skipped.
|
|
12795
|
+
*
|
|
12796
|
+
* An earlier revision had this backwards twice over, and both failures are
|
|
12797
|
+
* worth naming because each looks reasonable in isolation.
|
|
12798
|
+
*
|
|
12799
|
+
* It first declined to override whenever the ambient region was UNKNOWN,
|
|
12800
|
+
* reasoning that overriding on an unproven mismatch might re-point a lookup
|
|
12801
|
+
* that works today. That fails OPEN, on the COMMON configuration: `aws
|
|
12802
|
+
* configure` writes the region to `~/.aws/config`, and `cdkd scrub` sets a
|
|
12803
|
+
* client region only when `--region` is passed. The disclosure this issue
|
|
12804
|
+
* exists to close stayed reachable — profile region `us-east-1`, stack B in
|
|
12805
|
+
* `ap-northeast-1`, a name that is `String` in A and `SecureString` in B,
|
|
12806
|
+
* `cdkd scrub --all` with no flags: B's reference answered by A, classified
|
|
12807
|
+
* public, plaintext left in `state.json`.
|
|
12808
|
+
*
|
|
12809
|
+
* It then determined the ambient region by reading `process.env` here, which
|
|
12810
|
+
* is worse than not knowing: the SDK memoizes a region-less client's region
|
|
12811
|
+
* at its first resolution while `deploy.ts`'s `switchRegion` keeps mutating
|
|
12812
|
+
* `AWS_REGION` per stack and restores it in each stack's `finally`, so the
|
|
12813
|
+
* environment could say `baseRegion` for a client long since pinned
|
|
12814
|
+
* elsewhere — and this method would conclude MATCH and hand back clients
|
|
12815
|
+
* pointing somewhere else.
|
|
12816
|
+
*
|
|
12817
|
+
* The fix for THAT was to ask the SDK (`ssm.config.region()`), and it was
|
|
12818
|
+
* still wrong, in a way worth writing down because it looks airtight. An
|
|
12819
|
+
* unconfigured `AwsClients` is not a bag of clients, it is a bag of DEFERRED
|
|
12820
|
+
* client constructions: `clientOptions` omits `region`, the getters are lazy,
|
|
12821
|
+
* and each member therefore samples the mutating environment at its own
|
|
12822
|
+
* instant and memoizes a possibly DIFFERENT region. Asking `ssm` measures one
|
|
12823
|
+
* member and says nothing about `secretsManager`, so the seam could short-
|
|
12824
|
+
* circuit on a us-west-2 `ssm` and then hand out a bag whose `secretsManager`
|
|
12825
|
+
* pins us-east-1 a moment later — issue #1957's Site 1 surviving inside the
|
|
12826
|
+
* arm meant to fix it.
|
|
12827
|
+
*
|
|
12828
|
+
* So the short-circuit is taken ONLY when the ambient's region is
|
|
12829
|
+
* CONFIGURED. That is not a heuristic: a configured bag passes `region` to
|
|
12830
|
+
* every member ({@link AwsClients.clientOptions}), so its members agree by
|
|
12831
|
+
* construction, and {@link AwsClients.withRegion} always sets one, so every
|
|
12832
|
+
* derived bag is internally consistent too. An unconfigured ambient is not
|
|
12833
|
+
* "of unknown region", it is "of not-yet-decided region", and there is
|
|
12834
|
+
* nothing to compare against — so it SCOPES. That is the same "unknown means
|
|
12835
|
+
* SCOPE, not skip" rule as above, applied one level deeper.
|
|
12836
|
+
*
|
|
12837
|
+
* Three arms return the ambient instance, each for a reason that is not
|
|
12838
|
+
* "we could not prove a mismatch":
|
|
12839
|
+
*
|
|
12840
|
+
* 1. No `targetRegion` — no region was ever named (see
|
|
12841
|
+
* {@link explicitRegion}), so there is nothing to bind to.
|
|
12842
|
+
* 2. `targetRegion` is not safe to build a client from (see
|
|
12843
|
+
* {@link isClientSafeRegion}) — which THROWS. An earlier revision warned
|
|
12844
|
+
* and fell back to the ambient clients, reasoning that a malformed region
|
|
12845
|
+
* reaching here is a cdkd bug and failing every lookup would turn it into
|
|
12846
|
+
* an outage. That put this arm on the wrong side of the two-severity
|
|
12847
|
+
* design: falling back to the ambient means READING ANOTHER REGION, which
|
|
12848
|
+
* for `scrub` / `drift` / `import` — whose region is state-derived — is
|
|
12849
|
+
* the disclosure this issue exists to close (a region-B `SecureString`
|
|
12850
|
+
* classified against a region-A `String`). A stopped command is strictly
|
|
12851
|
+
* better than a silent wrong-region read. The `Fn::GetAZs` entry still
|
|
12852
|
+
* validates EARLIER so it can give a message naming the template
|
|
12853
|
+
* construct; this arm is the backstop that guarantees no call site,
|
|
12854
|
+
* present or future, routes unvalidated input into an SDK endpoint.
|
|
12855
|
+
* 3. The installed clients cannot DERIVE a sibling — `withRegion` is absent.
|
|
12856
|
+
* In production that never happens: `getAwsClients()` returns an
|
|
12857
|
+
* `AwsClients`. It is true only of a test double, and it is checked
|
|
12858
|
+
* EXPLICITLY rather than left to emerge, for a reason the review of this
|
|
12859
|
+
* change made concrete. The ~260 suites that stub `getAwsClients()` with a
|
|
12860
|
+
* plain object used to stay on the ambient path as a side effect of the
|
|
12861
|
+
* `undefined`-region guard above — the very guard that made the disclosure
|
|
12862
|
+
* reachable. Removing that guard without putting something deliberate in
|
|
12863
|
+
* its place would have traded a security hole for ~260 `TypeError`s, so
|
|
12864
|
+
* the test-double case is now its own named arm and the security arm no
|
|
12865
|
+
* longer has a testing job to do. Suites that are ABOUT region scoping use
|
|
12866
|
+
* a real `AwsClients` and are unaffected by it.
|
|
12867
|
+
*
|
|
12868
|
+
* Regions are canonicalised on both sides before comparing, because
|
|
12869
|
+
* `--region US-EAST-1` is a documented input and the repo lowercases
|
|
12870
|
+
* elsewhere (`canonicalizeRegion`, issues #1795 / #1850). Without it an
|
|
12871
|
+
* uppercase spelling would build a second client for the same physical
|
|
12872
|
+
* region — benign, but wasteful and confusing in a debug log.
|
|
12873
|
+
*/
|
|
12874
|
+
clientsForRegion(targetRegion) {
|
|
12875
|
+
const ambient = getAwsClients();
|
|
12876
|
+
if (!targetRegion) return ambient;
|
|
12877
|
+
const target = canonicalizeRegion(targetRegion);
|
|
12878
|
+
if (!isClientSafeRegion(target)) throw new Error(`Refusing to build AWS clients for the region '${stripControlChars(target).slice(0, 64)}': it is not a valid AWS region name, and a region is substituted into the AWS service hostname.`);
|
|
12879
|
+
if (typeof ambient.withRegion !== "function") return ambient;
|
|
12880
|
+
const cached = this.regionScopedClients.get(target);
|
|
12881
|
+
if (cached) return cached;
|
|
12882
|
+
if (canonicalizeRegion(ambient.configuredRegion) === target) return ambient;
|
|
12883
|
+
const scoped = ambient.withRegion(target);
|
|
12884
|
+
this.regionScopedClients.set(target, scoped);
|
|
12885
|
+
this.logger.debug(`Using region-scoped AWS clients for ${target}`);
|
|
12886
|
+
return scoped;
|
|
12887
|
+
}
|
|
12888
|
+
/**
|
|
12569
12889
|
* Resolve parameter values from template Parameters section
|
|
12570
12890
|
*
|
|
12571
12891
|
* Merges default values from template with user-provided parameter values.
|
|
@@ -12617,7 +12937,7 @@ var IntrinsicFunctionResolver = class {
|
|
|
12617
12937
|
* Used for parameters with type AWS::SSM::Parameter::Value<...>.
|
|
12618
12938
|
*/
|
|
12619
12939
|
async resolveSSMParameter(parameterName) {
|
|
12620
|
-
return (await
|
|
12940
|
+
return (await this.clientsForRegion(this.explicitRegion).ssm.send(new GetParameterCommand({ Name: parameterName }))).Parameter?.Value ?? "";
|
|
12621
12941
|
}
|
|
12622
12942
|
/**
|
|
12623
12943
|
* Coerce parameter value to the correct type based on parameter definition
|
|
@@ -13190,7 +13510,7 @@ var IntrinsicFunctionResolver = class {
|
|
|
13190
13510
|
const cached = cachedEc2InstanceAttributes[cacheKey];
|
|
13191
13511
|
if (cached !== void 0) return cached;
|
|
13192
13512
|
try {
|
|
13193
|
-
const instance = (await
|
|
13513
|
+
const instance = (await this.clientsForRegion(this.explicitRegion).ec2.send(new DescribeInstancesCommand({ InstanceIds: [physicalId] }))).Reservations?.[0]?.Instances?.[0];
|
|
13194
13514
|
let value;
|
|
13195
13515
|
switch (attributeName) {
|
|
13196
13516
|
case "PrivateIp":
|
|
@@ -13224,7 +13544,7 @@ var IntrinsicFunctionResolver = class {
|
|
|
13224
13544
|
if (resourceType === "AWS::EC2::LaunchTemplate") {
|
|
13225
13545
|
if (attributeName === "LatestVersionNumber" || attributeName === "DefaultVersionNumber") {
|
|
13226
13546
|
try {
|
|
13227
|
-
const lt = (await
|
|
13547
|
+
const lt = (await this.clientsForRegion(this.explicitRegion).ec2.send(new DescribeLaunchTemplatesCommand({ LaunchTemplateIds: [physicalId] }))).LaunchTemplates?.[0];
|
|
13228
13548
|
const value = attributeName === "LatestVersionNumber" ? lt?.LatestVersionNumber : lt?.DefaultVersionNumber;
|
|
13229
13549
|
if (value !== void 0 && value !== null) return String(value);
|
|
13230
13550
|
} catch (err) {
|
|
@@ -13873,7 +14193,9 @@ var IntrinsicFunctionResolver = class {
|
|
|
13873
14193
|
if ("Region" in args && args["Region"] !== void 0 && args["Region"] !== null) {
|
|
13874
14194
|
const resolvedRegion = await this.resolveValue(args["Region"], context);
|
|
13875
14195
|
if (typeof resolvedRegion !== "string" || resolvedRegion === "") throw new Error(`Fn::GetStackOutput: Region must resolve to a non-empty string, got ${typeof resolvedRegion}`);
|
|
13876
|
-
|
|
14196
|
+
const requestedRegion = canonicalizeRegion(resolvedRegion);
|
|
14197
|
+
if (!isClientSafeRegion(requestedRegion)) throw new Error(`Fn::GetStackOutput: '${stripControlChars(resolvedRegion).slice(0, 64)}' is not a valid AWS region name. The region selects both the AWS endpoint and the state-file key, so cdkd will not use it.`);
|
|
14198
|
+
region = requestedRegion;
|
|
13877
14199
|
}
|
|
13878
14200
|
let roleArn;
|
|
13879
14201
|
if ("RoleArn" in args && args["RoleArn"] !== void 0 && args["RoleArn"] !== null) {
|
|
@@ -14056,28 +14378,52 @@ var IntrinsicFunctionResolver = class {
|
|
|
14056
14378
|
async resolveGetAZs(value, context) {
|
|
14057
14379
|
const resolvedValue = await this.resolveValue(value, context);
|
|
14058
14380
|
let region;
|
|
14059
|
-
|
|
14060
|
-
|
|
14381
|
+
/**
|
|
14382
|
+
* Which region's clients answer the `DescribeAvailabilityZones` below.
|
|
14383
|
+
*
|
|
14384
|
+
* `DescribeAvailabilityZones` lists the AZs of the region the CLIENT is
|
|
14385
|
+
* pointed at; the `region-name` filter narrows that listing, it does not
|
|
14386
|
+
* widen it to another region. So a foreign-region client returns an EMPTY
|
|
14387
|
+
* list, which this method then caches and hands back as the resolved value
|
|
14388
|
+
* of `Fn::GetAZs` — silently, since an empty list is not an error here
|
|
14389
|
+
* (issue #1957).
|
|
14390
|
+
*
|
|
14391
|
+
* When the template names a region explicitly, THAT is the region to talk
|
|
14392
|
+
* to. Otherwise fall back to the resolver's own — but only when it was
|
|
14393
|
+
* given explicitly (see {@link explicitRegion}).
|
|
14394
|
+
*/
|
|
14395
|
+
let clientRegion;
|
|
14396
|
+
if (typeof resolvedValue === "string" && resolvedValue !== "") {
|
|
14397
|
+
const requested = canonicalizeRegion(resolvedValue);
|
|
14398
|
+
if (!isClientSafeRegion(requested)) throw new Error(`Fn::GetAZs: '${stripControlChars(resolvedValue).slice(0, 64)}' is not a valid AWS region name. A region is substituted into the AWS service hostname, so cdkd will not build a client from it.`);
|
|
14399
|
+
region = requested;
|
|
14400
|
+
clientRegion = requested;
|
|
14401
|
+
} else {
|
|
14402
|
+
region = (await getAccountInfo(this.resolverRegion)).region;
|
|
14403
|
+
clientRegion = this.explicitRegion;
|
|
14404
|
+
}
|
|
14061
14405
|
const cached = cachedAvailabilityZones[region];
|
|
14062
14406
|
if (cached) {
|
|
14063
14407
|
this.logger.debug(`Resolved Fn::GetAZs from cache: ${region} -> ${JSON.stringify(cached)}`);
|
|
14064
14408
|
return cached;
|
|
14065
14409
|
}
|
|
14066
|
-
const ec2Client =
|
|
14410
|
+
const ec2Client = this.clientsForRegion(clientRegion).ec2;
|
|
14411
|
+
let azNames;
|
|
14067
14412
|
try {
|
|
14068
|
-
|
|
14413
|
+
azNames = ((await ec2Client.send(new DescribeAvailabilityZonesCommand({ Filters: [{
|
|
14069
14414
|
Name: "region-name",
|
|
14070
14415
|
Values: [region]
|
|
14071
14416
|
}, {
|
|
14072
14417
|
Name: "state",
|
|
14073
14418
|
Values: ["available"]
|
|
14074
14419
|
}] }))).AvailabilityZones || []).map((az) => az.ZoneName).filter((name) => name !== void 0).sort();
|
|
14075
|
-
cachedAvailabilityZones[region] = azNames;
|
|
14076
|
-
this.logger.debug(`Resolved Fn::GetAZs: ${region} -> ${JSON.stringify(azNames)}`);
|
|
14077
|
-
return azNames;
|
|
14078
14420
|
} catch (error) {
|
|
14079
14421
|
throw new Error(`Fn::GetAZs: failed to describe availability zones for region '${region}': ${error instanceof Error ? error.message : String(error)}`);
|
|
14080
14422
|
}
|
|
14423
|
+
if (azNames.length === 0) throw new Error(`Fn::GetAZs: no availability zones returned for region '${region}'. Either the region is not enabled on this account (opt-in regions must be enabled before use), or the request was answered by a different region's endpoint.`);
|
|
14424
|
+
cachedAvailabilityZones[region] = azNames;
|
|
14425
|
+
this.logger.debug(`Resolved Fn::GetAZs: ${region} -> ${JSON.stringify(azNames)}`);
|
|
14426
|
+
return azNames;
|
|
14081
14427
|
}
|
|
14082
14428
|
/**
|
|
14083
14429
|
* Resolve pseudo parameters
|
|
@@ -14226,7 +14572,7 @@ var IntrinsicFunctionResolver = class {
|
|
|
14226
14572
|
if (!versionStage) versionStage = "AWSCURRENT";
|
|
14227
14573
|
if (!secretId) throw new Error("Dynamic reference: secretsmanager SECRET_ID is required");
|
|
14228
14574
|
this.logger.debug(`Resolving dynamic reference: secretsmanager:${secretId}:SecretString:${jsonKey}:${versionStage}:${versionId}`);
|
|
14229
|
-
const client =
|
|
14575
|
+
const client = this.clientsForRegion(this.explicitRegion).secretsManager;
|
|
14230
14576
|
const command = new GetSecretValueCommand({
|
|
14231
14577
|
SecretId: secretId,
|
|
14232
14578
|
...versionStage && versionStage !== "" && { VersionStage: versionStage },
|
|
@@ -14354,10 +14700,13 @@ var IntrinsicFunctionResolver = class {
|
|
|
14354
14700
|
* - It says NOTHING about concurrency. The client is captured before the
|
|
14355
14701
|
* first attempt, so a sibling stack's teardown (`stackAwsClients.destroy()`
|
|
14356
14702
|
* in `deploy.ts`) during a backoff surfaces as a raw, non-throttle-shaped
|
|
14357
|
-
* failure on the next attempt
|
|
14358
|
-
*
|
|
14359
|
-
*
|
|
14360
|
-
*
|
|
14703
|
+
* failure on the next attempt, and the retry does not make that safe. Issue
|
|
14704
|
+
* [#1957](https://github.com/go-to-k/cdkd/issues/1957) NARROWED this rather
|
|
14705
|
+
* than removing it: a lookup whose region differs from the ambient one now
|
|
14706
|
+
* runs on {@link clientsForRegion}'s own clients, which no sibling stack can
|
|
14707
|
+
* destroy because nothing else in the process holds a reference to them. A
|
|
14708
|
+
* sibling in the SAME region still shares the ambient instance, so the
|
|
14709
|
+
* window survives exactly where the two stacks agree on the region.
|
|
14361
14710
|
*/
|
|
14362
14711
|
sendWithThrottleRetry(operation, label) {
|
|
14363
14712
|
return withRetry(operation, label, {
|
|
@@ -14386,7 +14735,7 @@ var IntrinsicFunctionResolver = class {
|
|
|
14386
14735
|
const parameterName = parts.slice(1).join(":");
|
|
14387
14736
|
if (!parameterName) throw new Error("Dynamic reference: ssm PARAMETER_NAME is required");
|
|
14388
14737
|
this.logger.debug(`Resolving dynamic reference: ssm:${parameterName}`);
|
|
14389
|
-
const client =
|
|
14738
|
+
const client = this.clientsForRegion(this.explicitRegion).ssm;
|
|
14390
14739
|
const command = new GetParameterCommand({
|
|
14391
14740
|
Name: parameterName,
|
|
14392
14741
|
WithDecryption: decrypt
|
|
@@ -15269,7 +15618,7 @@ var CloudControlProvider = class {
|
|
|
15269
15618
|
if (context?.finalSnapshotIdentifier !== void 0) throw new ProvisioningError(`${logicalId} (${resourceType}) requires a final snapshot (DeletionPolicy: Snapshot), but the Cloud Control API delete route has no final-snapshot parameter. Re-run with --skip-final-snapshot after snapshotting manually, or retain the resource.`, resourceType, logicalId, physicalId);
|
|
15270
15619
|
if (context?.removeProtection === true && resourceType === "AWS::AutoScaling::AutoScalingGroup") {
|
|
15271
15620
|
this.logger.debug(`Delegating protected AutoScalingGroup ${logicalId} delete to the SDK ASGProvider (Cloud Control cannot force-delete a protected ASG)`);
|
|
15272
|
-
const { ASGProvider } = await import("./asg-provider-
|
|
15621
|
+
const { ASGProvider } = await import("./asg-provider-C1NtROYt.js").then((n) => n.n);
|
|
15273
15622
|
return await new ASGProvider().delete(logicalId, physicalId, resourceType, _properties, context);
|
|
15274
15623
|
}
|
|
15275
15624
|
const isProtectedEc2Instance = context?.removeProtection === true && resourceType === "AWS::EC2::Instance";
|
|
@@ -21875,21 +22224,52 @@ const SKIP_FINAL_SNAPSHOT_FLAG = "--skip-final-snapshot";
|
|
|
21875
22224
|
* engine's five sites (CREATE, the property-driven replacement, the
|
|
21876
22225
|
* `--recreate-via-*` destroy-then-create, the `--replace` delete-first
|
|
21877
22226
|
* fallback, and the update-failure replacement) are all driven by freshly
|
|
21878
|
-
* resolved TEMPLATE properties, so they
|
|
21879
|
-
*
|
|
22227
|
+
* resolved TEMPLATE properties, so they never set THIS FLAG and the refusal
|
|
22228
|
+
* stands where the user can edit the input. They DO pass a context — since
|
|
22229
|
+
* issue #1932 every create site REACHED FROM THE ENGINE carries a
|
|
22230
|
+
* `maskSecrets` capability — so the invariant is "no `replayingState`", not
|
|
22231
|
+
* "no context object". (A provider that re-creates inside its own `update()`
|
|
22232
|
+
* still passes none; see `CreateContext`.)
|
|
21880
22233
|
*
|
|
21881
22234
|
* The remaining call sites are the providers that re-create inside their own
|
|
21882
22235
|
* `update()` (`this.create(...)` in ACM certificate / IAM managed policy / IAM
|
|
21883
22236
|
* role / Lambda permission / SNS subscription). Those are NOT template-driven
|
|
21884
22237
|
* — this executor's `revert` arm calls `provider.update(...)` with
|
|
21885
22238
|
* `previousState.properties`, so they forward a STATE record on a replay — but
|
|
21886
|
-
* they CANNOT receive a
|
|
22239
|
+
* they CANNOT receive a `CreateContext`: `update()`'s own context is an
|
|
22240
|
+
* `UpdateContext`, which carries no `replayingState` to forward.
|
|
21887
22241
|
* The constraint that follows is on providers, not on this constant: a
|
|
21888
22242
|
* provider with a create-side pre-flight refusal must not re-create inside
|
|
21889
22243
|
* `update()`. See `CreateContext` in `src/types/resource.ts`.
|
|
21890
22244
|
*/
|
|
21891
22245
|
const REPLAYING_STATE_CREATE_CONTEXT = { replayingState: true };
|
|
21892
22246
|
/**
|
|
22247
|
+
* The rollback arms' {@link CreateContext}, with this op's secret masker bound
|
|
22248
|
+
* in (issue #1932 item 3).
|
|
22249
|
+
*
|
|
22250
|
+
* The rollback path needs this MORE than the forward deploy does, not less:
|
|
22251
|
+
* {@link resolveReplayProps} deliberately re-resolves every redacted
|
|
22252
|
+
* `{{resolve:...}}` expression back to plaintext before handing the bag to a
|
|
22253
|
+
* provider, so a replayed bag is guaranteed to carry the concrete secret
|
|
22254
|
+
* whenever the resource has one. Leaving the masker off here would have left
|
|
22255
|
+
* the contract applied at one caller and absent at the one whose bag is
|
|
22256
|
+
* provably plaintext.
|
|
22257
|
+
*
|
|
22258
|
+
* Spreads the shared constant rather than mutating it: `maskSecrets` is
|
|
22259
|
+
* per-op, and a module-level object is shared by every op in the run.
|
|
22260
|
+
*
|
|
22261
|
+
* Called AFTER `resolveReplayProps` has filled `secrets` at every call site, so
|
|
22262
|
+
* the masker sees this op's re-resolved values. `createSecretMasker` reads the
|
|
22263
|
+
* bag by reference on every call and so does not depend on that ordering, but
|
|
22264
|
+
* the ordering is what makes it correct here without relying on that.
|
|
22265
|
+
*/
|
|
22266
|
+
function replayingStateCreateContext(secrets) {
|
|
22267
|
+
return {
|
|
22268
|
+
...REPLAYING_STATE_CREATE_CONTEXT,
|
|
22269
|
+
maskSecrets: createSecretMasker(secrets)
|
|
22270
|
+
};
|
|
22271
|
+
}
|
|
22272
|
+
/**
|
|
21893
22273
|
* Which provisioning layer a delete must be judged against: the CURRENT
|
|
21894
22274
|
* state record wins (it is what state says AWS holds right now), with the
|
|
21895
22275
|
* journaled op's routing as the legacy-state fallback. Shared by both
|
|
@@ -22444,7 +22824,7 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
|
|
|
22444
22824
|
let deletedNewFirst = false;
|
|
22445
22825
|
let createResult;
|
|
22446
22826
|
try {
|
|
22447
|
-
createResult = await withRetry(() => createProvider.create(op.logicalId, op.resourceType, { ...resolvedPrevProps },
|
|
22827
|
+
createResult = await withRetry(() => createProvider.create(op.logicalId, op.resourceType, { ...resolvedPrevProps }, replayingStateCreateContext(secrets)), op.logicalId, {
|
|
22448
22828
|
...RECREATE_RETRY_SCHEDULE,
|
|
22449
22829
|
logger,
|
|
22450
22830
|
...isInterrupted && {
|
|
@@ -22467,7 +22847,7 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
|
|
|
22467
22847
|
delete stateResources[op.logicalId];
|
|
22468
22848
|
await afterOp?.(op.logicalId);
|
|
22469
22849
|
try {
|
|
22470
|
-
createResult = await withRetry(() => createProvider.create(op.logicalId, op.resourceType, { ...resolvedPrevProps },
|
|
22850
|
+
createResult = await withRetry(() => createProvider.create(op.logicalId, op.resourceType, { ...resolvedPrevProps }, replayingStateCreateContext(secrets)), op.logicalId, {
|
|
22471
22851
|
...RECREATE_RETRY_SCHEDULE,
|
|
22472
22852
|
logger,
|
|
22473
22853
|
...isInterrupted && {
|
|
@@ -22540,7 +22920,8 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
|
|
|
22540
22920
|
current.physicalId,
|
|
22541
22921
|
op.resourceType,
|
|
22542
22922
|
desiredProps ?? {},
|
|
22543
|
-
currentProps ?? {}
|
|
22923
|
+
currentProps ?? {},
|
|
22924
|
+
{ maskSecrets: createSecretMasker(secrets) }
|
|
22544
22925
|
], op.logicalId, logger, isInterrupted);
|
|
22545
22926
|
stateResources[op.logicalId] = redactRollbackRecord(recordAfterRollbackUpdate(previousState, revertResult), secrets, previousState.properties);
|
|
22546
22927
|
const rollbackPartial = updatePartialReason(revertResult);
|
|
@@ -22683,7 +23064,8 @@ async function replayFailedOperations(failedOps, stateResources, stackName, ctx,
|
|
|
22683
23064
|
current.physicalId,
|
|
22684
23065
|
op.resourceType,
|
|
22685
23066
|
desiredProps ?? {},
|
|
22686
|
-
attemptedProps ?? {}
|
|
23067
|
+
attemptedProps ?? {},
|
|
23068
|
+
{ maskSecrets: createSecretMasker(secrets) }
|
|
22687
23069
|
], op.logicalId, logger, options.isInterrupted);
|
|
22688
23070
|
stateResources[op.logicalId] = redactRollbackRecord(recordAfterRollbackUpdate(prev, revertFailedResult), secrets, prev.properties);
|
|
22689
23071
|
const revertFailedPartial = updatePartialReason(revertFailedResult);
|
|
@@ -22814,7 +23196,7 @@ const FLUSH_INTERVAL_MS = 2e3;
|
|
|
22814
23196
|
const FLUSH_EVENT_THRESHOLD = 50;
|
|
22815
23197
|
/** Build-time cdkd version, with a dev fallback for non-built contexts. */
|
|
22816
23198
|
function getCdkdVersion() {
|
|
22817
|
-
return "0.284.
|
|
23199
|
+
return "0.284.5";
|
|
22818
23200
|
}
|
|
22819
23201
|
/**
|
|
22820
23202
|
* Generate a time-sortable unique run id, e.g.
|
|
@@ -24582,7 +24964,7 @@ var DeployEngine = class {
|
|
|
24582
24964
|
* #960 follow-up) and the name-idempotent same-id guard (issue #1238) so
|
|
24583
24965
|
* the two --replace escape hatches cannot drift apart.
|
|
24584
24966
|
*/
|
|
24585
|
-
async replaceDeleteFirstAndRecreate(logicalId, resourceType, currentResource, oldDeleteProvider, replaceProvider, replaceProps, updateReplacePolicy) {
|
|
24967
|
+
async replaceDeleteFirstAndRecreate(logicalId, resourceType, currentResource, oldDeleteProvider, replaceProvider, replaceProps, createContext, updateReplacePolicy) {
|
|
24586
24968
|
const finalSnapshotIdentifier = await this.prepareFinalSnapshotForDelete(logicalId, resourceType, currentResource, updateReplacePolicy);
|
|
24587
24969
|
let deleteResult;
|
|
24588
24970
|
try {
|
|
@@ -24599,7 +24981,7 @@ var DeployEngine = class {
|
|
|
24599
24981
|
this.logger.info(` ${green("✓")} Old resource deleted`);
|
|
24600
24982
|
this.logger.info(` Re-creating ${logicalId}...`);
|
|
24601
24983
|
try {
|
|
24602
|
-
return await withRetry(() => this.withRetry(() => replaceProvider.create(logicalId, resourceType, replaceProps), logicalId, void 0, void 0, replaceProvider), logicalId, {
|
|
24984
|
+
return await withRetry(() => this.withRetry(() => replaceProvider.create(logicalId, resourceType, replaceProps, createContext), logicalId, void 0, void 0, replaceProvider), logicalId, {
|
|
24603
24985
|
maxRetries: 8,
|
|
24604
24986
|
initialDelayMs: 2e3,
|
|
24605
24987
|
maxDelayMs: 1e4,
|
|
@@ -24633,6 +25015,7 @@ var DeployEngine = class {
|
|
|
24633
25015
|
}, stackName);
|
|
24634
25016
|
const resolvedProps = await this.resolver.resolve(desiredProps, context);
|
|
24635
25017
|
this.perResourceTemplateProps.set(logicalId, desiredProps);
|
|
25018
|
+
const createSecrets = context.recordedSecretValues ?? /* @__PURE__ */ new Map();
|
|
24636
25019
|
if (context.recordedSecretValues) this.perResourceSecrets.set(logicalId, context.recordedSecretValues);
|
|
24637
25020
|
this.auditResolvedAssetReferences(logicalId, resourceType, resolvedProps);
|
|
24638
25021
|
this.attemptedResolvedProps.set(logicalId, resolvedProps);
|
|
@@ -24642,7 +25025,7 @@ var DeployEngine = class {
|
|
|
24642
25025
|
});
|
|
24643
25026
|
const createProvider = createDecision.provider;
|
|
24644
25027
|
const createProps = createDecision.provisionedBy === "cc-api" ? this.preparePropertiesForCcApi(resourceType, resolvedProps, logicalId) : resolvedProps;
|
|
24645
|
-
const result = await this.withRetry(() => createProvider.create(logicalId, resourceType, createProps), logicalId, void 0, void 0, createProvider);
|
|
25028
|
+
const result = await this.withRetry(() => createProvider.create(logicalId, resourceType, createProps, { maskSecrets: createSecretMasker(createSecrets) }), logicalId, void 0, void 0, createProvider);
|
|
24646
25029
|
const dependencies = this.extractAllDependencies(template, logicalId);
|
|
24647
25030
|
const templateAttrs = this.extractTemplateAttributes(template, logicalId);
|
|
24648
25031
|
stateResources[logicalId] = {
|
|
@@ -24753,7 +25136,7 @@ var DeployEngine = class {
|
|
|
24753
25136
|
this.logger.info(` ${green("✓")} Old resource deleted`);
|
|
24754
25137
|
}
|
|
24755
25138
|
this.logger.info(` Creating new ${logicalId}...`);
|
|
24756
|
-
createResult = await withRetry(() => this.withRetry(() => replaceProvider.create(logicalId, resourceType, replaceProps), logicalId, void 0, void 0, replaceProvider), logicalId, {
|
|
25139
|
+
createResult = await withRetry(() => this.withRetry(() => replaceProvider.create(logicalId, resourceType, replaceProps, { maskSecrets: createSecretMasker(updateSecrets) }), logicalId, void 0, void 0, replaceProvider), logicalId, {
|
|
24757
25140
|
maxRetries: 8,
|
|
24758
25141
|
initialDelayMs: 2e3,
|
|
24759
25142
|
maxDelayMs: 1e4,
|
|
@@ -24767,7 +25150,7 @@ var DeployEngine = class {
|
|
|
24767
25150
|
this.logger.info(` Creating new ${logicalId}...`);
|
|
24768
25151
|
let deletedOldFirst = false;
|
|
24769
25152
|
try {
|
|
24770
|
-
createResult = await this.withRetry(() => replaceProvider.create(logicalId, resourceType, replaceProps), logicalId, void 0, void 0, replaceProvider);
|
|
25153
|
+
createResult = await this.withRetry(() => replaceProvider.create(logicalId, resourceType, replaceProps, { maskSecrets: createSecretMasker(updateSecrets) }), logicalId, void 0, void 0, replaceProvider);
|
|
24771
25154
|
} catch (createError) {
|
|
24772
25155
|
const createMsg = createError instanceof Error ? createError.message : String(createError);
|
|
24773
25156
|
if (!isNameCollisionError(createMsg)) throw createError;
|
|
@@ -24776,7 +25159,7 @@ var DeployEngine = class {
|
|
|
24776
25159
|
if (this.options.replace !== true) throw new CdkdError(`${logicalId} (${resourceType}) requires replacement, but the create-first attempt collided with the existing resource: ${createMsg}. ${nameOrigin.descriptor}, so the CloudFormation-style safe replacement order (create the new resource before deleting the old) cannot reuse the occupied name — CloudFormation refuses this shape with "cannot update a stack when a custom-named resource requires replacing". ${nameOrigin.remedy}, or re-run with \`cdkd deploy --replace\` to delete the old resource FIRST and recreate it under the same name (the resource is briefly unavailable while it is recreated).`, "NAMED_REPLACEMENT_COLLISION");
|
|
24777
25160
|
this.logger.info(` Create-first collided with the existing resource's name and --replace is set — deleting old ${logicalId} (${currentResource.physicalId}) first...`);
|
|
24778
25161
|
deletedOldFirst = true;
|
|
24779
|
-
createResult = await this.replaceDeleteFirstAndRecreate(logicalId, resourceType, currentResource, oldDeleteProvider, replaceProvider, replaceProps, updateReplacePolicy);
|
|
25162
|
+
createResult = await this.replaceDeleteFirstAndRecreate(logicalId, resourceType, currentResource, oldDeleteProvider, replaceProvider, replaceProps, { maskSecrets: createSecretMasker(updateSecrets) }, updateReplacePolicy);
|
|
24780
25163
|
}
|
|
24781
25164
|
if (!deletedOldFirst && createResult.physicalId === currentResource.physicalId) {
|
|
24782
25165
|
const idempotentNameOrigin = this.replacementNameOrigin(logicalId, currentResource.physicalId);
|
|
@@ -24784,7 +25167,7 @@ var DeployEngine = class {
|
|
|
24784
25167
|
if (this.options.replace !== true) throw new CdkdError(`${logicalId} (${resourceType}) requires replacement, but its Create API is name-idempotent: the create-first attempt returned the EXISTING resource (${currentResource.physicalId}) instead of creating a new one, so deleting the "old" resource would silently destroy the resource the deploy just reported as created. ${idempotentNameOrigin.descriptor}; ${idempotentNameOrigin.remedy}, or re-run with \`cdkd deploy --replace\` to delete the old resource FIRST and recreate it under the same name (the resource is briefly unavailable while it is recreated). Note: this branch is also reached when the old resource was deleted out-of-band and the physical id is name-derived — there the create was a genuine fresh create; \`--replace\` converges that case too.`, "NAMED_REPLACEMENT_IDEMPOTENT_CREATE");
|
|
24785
25168
|
this.logger.info(` Create-first returned the existing resource (name-idempotent Create API) and --replace is set — deleting old ${logicalId} (${currentResource.physicalId}) first...`);
|
|
24786
25169
|
deletedOldFirst = true;
|
|
24787
|
-
createResult = await this.replaceDeleteFirstAndRecreate(logicalId, resourceType, currentResource, oldDeleteProvider, replaceProvider, replaceProps, updateReplacePolicy);
|
|
25170
|
+
createResult = await this.replaceDeleteFirstAndRecreate(logicalId, resourceType, currentResource, oldDeleteProvider, replaceProvider, replaceProps, { maskSecrets: createSecretMasker(updateSecrets) }, updateReplacePolicy);
|
|
24788
25171
|
}
|
|
24789
25172
|
if (deletedOldFirst) {} else if (updateReplacePolicy === "Retain") this.logger.info(` Retaining old ${logicalId} (${currentResource.physicalId}) - UpdateReplacePolicy: Retain`);
|
|
24790
25173
|
else {
|
|
@@ -24844,7 +25227,7 @@ var DeployEngine = class {
|
|
|
24844
25227
|
let result;
|
|
24845
25228
|
let resultProvisionedBy = updateDecision.provisionedBy;
|
|
24846
25229
|
try {
|
|
24847
|
-
result = await this.withRetry(() => updateProvider.update(logicalId, currentResource.physicalId, resourceType, updateProps, currentProps), logicalId, void 0, void 0, updateProvider);
|
|
25230
|
+
result = await this.withRetry(() => updateProvider.update(logicalId, currentResource.physicalId, resourceType, updateProps, currentProps, { maskSecrets: createSecretMasker(updateSecrets) }), logicalId, void 0, void 0, updateProvider);
|
|
24848
25231
|
} catch (updateError) {
|
|
24849
25232
|
const msg = updateError instanceof Error ? updateError.message : String(updateError);
|
|
24850
25233
|
const ccUnsupported = msg.includes("UnsupportedActionException") || msg.includes("does not support UPDATE");
|
|
@@ -24876,7 +25259,7 @@ var DeployEngine = class {
|
|
|
24876
25259
|
});
|
|
24877
25260
|
const replProvider = replDecision.provider;
|
|
24878
25261
|
const replProps = replDecision.provisionedBy === "cc-api" ? this.preparePropertiesForCcApi(resourceType, resolvedProps, logicalId) : resolvedProps;
|
|
24879
|
-
const createResult = await this.withRetry(() => replProvider.create(logicalId, resourceType, replProps), logicalId, void 0, void 0, replProvider);
|
|
25262
|
+
const createResult = await this.withRetry(() => replProvider.create(logicalId, resourceType, replProps, { maskSecrets: createSecretMasker(updateSecrets) }), logicalId, void 0, void 0, replProvider);
|
|
24880
25263
|
const replacementResult = {
|
|
24881
25264
|
physicalId: createResult.physicalId,
|
|
24882
25265
|
wasReplaced: true,
|
|
@@ -25281,5 +25664,5 @@ var DeployEngine = class {
|
|
|
25281
25664
|
};
|
|
25282
25665
|
|
|
25283
25666
|
//#endregion
|
|
25284
|
-
export { IntrinsicFunctionResolver as $,
|
|
25285
|
-
//# sourceMappingURL=deploy-engine-
|
|
25667
|
+
export { IntrinsicFunctionResolver as $, StackHasActiveImportsError as $n, validateContainerRepoName as $t, formatResourceLine as A, AssemblyReader as An, describeTypeWithThrottleRetry as At, isExportAliasCollision as B, ConfigError as Bn, WorkGraph as Bt, refusesFinalSnapshot as C, MIGRATE_TMP_PREFIX as Cn, s3BucketDomainName as Ct, MULTI_REGION_RECREATE_BLOCKED_TYPES as D, PARTITION_TABLE as Dn, applyRoleArnIfSet as Dt, extractDeploymentEventError as E, expectedOwnerParam as En, s3BucketWebsiteUrl as Et, red as F, getAwsClients as Fn, S3StateBackend as Ft, clearOnUpdateRemoval as G, LocalStartServiceError as Gn, escapeRegExp$1 as Gt, stateKeySecretExposure as H, DeployCancelledError as Hn, createAssetRedirectResolver as Ht, yellow as I, resetAwsClients as In, rebuildClientForBucketRegion as It, findSilentDropProperties as J, NestedStackChildDirectDestroyError as Jn, BOOTSTRAP_MARKER_PREFIX as Jt, ProviderRegistry as K, LockError as Kn, stripControlChars as Kt, collectDeclaredOutputNames as L, setAwsClients as Ln, shouldRetainResource as Lt, cyan as M, clearBucketRegionCache as Mn, DagBuilder as Mt, gray as N, resolveBucketRegion as Nn, TemplateParser as Nt, isStatefulRecreateTargetSync as O, canonicalizeRegion as On, DiffCalculator as Ot, green as P, AwsClients as Pn, LockManager as Pt, isTerminationProtectionPropagationError as Q, ResourceUpdateNotSupportedError as Qn, validateAssetBucketName as Qt, collectPublishedOutputNames as R, AssetError as Rn, AssetPublisher as Rt, isFinalSnapshotError as S, CFN_TEMPLATE_URL_LIMIT as Sn, s3BucketArn as St, makeCanonicalizePropertiesFn as T, uploadCfnTemplate as Tn, s3BucketRegionalDomainName as Tt, IAMRoleProvider as U, LocalInvokeBuildError as Un, loadPublishableAssetManifest as Ut, secretBearingStateKeyWarning as V, DependencyError as Vn, buildAssetRedirectMap as Vt, collectInlinePolicyNamesManagedBySiblings as W, LocalMigrateError as Wn, rewriteTemplateAssetReferences as Wt, slowCcOperationTimeoutMs as X, ProvisioningError as Xn, getBootstrapMarkerKey as Xt, CloudControlProvider as Y, PartialFailureError as Yn, ensureAssetStorage as Yt, disableInstanceApiTermination as Z, ResourceTimeoutError as Zn, parseBootstrapMarker as Zt, ATOMIC_FINAL_SNAPSHOT_TYPES as _, resolveStateBucketWithDefaultAndSource as _n, TEMPLATE_SOURCED_RULES as _t, DeploymentEventsStore as a, runDockerStreaming as an, normalizeAwsError as ar, resolveExplicitPhysicalId as at, ccRoutedFinalSnapshotError as b, warnDeprecatedNoPrefixCliFlag as bn, redactSecretsForState as bt, replayFailedOperations as c, Synthesizer as cn, isRetryableTransientError as cr, configBooleanRefusal as ct, updatePartialReason as d, getLegacyStateBucketName as dn, __exportAll as dr, replayWarn as dt, buildDenyExternalAccessPolicy as en, StackTerminationProtectionError as er, cfnRefValueFromPhysicalId as et, UNSPECIFIED_SKIP_REASON as f, resolveApp as fn, requireConfigArray as ft, computeImplicitDeleteEdges as g, resolveStateBucketWithDefault as gn, STATE_SOURCED_READBACK_RULES as gt, IMPLICIT_DELETE_DEPENDENCIES as h, resolveSkipPrefix as hn, STATE_SOURCED_CROSS_GENERATION_RULES as ht, DeploymentEventsReader as i, runDockerForeground as in, isCdkdError as ir, normalizeAwsTagsToCfn as it, bold as j, processStackMessages as jn, withRetry as jt, renderStatefulReason as k, derivePartitionAndUrlSuffix as kn, INTRINSIC_KEYS as kt, replayRollback as l, synthesisStatusMessage as ln, isThrottlingError as lr, configStringRefusal as lt, withResourceDeadline as m, resolveCaptureObservedState as mn, requireConfigString as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, formatDockerLoginError as nn, SynthesisError as nr, refStateLookupFromResource as nt, planFailedOps as o, AssetManifestLoader as on, withErrorHandling as or, assertRegionMatch as ot, deleteSkipReason as p, resolveAutoAssetStorage as pn, requireConfigObject as pt, findActionableSilentDrops as q, MissingCdkCliError as qn, AssetModeResolver as qt, DeployEngine as r, getDockerCmd as rn, formatError as rr, WAFv2WebACLProvider as rt, planRollback as s, getDockerImageBySourceHash as sn, isMarkedNonRetryable as sr, coerceCfnBoolean as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, buildDockerImage as tn, StateError as tr, getAccountInfo as tt, updatePartialMessage as u, getDefaultStateBucketName as un, markNonRetryable as ur, readConfigString as ut, PRE_DELETE_SNAPSHOT_TYPES as v, resolveUseCdkBootstrapAssets as vn, createSecretMasker as vt, unsupportedFinalSnapshotError as w, findLargeInlineResources as wn, s3BucketDualStackDomainName as wt, createPreDeleteFinalSnapshot as x, CFN_TEMPLATE_BODY_LIMIT as xn, scrubResourceRecord as xt, buildFinalSnapshotIdentifier as y, stateBucketExistenceConfirmed as yn, maskSecretsInText as yt, exportAliasCollisionScrubWarning as z, CdkdError as zn, stringifyValue as zt };
|
|
25668
|
+
//# sourceMappingURL=deploy-engine-BKSfYWps.js.map
|