@go-to-k/cdkd 0.284.22 → 0.284.23
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-Cucl2K22.js} +2 -2
- package/dist/{asg-provider-DLU0EEhO.js.map → asg-provider-Cucl2K22.js.map} +1 -1
- package/dist/cli.js +5 -4
- package/dist/cli.js.map +1 -1
- package/dist/{deploy-engine-DOlhIeGv.js → deploy-engine-HfFU96oJ.js} +468 -35
- package/dist/{deploy-engine-DOlhIeGv.js.map → deploy-engine-HfFU96oJ.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-Cucl2K22.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";
|
|
@@ -24227,7 +24227,7 @@ async function replayRollback(operations, stateResources, stackName, ctx, option
|
|
|
24227
24227
|
eventType: "ROLLBACK_STARTED",
|
|
24228
24228
|
stackName
|
|
24229
24229
|
});
|
|
24230
|
-
const resolver = new
|
|
24230
|
+
const resolver = new ReplayResolvers(ctx.region);
|
|
24231
24231
|
const { createOps, otherOps } = partitionOps(operations);
|
|
24232
24232
|
for (let i = otherOps.length - 1; i >= 0; i--) {
|
|
24233
24233
|
if (options.isInterrupted?.()) {
|
|
@@ -24292,6 +24292,21 @@ async function replayRollback(operations, stateResources, stackName, ctx, option
|
|
|
24292
24292
|
* Cognito `client_secret`). Rollback is synth-free, so re-resolve straight from
|
|
24293
24293
|
* the expression string here.
|
|
24294
24294
|
*
|
|
24295
|
+
* BOTH SIDES OF A DIFF ARE CLASSIFIED, not just the bag that is written, and
|
|
24296
|
+
* that is deliberate (issue #2057 review). The `revert` / `--revert-failed`
|
|
24297
|
+
* arms call this twice — once for the desired bag and once for the CURRENT /
|
|
24298
|
+
* ATTEMPTED one, which only becomes the provider's `previousProperties`. Two
|
|
24299
|
+
* things make a wrong-region value there consequential rather than cosmetic:
|
|
24300
|
+
* a patch-based provider computes its patch previous-vs-desired, so a wrong
|
|
24301
|
+
* previous side can emit a wrong patch or, when both sides carry the same
|
|
24302
|
+
* expression and resolve to the same wrong value, silently compute a NO-OP and
|
|
24303
|
+
* skip the revert entirely; and every resolved plaintext lands in the SHARED
|
|
24304
|
+
* per-op `secrets` map, which is the redaction needle for the state record this
|
|
24305
|
+
* op persists, so a foreign-region plaintext mis-redacts that record. In
|
|
24306
|
+
* practice both bags carry the SAME expression (state redacts them identically),
|
|
24307
|
+
* so scoping the refusal to the written bag would buy a rare case at the cost of
|
|
24308
|
+
* a rule nobody could apply by reading one call site.
|
|
24309
|
+
*
|
|
24295
24310
|
* Records each `plaintext -> expression` into `secrets` so the caller can redact
|
|
24296
24311
|
* the persisted state record back to the expression — the same
|
|
24297
24312
|
* resolve-for-provider + redact-for-state split the deploy engine applies at its
|
|
@@ -24305,28 +24320,365 @@ async function replayRollback(operations, stateResources, stackName, ctx, option
|
|
|
24305
24320
|
* `StringList` parameter is public config, stored resolved, and never appears
|
|
24306
24321
|
* as an expression in the journal.
|
|
24307
24322
|
*/
|
|
24308
|
-
async function resolveReplayProps(props,
|
|
24323
|
+
async function resolveReplayProps(props, resolvers, secrets, execCtx, logicalId) {
|
|
24309
24324
|
if (props === void 0) return void 0;
|
|
24310
|
-
const
|
|
24325
|
+
const resolverContext = {
|
|
24311
24326
|
template: { Resources: {} },
|
|
24312
24327
|
resources: {},
|
|
24313
24328
|
recordedSecretValues: secrets
|
|
24314
24329
|
};
|
|
24315
|
-
const walk = async (v) => {
|
|
24316
|
-
if (typeof v === "string")
|
|
24330
|
+
const walk = async (v, path) => {
|
|
24331
|
+
if (typeof v === "string") {
|
|
24332
|
+
if (!v.includes("{{resolve:")) return v;
|
|
24333
|
+
return await resolveLeafByRegion(v, path, logicalId, execCtx, resolvers, resolverContext);
|
|
24334
|
+
}
|
|
24317
24335
|
if (Array.isArray(v)) {
|
|
24318
24336
|
const out = new Array(v.length);
|
|
24319
|
-
for (let i = 0; i < v.length; i++) out[i] = await walk(v[i]);
|
|
24337
|
+
for (let i = 0; i < v.length; i++) out[i] = await walk(v[i], `${path}[${i}]`);
|
|
24320
24338
|
return out;
|
|
24321
24339
|
}
|
|
24322
24340
|
if (v !== null && typeof v === "object") {
|
|
24323
24341
|
const out = {};
|
|
24324
|
-
for (const [k, val] of Object.entries(v)) out[k] = await walk(val);
|
|
24342
|
+
for (const [k, val] of Object.entries(v)) out[k] = await walk(val, path === "" ? k : `${path}.${k}`);
|
|
24325
24343
|
return out;
|
|
24326
24344
|
}
|
|
24327
24345
|
return v;
|
|
24328
24346
|
};
|
|
24329
|
-
return await walk(props);
|
|
24347
|
+
return await walk(props, "");
|
|
24348
|
+
}
|
|
24349
|
+
/**
|
|
24350
|
+
* The `{{resolve:<service>:...}}` families whose value can be a SECRET, and
|
|
24351
|
+
* therefore the only ones the region question below is asked about: every
|
|
24352
|
+
* `secretsmanager` reference by spelling, and every `ssm` one, which is secret
|
|
24353
|
+
* exactly when its parameter is a `SecureString` (issue #1901).
|
|
24354
|
+
*
|
|
24355
|
+
* Every OTHER service is `local` because cdkd cannot resolve it at all, NOT
|
|
24356
|
+
* because it is public. `ssm-secure` is the live example and is emphatically
|
|
24357
|
+
* not public: `resolveDynamicReferences` has no arm for it, so the literal
|
|
24358
|
+
* token is passed through to AWS and CloudFormation resolves it SERVER-side.
|
|
24359
|
+
* cdkd never holds its value, so there is no region for cdkd to get wrong —
|
|
24360
|
+
* which is the only reason it can be waved through here.
|
|
24361
|
+
*/
|
|
24362
|
+
const REPLAY_SECRET_SERVICES = /* @__PURE__ */ new Set(["secretsmanager", "ssm"]);
|
|
24363
|
+
/**
|
|
24364
|
+
* Split a `{{resolve:secretsmanager:...}}` inner body into its SECRET_ID.
|
|
24365
|
+
*
|
|
24366
|
+
* Mirrors `IntrinsicFunctionResolver.resolveSecretsManagerReference`'s own
|
|
24367
|
+
* split — including the END-ANCHORED whole-secret form — because a secret ID
|
|
24368
|
+
* may legitimately contain colons (an ARN always does), so `split(':')[1]` is
|
|
24369
|
+
* wrong for exactly the shape this file cares about most.
|
|
24370
|
+
*/
|
|
24371
|
+
function secretsManagerSecretId(inner) {
|
|
24372
|
+
const afterService = inner.substring(15);
|
|
24373
|
+
let stringIdx = afterService.indexOf(":SecretString:");
|
|
24374
|
+
let binaryIdx = afterService.indexOf(":SecretBinary:");
|
|
24375
|
+
if (stringIdx < 0 && afterService.endsWith(":SecretString")) stringIdx = afterService.length - 13;
|
|
24376
|
+
if (binaryIdx < 0 && afterService.endsWith(":SecretBinary")) binaryIdx = afterService.length - 13;
|
|
24377
|
+
const delimiterIdx = stringIdx >= 0 && binaryIdx >= 0 ? Math.min(stringIdx, binaryIdx) : stringIdx >= 0 ? stringIdx : binaryIdx;
|
|
24378
|
+
return delimiterIdx >= 0 ? afterService.substring(0, delimiterIdx) : afterService;
|
|
24379
|
+
}
|
|
24380
|
+
/**
|
|
24381
|
+
* The parameter name an `{{resolve:ssm:...}}` reference asks for — byte-for-byte
|
|
24382
|
+
* what `IntrinsicFunctionResolver.resolveSSMReference` passes as `GetParameter`'s
|
|
24383
|
+
* `Name`, which is `parts.slice(1).join(':')` on the colon-split inner body.
|
|
24384
|
+
*
|
|
24385
|
+
* The whole remainder, deliberately, with NOTHING stripped:
|
|
24386
|
+
*
|
|
24387
|
+
* - An SSM dynamic reference CAN name a full ARN. The resolver joins the tail
|
|
24388
|
+
* back together, so `{{resolve:ssm:arn:aws:ssm:us-west-2:111122223333:parameter/db/pw}}`
|
|
24389
|
+
* reaches AWS as that ARN. A `split(':')[1]` here would yield the literal
|
|
24390
|
+
* `'arn'` — a parameter that does not exist — and then report the reference
|
|
24391
|
+
* as region-LESS and refuse it, which is the guess-in-the-other-direction the
|
|
24392
|
+
* `named-region` arm exists to prevent.
|
|
24393
|
+
* - A trailing `:<version>` / `:<label>` is part of the name AS SSM PARSES IT
|
|
24394
|
+
* (`GetParameter` accepts `name:3` / `name:prod`), so stripping it would name
|
|
24395
|
+
* a different thing in the refusal message than the one that would be read.
|
|
24396
|
+
*/
|
|
24397
|
+
function ssmParameterName(inner) {
|
|
24398
|
+
return inner.substring(4);
|
|
24399
|
+
}
|
|
24400
|
+
/**
|
|
24401
|
+
* The region an ARN names, or `undefined` for anything that is not an ARN with
|
|
24402
|
+
* a populated region field (`arn:<partition>:<service>:<region>:...`).
|
|
24403
|
+
*/
|
|
24404
|
+
function arnRegion(secretId) {
|
|
24405
|
+
if (!secretId.startsWith("arn:")) return void 0;
|
|
24406
|
+
const region = secretId.split(":")[3];
|
|
24407
|
+
return region ? region : void 0;
|
|
24408
|
+
}
|
|
24409
|
+
/**
|
|
24410
|
+
* The producer regions a stack's persisted cross-stack reads name, for
|
|
24411
|
+
* {@link RollbackExecutorContext.importedProducerRegions} (issue #2057).
|
|
24412
|
+
*
|
|
24413
|
+
* Both record kinds count, and for the same reason: each one is a value this
|
|
24414
|
+
* stack read out of ANOTHER region's state, so each one is a way a
|
|
24415
|
+
* foreign-region `{{resolve:...}}` expression can have reached this stack's own
|
|
24416
|
+
* record. `imports` is the strong `Fn::ImportValue` edge; `outputReads` is the
|
|
24417
|
+
* weak `Fn::GetStackOutput` one (schema v8), which is the EASIER of the two to
|
|
24418
|
+
* point across a region boundary because the reference carries its own
|
|
24419
|
+
* `Region` argument.
|
|
24420
|
+
*
|
|
24421
|
+
* Deduplicated case-insensitively, keeping each region's first-recorded
|
|
24422
|
+
* spelling so the refusal message echoes what the user will see in
|
|
24423
|
+
* `state.json`. The consumer's own region is deliberately NOT filtered here —
|
|
24424
|
+
* {@link classifyReplaySecretRegion} does that, because it is the one that
|
|
24425
|
+
* knows which region is asking.
|
|
24426
|
+
*
|
|
24427
|
+
* Exported so the two `RollbackExecutorContext` construction sites derive the
|
|
24428
|
+
* list identically — `cdkd rollback` from the state it loaded, and
|
|
24429
|
+
* `DeployEngine.rollbackExecutorContext` from `crossStackReadsForPartialSave`,
|
|
24430
|
+
* which unions that snapshot with the reads the failing deploy itself made.
|
|
24431
|
+
*/
|
|
24432
|
+
function producerRegionsFromState(state) {
|
|
24433
|
+
const seen = /* @__PURE__ */ new Set();
|
|
24434
|
+
const regions = [];
|
|
24435
|
+
for (const entry of [...state.imports ?? [], ...state.outputReads ?? []]) {
|
|
24436
|
+
const canonical = canonicalizeRegion(entry.sourceRegion);
|
|
24437
|
+
if (!canonical || seen.has(canonical)) continue;
|
|
24438
|
+
seen.add(canonical);
|
|
24439
|
+
regions.push(entry.sourceRegion);
|
|
24440
|
+
}
|
|
24441
|
+
return regions;
|
|
24442
|
+
}
|
|
24443
|
+
/**
|
|
24444
|
+
* Decide which region must answer for a single `{{resolve:...}}` expression a
|
|
24445
|
+
* rollback replay is about to re-resolve — issue
|
|
24446
|
+
* [#2057](https://github.com/go-to-k/cdkd/issues/2057).
|
|
24447
|
+
*
|
|
24448
|
+
* WHY A REPLAY CAN BE HOLDING A FOREIGN REGION'S EXPRESSION AT ALL. Since
|
|
24449
|
+
* issue #1934 a cross-stack consumer re-resolves a redacted producer value in
|
|
24450
|
+
* the PRODUCER's region (`reresolveCrossStackValue` /
|
|
24451
|
+
* `resolverForProducerRegion`) — correct, because a Secrets Manager secret or
|
|
24452
|
+
* an SSM `SecureString` of the same NAME in two regions is two independent
|
|
24453
|
+
* values. The plaintext is then recorded into the CONSUMER's
|
|
24454
|
+
* `recordedSecretValues`, so the consumer's `state.json` (and from there the
|
|
24455
|
+
* rollback journal) persists the PRODUCER's spelling of the expression. That is
|
|
24456
|
+
* the right thing to persist, and it is region-less: the reader cannot tell
|
|
24457
|
+
* from the string which region produced it.
|
|
24458
|
+
*
|
|
24459
|
+
* The replay rebuilds its resolver from the CONSUMER's region alone, so
|
|
24460
|
+
* re-resolving that expression locally answers from a same-named secret in the
|
|
24461
|
+
* wrong region and writes it to a LIVE resource. Silent, and on the recovery
|
|
24462
|
+
* path. The rule applied here is the family's, from issue #1957: A NAMED REGION
|
|
24463
|
+
* BINDS; NEVER SUBSTITUTE A GUESS. The three verdicts are that one sentence:
|
|
24464
|
+
*
|
|
24465
|
+
* - **`named-region`** — the expression's SECRET_ID is an ARN, which names its
|
|
24466
|
+
* own region. The region is ESTABLISHED, so it binds: the caller resolves
|
|
24467
|
+
* through a resolver pinned to it ({@link ReplayResolvers.forRegion}) rather
|
|
24468
|
+
* than refusing. Refusing here would be the guess in the other direction.
|
|
24469
|
+
*
|
|
24470
|
+
* cdkd would otherwise get this wrong, which is why the arm exists at all:
|
|
24471
|
+
* `resolveSecretsManagerReference` builds its client from
|
|
24472
|
+
* `this.explicitRegion` and passes the ARN through as an opaque `SecretId`,
|
|
24473
|
+
* and `@aws-sdk/client-secrets-manager`'s endpoint ruleset has NO
|
|
24474
|
+
* ARN-derived endpoint rule (unlike, say, S3 access points), so a
|
|
24475
|
+
* foreign-region ARN is sent to the stack's own regional endpoint. What the
|
|
24476
|
+
* SERVICE then does with it is not something this repo can settle offline —
|
|
24477
|
+
* see the fixture note in
|
|
24478
|
+
* `tests/integration/rollback-cross-region-secret/README.md`. Pinning the
|
|
24479
|
+
* client to the ARN's region is correct either way: if Secrets Manager would
|
|
24480
|
+
* have refused the foreign ARN, this turns a hard failure into a correct
|
|
24481
|
+
* resolution; if it would have honoured it, this reaches the same value by
|
|
24482
|
+
* the documented route. Neither outcome is a regression.
|
|
24483
|
+
*
|
|
24484
|
+
* - **`ambiguous`** — the expression names no region (the plain name form) AND
|
|
24485
|
+
* this stack has a foreign producer region on record
|
|
24486
|
+
* ({@link RollbackExecutorContext.importedProducerRegions}). Nothing on hand
|
|
24487
|
+
* can establish the origin, so the replay refuses instead of guessing.
|
|
24488
|
+
*
|
|
24489
|
+
* KNOWN OVER-REFUSAL, accepted deliberately, and WIDER THAN THE SSM CASE
|
|
24490
|
+
* ALONE — state both, because the second one is the common shape:
|
|
24491
|
+
*
|
|
24492
|
+
* (a) Any NAME-FORM `secretsmanager` reference in a stack that has ANY
|
|
24493
|
+
* foreign producer region on record is refused, even when that secret is
|
|
24494
|
+
* the stack's own purely-local one and has nothing to do with the
|
|
24495
|
+
* cross-region read. The evidence is per-STACK, not per-reference, so one
|
|
24496
|
+
* cross-region export plus one ordinary
|
|
24497
|
+
* `{{resolve:secretsmanager:mysecret:SecretString:pw}}` is enough — and CDK's
|
|
24498
|
+
* `secretValueFromJson` emits exactly that name form, so this is the shape
|
|
24499
|
+
* most people will meet. It also persists: with the union the producer
|
|
24500
|
+
* region stays on record until the next SUCCESSFUL deploy. Per-reference
|
|
24501
|
+
* evidence is what would narrow it, and that needs the region recorded
|
|
24502
|
+
* ALONGSIDE the expression — the persisted-shape change issue #2057
|
|
24503
|
+
* deliberately deferred (its options 1 and 2). Until then the refusal is
|
|
24504
|
+
* loud, names the ARN spelling as the remedy, and is the fail-closed side
|
|
24505
|
+
* of a trade whose other side is a silent wrong-secret write.
|
|
24506
|
+
*
|
|
24507
|
+
* (b) An `ssm` reference is secret only when its parameter is a
|
|
24508
|
+
* `SecureString`, and this arm cannot tell. So a `{{resolve:ssm:/app/env}}`
|
|
24509
|
+
* naming a PUBLIC `String` that reached a persisted bag (issue #2036's
|
|
24510
|
+
* acknowledged over-redaction) is refused too. Narrowing it by
|
|
24511
|
+
* `isRecordedSecretExpression` was considered and REJECTED, and not because
|
|
24512
|
+
* the store is unreachable — it is imported by this very file. It is
|
|
24513
|
+
* unusable: `recordedSecretExpressions` is populated BY resolution, and in
|
|
24514
|
+
* the standalone `cdkd rollback` process nothing has resolved anything when
|
|
24515
|
+
* the first op is classified, so the store is empty and every `ssm` verdict
|
|
24516
|
+
* would come back "not secret" — turning the protection off for exactly the
|
|
24517
|
+
* SecureString case it exists for. Worse, once one op DID resolve a
|
|
24518
|
+
* reference the store would be warm for the next, so the verdict would
|
|
24519
|
+
* depend on OP ORDER. A resolve-the-type-first probe is unsound for the
|
|
24520
|
+
* same reason the whole issue exists: the TYPE is region-dependent (#1957),
|
|
24521
|
+
* so probing locally can report `String` for a name that is `SecureString`
|
|
24522
|
+
* in the producer's region and wave through the very write this refuses.
|
|
24523
|
+
* The residual is therefore a loud, actionable error on a narrow
|
|
24524
|
+
* intersection (an over-redacted public ssm reference AND a cross-region
|
|
24525
|
+
* read on record), which is the fail-closed side of the trade.
|
|
24526
|
+
*
|
|
24527
|
+
* - **`local`** — everything else, which is the overwhelmingly common case:
|
|
24528
|
+
* every non-secret service, every same-region ARN (the ordinary CDK
|
|
24529
|
+
* `secretValueFromJson` shape), and every name-form expression in a stack
|
|
24530
|
+
* with no foreign producer region recorded. Resolved exactly as before this
|
|
24531
|
+
* change.
|
|
24532
|
+
*
|
|
24533
|
+
* A same-region ARN answers `local` even when a foreign producer region IS on
|
|
24534
|
+
* record: the expression settles the question itself, so the weaker evidence
|
|
24535
|
+
* never gets consulted.
|
|
24536
|
+
*/
|
|
24537
|
+
function classifyReplaySecretRegion(expression, consumerRegion, importedProducerRegions) {
|
|
24538
|
+
const inner = expression.startsWith("{{resolve:") ? expression.slice(10, -2) : void 0;
|
|
24539
|
+
if (inner === void 0) return { kind: "local" };
|
|
24540
|
+
const service = inner.split(":")[0];
|
|
24541
|
+
if (service === void 0 || !REPLAY_SECRET_SERVICES.has(service)) return { kind: "local" };
|
|
24542
|
+
const secretName = service === "secretsmanager" ? secretsManagerSecretId(inner) : ssmParameterName(inner);
|
|
24543
|
+
if (!secretName) return { kind: "local" };
|
|
24544
|
+
const named = arnRegion(secretName);
|
|
24545
|
+
if (named !== void 0) return canonicalizeRegion(named) === canonicalizeRegion(consumerRegion) ? { kind: "local" } : {
|
|
24546
|
+
kind: "named-region",
|
|
24547
|
+
secretName,
|
|
24548
|
+
region: named
|
|
24549
|
+
};
|
|
24550
|
+
const seen = /* @__PURE__ */ new Set();
|
|
24551
|
+
const foreignProducerRegions = [];
|
|
24552
|
+
for (const candidate of importedProducerRegions ?? []) {
|
|
24553
|
+
const canonical = canonicalizeRegion(candidate);
|
|
24554
|
+
if (!canonical || canonical === canonicalizeRegion(consumerRegion)) continue;
|
|
24555
|
+
if (seen.has(canonical)) continue;
|
|
24556
|
+
seen.add(canonical);
|
|
24557
|
+
foreignProducerRegions.push(candidate);
|
|
24558
|
+
}
|
|
24559
|
+
if (foreignProducerRegions.length === 0) return { kind: "local" };
|
|
24560
|
+
return {
|
|
24561
|
+
kind: "ambiguous",
|
|
24562
|
+
secretName,
|
|
24563
|
+
foreignProducerRegions
|
|
24564
|
+
};
|
|
24565
|
+
}
|
|
24566
|
+
/**
|
|
24567
|
+
* The replay's resolvers: the stack's own, plus one pinned sibling per FOREIGN
|
|
24568
|
+
* region an ARN-named reference asks for (issue #2057).
|
|
24569
|
+
*
|
|
24570
|
+
* One instance per replay, not per op — the resolved-value cache lives on the
|
|
24571
|
+
* resolver INSTANCE since issue #1933, so a resolver per op would re-fetch every
|
|
24572
|
+
* referenced secret once per op. The pinned siblings are cached here for the
|
|
24573
|
+
* same reason: a 100-op replay of a bag carrying one foreign ARN must pay one
|
|
24574
|
+
* `GetSecretValue`, not a hundred.
|
|
24575
|
+
*
|
|
24576
|
+
* A pinned sibling is a PLAIN resolver, deliberately NOT the resolver class's
|
|
24577
|
+
* own `producerRegionGuest` (which the class sets on the siblings
|
|
24578
|
+
* `resolverForProducerRegion` builds, to stop a foreign region pinning a verdict
|
|
24579
|
+
* in the process-global `recordedSecretExpressions` store — the issue #1933
|
|
24580
|
+
* shape, where an `ssm` parameter whose TYPE differs by region has one region's
|
|
24581
|
+
* verdict decide the other's redaction).
|
|
24582
|
+
*
|
|
24583
|
+
* WHY A GUEST FLAG IS NOT NEEDED HERE, and the argument has to be this one
|
|
24584
|
+
* rather than "only `secretsmanager` routes to a sibling" (that earlier claim
|
|
24585
|
+
* was FALSE — `resolveSSMReference` joins its colon-split tail back together, so
|
|
24586
|
+
* an `ssm` reference CAN name a full ARN and CAN therefore route here):
|
|
24587
|
+
*
|
|
24588
|
+
* {@link ReplayResolvers.forRegion} is reached ONLY from a `named-region`
|
|
24589
|
+
* verdict, which `classifyReplaySecretRegion` returns only when the
|
|
24590
|
+
* SECRET_ID / parameter name starts with `arn:` and carries a region. So a
|
|
24591
|
+
* pinned sibling only ever resolves an expression whose KEY EMBEDS THE
|
|
24592
|
+
* REGION IT IS BEING RESOLVED IN.
|
|
24593
|
+
*
|
|
24594
|
+
* The store is keyed by the expression string alone, and that is exactly what
|
|
24595
|
+
* makes #1933 possible: two regions sharing one key. An ARN-form key cannot be
|
|
24596
|
+
* shared by two regions, so a verdict pinned from a sibling can never contradict
|
|
24597
|
+
* another region's for the same key. If a future change ever routes a
|
|
24598
|
+
* region-LESS expression to `forRegion`, this argument dies with it and the
|
|
24599
|
+
* sibling needs the guest flag.
|
|
24600
|
+
*/
|
|
24601
|
+
var ReplayResolvers = class {
|
|
24602
|
+
/** The stack's own resolver — every `local` verdict resolves through this. */
|
|
24603
|
+
primary;
|
|
24604
|
+
pinned = /* @__PURE__ */ new Map();
|
|
24605
|
+
stackRegion;
|
|
24606
|
+
constructor(stackRegion) {
|
|
24607
|
+
this.stackRegion = stackRegion;
|
|
24608
|
+
this.primary = new IntrinsicFunctionResolver(stackRegion);
|
|
24609
|
+
}
|
|
24610
|
+
/** The resolver that must answer for `region` — `primary` when it is the stack's own. */
|
|
24611
|
+
forRegion(region) {
|
|
24612
|
+
const target = canonicalizeRegion(region);
|
|
24613
|
+
if (target === canonicalizeRegion(this.stackRegion)) return this.primary;
|
|
24614
|
+
const cached = this.pinned.get(target);
|
|
24615
|
+
if (cached) return cached;
|
|
24616
|
+
const scoped = new IntrinsicFunctionResolver(target);
|
|
24617
|
+
this.pinned.set(target, scoped);
|
|
24618
|
+
return scoped;
|
|
24619
|
+
}
|
|
24620
|
+
};
|
|
24621
|
+
/**
|
|
24622
|
+
* The refusal an `ambiguous` replay reference throws (issue #2057).
|
|
24623
|
+
*
|
|
24624
|
+
* A plain throw, like the final-snapshot refusals above and for the same
|
|
24625
|
+
* reason: the per-op catch in {@link replaySingle} /
|
|
24626
|
+
* {@link replayFailedOperations} counts it as a failure, which keeps the
|
|
24627
|
+
* journal segment and lets the user re-run once the reference is disambiguated.
|
|
24628
|
+
* Refusing is strictly better than the alternative it replaces — resolving a
|
|
24629
|
+
* producer-region reference against the consumer's region does not fail, it
|
|
24630
|
+
* succeeds with the WRONG credential and writes it to a resource that is live.
|
|
24631
|
+
*
|
|
24632
|
+
* Names the reference, the regions, and the remedy. Never the resolved value:
|
|
24633
|
+
* nothing here has resolved anything yet, and the expression is the same string
|
|
24634
|
+
* `state.json` already stores in the clear.
|
|
24635
|
+
*/
|
|
24636
|
+
function regionAmbiguousReplaySecretError(logicalId, propertyPath, secretName, foreignProducerRegions, consumerRegion) {
|
|
24637
|
+
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");
|
|
24638
|
+
}
|
|
24639
|
+
/**
|
|
24640
|
+
* Re-resolve one LEAF string, sending each `{{resolve:...}}` reference in it to
|
|
24641
|
+
* the region {@link classifyReplaySecretRegion} says must answer (issue #2057).
|
|
24642
|
+
*
|
|
24643
|
+
* Refuses FIRST, over the whole leaf, before any reference is fetched: a leaf
|
|
24644
|
+
* can splice several references together, and resolving the safe ones first
|
|
24645
|
+
* would leave half a credential fetched (and cached, and recorded as a
|
|
24646
|
+
* redaction needle) for an op that is about to be refused anyway.
|
|
24647
|
+
*
|
|
24648
|
+
* Then TWO paths, and the split is deliberate rather than an optimisation:
|
|
24649
|
+
*
|
|
24650
|
+
* - With no foreign-region reference — every leaf on every existing code path
|
|
24651
|
+
* — the leaf goes to `resolveDynamicReferences` WHOLE, exactly as before this
|
|
24652
|
+
* change. That method has its own well-tested substitution semantics (it
|
|
24653
|
+
* collects matches from the ORIGINAL string, so a resolved plaintext that is
|
|
24654
|
+
* itself token-shaped is never re-resolved — issue #1917), and this change
|
|
24655
|
+
* does not want to relitigate any of it.
|
|
24656
|
+
* - With one, the leaf is rebuilt segment by segment so each reference can be
|
|
24657
|
+
* resolved by its OWN region's resolver. `resolveDynamicReferences` resolves
|
|
24658
|
+
* every token in the string it is handed with the one resolver it is called
|
|
24659
|
+
* on, so a mixed leaf cannot be served by a single call. Each token is
|
|
24660
|
+
* resolved ALONE and its result concatenated, which means no resolved value
|
|
24661
|
+
* is ever re-scanned for tokens either.
|
|
24662
|
+
*
|
|
24663
|
+
* `dynamicReferenceTokens` returns the tokens in order and non-overlapping, so
|
|
24664
|
+
* walking the leaf with a moving `indexOf` cursor reproduces their positions
|
|
24665
|
+
* exactly, duplicates included.
|
|
24666
|
+
*/
|
|
24667
|
+
async function resolveLeafByRegion(leaf, propertyPath, logicalId, execCtx, resolvers, resolverContext) {
|
|
24668
|
+
const verdicts = dynamicReferenceTokens(leaf).map((token) => [token, classifyReplaySecretRegion(token, execCtx.region, execCtx.importedProducerRegions)]);
|
|
24669
|
+
for (const [, verdict] of verdicts) if (verdict.kind === "ambiguous") throw regionAmbiguousReplaySecretError(logicalId, propertyPath, verdict.secretName, verdict.foreignProducerRegions, execCtx.region);
|
|
24670
|
+
if (!verdicts.some(([, verdict]) => verdict.kind === "named-region")) return await resolvers.primary.resolveDynamicReferences(leaf, resolverContext);
|
|
24671
|
+
let out = "";
|
|
24672
|
+
let cursor = 0;
|
|
24673
|
+
for (const [token, verdict] of verdicts) {
|
|
24674
|
+
const at = leaf.indexOf(token, cursor);
|
|
24675
|
+
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");
|
|
24676
|
+
out += leaf.slice(cursor, at);
|
|
24677
|
+
const resolver = verdict.kind === "named-region" ? resolvers.forRegion(verdict.region) : resolvers.primary;
|
|
24678
|
+
out += await resolver.resolveDynamicReferences(token, resolverContext);
|
|
24679
|
+
cursor = at + token.length;
|
|
24680
|
+
}
|
|
24681
|
+
return out + leaf.slice(cursor);
|
|
24330
24682
|
}
|
|
24331
24683
|
/**
|
|
24332
24684
|
* Redact resolved secret plaintext back out of a post-rollback state record
|
|
@@ -24650,7 +25002,7 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
|
|
|
24650
25002
|
case "reverse-replacement": {
|
|
24651
25003
|
const current = stateResources[op.logicalId];
|
|
24652
25004
|
const prev = op.previousState;
|
|
24653
|
-
const resolvedPrevProps = await resolveReplayProps(prev.properties, resolver, secrets) ?? {};
|
|
25005
|
+
const resolvedPrevProps = await resolveReplayProps(prev.properties, resolver, secrets, ctx, op.logicalId) ?? {};
|
|
24654
25006
|
logger.info(` Rollback: Reversing replacement of ${op.logicalId} (${op.resourceType}) — re-creating the old resource and deleting the new one`);
|
|
24655
25007
|
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
25008
|
const { provider: createProvider } = ctx.providerRegistry.getProviderFor({
|
|
@@ -24742,8 +25094,8 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
|
|
|
24742
25094
|
resourceType: op.resourceType,
|
|
24743
25095
|
provisionedBy: op.provisionedBy
|
|
24744
25096
|
});
|
|
24745
|
-
const desiredProps = await resolveReplayProps(previousState.properties, resolver, secrets);
|
|
24746
|
-
const currentProps = await resolveReplayProps(current.properties, resolver, secrets);
|
|
25097
|
+
const desiredProps = await resolveReplayProps(previousState.properties, resolver, secrets, ctx, op.logicalId);
|
|
25098
|
+
const currentProps = await resolveReplayProps(current.properties, resolver, secrets, ctx, op.logicalId);
|
|
24747
25099
|
const revertResult = await updateWithRollbackRetry(provider, [
|
|
24748
25100
|
op.logicalId,
|
|
24749
25101
|
current.physicalId,
|
|
@@ -24803,7 +25155,7 @@ async function replayFailedOperations(failedOps, stateResources, stackName, ctx,
|
|
|
24803
25155
|
remainingFailedOps: []
|
|
24804
25156
|
};
|
|
24805
25157
|
const { logger } = ctx;
|
|
24806
|
-
const resolver = new
|
|
25158
|
+
const resolver = new ReplayResolvers(ctx.region);
|
|
24807
25159
|
const emitEnvelope = options.emitEnvelope === true && failedOps.length > 0;
|
|
24808
25160
|
if (emitEnvelope) ctx.recordEvent?.({
|
|
24809
25161
|
eventType: "ROLLBACK_STARTED",
|
|
@@ -24894,8 +25246,8 @@ async function replayFailedOperations(failedOps, stateResources, stackName, ctx,
|
|
|
24894
25246
|
resourceType: op.resourceType,
|
|
24895
25247
|
provisionedBy: op.provisionedBy ?? current.provisionedBy
|
|
24896
25248
|
});
|
|
24897
|
-
const desiredProps = await resolveReplayProps(prev.properties, resolver, secrets);
|
|
24898
|
-
const attemptedProps = await resolveReplayProps(op.attemptedProperties ?? current.properties, resolver, secrets);
|
|
25249
|
+
const desiredProps = await resolveReplayProps(prev.properties, resolver, secrets, ctx, op.logicalId);
|
|
25250
|
+
const attemptedProps = await resolveReplayProps(op.attemptedProperties ?? current.properties, resolver, secrets, ctx, op.logicalId);
|
|
24899
25251
|
const revertFailedResult = await updateWithRollbackRetry(provider, [
|
|
24900
25252
|
op.logicalId,
|
|
24901
25253
|
current.physicalId,
|
|
@@ -25033,7 +25385,7 @@ const FLUSH_INTERVAL_MS = 2e3;
|
|
|
25033
25385
|
const FLUSH_EVENT_THRESHOLD = 50;
|
|
25034
25386
|
/** Build-time cdkd version, with a dev fallback for non-built contexts. */
|
|
25035
25387
|
function getCdkdVersion() {
|
|
25036
|
-
return "0.284.
|
|
25388
|
+
return "0.284.23";
|
|
25037
25389
|
}
|
|
25038
25390
|
/**
|
|
25039
25391
|
* Generate a time-sortable unique run id, e.g.
|
|
@@ -25583,6 +25935,92 @@ function deepEqualValue(a, b) {
|
|
|
25583
25935
|
}
|
|
25584
25936
|
return true;
|
|
25585
25937
|
}
|
|
25938
|
+
/**
|
|
25939
|
+
* The `imports` / `outputReads` records to persist on a save that is NOT the
|
|
25940
|
+
* final success save — the UNION of the pre-deploy snapshot and what THIS
|
|
25941
|
+
* session resolved (issue
|
|
25942
|
+
* [#2057](https://github.com/go-to-k/cdkd/issues/2057) review).
|
|
25943
|
+
*
|
|
25944
|
+
* Every non-success save used to write `currentState.imports` /
|
|
25945
|
+
* `currentState.outputReads` verbatim, i.e. the PRE-DEPLOY snapshot, while
|
|
25946
|
+
* writing the POST-deploy `newResources` beside it. So a deploy that
|
|
25947
|
+
* introduced a cross-stack read and then failed persisted resources built FROM
|
|
25948
|
+
* that read next to a record that does not mention it. Two consequences, and
|
|
25949
|
+
* only the first is about #2057:
|
|
25950
|
+
*
|
|
25951
|
+
* 1. A rollback journal exists only after a FAILED deploy, so
|
|
25952
|
+
* {@link producerRegionsFromState} saw an empty list on exactly the deploy
|
|
25953
|
+
* that introduces a cross-region secret read — and
|
|
25954
|
+
* `classifyReplaySecretRegion` answered `local`, resolving the producer's
|
|
25955
|
+
* region-less expression in the consumer's region. The refusal was inert
|
|
25956
|
+
* where it mattered most.
|
|
25957
|
+
* 2. INDEPENDENT PRE-EXISTING BUG. `state.imports[]` is what
|
|
25958
|
+
* `findActiveImportConsumers` (`src/cli/commands/destroy-runner.ts`) scans
|
|
25959
|
+
* to refuse destroying a producer while a consumer still imports from it,
|
|
25960
|
+
* and `state.outputReads[]` is what `findDownstreamConsumers`
|
|
25961
|
+
* (`src/cli/commands/recreate-downstream-consumers.ts`) enumerates. A
|
|
25962
|
+
* failed deploy therefore silently DOWNGRADED a fresh strong reference to
|
|
25963
|
+
* no reference: the consumer's resource is live and recorded, its import is
|
|
25964
|
+
* not, and `cdkd destroy` on the producer sails through the strong-ref
|
|
25965
|
+
* pre-flight. This exists on main today, with or without #2057.
|
|
25966
|
+
*
|
|
25967
|
+
* DIRECTION OF THE RESIDUAL, stated rather than left to be discovered: a union
|
|
25968
|
+
* never drops a record, so a stack that STOPS reading across a region keeps the
|
|
25969
|
+
* stale entry until its next SUCCESSFUL deploy, whose save replaces the list
|
|
25970
|
+
* wholesale (`imports: [...this.recordedImports]`). Until then a purely-local
|
|
25971
|
+
* rollback can be refused on the strength of a read the template no longer has.
|
|
25972
|
+
* That is the fail-closed side — a clear error naming the region to reconcile,
|
|
25973
|
+
* versus a silent wrong-secret write — and the same asymmetry already justifies
|
|
25974
|
+
* preserving the snapshot at all (dropping it would strip a live strong-ref
|
|
25975
|
+
* record on every diff-clean deploy).
|
|
25976
|
+
*
|
|
25977
|
+
* THE RULE IS "EVERY SAVE EXCEPT THE TERMINAL SUCCESS ONE", and it is stated
|
|
25978
|
+
* that way rather than as "every non-success save" because the latter is loose
|
|
25979
|
+
* in both directions: the diff-clean no-change save in `doDeploy` is a SUCCESS
|
|
25980
|
+
* outcome and unions anyway (nothing was re-resolved, so the union is an
|
|
25981
|
+
* identity there and one rule beats an exception), while
|
|
25982
|
+
* `persistStateAfterOutputFailure` looks like a success save — provisioning
|
|
25983
|
+
* was clean — and is not one.
|
|
25984
|
+
*
|
|
25985
|
+
* THE ENUMERATION IS NOT KEPT HERE, DELIBERATELY. Two prose counts in this
|
|
25986
|
+
* lane were measured wrong (an "ALL FIVE" that missed
|
|
25987
|
+
* `persistStateAfterOutputFailure`, and a "three post-rollback saves" that is
|
|
25988
|
+
* two), and each wrong count is worse than none: it is the sentence a reader
|
|
25989
|
+
* uses to conclude the rule is already applied everywhere.
|
|
25990
|
+
* `tests/unit/deployment/deploy-engine-cross-stack-read-writers.test.ts`
|
|
25991
|
+
* derives the set instead — it SCANS this file for every `imports:` /
|
|
25992
|
+
* `outputReads:` object key that writes a VALUE and fails on any that is not
|
|
25993
|
+
* the one allow-listed success-path write, with a positive control proving the
|
|
25994
|
+
* scan can see a violation. A save site added here fails that test rather than
|
|
25995
|
+
* escaping silently, so the authority on "where is this applied" is a grep the
|
|
25996
|
+
* test performs, not a number anybody has to maintain.
|
|
25997
|
+
*/
|
|
25998
|
+
function crossStackReadsForPartialSave(previous, recordedImports, recordedOutputReads) {
|
|
25999
|
+
const imports = unionCrossStackReads(previous.imports, recordedImports, (e) => `${e.sourceStack}\u0000${canonicalizeRegion(e.sourceRegion)}\u0000${e.exportName}`);
|
|
26000
|
+
const outputReads = unionCrossStackReads(previous.outputReads, recordedOutputReads, (e) => `${e.sourceStack}\u0000${canonicalizeRegion(e.sourceRegion)}\u0000${e.outputName}`);
|
|
26001
|
+
return {
|
|
26002
|
+
...imports.length > 0 && { imports },
|
|
26003
|
+
...outputReads.length > 0 && { outputReads }
|
|
26004
|
+
};
|
|
26005
|
+
}
|
|
26006
|
+
/**
|
|
26007
|
+
* Concatenate two cross-stack-read lists, dropping a later duplicate of an
|
|
26008
|
+
* identity an earlier entry already carries. First-seen wins, so the PRE-DEPLOY
|
|
26009
|
+
* spelling of a region survives — entries are COMPARED on a canonicalized
|
|
26010
|
+
* region but STORED verbatim, mirroring `producerRegionsFromState`.
|
|
26011
|
+
*/
|
|
26012
|
+
function unionCrossStackReads(previous, recorded, identity) {
|
|
26013
|
+
const seen = /* @__PURE__ */ new Set();
|
|
26014
|
+
const out = [];
|
|
26015
|
+
for (const entry of [...previous ?? [], ...recorded]) {
|
|
26016
|
+
if (entry === null || typeof entry !== "object") continue;
|
|
26017
|
+
const key = identity(entry);
|
|
26018
|
+
if (seen.has(key)) continue;
|
|
26019
|
+
seen.add(key);
|
|
26020
|
+
out.push(entry);
|
|
26021
|
+
}
|
|
26022
|
+
return out;
|
|
26023
|
+
}
|
|
25586
26024
|
var DeployEngine = class {
|
|
25587
26025
|
logger = getLogger().child("DeployEngine");
|
|
25588
26026
|
resolver;
|
|
@@ -26128,8 +26566,7 @@ var DeployEngine = class {
|
|
|
26128
26566
|
stackName: currentState.stackName,
|
|
26129
26567
|
resources: currentState.resources,
|
|
26130
26568
|
outputs: outputsChanged ? resolvedOutputs : persistedOutputs,
|
|
26131
|
-
...currentState
|
|
26132
|
-
...currentState.outputReads && currentState.outputReads.length > 0 && { outputReads: currentState.outputReads },
|
|
26569
|
+
...crossStackReadsForPartialSave(currentState, this.recordedImports, this.recordedOutputReads),
|
|
26133
26570
|
lastModified: Date.now()
|
|
26134
26571
|
};
|
|
26135
26572
|
const saveOptions = {};
|
|
@@ -26247,8 +26684,7 @@ var DeployEngine = class {
|
|
|
26247
26684
|
stackName: currentState.stackName,
|
|
26248
26685
|
resources: newResources,
|
|
26249
26686
|
outputs: currentState.outputs,
|
|
26250
|
-
...currentState
|
|
26251
|
-
...currentState.outputReads && currentState.outputReads.length > 0 && { outputReads: currentState.outputReads },
|
|
26687
|
+
...crossStackReadsForPartialSave(currentState, this.recordedImports, this.recordedOutputReads),
|
|
26252
26688
|
lastModified: Date.now()
|
|
26253
26689
|
};
|
|
26254
26690
|
const migrate = pendingMigration;
|
|
@@ -26378,8 +26814,7 @@ var DeployEngine = class {
|
|
|
26378
26814
|
stackName: currentState.stackName,
|
|
26379
26815
|
resources: newResources,
|
|
26380
26816
|
outputs: currentState.outputs,
|
|
26381
|
-
...currentState
|
|
26382
|
-
...currentState.outputReads && currentState.outputReads.length > 0 && { outputReads: currentState.outputReads },
|
|
26817
|
+
...crossStackReadsForPartialSave(currentState, this.recordedImports, this.recordedOutputReads),
|
|
26383
26818
|
lastModified: Date.now()
|
|
26384
26819
|
};
|
|
26385
26820
|
const migrate = pendingMigration;
|
|
@@ -26405,7 +26840,7 @@ var DeployEngine = class {
|
|
|
26405
26840
|
this.logger.warn("Partial state has been saved. Run 'cdkd deploy' to resume, 'cdkd rollback' to revert, or destroy to clean up.");
|
|
26406
26841
|
} else {
|
|
26407
26842
|
await this.writeRollbackJournalSegment(stackName, completedOperations, failedOperations, "auto-rollback-started", initialDeploy);
|
|
26408
|
-
autoRollbackClean = (await this.performRollback(completedOperations, newResources, stackName)).failures === 0;
|
|
26843
|
+
autoRollbackClean = (await this.performRollback(completedOperations, newResources, stackName, currentState)).failures === 0;
|
|
26409
26844
|
}
|
|
26410
26845
|
try {
|
|
26411
26846
|
const postRollbackState = {
|
|
@@ -26414,8 +26849,7 @@ var DeployEngine = class {
|
|
|
26414
26849
|
stackName: currentState.stackName,
|
|
26415
26850
|
resources: newResources,
|
|
26416
26851
|
outputs: currentState.outputs,
|
|
26417
|
-
...currentState
|
|
26418
|
-
...currentState.outputReads && currentState.outputReads.length > 0 && { outputReads: currentState.outputReads },
|
|
26852
|
+
...crossStackReadsForPartialSave(currentState, this.recordedImports, this.recordedOutputReads),
|
|
26419
26853
|
lastModified: Date.now()
|
|
26420
26854
|
};
|
|
26421
26855
|
await this.stateBackend.saveState(stackName, this.stackRegion, this.withParentInfo(postRollbackState), { ...currentEtag !== void 0 && { expectedEtag: currentEtag } });
|
|
@@ -26431,8 +26865,7 @@ var DeployEngine = class {
|
|
|
26431
26865
|
stackName: currentState.stackName,
|
|
26432
26866
|
resources: newResources,
|
|
26433
26867
|
outputs: currentState.outputs,
|
|
26434
|
-
...currentState
|
|
26435
|
-
...currentState.outputReads && currentState.outputReads.length > 0 && { outputReads: currentState.outputReads },
|
|
26868
|
+
...crossStackReadsForPartialSave(currentState, this.recordedImports, this.recordedOutputReads),
|
|
26436
26869
|
lastModified: Date.now()
|
|
26437
26870
|
};
|
|
26438
26871
|
await this.stateBackend.saveState(stackName, this.stackRegion, this.withParentInfo(postRollbackState), { ...freshEtag !== void 0 && { expectedEtag: freshEtag } });
|
|
@@ -26497,8 +26930,7 @@ var DeployEngine = class {
|
|
|
26497
26930
|
stackName: currentState.stackName,
|
|
26498
26931
|
resources: newResources,
|
|
26499
26932
|
outputs: currentState.outputs,
|
|
26500
|
-
...this.recordedImports
|
|
26501
|
-
...this.recordedOutputReads.length > 0 && { outputReads: [...this.recordedOutputReads] },
|
|
26933
|
+
...crossStackReadsForPartialSave(currentState, this.recordedImports, this.recordedOutputReads),
|
|
26502
26934
|
lastModified: Date.now()
|
|
26503
26935
|
});
|
|
26504
26936
|
try {
|
|
@@ -26525,8 +26957,8 @@ var DeployEngine = class {
|
|
|
26525
26957
|
* command drives identical semantics). Thin wrapper that builds the
|
|
26526
26958
|
* executor context from the engine's collaborators and delegates.
|
|
26527
26959
|
*/
|
|
26528
|
-
async performRollback(completedOperations, stateResources, stackName) {
|
|
26529
|
-
const result = await replayRollback(completedOperations, stateResources, stackName, this.rollbackExecutorContext());
|
|
26960
|
+
async performRollback(completedOperations, stateResources, stackName, previousState) {
|
|
26961
|
+
const result = await replayRollback(completedOperations, stateResources, stackName, this.rollbackExecutorContext(previousState));
|
|
26530
26962
|
return {
|
|
26531
26963
|
failures: result.failures,
|
|
26532
26964
|
warnings: result.warnings
|
|
@@ -26585,14 +27017,15 @@ var DeployEngine = class {
|
|
|
26585
27017
|
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
27018
|
}
|
|
26587
27019
|
/** Build the {@link RollbackExecutorContext} from the engine's fields. */
|
|
26588
|
-
rollbackExecutorContext() {
|
|
27020
|
+
rollbackExecutorContext(previousState) {
|
|
26589
27021
|
return {
|
|
26590
27022
|
providerRegistry: this.providerRegistry,
|
|
26591
27023
|
region: this.stackRegion,
|
|
26592
27024
|
logger: this.logger,
|
|
26593
27025
|
recordEvent: (event) => this.recordEvent(event),
|
|
26594
27026
|
finalSnapshotClients: this.options.finalSnapshotClients,
|
|
26595
|
-
skipFinalSnapshot: this.options.skipFinalSnapshot
|
|
27027
|
+
skipFinalSnapshot: this.options.skipFinalSnapshot,
|
|
27028
|
+
importedProducerRegions: producerRegionsFromState(crossStackReadsForPartialSave(previousState, this.recordedImports, this.recordedOutputReads))
|
|
26596
27029
|
};
|
|
26597
27030
|
}
|
|
26598
27031
|
/**
|
|
@@ -27572,5 +28005,5 @@ var DeployEngine = class {
|
|
|
27572
28005
|
};
|
|
27573
28006
|
|
|
27574
28007
|
//#endregion
|
|
27575
|
-
export {
|
|
27576
|
-
//# sourceMappingURL=deploy-engine-
|
|
28008
|
+
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 };
|
|
28009
|
+
//# sourceMappingURL=deploy-engine-HfFU96oJ.js.map
|