@go-to-k/cdkd 0.284.22 → 0.284.24
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-DLU0EEhO.js → asg-provider-Bh0odlSk.js} +2 -2
- package/dist/{asg-provider-DLU0EEhO.js.map → asg-provider-Bh0odlSk.js.map} +1 -1
- package/dist/cli.js +86 -15
- package/dist/cli.js.map +1 -1
- package/dist/{deploy-engine-DOlhIeGv.js → deploy-engine-BJQbni1s.js} +632 -58
- package/dist/{deploy-engine-DOlhIeGv.js.map → deploy-engine-BJQbni1s.js.map} +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
|
@@ -16860,7 +16860,7 @@ var CloudControlProvider = class {
|
|
|
16860
16860
|
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);
|
|
16861
16861
|
if (context?.removeProtection === true && resourceType === "AWS::AutoScaling::AutoScalingGroup") {
|
|
16862
16862
|
this.logger.debug(`Delegating protected AutoScalingGroup ${logicalId} delete to the SDK ASGProvider (Cloud Control cannot force-delete a protected ASG)`);
|
|
16863
|
-
const { ASGProvider } = await import("./asg-provider-
|
|
16863
|
+
const { ASGProvider } = await import("./asg-provider-Bh0odlSk.js").then((n) => n.n);
|
|
16864
16864
|
return await new ASGProvider().delete(logicalId, physicalId, resourceType, _properties, context);
|
|
16865
16865
|
}
|
|
16866
16866
|
const isProtectedEc2Instance = context?.removeProtection === true && resourceType === "AWS::EC2::Instance";
|
|
@@ -17575,6 +17575,47 @@ const CR_NO_SERVICE_TOKEN_SKIP_REASON = "no ServiceToken in state — Delete han
|
|
|
17575
17575
|
*/
|
|
17576
17576
|
const CR_DELETE_INVOKE_FAILED_SKIP_REASON = "Delete request to the handler did not complete — resource unproven";
|
|
17577
17577
|
/**
|
|
17578
|
+
* Fourth sibling of the three above, for the arm where the handler RAN, was
|
|
17579
|
+
* reached, and answered `Status: 'FAILED'` (issue
|
|
17580
|
+
* [#2054](https://github.com/go-to-k/cdkd/issues/2054)).
|
|
17581
|
+
*
|
|
17582
|
+
* The terminal FAILED arm used to warn and fall through to `return undefined`,
|
|
17583
|
+
* which `deleteSkipReason` reads as DELETED — so cdkd dropped the state
|
|
17584
|
+
* record, printed the row as deleted and exited 0 over a resource the handler
|
|
17585
|
+
* had EXPLICITLY said it did not delete. It is the same silent-orphan class
|
|
17586
|
+
* {@link CR_DELETE_INVOKE_FAILED_SKIP_REASON} removed from the throw arm,
|
|
17587
|
+
* reached through the handler's RESPONSE instead.
|
|
17588
|
+
*
|
|
17589
|
+
* **Unconditional, with no already-gone classifier.** A handler that reports
|
|
17590
|
+
* FAILED because the thing it manages was already absent is a real and common
|
|
17591
|
+
* shape, and today's leniency lets those destroys finish green. Classifying
|
|
17592
|
+
* the reason to keep them green was rejected: the reason is free text a user's
|
|
17593
|
+
* handler writes, so any classifier is a guess, and a wrong guess
|
|
17594
|
+
* re-introduces exactly the orphan this arm exists to stop.
|
|
17595
|
+
*
|
|
17596
|
+
* This is therefore a COMPATIBILITY BREAK: a destroy whose delete handler
|
|
17597
|
+
* reports FAILED now exits 2 with the record kept, where it used to exit 0
|
|
17598
|
+
* with the record dropped.
|
|
17599
|
+
*
|
|
17600
|
+
* **The two callers have DIFFERENT escape hatches, and only one of them is a
|
|
17601
|
+
* flag.** On `cdkd deploy` the skip is forced back to exit 0 by
|
|
17602
|
+
* `--allow-unaddressed` (issue #1960, the flag that settled the analogous
|
|
17603
|
+
* exit-code question). `cdkd destroy` has no such flag — a skip raises
|
|
17604
|
+
* `PartialFailureError` unconditionally (`src/cli/commands/destroy.ts`) — so
|
|
17605
|
+
* there the remedy is the one that command's own summary names: confirm the
|
|
17606
|
+
* resource is gone, then drop the record with `cdkd state orphan <stack>`.
|
|
17607
|
+
* Messages must not offer the flag on the destroy path, which is the path this
|
|
17608
|
+
* arm is mostly reached from.
|
|
17609
|
+
*
|
|
17610
|
+
* **Fixed wording, no interpolation**, for the reason spelled out on
|
|
17611
|
+
* {@link CR_DELETE_INVOKE_FAILED_SKIP_REASON}: the handler's own `Reason` is a
|
|
17612
|
+
* user-authored string, it goes out on the `logger.warn` beside this, and a
|
|
17613
|
+
* `Reason` carrying `does not exist` / `not found` would make the deploy-side
|
|
17614
|
+
* replacement sites classify the skip as "already gone" and drop the record
|
|
17615
|
+
* one layer further out.
|
|
17616
|
+
*/
|
|
17617
|
+
const CR_DELETE_HANDLER_FAILED_SKIP_REASON = "Delete handler reported FAILED — resource unproven";
|
|
17618
|
+
/**
|
|
17578
17619
|
* The deploy-side caveat both skip warnings in this file carry (issue
|
|
17579
17620
|
* [#1762](https://github.com/go-to-k/cdkd/issues/1762)).
|
|
17580
17621
|
*
|
|
@@ -17585,6 +17626,29 @@ const CR_DELETE_INVOKE_FAILED_SKIP_REASON = "Delete request to the handler did n
|
|
|
17585
17626
|
* torn down by hand. Mirrors the caveat `compositeIdFormatMessage` already
|
|
17586
17627
|
* carries for the composite-id family.
|
|
17587
17628
|
*/
|
|
17629
|
+
/**
|
|
17630
|
+
* The bound BOTH delete-path skips in this file have to state (found in review
|
|
17631
|
+
* of issue [#2054](https://github.com/go-to-k/cdkd/issues/2054)).
|
|
17632
|
+
*
|
|
17633
|
+
* A skip KEEPS the state record, and the natural thing to promise is that a
|
|
17634
|
+
* re-run retries the handler. **On `cdkd destroy` that promise is false**, and
|
|
17635
|
+
* it is false in the direction that matters. `destroy-runner.ts` walks every
|
|
17636
|
+
* reverse-DAG level regardless of skips, so the SAME run that skipped the
|
|
17637
|
+
* custom resource goes on to delete its backing Lambda. The next
|
|
17638
|
+
* `cdkd destroy` therefore reaches the issue-#804 pre-check above, finds the
|
|
17639
|
+
* function gone, and treats the resource as already deleted — dropping the
|
|
17640
|
+
* record and exiting 0 over a resource the handler explicitly refused to
|
|
17641
|
+
* remove, which is the very silent orphan #2054 removed one run earlier.
|
|
17642
|
+
*
|
|
17643
|
+
* Closing it properly means making that pre-check answer `'skipped'` when the
|
|
17644
|
+
* teardown was never PROVEN, which needs a durable "a prior run skipped this"
|
|
17645
|
+
* signal. Every candidate is outside this file: a `ResourceState` field (a
|
|
17646
|
+
* state-schema bump), or a `DeleteContext` flag threaded from
|
|
17647
|
+
* `destroy-runner.ts`. So the record is described here as what it actually is
|
|
17648
|
+
* — a POINTER to something that has to be torn down by hand — rather than as a
|
|
17649
|
+
* retry that will not happen.
|
|
17650
|
+
*/
|
|
17651
|
+
const CR_SKIP_NOT_A_RETRY_CAVEAT = "NOTE this record is a POINTER, not a retry: the same destroy run deletes the backing Lambda, so the next 'cdkd destroy' finds the handler gone and DROPS this record (issue 804 pre-check). Tear the resource down by hand, then clear the stack's records with 'cdkd state orphan <stack>' — that command drops EVERY record for the stack, not just this one.";
|
|
17588
17652
|
const DEPLOY_SKIP_CAVEAT = "NOTE this arm is ALSO reached from cdkd deploy. Since issue 1762 the DELETE of a resource removed from the template behaves like destroy — the record is KEPT and the next deploy re-attempts it — but a REPLACEMENT / rollback delete FAILS the resource instead (https://github.com/go-to-k/cdkd/issues/1762), leaving the old one untracked; there, tear the resource down by hand.";
|
|
17589
17653
|
/**
|
|
17590
17654
|
* Type guard to validate Lambda response payload structure
|
|
@@ -17898,25 +17962,50 @@ const CR_LOG_TAIL_BOILERPLATE = /^(START|END|REPORT|XRAY|INIT_START|INIT_REPORT|
|
|
|
17898
17962
|
*/
|
|
17899
17963
|
const SNS_SERVICE_TOKEN_ARN_RE = /^arn:aws[a-z0-9-]*:sns:/;
|
|
17900
17964
|
/**
|
|
17965
|
+
* Account segment {@link syntheticStackId} falls back to when STS could not
|
|
17966
|
+
* answer (`AwsAccountInfo.fabricated`, issue
|
|
17967
|
+
* [#1730](https://github.com/go-to-k/cdkd/issues/1730)).
|
|
17968
|
+
*
|
|
17969
|
+
* The Cloud Control enrichment sites answer a fabricated account by OMITTING
|
|
17970
|
+
* the value they would have built. That is not available here — `StackId` is a
|
|
17971
|
+
* REQUIRED member of the custom-resource request payload — so the choice is
|
|
17972
|
+
* between two wrong strings, and the honest one is the one a handler cannot
|
|
17973
|
+
* mistake for real. `getAccountInfo`'s own fallback id (`123456789012`) is
|
|
17974
|
+
* shaped exactly like a live account; the all-zero id is not a valid AWS
|
|
17975
|
+
* account and reads as the placeholder it is.
|
|
17976
|
+
*/
|
|
17977
|
+
const SYNTHETIC_STACK_ID_PLACEHOLDER_ACCOUNT = "000000000000";
|
|
17978
|
+
/**
|
|
17901
17979
|
* The synthetic `StackId` handed to a custom-resource handler in place of the
|
|
17902
17980
|
* CloudFormation stack ARN cdkd does not have.
|
|
17903
17981
|
*
|
|
17904
|
-
*
|
|
17905
|
-
*
|
|
17906
|
-
*
|
|
17907
|
-
*
|
|
17908
|
-
*
|
|
17909
|
-
*
|
|
17910
|
-
*
|
|
17911
|
-
*
|
|
17912
|
-
*
|
|
17913
|
-
*
|
|
17982
|
+
* Partition / region / account are synthesized TOGETHER from the real deploy
|
|
17983
|
+
* context (issue [#1866](https://github.com/go-to-k/cdkd/issues/1866)). Every
|
|
17984
|
+
* segment used to be fabricated — `arn:aws:cloudformation:us-east-1:0000...`
|
|
17985
|
+
* regardless of where the deploy actually ran — and CloudFormation-authored
|
|
17986
|
+
* handlers DO read `event.StackId`: to re-derive the region / account they are
|
|
17987
|
+
* running in, to build ARNs, to name log streams, to correlate a response.
|
|
17988
|
+
* Each of those read a coherent-looking ARN and got an answer that addresses
|
|
17989
|
+
* nothing.
|
|
17990
|
+
*
|
|
17991
|
+
* Deriving only ONE segment is worse than deriving none, which is why issue
|
|
17992
|
+
* #1815 deliberately left the hardcoded `arn:aws:` prefix alone rather than
|
|
17993
|
+
* partition-deriving it in isolation: `arn:aws-cn:cloudformation:us-east-1:…`
|
|
17994
|
+
* is a China partition carrying a commercial region, strictly LESS coherent
|
|
17995
|
+
* than a uniformly-commercial fabrication. So this takes the whole
|
|
17996
|
+
* {@link AwsAccountInfo} — where `partition` is already derived FROM `region`
|
|
17997
|
+
* — rather than any one field.
|
|
17998
|
+
*
|
|
17999
|
+
* The stack-NAME segment stays synthetic (`cdkd-<logicalId>`): cdkd has no
|
|
18000
|
+
* CloudFormation stack, so there is no real value to put there.
|
|
17914
18001
|
*
|
|
17915
18002
|
* Factored into one place so the rationale cannot go stale against two other
|
|
17916
|
-
* copies: the create / update / delete request builders all use it
|
|
18003
|
+
* copies: the create / update / delete request builders all use it, through
|
|
18004
|
+
* {@link CustomResourceProvider.resolveSyntheticStackId}.
|
|
17917
18005
|
*/
|
|
17918
|
-
function syntheticStackId(logicalId) {
|
|
17919
|
-
|
|
18006
|
+
function syntheticStackId(logicalId, accountInfo) {
|
|
18007
|
+
const account = accountInfo.fabricated ? SYNTHETIC_STACK_ID_PLACEHOLDER_ACCOUNT : accountInfo.accountId;
|
|
18008
|
+
return `arn:${accountInfo.partition}:cloudformation:${accountInfo.region}:${account}:stack/cdkd-${logicalId}/cdkd`;
|
|
17920
18009
|
}
|
|
17921
18010
|
/**
|
|
17922
18011
|
* `true` when the tail contains at least one line the HANDLER produced.
|
|
@@ -18109,11 +18198,29 @@ var CustomResourceProvider = class CustomResourceProvider {
|
|
|
18109
18198
|
* the placeholder `PutObject`'s `withRetry` takes no knob either.
|
|
18110
18199
|
*/
|
|
18111
18200
|
preDeliveryAuthzMaxRetries = 26;
|
|
18201
|
+
/**
|
|
18202
|
+
* The region the client bag this provider was built from was EXPLICITLY
|
|
18203
|
+
* configured with, or `undefined` (issue #1866).
|
|
18204
|
+
*
|
|
18205
|
+
* Captured in the constructor, beside the clients, rather than read per call:
|
|
18206
|
+
* `cdkd deploy` builds a region-configured `AwsClients` and a fresh
|
|
18207
|
+
* `ProviderRegistry` per stack, and with `--stack-concurrency` (default 4) it
|
|
18208
|
+
* swaps the process-global bag while other stacks are mid-flight — so a
|
|
18209
|
+
* call-time read can hand a SIBLING stack's region. Pairing it with the
|
|
18210
|
+
* clients keeps the two consistent by construction.
|
|
18211
|
+
*
|
|
18212
|
+
* `AwsClients.configuredRegion` is deliberately the only region a client bag
|
|
18213
|
+
* will answer (see its own note on why `client.config.region()` is unsound),
|
|
18214
|
+
* and `undefined` means no region was pinned anywhere — which
|
|
18215
|
+
* {@link getAccountInfo} then resolves from `AWS_REGION` itself.
|
|
18216
|
+
*/
|
|
18217
|
+
configuredRegion;
|
|
18112
18218
|
constructor(config) {
|
|
18113
18219
|
const awsClients = getAwsClients();
|
|
18114
18220
|
this.lambdaClient = awsClients.lambda;
|
|
18115
18221
|
this.snsClient = awsClients.sns;
|
|
18116
18222
|
this.s3Client = awsClients.s3;
|
|
18223
|
+
this.configuredRegion = awsClients.configuredRegion;
|
|
18117
18224
|
this.responseBucket = config?.responseBucket;
|
|
18118
18225
|
this.responsePrefix = config?.responsePrefix ?? "custom-resource-responses";
|
|
18119
18226
|
this.asyncResponseTimeoutMs = config?.asyncResponseTimeoutMs ?? CustomResourceProvider.DEFAULT_ASYNC_RESPONSE_TIMEOUT_MS;
|
|
@@ -18211,6 +18318,22 @@ var CustomResourceProvider = class CustomResourceProvider {
|
|
|
18211
18318
|
return this.responseClientResolveInFlight;
|
|
18212
18319
|
}
|
|
18213
18320
|
/**
|
|
18321
|
+
* Resolve {@link syntheticStackId} against this deploy's REAL account /
|
|
18322
|
+
* region / partition (issue #1866).
|
|
18323
|
+
*
|
|
18324
|
+
* `getAccountInfo` never throws — it answers a `fabricated` account when STS
|
|
18325
|
+
* cannot, which {@link SYNTHETIC_STACK_ID_PLACEHOLDER_ACCOUNT} handles — so
|
|
18326
|
+
* this cannot turn a working deploy into a failing one on the credential
|
|
18327
|
+
* path. It is resolved ONCE per `create` / `update` / `delete` rather than
|
|
18328
|
+
* per invocation attempt: the value does not vary between attempts, and the
|
|
18329
|
+
* request builder the retry loop re-runs is synchronous.
|
|
18330
|
+
*/
|
|
18331
|
+
async resolveSyntheticStackId(logicalId) {
|
|
18332
|
+
const accountInfo = await getAccountInfo(this.configuredRegion);
|
|
18333
|
+
if (accountInfo.fabricated) this.logger.warn(`Custom resource ${logicalId}: STS did not report this deploy's account id, so the synthetic StackId handed to the handler carries the placeholder account ${SYNTHETIC_STACK_ID_PLACEHOLDER_ACCOUNT}. A handler that parses StackId to re-derive the account it is running in will not get a usable one — fix the credentials (or set AWS_ACCOUNT_ID) and re-run.`);
|
|
18334
|
+
return syntheticStackId(logicalId, accountInfo);
|
|
18335
|
+
}
|
|
18336
|
+
/**
|
|
18214
18337
|
* Create a custom resource by invoking its Lambda handler
|
|
18215
18338
|
*/
|
|
18216
18339
|
async create(logicalId, resourceType, properties) {
|
|
@@ -18225,7 +18348,7 @@ var CustomResourceProvider = class CustomResourceProvider {
|
|
|
18225
18348
|
ResponseURL: invocation.responseURL,
|
|
18226
18349
|
ResourceType: resourceType,
|
|
18227
18350
|
LogicalResourceId: logicalId,
|
|
18228
|
-
StackId:
|
|
18351
|
+
StackId: invocation.stackId,
|
|
18229
18352
|
ResourceProperties: this.stringifyProperties(properties)
|
|
18230
18353
|
}));
|
|
18231
18354
|
if (cfnResponse.Status === "FAILED") throw new Error(`Custom resource handler returned FAILED: ${cfnResponse.Reason || "Unknown reason"}`);
|
|
@@ -18257,7 +18380,7 @@ var CustomResourceProvider = class CustomResourceProvider {
|
|
|
18257
18380
|
ResourceType: resourceType,
|
|
18258
18381
|
LogicalResourceId: logicalId,
|
|
18259
18382
|
PhysicalResourceId: physicalId,
|
|
18260
|
-
StackId:
|
|
18383
|
+
StackId: invocation.stackId,
|
|
18261
18384
|
ResourceProperties: this.stringifyProperties(properties),
|
|
18262
18385
|
OldResourceProperties: this.stringifyProperties(previousProperties)
|
|
18263
18386
|
}));
|
|
@@ -18298,7 +18421,7 @@ var CustomResourceProvider = class CustomResourceProvider {
|
|
|
18298
18421
|
}
|
|
18299
18422
|
if (typeof serviceToken !== "string") throw new ProvisioningError(`Custom Resource ${logicalId}: ServiceToken is not a resolved string ARN (got ${typeof serviceToken}). This usually indicates state was written by a pre-fix cdkd import; re-run \`cdkd import\` or \`cdkd state orphan <stack>\` to recover.`, resourceType, logicalId, physicalId);
|
|
18300
18423
|
if (!this.isSnsServiceToken(serviceToken) && await this.isBackingLambdaGone(serviceToken)) {
|
|
18301
|
-
this.logger.warn(`Backing Lambda for custom resource ${logicalId} no longer exists (${serviceToken}); treating the custom resource as already deleted
|
|
18424
|
+
this.logger.warn(`Backing Lambda for custom resource ${logicalId} no longer exists (${serviceToken}); treating the custom resource as already deleted and DROPPING its state record. The handler can never run again, so if its teardown was never PROVEN — e.g. an earlier run reported this resource as skipped (issue 2054) — whatever it manages is still LIVE and is now untracked by cdkd. Check for leftovers before treating the stack as gone.`);
|
|
18302
18425
|
return;
|
|
18303
18426
|
}
|
|
18304
18427
|
try {
|
|
@@ -18309,13 +18432,19 @@ var CustomResourceProvider = class CustomResourceProvider {
|
|
|
18309
18432
|
ResourceType: resourceType,
|
|
18310
18433
|
LogicalResourceId: logicalId,
|
|
18311
18434
|
PhysicalResourceId: physicalId,
|
|
18312
|
-
StackId:
|
|
18435
|
+
StackId: invocation.stackId,
|
|
18313
18436
|
ResourceProperties: this.stringifyProperties(properties)
|
|
18314
18437
|
}));
|
|
18315
|
-
if (cfnResponse.Status === "FAILED")
|
|
18316
|
-
|
|
18438
|
+
if (cfnResponse.Status === "FAILED") {
|
|
18439
|
+
this.logger.warn(`Custom resource delete handler returned FAILED for ${logicalId}: ${cfnResponse.Reason || "Unknown reason"}. The handler reported that it did NOT delete, so anything this custom resource manages is LEFT IN PLACE — cdkd is KEEPING the state record and the run exits non-zero. ${CR_SKIP_NOT_A_RETRY_CAVEAT} ('cdkd deploy' also accepts --allow-unaddressed, which forces exit 0; 'cdkd destroy' has no such flag.) ${DEPLOY_SKIP_CAVEAT}`);
|
|
18440
|
+
return {
|
|
18441
|
+
outcome: "skipped",
|
|
18442
|
+
reason: CR_DELETE_HANDLER_FAILED_SKIP_REASON
|
|
18443
|
+
};
|
|
18444
|
+
}
|
|
18445
|
+
this.logger.debug(`Successfully deleted custom resource ${logicalId}`);
|
|
18317
18446
|
} catch (error) {
|
|
18318
|
-
this.logger.warn(`Failed to delete custom resource ${logicalId}, but continuing: ${error instanceof Error ? error.message : String(error)}. The Delete handler did not complete, so anything this custom resource manages may still be LIVE — cdkd is KEEPING the state record
|
|
18447
|
+
this.logger.warn(`Failed to delete custom resource ${logicalId}, but continuing: ${error instanceof Error ? error.message : String(error)}. The Delete handler did not complete, so anything this custom resource manages may still be LIVE — cdkd is KEEPING the state record and the run exits non-zero. ${CR_SKIP_NOT_A_RETRY_CAVEAT} ${DEPLOY_SKIP_CAVEAT}`);
|
|
18319
18448
|
return {
|
|
18320
18449
|
outcome: "skipped",
|
|
18321
18450
|
reason: CR_DELETE_INVOKE_FAILED_SKIP_REASON
|
|
@@ -18391,13 +18520,22 @@ var CustomResourceProvider = class CustomResourceProvider {
|
|
|
18391
18520
|
* PRE-delivery throw is safe to replay at all.
|
|
18392
18521
|
*
|
|
18393
18522
|
* `buildRequest` is called once per attempt with the fresh invocation so the
|
|
18394
|
-
* CFn request body always carries the matching ResponseURL / RequestId.
|
|
18523
|
+
* CFn request body always carries the matching ResponseURL / RequestId. The
|
|
18524
|
+
* synthetic `StackId` rides the same bag although it is stable across
|
|
18525
|
+
* attempts (issue #1866), for an ORDERING reason rather than a freshness one:
|
|
18526
|
+
* resolving it needs an `await`, and every await before the SIGINT watch
|
|
18527
|
+
* below is installed is a window in which Ctrl-C is dead — `docs/
|
|
18528
|
+
* provider-development.md` requires a new wait site to be interruptible, and
|
|
18529
|
+
* the pre-delivery backoff this method owns is 47.75s long.
|
|
18530
|
+
*
|
|
18395
18531
|
* Returns the final response; the caller decides what a terminal FAILED means
|
|
18396
|
-
* (create/update throw
|
|
18532
|
+
* (create / update throw; delete warns and returns `'skipped'` — issue
|
|
18533
|
+
* #2054, which replaced its warn-and-continue).
|
|
18397
18534
|
*/
|
|
18398
18535
|
async invokeCustomResourceWithRetry(serviceToken, logicalId, operation, buildRequest) {
|
|
18399
18536
|
const watch = this.startInterruptWatch(logicalId);
|
|
18400
18537
|
try {
|
|
18538
|
+
const stackId = await this.resolveSyntheticStackId(logicalId);
|
|
18401
18539
|
let preDeliveryRetries = 0;
|
|
18402
18540
|
let failedResponseRetries = 0;
|
|
18403
18541
|
for (let attempt = 0;; attempt++) {
|
|
@@ -18407,7 +18545,10 @@ var CustomResourceProvider = class CustomResourceProvider {
|
|
|
18407
18545
|
let invocation;
|
|
18408
18546
|
try {
|
|
18409
18547
|
invocation = await this.prepareInvocation(logicalId, watch);
|
|
18410
|
-
const request = buildRequest(
|
|
18548
|
+
const request = buildRequest({
|
|
18549
|
+
...invocation,
|
|
18550
|
+
stackId
|
|
18551
|
+
});
|
|
18411
18552
|
this.logger.debug(`Sending custom resource ${operation.toLowerCase()} request: ${serviceToken}`);
|
|
18412
18553
|
const sent = await this.sendRequest(serviceToken, request, invocation.responseKey, logicalId, operation, () => {
|
|
18413
18554
|
delivered = true;
|
|
@@ -24227,7 +24368,7 @@ async function replayRollback(operations, stateResources, stackName, ctx, option
|
|
|
24227
24368
|
eventType: "ROLLBACK_STARTED",
|
|
24228
24369
|
stackName
|
|
24229
24370
|
});
|
|
24230
|
-
const resolver = new
|
|
24371
|
+
const resolver = new ReplayResolvers(ctx.region);
|
|
24231
24372
|
const { createOps, otherOps } = partitionOps(operations);
|
|
24232
24373
|
for (let i = otherOps.length - 1; i >= 0; i--) {
|
|
24233
24374
|
if (options.isInterrupted?.()) {
|
|
@@ -24292,6 +24433,21 @@ async function replayRollback(operations, stateResources, stackName, ctx, option
|
|
|
24292
24433
|
* Cognito `client_secret`). Rollback is synth-free, so re-resolve straight from
|
|
24293
24434
|
* the expression string here.
|
|
24294
24435
|
*
|
|
24436
|
+
* BOTH SIDES OF A DIFF ARE CLASSIFIED, not just the bag that is written, and
|
|
24437
|
+
* that is deliberate (issue #2057 review). The `revert` / `--revert-failed`
|
|
24438
|
+
* arms call this twice — once for the desired bag and once for the CURRENT /
|
|
24439
|
+
* ATTEMPTED one, which only becomes the provider's `previousProperties`. Two
|
|
24440
|
+
* things make a wrong-region value there consequential rather than cosmetic:
|
|
24441
|
+
* a patch-based provider computes its patch previous-vs-desired, so a wrong
|
|
24442
|
+
* previous side can emit a wrong patch or, when both sides carry the same
|
|
24443
|
+
* expression and resolve to the same wrong value, silently compute a NO-OP and
|
|
24444
|
+
* skip the revert entirely; and every resolved plaintext lands in the SHARED
|
|
24445
|
+
* per-op `secrets` map, which is the redaction needle for the state record this
|
|
24446
|
+
* op persists, so a foreign-region plaintext mis-redacts that record. In
|
|
24447
|
+
* practice both bags carry the SAME expression (state redacts them identically),
|
|
24448
|
+
* so scoping the refusal to the written bag would buy a rare case at the cost of
|
|
24449
|
+
* a rule nobody could apply by reading one call site.
|
|
24450
|
+
*
|
|
24295
24451
|
* Records each `plaintext -> expression` into `secrets` so the caller can redact
|
|
24296
24452
|
* the persisted state record back to the expression — the same
|
|
24297
24453
|
* resolve-for-provider + redact-for-state split the deploy engine applies at its
|
|
@@ -24305,28 +24461,365 @@ async function replayRollback(operations, stateResources, stackName, ctx, option
|
|
|
24305
24461
|
* `StringList` parameter is public config, stored resolved, and never appears
|
|
24306
24462
|
* as an expression in the journal.
|
|
24307
24463
|
*/
|
|
24308
|
-
async function resolveReplayProps(props,
|
|
24464
|
+
async function resolveReplayProps(props, resolvers, secrets, execCtx, logicalId) {
|
|
24309
24465
|
if (props === void 0) return void 0;
|
|
24310
|
-
const
|
|
24466
|
+
const resolverContext = {
|
|
24311
24467
|
template: { Resources: {} },
|
|
24312
24468
|
resources: {},
|
|
24313
24469
|
recordedSecretValues: secrets
|
|
24314
24470
|
};
|
|
24315
|
-
const walk = async (v) => {
|
|
24316
|
-
if (typeof v === "string")
|
|
24471
|
+
const walk = async (v, path) => {
|
|
24472
|
+
if (typeof v === "string") {
|
|
24473
|
+
if (!v.includes("{{resolve:")) return v;
|
|
24474
|
+
return await resolveLeafByRegion(v, path, logicalId, execCtx, resolvers, resolverContext);
|
|
24475
|
+
}
|
|
24317
24476
|
if (Array.isArray(v)) {
|
|
24318
24477
|
const out = new Array(v.length);
|
|
24319
|
-
for (let i = 0; i < v.length; i++) out[i] = await walk(v[i]);
|
|
24478
|
+
for (let i = 0; i < v.length; i++) out[i] = await walk(v[i], `${path}[${i}]`);
|
|
24320
24479
|
return out;
|
|
24321
24480
|
}
|
|
24322
24481
|
if (v !== null && typeof v === "object") {
|
|
24323
24482
|
const out = {};
|
|
24324
|
-
for (const [k, val] of Object.entries(v)) out[k] = await walk(val);
|
|
24483
|
+
for (const [k, val] of Object.entries(v)) out[k] = await walk(val, path === "" ? k : `${path}.${k}`);
|
|
24325
24484
|
return out;
|
|
24326
24485
|
}
|
|
24327
24486
|
return v;
|
|
24328
24487
|
};
|
|
24329
|
-
return await walk(props);
|
|
24488
|
+
return await walk(props, "");
|
|
24489
|
+
}
|
|
24490
|
+
/**
|
|
24491
|
+
* The `{{resolve:<service>:...}}` families whose value can be a SECRET, and
|
|
24492
|
+
* therefore the only ones the region question below is asked about: every
|
|
24493
|
+
* `secretsmanager` reference by spelling, and every `ssm` one, which is secret
|
|
24494
|
+
* exactly when its parameter is a `SecureString` (issue #1901).
|
|
24495
|
+
*
|
|
24496
|
+
* Every OTHER service is `local` because cdkd cannot resolve it at all, NOT
|
|
24497
|
+
* because it is public. `ssm-secure` is the live example and is emphatically
|
|
24498
|
+
* not public: `resolveDynamicReferences` has no arm for it, so the literal
|
|
24499
|
+
* token is passed through to AWS and CloudFormation resolves it SERVER-side.
|
|
24500
|
+
* cdkd never holds its value, so there is no region for cdkd to get wrong —
|
|
24501
|
+
* which is the only reason it can be waved through here.
|
|
24502
|
+
*/
|
|
24503
|
+
const REPLAY_SECRET_SERVICES = /* @__PURE__ */ new Set(["secretsmanager", "ssm"]);
|
|
24504
|
+
/**
|
|
24505
|
+
* Split a `{{resolve:secretsmanager:...}}` inner body into its SECRET_ID.
|
|
24506
|
+
*
|
|
24507
|
+
* Mirrors `IntrinsicFunctionResolver.resolveSecretsManagerReference`'s own
|
|
24508
|
+
* split — including the END-ANCHORED whole-secret form — because a secret ID
|
|
24509
|
+
* may legitimately contain colons (an ARN always does), so `split(':')[1]` is
|
|
24510
|
+
* wrong for exactly the shape this file cares about most.
|
|
24511
|
+
*/
|
|
24512
|
+
function secretsManagerSecretId(inner) {
|
|
24513
|
+
const afterService = inner.substring(15);
|
|
24514
|
+
let stringIdx = afterService.indexOf(":SecretString:");
|
|
24515
|
+
let binaryIdx = afterService.indexOf(":SecretBinary:");
|
|
24516
|
+
if (stringIdx < 0 && afterService.endsWith(":SecretString")) stringIdx = afterService.length - 13;
|
|
24517
|
+
if (binaryIdx < 0 && afterService.endsWith(":SecretBinary")) binaryIdx = afterService.length - 13;
|
|
24518
|
+
const delimiterIdx = stringIdx >= 0 && binaryIdx >= 0 ? Math.min(stringIdx, binaryIdx) : stringIdx >= 0 ? stringIdx : binaryIdx;
|
|
24519
|
+
return delimiterIdx >= 0 ? afterService.substring(0, delimiterIdx) : afterService;
|
|
24520
|
+
}
|
|
24521
|
+
/**
|
|
24522
|
+
* The parameter name an `{{resolve:ssm:...}}` reference asks for — byte-for-byte
|
|
24523
|
+
* what `IntrinsicFunctionResolver.resolveSSMReference` passes as `GetParameter`'s
|
|
24524
|
+
* `Name`, which is `parts.slice(1).join(':')` on the colon-split inner body.
|
|
24525
|
+
*
|
|
24526
|
+
* The whole remainder, deliberately, with NOTHING stripped:
|
|
24527
|
+
*
|
|
24528
|
+
* - An SSM dynamic reference CAN name a full ARN. The resolver joins the tail
|
|
24529
|
+
* back together, so `{{resolve:ssm:arn:aws:ssm:us-west-2:111122223333:parameter/db/pw}}`
|
|
24530
|
+
* reaches AWS as that ARN. A `split(':')[1]` here would yield the literal
|
|
24531
|
+
* `'arn'` — a parameter that does not exist — and then report the reference
|
|
24532
|
+
* as region-LESS and refuse it, which is the guess-in-the-other-direction the
|
|
24533
|
+
* `named-region` arm exists to prevent.
|
|
24534
|
+
* - A trailing `:<version>` / `:<label>` is part of the name AS SSM PARSES IT
|
|
24535
|
+
* (`GetParameter` accepts `name:3` / `name:prod`), so stripping it would name
|
|
24536
|
+
* a different thing in the refusal message than the one that would be read.
|
|
24537
|
+
*/
|
|
24538
|
+
function ssmParameterName(inner) {
|
|
24539
|
+
return inner.substring(4);
|
|
24540
|
+
}
|
|
24541
|
+
/**
|
|
24542
|
+
* The region an ARN names, or `undefined` for anything that is not an ARN with
|
|
24543
|
+
* a populated region field (`arn:<partition>:<service>:<region>:...`).
|
|
24544
|
+
*/
|
|
24545
|
+
function arnRegion(secretId) {
|
|
24546
|
+
if (!secretId.startsWith("arn:")) return void 0;
|
|
24547
|
+
const region = secretId.split(":")[3];
|
|
24548
|
+
return region ? region : void 0;
|
|
24549
|
+
}
|
|
24550
|
+
/**
|
|
24551
|
+
* The producer regions a stack's persisted cross-stack reads name, for
|
|
24552
|
+
* {@link RollbackExecutorContext.importedProducerRegions} (issue #2057).
|
|
24553
|
+
*
|
|
24554
|
+
* Both record kinds count, and for the same reason: each one is a value this
|
|
24555
|
+
* stack read out of ANOTHER region's state, so each one is a way a
|
|
24556
|
+
* foreign-region `{{resolve:...}}` expression can have reached this stack's own
|
|
24557
|
+
* record. `imports` is the strong `Fn::ImportValue` edge; `outputReads` is the
|
|
24558
|
+
* weak `Fn::GetStackOutput` one (schema v8), which is the EASIER of the two to
|
|
24559
|
+
* point across a region boundary because the reference carries its own
|
|
24560
|
+
* `Region` argument.
|
|
24561
|
+
*
|
|
24562
|
+
* Deduplicated case-insensitively, keeping each region's first-recorded
|
|
24563
|
+
* spelling so the refusal message echoes what the user will see in
|
|
24564
|
+
* `state.json`. The consumer's own region is deliberately NOT filtered here —
|
|
24565
|
+
* {@link classifyReplaySecretRegion} does that, because it is the one that
|
|
24566
|
+
* knows which region is asking.
|
|
24567
|
+
*
|
|
24568
|
+
* Exported so the two `RollbackExecutorContext` construction sites derive the
|
|
24569
|
+
* list identically — `cdkd rollback` from the state it loaded, and
|
|
24570
|
+
* `DeployEngine.rollbackExecutorContext` from `crossStackReadsForPartialSave`,
|
|
24571
|
+
* which unions that snapshot with the reads the failing deploy itself made.
|
|
24572
|
+
*/
|
|
24573
|
+
function producerRegionsFromState(state) {
|
|
24574
|
+
const seen = /* @__PURE__ */ new Set();
|
|
24575
|
+
const regions = [];
|
|
24576
|
+
for (const entry of [...state.imports ?? [], ...state.outputReads ?? []]) {
|
|
24577
|
+
const canonical = canonicalizeRegion(entry.sourceRegion);
|
|
24578
|
+
if (!canonical || seen.has(canonical)) continue;
|
|
24579
|
+
seen.add(canonical);
|
|
24580
|
+
regions.push(entry.sourceRegion);
|
|
24581
|
+
}
|
|
24582
|
+
return regions;
|
|
24583
|
+
}
|
|
24584
|
+
/**
|
|
24585
|
+
* Decide which region must answer for a single `{{resolve:...}}` expression a
|
|
24586
|
+
* rollback replay is about to re-resolve — issue
|
|
24587
|
+
* [#2057](https://github.com/go-to-k/cdkd/issues/2057).
|
|
24588
|
+
*
|
|
24589
|
+
* WHY A REPLAY CAN BE HOLDING A FOREIGN REGION'S EXPRESSION AT ALL. Since
|
|
24590
|
+
* issue #1934 a cross-stack consumer re-resolves a redacted producer value in
|
|
24591
|
+
* the PRODUCER's region (`reresolveCrossStackValue` /
|
|
24592
|
+
* `resolverForProducerRegion`) — correct, because a Secrets Manager secret or
|
|
24593
|
+
* an SSM `SecureString` of the same NAME in two regions is two independent
|
|
24594
|
+
* values. The plaintext is then recorded into the CONSUMER's
|
|
24595
|
+
* `recordedSecretValues`, so the consumer's `state.json` (and from there the
|
|
24596
|
+
* rollback journal) persists the PRODUCER's spelling of the expression. That is
|
|
24597
|
+
* the right thing to persist, and it is region-less: the reader cannot tell
|
|
24598
|
+
* from the string which region produced it.
|
|
24599
|
+
*
|
|
24600
|
+
* The replay rebuilds its resolver from the CONSUMER's region alone, so
|
|
24601
|
+
* re-resolving that expression locally answers from a same-named secret in the
|
|
24602
|
+
* wrong region and writes it to a LIVE resource. Silent, and on the recovery
|
|
24603
|
+
* path. The rule applied here is the family's, from issue #1957: A NAMED REGION
|
|
24604
|
+
* BINDS; NEVER SUBSTITUTE A GUESS. The three verdicts are that one sentence:
|
|
24605
|
+
*
|
|
24606
|
+
* - **`named-region`** — the expression's SECRET_ID is an ARN, which names its
|
|
24607
|
+
* own region. The region is ESTABLISHED, so it binds: the caller resolves
|
|
24608
|
+
* through a resolver pinned to it ({@link ReplayResolvers.forRegion}) rather
|
|
24609
|
+
* than refusing. Refusing here would be the guess in the other direction.
|
|
24610
|
+
*
|
|
24611
|
+
* cdkd would otherwise get this wrong, which is why the arm exists at all:
|
|
24612
|
+
* `resolveSecretsManagerReference` builds its client from
|
|
24613
|
+
* `this.explicitRegion` and passes the ARN through as an opaque `SecretId`,
|
|
24614
|
+
* and `@aws-sdk/client-secrets-manager`'s endpoint ruleset has NO
|
|
24615
|
+
* ARN-derived endpoint rule (unlike, say, S3 access points), so a
|
|
24616
|
+
* foreign-region ARN is sent to the stack's own regional endpoint. What the
|
|
24617
|
+
* SERVICE then does with it is not something this repo can settle offline —
|
|
24618
|
+
* see the fixture note in
|
|
24619
|
+
* `tests/integration/rollback-cross-region-secret/README.md`. Pinning the
|
|
24620
|
+
* client to the ARN's region is correct either way: if Secrets Manager would
|
|
24621
|
+
* have refused the foreign ARN, this turns a hard failure into a correct
|
|
24622
|
+
* resolution; if it would have honoured it, this reaches the same value by
|
|
24623
|
+
* the documented route. Neither outcome is a regression.
|
|
24624
|
+
*
|
|
24625
|
+
* - **`ambiguous`** — the expression names no region (the plain name form) AND
|
|
24626
|
+
* this stack has a foreign producer region on record
|
|
24627
|
+
* ({@link RollbackExecutorContext.importedProducerRegions}). Nothing on hand
|
|
24628
|
+
* can establish the origin, so the replay refuses instead of guessing.
|
|
24629
|
+
*
|
|
24630
|
+
* KNOWN OVER-REFUSAL, accepted deliberately, and WIDER THAN THE SSM CASE
|
|
24631
|
+
* ALONE — state both, because the second one is the common shape:
|
|
24632
|
+
*
|
|
24633
|
+
* (a) Any NAME-FORM `secretsmanager` reference in a stack that has ANY
|
|
24634
|
+
* foreign producer region on record is refused, even when that secret is
|
|
24635
|
+
* the stack's own purely-local one and has nothing to do with the
|
|
24636
|
+
* cross-region read. The evidence is per-STACK, not per-reference, so one
|
|
24637
|
+
* cross-region export plus one ordinary
|
|
24638
|
+
* `{{resolve:secretsmanager:mysecret:SecretString:pw}}` is enough — and CDK's
|
|
24639
|
+
* `secretValueFromJson` emits exactly that name form, so this is the shape
|
|
24640
|
+
* most people will meet. It also persists: with the union the producer
|
|
24641
|
+
* region stays on record until the next SUCCESSFUL deploy. Per-reference
|
|
24642
|
+
* evidence is what would narrow it, and that needs the region recorded
|
|
24643
|
+
* ALONGSIDE the expression — the persisted-shape change issue #2057
|
|
24644
|
+
* deliberately deferred (its options 1 and 2). Until then the refusal is
|
|
24645
|
+
* loud, names the ARN spelling as the remedy, and is the fail-closed side
|
|
24646
|
+
* of a trade whose other side is a silent wrong-secret write.
|
|
24647
|
+
*
|
|
24648
|
+
* (b) An `ssm` reference is secret only when its parameter is a
|
|
24649
|
+
* `SecureString`, and this arm cannot tell. So a `{{resolve:ssm:/app/env}}`
|
|
24650
|
+
* naming a PUBLIC `String` that reached a persisted bag (issue #2036's
|
|
24651
|
+
* acknowledged over-redaction) is refused too. Narrowing it by
|
|
24652
|
+
* `isRecordedSecretExpression` was considered and REJECTED, and not because
|
|
24653
|
+
* the store is unreachable — it is imported by this very file. It is
|
|
24654
|
+
* unusable: `recordedSecretExpressions` is populated BY resolution, and in
|
|
24655
|
+
* the standalone `cdkd rollback` process nothing has resolved anything when
|
|
24656
|
+
* the first op is classified, so the store is empty and every `ssm` verdict
|
|
24657
|
+
* would come back "not secret" — turning the protection off for exactly the
|
|
24658
|
+
* SecureString case it exists for. Worse, once one op DID resolve a
|
|
24659
|
+
* reference the store would be warm for the next, so the verdict would
|
|
24660
|
+
* depend on OP ORDER. A resolve-the-type-first probe is unsound for the
|
|
24661
|
+
* same reason the whole issue exists: the TYPE is region-dependent (#1957),
|
|
24662
|
+
* so probing locally can report `String` for a name that is `SecureString`
|
|
24663
|
+
* in the producer's region and wave through the very write this refuses.
|
|
24664
|
+
* The residual is therefore a loud, actionable error on a narrow
|
|
24665
|
+
* intersection (an over-redacted public ssm reference AND a cross-region
|
|
24666
|
+
* read on record), which is the fail-closed side of the trade.
|
|
24667
|
+
*
|
|
24668
|
+
* - **`local`** — everything else, which is the overwhelmingly common case:
|
|
24669
|
+
* every non-secret service, every same-region ARN (the ordinary CDK
|
|
24670
|
+
* `secretValueFromJson` shape), and every name-form expression in a stack
|
|
24671
|
+
* with no foreign producer region recorded. Resolved exactly as before this
|
|
24672
|
+
* change.
|
|
24673
|
+
*
|
|
24674
|
+
* A same-region ARN answers `local` even when a foreign producer region IS on
|
|
24675
|
+
* record: the expression settles the question itself, so the weaker evidence
|
|
24676
|
+
* never gets consulted.
|
|
24677
|
+
*/
|
|
24678
|
+
function classifyReplaySecretRegion(expression, consumerRegion, importedProducerRegions) {
|
|
24679
|
+
const inner = expression.startsWith("{{resolve:") ? expression.slice(10, -2) : void 0;
|
|
24680
|
+
if (inner === void 0) return { kind: "local" };
|
|
24681
|
+
const service = inner.split(":")[0];
|
|
24682
|
+
if (service === void 0 || !REPLAY_SECRET_SERVICES.has(service)) return { kind: "local" };
|
|
24683
|
+
const secretName = service === "secretsmanager" ? secretsManagerSecretId(inner) : ssmParameterName(inner);
|
|
24684
|
+
if (!secretName) return { kind: "local" };
|
|
24685
|
+
const named = arnRegion(secretName);
|
|
24686
|
+
if (named !== void 0) return canonicalizeRegion(named) === canonicalizeRegion(consumerRegion) ? { kind: "local" } : {
|
|
24687
|
+
kind: "named-region",
|
|
24688
|
+
secretName,
|
|
24689
|
+
region: named
|
|
24690
|
+
};
|
|
24691
|
+
const seen = /* @__PURE__ */ new Set();
|
|
24692
|
+
const foreignProducerRegions = [];
|
|
24693
|
+
for (const candidate of importedProducerRegions ?? []) {
|
|
24694
|
+
const canonical = canonicalizeRegion(candidate);
|
|
24695
|
+
if (!canonical || canonical === canonicalizeRegion(consumerRegion)) continue;
|
|
24696
|
+
if (seen.has(canonical)) continue;
|
|
24697
|
+
seen.add(canonical);
|
|
24698
|
+
foreignProducerRegions.push(candidate);
|
|
24699
|
+
}
|
|
24700
|
+
if (foreignProducerRegions.length === 0) return { kind: "local" };
|
|
24701
|
+
return {
|
|
24702
|
+
kind: "ambiguous",
|
|
24703
|
+
secretName,
|
|
24704
|
+
foreignProducerRegions
|
|
24705
|
+
};
|
|
24706
|
+
}
|
|
24707
|
+
/**
|
|
24708
|
+
* The replay's resolvers: the stack's own, plus one pinned sibling per FOREIGN
|
|
24709
|
+
* region an ARN-named reference asks for (issue #2057).
|
|
24710
|
+
*
|
|
24711
|
+
* One instance per replay, not per op — the resolved-value cache lives on the
|
|
24712
|
+
* resolver INSTANCE since issue #1933, so a resolver per op would re-fetch every
|
|
24713
|
+
* referenced secret once per op. The pinned siblings are cached here for the
|
|
24714
|
+
* same reason: a 100-op replay of a bag carrying one foreign ARN must pay one
|
|
24715
|
+
* `GetSecretValue`, not a hundred.
|
|
24716
|
+
*
|
|
24717
|
+
* A pinned sibling is a PLAIN resolver, deliberately NOT the resolver class's
|
|
24718
|
+
* own `producerRegionGuest` (which the class sets on the siblings
|
|
24719
|
+
* `resolverForProducerRegion` builds, to stop a foreign region pinning a verdict
|
|
24720
|
+
* in the process-global `recordedSecretExpressions` store — the issue #1933
|
|
24721
|
+
* shape, where an `ssm` parameter whose TYPE differs by region has one region's
|
|
24722
|
+
* verdict decide the other's redaction).
|
|
24723
|
+
*
|
|
24724
|
+
* WHY A GUEST FLAG IS NOT NEEDED HERE, and the argument has to be this one
|
|
24725
|
+
* rather than "only `secretsmanager` routes to a sibling" (that earlier claim
|
|
24726
|
+
* was FALSE — `resolveSSMReference` joins its colon-split tail back together, so
|
|
24727
|
+
* an `ssm` reference CAN name a full ARN and CAN therefore route here):
|
|
24728
|
+
*
|
|
24729
|
+
* {@link ReplayResolvers.forRegion} is reached ONLY from a `named-region`
|
|
24730
|
+
* verdict, which `classifyReplaySecretRegion` returns only when the
|
|
24731
|
+
* SECRET_ID / parameter name starts with `arn:` and carries a region. So a
|
|
24732
|
+
* pinned sibling only ever resolves an expression whose KEY EMBEDS THE
|
|
24733
|
+
* REGION IT IS BEING RESOLVED IN.
|
|
24734
|
+
*
|
|
24735
|
+
* The store is keyed by the expression string alone, and that is exactly what
|
|
24736
|
+
* makes #1933 possible: two regions sharing one key. An ARN-form key cannot be
|
|
24737
|
+
* shared by two regions, so a verdict pinned from a sibling can never contradict
|
|
24738
|
+
* another region's for the same key. If a future change ever routes a
|
|
24739
|
+
* region-LESS expression to `forRegion`, this argument dies with it and the
|
|
24740
|
+
* sibling needs the guest flag.
|
|
24741
|
+
*/
|
|
24742
|
+
var ReplayResolvers = class {
|
|
24743
|
+
/** The stack's own resolver — every `local` verdict resolves through this. */
|
|
24744
|
+
primary;
|
|
24745
|
+
pinned = /* @__PURE__ */ new Map();
|
|
24746
|
+
stackRegion;
|
|
24747
|
+
constructor(stackRegion) {
|
|
24748
|
+
this.stackRegion = stackRegion;
|
|
24749
|
+
this.primary = new IntrinsicFunctionResolver(stackRegion);
|
|
24750
|
+
}
|
|
24751
|
+
/** The resolver that must answer for `region` — `primary` when it is the stack's own. */
|
|
24752
|
+
forRegion(region) {
|
|
24753
|
+
const target = canonicalizeRegion(region);
|
|
24754
|
+
if (target === canonicalizeRegion(this.stackRegion)) return this.primary;
|
|
24755
|
+
const cached = this.pinned.get(target);
|
|
24756
|
+
if (cached) return cached;
|
|
24757
|
+
const scoped = new IntrinsicFunctionResolver(target);
|
|
24758
|
+
this.pinned.set(target, scoped);
|
|
24759
|
+
return scoped;
|
|
24760
|
+
}
|
|
24761
|
+
};
|
|
24762
|
+
/**
|
|
24763
|
+
* The refusal an `ambiguous` replay reference throws (issue #2057).
|
|
24764
|
+
*
|
|
24765
|
+
* A plain throw, like the final-snapshot refusals above and for the same
|
|
24766
|
+
* reason: the per-op catch in {@link replaySingle} /
|
|
24767
|
+
* {@link replayFailedOperations} counts it as a failure, which keeps the
|
|
24768
|
+
* journal segment and lets the user re-run once the reference is disambiguated.
|
|
24769
|
+
* Refusing is strictly better than the alternative it replaces — resolving a
|
|
24770
|
+
* producer-region reference against the consumer's region does not fail, it
|
|
24771
|
+
* succeeds with the WRONG credential and writes it to a resource that is live.
|
|
24772
|
+
*
|
|
24773
|
+
* Names the reference, the regions, and the remedy. Never the resolved value:
|
|
24774
|
+
* nothing here has resolved anything yet, and the expression is the same string
|
|
24775
|
+
* `state.json` already stores in the clear.
|
|
24776
|
+
*/
|
|
24777
|
+
function regionAmbiguousReplaySecretError(logicalId, propertyPath, secretName, foreignProducerRegions, consumerRegion) {
|
|
24778
|
+
return new CdkdError(`Rollback of ${logicalId}${propertyPath === "" ? "" : ` property '${propertyPath}'`} cannot re-resolve the secret reference '${secretName}': the reference carries no region of its own, and this stack read across a region boundary (producer region(s) on record: ${foreignProducerRegions.join(", ")}), so it may have been resolved in one of those rather than in '${consumerRegion}'. A secret of the same name in two regions is two independent values, so replaying this would write the WRONG secret to a live resource. Refusing instead. Resolve the reference in its own region and set the property directly (or spell it as a full ARN, which names its region and is resolved there), then re-run 'cdkd rollback'.`, "ROLLBACK_SECRET_REGION_AMBIGUOUS");
|
|
24779
|
+
}
|
|
24780
|
+
/**
|
|
24781
|
+
* Re-resolve one LEAF string, sending each `{{resolve:...}}` reference in it to
|
|
24782
|
+
* the region {@link classifyReplaySecretRegion} says must answer (issue #2057).
|
|
24783
|
+
*
|
|
24784
|
+
* Refuses FIRST, over the whole leaf, before any reference is fetched: a leaf
|
|
24785
|
+
* can splice several references together, and resolving the safe ones first
|
|
24786
|
+
* would leave half a credential fetched (and cached, and recorded as a
|
|
24787
|
+
* redaction needle) for an op that is about to be refused anyway.
|
|
24788
|
+
*
|
|
24789
|
+
* Then TWO paths, and the split is deliberate rather than an optimisation:
|
|
24790
|
+
*
|
|
24791
|
+
* - With no foreign-region reference — every leaf on every existing code path
|
|
24792
|
+
* — the leaf goes to `resolveDynamicReferences` WHOLE, exactly as before this
|
|
24793
|
+
* change. That method has its own well-tested substitution semantics (it
|
|
24794
|
+
* collects matches from the ORIGINAL string, so a resolved plaintext that is
|
|
24795
|
+
* itself token-shaped is never re-resolved — issue #1917), and this change
|
|
24796
|
+
* does not want to relitigate any of it.
|
|
24797
|
+
* - With one, the leaf is rebuilt segment by segment so each reference can be
|
|
24798
|
+
* resolved by its OWN region's resolver. `resolveDynamicReferences` resolves
|
|
24799
|
+
* every token in the string it is handed with the one resolver it is called
|
|
24800
|
+
* on, so a mixed leaf cannot be served by a single call. Each token is
|
|
24801
|
+
* resolved ALONE and its result concatenated, which means no resolved value
|
|
24802
|
+
* is ever re-scanned for tokens either.
|
|
24803
|
+
*
|
|
24804
|
+
* `dynamicReferenceTokens` returns the tokens in order and non-overlapping, so
|
|
24805
|
+
* walking the leaf with a moving `indexOf` cursor reproduces their positions
|
|
24806
|
+
* exactly, duplicates included.
|
|
24807
|
+
*/
|
|
24808
|
+
async function resolveLeafByRegion(leaf, propertyPath, logicalId, execCtx, resolvers, resolverContext) {
|
|
24809
|
+
const verdicts = dynamicReferenceTokens(leaf).map((token) => [token, classifyReplaySecretRegion(token, execCtx.region, execCtx.importedProducerRegions)]);
|
|
24810
|
+
for (const [, verdict] of verdicts) if (verdict.kind === "ambiguous") throw regionAmbiguousReplaySecretError(logicalId, propertyPath, verdict.secretName, verdict.foreignProducerRegions, execCtx.region);
|
|
24811
|
+
if (!verdicts.some(([, verdict]) => verdict.kind === "named-region")) return await resolvers.primary.resolveDynamicReferences(leaf, resolverContext);
|
|
24812
|
+
let out = "";
|
|
24813
|
+
let cursor = 0;
|
|
24814
|
+
for (const [token, verdict] of verdicts) {
|
|
24815
|
+
const at = leaf.indexOf(token, cursor);
|
|
24816
|
+
if (at < 0) throw new CdkdError(`Rollback of ${logicalId}${propertyPath === "" ? "" : ` property '${propertyPath}'`} could not locate a scanned dynamic reference in the value it was scanned from. Refusing rather than resolving it in '${execCtx.region}', which would be the wrong region for a reference that names another one. This is an internal invariant failure — please report it with the resource type and property path.`, "ROLLBACK_SECRET_TOKEN_SCAN_MISMATCH");
|
|
24817
|
+
out += leaf.slice(cursor, at);
|
|
24818
|
+
const resolver = verdict.kind === "named-region" ? resolvers.forRegion(verdict.region) : resolvers.primary;
|
|
24819
|
+
out += await resolver.resolveDynamicReferences(token, resolverContext);
|
|
24820
|
+
cursor = at + token.length;
|
|
24821
|
+
}
|
|
24822
|
+
return out + leaf.slice(cursor);
|
|
24330
24823
|
}
|
|
24331
24824
|
/**
|
|
24332
24825
|
* Redact resolved secret plaintext back out of a post-rollback state record
|
|
@@ -24650,7 +25143,7 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
|
|
|
24650
25143
|
case "reverse-replacement": {
|
|
24651
25144
|
const current = stateResources[op.logicalId];
|
|
24652
25145
|
const prev = op.previousState;
|
|
24653
|
-
const resolvedPrevProps = await resolveReplayProps(prev.properties, resolver, secrets) ?? {};
|
|
25146
|
+
const resolvedPrevProps = await resolveReplayProps(prev.properties, resolver, secrets, ctx, op.logicalId) ?? {};
|
|
24654
25147
|
logger.info(` Rollback: Reversing replacement of ${op.logicalId} (${op.resourceType}) — re-creating the old resource and deleting the new one`);
|
|
24655
25148
|
if (STATEFUL_TYPES.has(op.resourceType)) logger.warn(` ⚠ ${op.logicalId} (${op.resourceType}) is a stateful type — the old physical resource's data was destroyed by the replacement and CANNOT be recovered; the re-created resource starts empty.`);
|
|
24656
25149
|
const { provider: createProvider } = ctx.providerRegistry.getProviderFor({
|
|
@@ -24742,8 +25235,8 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
|
|
|
24742
25235
|
resourceType: op.resourceType,
|
|
24743
25236
|
provisionedBy: op.provisionedBy
|
|
24744
25237
|
});
|
|
24745
|
-
const desiredProps = await resolveReplayProps(previousState.properties, resolver, secrets);
|
|
24746
|
-
const currentProps = await resolveReplayProps(current.properties, resolver, secrets);
|
|
25238
|
+
const desiredProps = await resolveReplayProps(previousState.properties, resolver, secrets, ctx, op.logicalId);
|
|
25239
|
+
const currentProps = await resolveReplayProps(current.properties, resolver, secrets, ctx, op.logicalId);
|
|
24747
25240
|
const revertResult = await updateWithRollbackRetry(provider, [
|
|
24748
25241
|
op.logicalId,
|
|
24749
25242
|
current.physicalId,
|
|
@@ -24803,7 +25296,7 @@ async function replayFailedOperations(failedOps, stateResources, stackName, ctx,
|
|
|
24803
25296
|
remainingFailedOps: []
|
|
24804
25297
|
};
|
|
24805
25298
|
const { logger } = ctx;
|
|
24806
|
-
const resolver = new
|
|
25299
|
+
const resolver = new ReplayResolvers(ctx.region);
|
|
24807
25300
|
const emitEnvelope = options.emitEnvelope === true && failedOps.length > 0;
|
|
24808
25301
|
if (emitEnvelope) ctx.recordEvent?.({
|
|
24809
25302
|
eventType: "ROLLBACK_STARTED",
|
|
@@ -24894,8 +25387,8 @@ async function replayFailedOperations(failedOps, stateResources, stackName, ctx,
|
|
|
24894
25387
|
resourceType: op.resourceType,
|
|
24895
25388
|
provisionedBy: op.provisionedBy ?? current.provisionedBy
|
|
24896
25389
|
});
|
|
24897
|
-
const desiredProps = await resolveReplayProps(prev.properties, resolver, secrets);
|
|
24898
|
-
const attemptedProps = await resolveReplayProps(op.attemptedProperties ?? current.properties, resolver, secrets);
|
|
25390
|
+
const desiredProps = await resolveReplayProps(prev.properties, resolver, secrets, ctx, op.logicalId);
|
|
25391
|
+
const attemptedProps = await resolveReplayProps(op.attemptedProperties ?? current.properties, resolver, secrets, ctx, op.logicalId);
|
|
24899
25392
|
const revertFailedResult = await updateWithRollbackRetry(provider, [
|
|
24900
25393
|
op.logicalId,
|
|
24901
25394
|
current.physicalId,
|
|
@@ -25033,7 +25526,7 @@ const FLUSH_INTERVAL_MS = 2e3;
|
|
|
25033
25526
|
const FLUSH_EVENT_THRESHOLD = 50;
|
|
25034
25527
|
/** Build-time cdkd version, with a dev fallback for non-built contexts. */
|
|
25035
25528
|
function getCdkdVersion() {
|
|
25036
|
-
return "0.284.
|
|
25529
|
+
return "0.284.24";
|
|
25037
25530
|
}
|
|
25038
25531
|
/**
|
|
25039
25532
|
* Generate a time-sortable unique run id, e.g.
|
|
@@ -25583,6 +26076,92 @@ function deepEqualValue(a, b) {
|
|
|
25583
26076
|
}
|
|
25584
26077
|
return true;
|
|
25585
26078
|
}
|
|
26079
|
+
/**
|
|
26080
|
+
* The `imports` / `outputReads` records to persist on a save that is NOT the
|
|
26081
|
+
* final success save — the UNION of the pre-deploy snapshot and what THIS
|
|
26082
|
+
* session resolved (issue
|
|
26083
|
+
* [#2057](https://github.com/go-to-k/cdkd/issues/2057) review).
|
|
26084
|
+
*
|
|
26085
|
+
* Every non-success save used to write `currentState.imports` /
|
|
26086
|
+
* `currentState.outputReads` verbatim, i.e. the PRE-DEPLOY snapshot, while
|
|
26087
|
+
* writing the POST-deploy `newResources` beside it. So a deploy that
|
|
26088
|
+
* introduced a cross-stack read and then failed persisted resources built FROM
|
|
26089
|
+
* that read next to a record that does not mention it. Two consequences, and
|
|
26090
|
+
* only the first is about #2057:
|
|
26091
|
+
*
|
|
26092
|
+
* 1. A rollback journal exists only after a FAILED deploy, so
|
|
26093
|
+
* {@link producerRegionsFromState} saw an empty list on exactly the deploy
|
|
26094
|
+
* that introduces a cross-region secret read — and
|
|
26095
|
+
* `classifyReplaySecretRegion` answered `local`, resolving the producer's
|
|
26096
|
+
* region-less expression in the consumer's region. The refusal was inert
|
|
26097
|
+
* where it mattered most.
|
|
26098
|
+
* 2. INDEPENDENT PRE-EXISTING BUG. `state.imports[]` is what
|
|
26099
|
+
* `findActiveImportConsumers` (`src/cli/commands/destroy-runner.ts`) scans
|
|
26100
|
+
* to refuse destroying a producer while a consumer still imports from it,
|
|
26101
|
+
* and `state.outputReads[]` is what `findDownstreamConsumers`
|
|
26102
|
+
* (`src/cli/commands/recreate-downstream-consumers.ts`) enumerates. A
|
|
26103
|
+
* failed deploy therefore silently DOWNGRADED a fresh strong reference to
|
|
26104
|
+
* no reference: the consumer's resource is live and recorded, its import is
|
|
26105
|
+
* not, and `cdkd destroy` on the producer sails through the strong-ref
|
|
26106
|
+
* pre-flight. This exists on main today, with or without #2057.
|
|
26107
|
+
*
|
|
26108
|
+
* DIRECTION OF THE RESIDUAL, stated rather than left to be discovered: a union
|
|
26109
|
+
* never drops a record, so a stack that STOPS reading across a region keeps the
|
|
26110
|
+
* stale entry until its next SUCCESSFUL deploy, whose save replaces the list
|
|
26111
|
+
* wholesale (`imports: [...this.recordedImports]`). Until then a purely-local
|
|
26112
|
+
* rollback can be refused on the strength of a read the template no longer has.
|
|
26113
|
+
* That is the fail-closed side — a clear error naming the region to reconcile,
|
|
26114
|
+
* versus a silent wrong-secret write — and the same asymmetry already justifies
|
|
26115
|
+
* preserving the snapshot at all (dropping it would strip a live strong-ref
|
|
26116
|
+
* record on every diff-clean deploy).
|
|
26117
|
+
*
|
|
26118
|
+
* THE RULE IS "EVERY SAVE EXCEPT THE TERMINAL SUCCESS ONE", and it is stated
|
|
26119
|
+
* that way rather than as "every non-success save" because the latter is loose
|
|
26120
|
+
* in both directions: the diff-clean no-change save in `doDeploy` is a SUCCESS
|
|
26121
|
+
* outcome and unions anyway (nothing was re-resolved, so the union is an
|
|
26122
|
+
* identity there and one rule beats an exception), while
|
|
26123
|
+
* `persistStateAfterOutputFailure` looks like a success save — provisioning
|
|
26124
|
+
* was clean — and is not one.
|
|
26125
|
+
*
|
|
26126
|
+
* THE ENUMERATION IS NOT KEPT HERE, DELIBERATELY. Two prose counts in this
|
|
26127
|
+
* lane were measured wrong (an "ALL FIVE" that missed
|
|
26128
|
+
* `persistStateAfterOutputFailure`, and a "three post-rollback saves" that is
|
|
26129
|
+
* two), and each wrong count is worse than none: it is the sentence a reader
|
|
26130
|
+
* uses to conclude the rule is already applied everywhere.
|
|
26131
|
+
* `tests/unit/deployment/deploy-engine-cross-stack-read-writers.test.ts`
|
|
26132
|
+
* derives the set instead — it SCANS this file for every `imports:` /
|
|
26133
|
+
* `outputReads:` object key that writes a VALUE and fails on any that is not
|
|
26134
|
+
* the one allow-listed success-path write, with a positive control proving the
|
|
26135
|
+
* scan can see a violation. A save site added here fails that test rather than
|
|
26136
|
+
* escaping silently, so the authority on "where is this applied" is a grep the
|
|
26137
|
+
* test performs, not a number anybody has to maintain.
|
|
26138
|
+
*/
|
|
26139
|
+
function crossStackReadsForPartialSave(previous, recordedImports, recordedOutputReads) {
|
|
26140
|
+
const imports = unionCrossStackReads(previous.imports, recordedImports, (e) => `${e.sourceStack}\u0000${canonicalizeRegion(e.sourceRegion)}\u0000${e.exportName}`);
|
|
26141
|
+
const outputReads = unionCrossStackReads(previous.outputReads, recordedOutputReads, (e) => `${e.sourceStack}\u0000${canonicalizeRegion(e.sourceRegion)}\u0000${e.outputName}`);
|
|
26142
|
+
return {
|
|
26143
|
+
...imports.length > 0 && { imports },
|
|
26144
|
+
...outputReads.length > 0 && { outputReads }
|
|
26145
|
+
};
|
|
26146
|
+
}
|
|
26147
|
+
/**
|
|
26148
|
+
* Concatenate two cross-stack-read lists, dropping a later duplicate of an
|
|
26149
|
+
* identity an earlier entry already carries. First-seen wins, so the PRE-DEPLOY
|
|
26150
|
+
* spelling of a region survives — entries are COMPARED on a canonicalized
|
|
26151
|
+
* region but STORED verbatim, mirroring `producerRegionsFromState`.
|
|
26152
|
+
*/
|
|
26153
|
+
function unionCrossStackReads(previous, recorded, identity) {
|
|
26154
|
+
const seen = /* @__PURE__ */ new Set();
|
|
26155
|
+
const out = [];
|
|
26156
|
+
for (const entry of [...previous ?? [], ...recorded]) {
|
|
26157
|
+
if (entry === null || typeof entry !== "object") continue;
|
|
26158
|
+
const key = identity(entry);
|
|
26159
|
+
if (seen.has(key)) continue;
|
|
26160
|
+
seen.add(key);
|
|
26161
|
+
out.push(entry);
|
|
26162
|
+
}
|
|
26163
|
+
return out;
|
|
26164
|
+
}
|
|
25586
26165
|
var DeployEngine = class {
|
|
25587
26166
|
logger = getLogger().child("DeployEngine");
|
|
25588
26167
|
resolver;
|
|
@@ -26128,8 +26707,7 @@ var DeployEngine = class {
|
|
|
26128
26707
|
stackName: currentState.stackName,
|
|
26129
26708
|
resources: currentState.resources,
|
|
26130
26709
|
outputs: outputsChanged ? resolvedOutputs : persistedOutputs,
|
|
26131
|
-
...currentState
|
|
26132
|
-
...currentState.outputReads && currentState.outputReads.length > 0 && { outputReads: currentState.outputReads },
|
|
26710
|
+
...crossStackReadsForPartialSave(currentState, this.recordedImports, this.recordedOutputReads),
|
|
26133
26711
|
lastModified: Date.now()
|
|
26134
26712
|
};
|
|
26135
26713
|
const saveOptions = {};
|
|
@@ -26247,8 +26825,7 @@ var DeployEngine = class {
|
|
|
26247
26825
|
stackName: currentState.stackName,
|
|
26248
26826
|
resources: newResources,
|
|
26249
26827
|
outputs: currentState.outputs,
|
|
26250
|
-
...currentState
|
|
26251
|
-
...currentState.outputReads && currentState.outputReads.length > 0 && { outputReads: currentState.outputReads },
|
|
26828
|
+
...crossStackReadsForPartialSave(currentState, this.recordedImports, this.recordedOutputReads),
|
|
26252
26829
|
lastModified: Date.now()
|
|
26253
26830
|
};
|
|
26254
26831
|
const migrate = pendingMigration;
|
|
@@ -26378,8 +26955,7 @@ var DeployEngine = class {
|
|
|
26378
26955
|
stackName: currentState.stackName,
|
|
26379
26956
|
resources: newResources,
|
|
26380
26957
|
outputs: currentState.outputs,
|
|
26381
|
-
...currentState
|
|
26382
|
-
...currentState.outputReads && currentState.outputReads.length > 0 && { outputReads: currentState.outputReads },
|
|
26958
|
+
...crossStackReadsForPartialSave(currentState, this.recordedImports, this.recordedOutputReads),
|
|
26383
26959
|
lastModified: Date.now()
|
|
26384
26960
|
};
|
|
26385
26961
|
const migrate = pendingMigration;
|
|
@@ -26405,7 +26981,7 @@ var DeployEngine = class {
|
|
|
26405
26981
|
this.logger.warn("Partial state has been saved. Run 'cdkd deploy' to resume, 'cdkd rollback' to revert, or destroy to clean up.");
|
|
26406
26982
|
} else {
|
|
26407
26983
|
await this.writeRollbackJournalSegment(stackName, completedOperations, failedOperations, "auto-rollback-started", initialDeploy);
|
|
26408
|
-
autoRollbackClean = (await this.performRollback(completedOperations, newResources, stackName)).failures === 0;
|
|
26984
|
+
autoRollbackClean = (await this.performRollback(completedOperations, newResources, stackName, currentState)).failures === 0;
|
|
26409
26985
|
}
|
|
26410
26986
|
try {
|
|
26411
26987
|
const postRollbackState = {
|
|
@@ -26414,8 +26990,7 @@ var DeployEngine = class {
|
|
|
26414
26990
|
stackName: currentState.stackName,
|
|
26415
26991
|
resources: newResources,
|
|
26416
26992
|
outputs: currentState.outputs,
|
|
26417
|
-
...currentState
|
|
26418
|
-
...currentState.outputReads && currentState.outputReads.length > 0 && { outputReads: currentState.outputReads },
|
|
26993
|
+
...crossStackReadsForPartialSave(currentState, this.recordedImports, this.recordedOutputReads),
|
|
26419
26994
|
lastModified: Date.now()
|
|
26420
26995
|
};
|
|
26421
26996
|
await this.stateBackend.saveState(stackName, this.stackRegion, this.withParentInfo(postRollbackState), { ...currentEtag !== void 0 && { expectedEtag: currentEtag } });
|
|
@@ -26431,8 +27006,7 @@ var DeployEngine = class {
|
|
|
26431
27006
|
stackName: currentState.stackName,
|
|
26432
27007
|
resources: newResources,
|
|
26433
27008
|
outputs: currentState.outputs,
|
|
26434
|
-
...currentState
|
|
26435
|
-
...currentState.outputReads && currentState.outputReads.length > 0 && { outputReads: currentState.outputReads },
|
|
27009
|
+
...crossStackReadsForPartialSave(currentState, this.recordedImports, this.recordedOutputReads),
|
|
26436
27010
|
lastModified: Date.now()
|
|
26437
27011
|
};
|
|
26438
27012
|
await this.stateBackend.saveState(stackName, this.stackRegion, this.withParentInfo(postRollbackState), { ...freshEtag !== void 0 && { expectedEtag: freshEtag } });
|
|
@@ -26497,8 +27071,7 @@ var DeployEngine = class {
|
|
|
26497
27071
|
stackName: currentState.stackName,
|
|
26498
27072
|
resources: newResources,
|
|
26499
27073
|
outputs: currentState.outputs,
|
|
26500
|
-
...this.recordedImports
|
|
26501
|
-
...this.recordedOutputReads.length > 0 && { outputReads: [...this.recordedOutputReads] },
|
|
27074
|
+
...crossStackReadsForPartialSave(currentState, this.recordedImports, this.recordedOutputReads),
|
|
26502
27075
|
lastModified: Date.now()
|
|
26503
27076
|
});
|
|
26504
27077
|
try {
|
|
@@ -26525,8 +27098,8 @@ var DeployEngine = class {
|
|
|
26525
27098
|
* command drives identical semantics). Thin wrapper that builds the
|
|
26526
27099
|
* executor context from the engine's collaborators and delegates.
|
|
26527
27100
|
*/
|
|
26528
|
-
async performRollback(completedOperations, stateResources, stackName) {
|
|
26529
|
-
const result = await replayRollback(completedOperations, stateResources, stackName, this.rollbackExecutorContext());
|
|
27101
|
+
async performRollback(completedOperations, stateResources, stackName, previousState) {
|
|
27102
|
+
const result = await replayRollback(completedOperations, stateResources, stackName, this.rollbackExecutorContext(previousState));
|
|
26530
27103
|
return {
|
|
26531
27104
|
failures: result.failures,
|
|
26532
27105
|
warnings: result.warnings
|
|
@@ -26585,14 +27158,15 @@ var DeployEngine = class {
|
|
|
26585
27158
|
this.logger.info(`The automatic rollback restored the pre-deploy state. The failed resource's pre-failure record was kept — if it was left partially applied, run 'cdkd rollback ${stackName} --revert-failed' to revert it.`);
|
|
26586
27159
|
}
|
|
26587
27160
|
/** Build the {@link RollbackExecutorContext} from the engine's fields. */
|
|
26588
|
-
rollbackExecutorContext() {
|
|
27161
|
+
rollbackExecutorContext(previousState) {
|
|
26589
27162
|
return {
|
|
26590
27163
|
providerRegistry: this.providerRegistry,
|
|
26591
27164
|
region: this.stackRegion,
|
|
26592
27165
|
logger: this.logger,
|
|
26593
27166
|
recordEvent: (event) => this.recordEvent(event),
|
|
26594
27167
|
finalSnapshotClients: this.options.finalSnapshotClients,
|
|
26595
|
-
skipFinalSnapshot: this.options.skipFinalSnapshot
|
|
27168
|
+
skipFinalSnapshot: this.options.skipFinalSnapshot,
|
|
27169
|
+
importedProducerRegions: producerRegionsFromState(crossStackReadsForPartialSave(previousState, this.recordedImports, this.recordedOutputReads))
|
|
26596
27170
|
};
|
|
26597
27171
|
}
|
|
26598
27172
|
/**
|
|
@@ -27572,5 +28146,5 @@ var DeployEngine = class {
|
|
|
27572
28146
|
};
|
|
27573
28147
|
|
|
27574
28148
|
//#endregion
|
|
27575
|
-
export {
|
|
27576
|
-
//# sourceMappingURL=deploy-engine-
|
|
28149
|
+
export { disableInstanceApiTermination as $, NestedStackChildDirectDestroyError as $n, ensureAssetStorage as $t, isStatefulRecreateTargetSync as A, uploadCfnTemplate as An, s3BucketWebsiteUrl as At, collectPublishedOutputNames as B, getAwsClients as Bn, rebuildClientForBucketRegion as Bt, createPreDeleteFinalSnapshot as C, resolveUseCdkBootstrapAssets as Cn, maskSecretsInText as Ct, makeCanonicalizePropertiesFn as D, CFN_TEMPLATE_URL_LIMIT as Dn, s3BucketDomainName as Dt, unsupportedFinalSnapshotError as E, CFN_TEMPLATE_BODY_LIMIT as En, s3BucketArn as Et, gray as F, AssemblyReader as Fn, withRetry as Ft, IAMRoleProvider as G, ConfigError as Gn, buildAssetRedirectMap as Gt, isExportAliasCollision as H, setAwsClients as Hn, AssetPublisher as Ht, green as I, processStackMessages as In, DagBuilder as It, ProviderRegistry as J, LocalInvokeBuildError as Jn, rewriteTemplateAssetReferences as Jt, collectInlinePolicyNamesManagedBySiblings as K, DependencyError as Kn, createAssetRedirectResolver as Kt, red as L, clearBucketRegionCache as Ln, TemplateParser as Lt, formatResourceLine as M, PARTITION_TABLE as Mn, DiffCalculator as Mt, bold as N, canonicalizeRegion as Nn, INTRINSIC_KEYS as Nt, extractDeploymentEventError as O, MIGRATE_TMP_PREFIX as On, s3BucketDualStackDomainName as Ot, cyan as P, derivePartitionAndUrlSuffix as Pn, describeTypeWithThrottleRetry as Pt, slowCcOperationTimeoutMs as Q, MissingCdkCliError as Qn, BOOTSTRAP_MARKER_PREFIX as Qt, yellow as R, resolveBucketRegion as Rn, LockManager as Rt, ccRoutedFinalSnapshotError as S, resolveStateBucketWithDefaultAndSource as Sn, isSingleDynamicReferenceToken as St, refusesFinalSnapshot as T, warnDeprecatedNoPrefixCliFlag as Tn, scrubResourceRecord as Tt, secretBearingStateKeyWarning as U, AssetError as Un, stringifyValue as Ut, exportAliasCollisionScrubWarning as V, resetAwsClients as Vn, shouldRetainResource as Vt, stateKeySecretExposure as W, CdkdError as Wn, WorkGraph as Wt, findSilentDropProperties as X, LocalStartServiceError as Xn, stripControlChars as Xt, findActionableSilentDrops as Y, LocalMigrateError as Yn, escapeRegExp$1 as Yt, CloudControlProvider as Z, LockError as Zn, AssetModeResolver as Zt, IMPLICIT_DELETE_DEPENDENCIES as _, resolveApp as _n, STATE_SOURCED_CROSS_GENERATION_RULES as _t, DeploymentEventsStore as a, buildDenyExternalAccessPolicy as an, StackTerminationProtectionError as ar, WAFv2WebACLProvider as at, PRE_DELETE_SNAPSHOT_TYPES as b, resolveSkipPrefix as bn, createSecretMasker as bt, producerRegionsFromState as c, getDockerCmd as cn, formatError as cr, assertRegionMatch as ct, updatePartialMessage as d, AssetManifestLoader as dn, withErrorHandling as dr, configStringRefusal as dt, getBootstrapMarkerKey as en, PartialFailureError as er, isTerminationProtectionPropagationError as et, updatePartialReason as f, getDockerImageBySourceHash as fn, isMarkedNonRetryable as fr, readConfigString as ft, maskingRetryLogger as g, getLegacyStateBucketName as gn, __exportAll as gr, requireConfigString as gt, withResourceDeadline as h, getDefaultStateBucketName as hn, markNonRetryable as hr, requireConfigObject as ht, DeploymentEventsReader as i, validateContainerRepoName as in, StackHasActiveImportsError as ir, refStateLookupFromResource as it, renderStatefulReason as j, expectedOwnerParam as jn, applyRoleArnIfSet as jt, MULTI_REGION_RECREATE_BLOCKED_TYPES as k, findLargeInlineResources as kn, s3BucketRegionalDomainName as kt, replayFailedOperations as l, runDockerForeground as ln, isCdkdError as lr, coerceCfnBoolean as lt, deleteSkipReason as m, synthesisStatusMessage as mn, isThrottlingError as mr, requireConfigArray as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, readBootstrapMarkerBody as nn, ResourceTimeoutError as nr, cfnRefValueFromPhysicalId as nt, planFailedOps as o, buildDockerImage as on, StateError as or, normalizeAwsTagsToCfn as ot, UNSPECIFIED_SKIP_REASON as p, Synthesizer as pn, isRetryableTransientError as pr, replayWarn as pt, clearOnUpdateRemoval as q, DeployCancelledError as qn, loadPublishableAssetManifest as qt, DeployEngine as r, validateAssetBucketName as rn, ResourceUpdateNotSupportedError as rr, getAccountInfo as rt, planRollback as s, formatDockerLoginError as sn, SynthesisError as sr, resolveExplicitPhysicalId as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, parseBootstrapMarker as tn, ProvisioningError as tr, IntrinsicFunctionResolver as tt, replayRollback as u, runDockerStreaming as un, normalizeAwsError as ur, configBooleanRefusal as ut, computeImplicitDeleteEdges as v, resolveAutoAssetStorage as vn, STATE_SOURCED_READBACK_RULES as vt, isFinalSnapshotError as w, stateBucketExistenceConfirmed as wn, redactSecretsForState as wt, buildFinalSnapshotIdentifier as x, resolveStateBucketWithDefault as xn, dynamicReferenceTokens as xt, ATOMIC_FINAL_SNAPSHOT_TYPES as y, resolveCaptureObservedState as yn, TEMPLATE_SOURCED_RULES as yt, collectDeclaredOutputNames as z, AwsClients as zn, S3StateBackend as zt };
|
|
28150
|
+
//# sourceMappingURL=deploy-engine-BJQbni1s.js.map
|