@go-to-k/cdkd 0.263.1 → 0.264.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -2
- package/dist/{asg-provider-DgyaW_9Q.js → asg-provider-B5N9p1vq.js} +2 -2
- package/dist/{asg-provider-DgyaW_9Q.js.map → asg-provider-B5N9p1vq.js.map} +1 -1
- package/dist/cli.js +79 -9
- package/dist/cli.js.map +1 -1
- package/dist/{deploy-engine-MdcxM5WC.js → deploy-engine-hlS6y_-0.js} +372 -17
- package/dist/deploy-engine-hlS6y_-0.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/deploy-engine-MdcxM5WC.js.map +0 -1
|
@@ -5821,6 +5821,26 @@ var S3StateBackend = class {
|
|
|
5821
5821
|
await this.putRawObject(this.getRollbackJournalKey(stackName, region), JSON.stringify(journal, null, 2));
|
|
5822
5822
|
}
|
|
5823
5823
|
/**
|
|
5824
|
+
* Replace the `failedOperations` list on the NEWEST journal segment
|
|
5825
|
+
* (issue #1198) with the ops STILL pending after a `--revert-failed`
|
|
5826
|
+
* replay — an empty list removes the field. Called right after the
|
|
5827
|
+
* failed-op replay, BEFORE the segment's completed ops replay, so a later
|
|
5828
|
+
* completed-op failure that keeps the segment for a re-run does not
|
|
5829
|
+
* re-issue the already-applied failed-op reverts (the journal's
|
|
5830
|
+
* `attemptedProperties` would generate a patch undoing changes that are no
|
|
5831
|
+
* longer present, which can fail on patch-based providers). Per-op — a
|
|
5832
|
+
* partially-successful replay strips only the handled ops. No-op when the
|
|
5833
|
+
* journal / segment / field is absent.
|
|
5834
|
+
*/
|
|
5835
|
+
async setRollbackJournalFailedOperations(stackName, region, remaining) {
|
|
5836
|
+
const journal = await this.loadRollbackJournal(stackName, region);
|
|
5837
|
+
const newest = journal?.segments[journal.segments.length - 1];
|
|
5838
|
+
if (!journal || !newest || !newest.failedOperations) return;
|
|
5839
|
+
if (remaining.length === 0) delete newest.failedOperations;
|
|
5840
|
+
else newest.failedOperations = remaining;
|
|
5841
|
+
await this.putRawObject(this.getRollbackJournalKey(stackName, region), JSON.stringify(journal, null, 2));
|
|
5842
|
+
}
|
|
5843
|
+
/**
|
|
5824
5844
|
* Pop the newest segment off the stack's rollback journal after it has
|
|
5825
5845
|
* been fully replayed. When the last segment is removed, the journal
|
|
5826
5846
|
* object is deleted entirely. Returns the number of segments remaining.
|
|
@@ -11240,7 +11260,7 @@ var CloudControlProvider = class {
|
|
|
11240
11260
|
this.logger.debug(`Deleting resource ${logicalId} (${resourceType}), physical ID: ${physicalId}`);
|
|
11241
11261
|
if (context?.removeProtection === true && resourceType === "AWS::AutoScaling::AutoScalingGroup") {
|
|
11242
11262
|
this.logger.debug(`Delegating protected AutoScalingGroup ${logicalId} delete to the SDK ASGProvider (Cloud Control cannot force-delete a protected ASG)`);
|
|
11243
|
-
const { ASGProvider } = await import("./asg-provider-
|
|
11263
|
+
const { ASGProvider } = await import("./asg-provider-B5N9p1vq.js").then((n) => n.n);
|
|
11244
11264
|
await new ASGProvider().delete(logicalId, physicalId, resourceType, _properties, context);
|
|
11245
11265
|
return;
|
|
11246
11266
|
}
|
|
@@ -11902,6 +11922,25 @@ var CustomResourceProvider = class CustomResourceProvider {
|
|
|
11902
11922
|
responseBucket;
|
|
11903
11923
|
responsePrefix;
|
|
11904
11924
|
/**
|
|
11925
|
+
* Memoization for the lazy response-bucket region correction
|
|
11926
|
+
* (`ensureResponseClient`). Mirrors the `clientResolved` /
|
|
11927
|
+
* `resolveInFlight` pattern of the three other state-bucket S3
|
|
11928
|
+
* consumers (S3StateBackend / LockManager / ExportIndexStore), plus a
|
|
11929
|
+
* generation counter: `setResponseBucket` bumps it so a probe that was
|
|
11930
|
+
* still in flight when the bucket was re-set cannot commit its stale
|
|
11931
|
+
* client / resolved flag against the new bucket.
|
|
11932
|
+
*/
|
|
11933
|
+
responseClientResolved = false;
|
|
11934
|
+
responseClientResolveInFlight = null;
|
|
11935
|
+
responseClientGeneration = 0;
|
|
11936
|
+
/**
|
|
11937
|
+
* Whether `this.s3Client` is a provider-OWNED client (built from the
|
|
11938
|
+
* `setResponseBucket` region hint or by a region-correction rebuild)
|
|
11939
|
+
* vs the shared `AwsClients.s3` instance from the constructor. Owned
|
|
11940
|
+
* clients are `destroy()`ed when replaced; the shared one never is.
|
|
11941
|
+
*/
|
|
11942
|
+
ownsS3Client = false;
|
|
11943
|
+
/**
|
|
11905
11944
|
* Opt out of the deploy engine's outer transient-error retry loop.
|
|
11906
11945
|
*
|
|
11907
11946
|
* The loop re-invokes `provider.create()` from the top on a transient
|
|
@@ -11981,7 +12020,67 @@ var CustomResourceProvider = class CustomResourceProvider {
|
|
|
11981
12020
|
*/
|
|
11982
12021
|
setResponseBucket(bucket, bucketRegion) {
|
|
11983
12022
|
this.responseBucket = bucket;
|
|
11984
|
-
if (bucketRegion) this.
|
|
12023
|
+
if (bucketRegion) this.replaceS3Client(new S3Client({ region: bucketRegion }));
|
|
12024
|
+
this.responseClientGeneration++;
|
|
12025
|
+
this.responseClientResolved = false;
|
|
12026
|
+
this.responseClientResolveInFlight = null;
|
|
12027
|
+
}
|
|
12028
|
+
/**
|
|
12029
|
+
* Swap `this.s3Client`, destroying the previous client when the
|
|
12030
|
+
* provider owned it (never the shared `AwsClients.s3` instance).
|
|
12031
|
+
* The optional call tolerates test doubles without a `destroy`.
|
|
12032
|
+
*/
|
|
12033
|
+
replaceS3Client(replacement) {
|
|
12034
|
+
if (this.ownsS3Client) this.s3Client.destroy?.();
|
|
12035
|
+
this.s3Client = replacement;
|
|
12036
|
+
this.ownsS3Client = true;
|
|
12037
|
+
}
|
|
12038
|
+
/**
|
|
12039
|
+
* Resolve the response bucket's actual region and, if it differs from the
|
|
12040
|
+
* current S3 client's configured region, swap in a region-corrected client
|
|
12041
|
+
* before any response-bucket S3 operation (placeholder `PutObject`,
|
|
12042
|
+
* pre-signed `ResponseURL` signing, response polling, cleanup).
|
|
12043
|
+
*
|
|
12044
|
+
* The response bucket is cdkd's state bucket, which can live in a
|
|
12045
|
+
* different region from the deploy region (`cdkd deploy --region` /
|
|
12046
|
+
* `AWS_REGION` against the account-scoped region-free default bucket).
|
|
12047
|
+
* A pre-signed URL's host is region-specific, so signing with the deploy
|
|
12048
|
+
* region against a foreign-region bucket makes S3 return a
|
|
12049
|
+
* 301 PermanentRedirect (issue #1195). Mirrors the lazy
|
|
12050
|
+
* `ensureClientForBucket()` correction the state backend (#60), the
|
|
12051
|
+
* LockManager (#803), and the ExportIndexStore (#819) already do via the
|
|
12052
|
+
* shared `rebuildClientForBucketRegion` helper (#827).
|
|
12053
|
+
*
|
|
12054
|
+
* `tolerateNonStandardClient` keeps test doubles (a bare `{ send }`
|
|
12055
|
+
* object from a mocked `getAwsClients`) on the no-rebuild path, and
|
|
12056
|
+
* `resolveBucketRegion` never throws (probe failures degrade to
|
|
12057
|
+
* "no rebuild"), so this can only improve the client's region.
|
|
12058
|
+
*/
|
|
12059
|
+
async ensureResponseClient() {
|
|
12060
|
+
if (this.responseClientResolved || !this.responseBucket) return;
|
|
12061
|
+
if (this.responseClientResolveInFlight) return this.responseClientResolveInFlight;
|
|
12062
|
+
const bucket = this.responseBucket;
|
|
12063
|
+
const generation = this.responseClientGeneration;
|
|
12064
|
+
this.responseClientResolveInFlight = (async () => {
|
|
12065
|
+
try {
|
|
12066
|
+
const replacement = await rebuildClientForBucketRegion(this.s3Client, bucket, {
|
|
12067
|
+
reuseClientCredentials: true,
|
|
12068
|
+
tolerateNonStandardClient: true,
|
|
12069
|
+
onRebuild: ({ bucketRegion, currentRegion }) => {
|
|
12070
|
+
this.logger.debug(`Custom resource response bucket '${bucket}' is in '${bucketRegion}' (client was '${String(currentRegion)}'); building a region-corrected S3 client for response operations.`);
|
|
12071
|
+
}
|
|
12072
|
+
});
|
|
12073
|
+
if (generation !== this.responseClientGeneration) {
|
|
12074
|
+
replacement?.destroy?.();
|
|
12075
|
+
return;
|
|
12076
|
+
}
|
|
12077
|
+
if (replacement) this.replaceS3Client(replacement);
|
|
12078
|
+
this.responseClientResolved = true;
|
|
12079
|
+
} finally {
|
|
12080
|
+
if (generation === this.responseClientGeneration) this.responseClientResolveInFlight = null;
|
|
12081
|
+
}
|
|
12082
|
+
})();
|
|
12083
|
+
return this.responseClientResolveInFlight;
|
|
11985
12084
|
}
|
|
11986
12085
|
/**
|
|
11987
12086
|
* Create a custom resource by invoking its Lambda handler
|
|
@@ -12354,6 +12453,7 @@ var CustomResourceProvider = class CustomResourceProvider {
|
|
|
12354
12453
|
*/
|
|
12355
12454
|
async generateResponseURL(responseKey) {
|
|
12356
12455
|
if (!this.responseBucket) return "https://localhost/cfn-response-not-configured";
|
|
12456
|
+
await this.ensureResponseClient();
|
|
12357
12457
|
await this.s3Client.send(new PutObjectCommand({
|
|
12358
12458
|
Bucket: this.responseBucket,
|
|
12359
12459
|
Key: responseKey,
|
|
@@ -16452,12 +16552,42 @@ function classifyRollbackOp(op, stateResources, orphanLogicalIds) {
|
|
|
16452
16552
|
}
|
|
16453
16553
|
const current = stateResources[op.logicalId];
|
|
16454
16554
|
if (!current) return "skip-absent";
|
|
16455
|
-
if (
|
|
16456
|
-
if (
|
|
16555
|
+
if (replacement) {
|
|
16556
|
+
if (current.physicalId === op.previousState.physicalId) return "skip-already-done";
|
|
16557
|
+
if (op.physicalId !== void 0 && current.physicalId !== op.physicalId) {
|
|
16558
|
+
if (deepEqual(current.properties, op.previousState.properties)) return "skip-already-done";
|
|
16559
|
+
return "skip-mismatch";
|
|
16560
|
+
}
|
|
16561
|
+
return op.previousState.updateReplacePolicy === "Retain" ? "reverse-replacement-readopt" : "reverse-replacement";
|
|
16457
16562
|
}
|
|
16563
|
+
if (op.previousState && deepEqual(current.properties, op.previousState.properties)) return "skip-already-done";
|
|
16458
16564
|
return "revert";
|
|
16459
16565
|
}
|
|
16460
16566
|
/**
|
|
16567
|
+
* Classify what reverting a FAILED in-flight op (issue #1198) will do
|
|
16568
|
+
* against the current state, without touching AWS. Pure — used by both the
|
|
16569
|
+
* command's `--revert-failed` plan preview and {@link replayFailedOperations}.
|
|
16570
|
+
*/
|
|
16571
|
+
function classifyFailedOp(op, stateResources) {
|
|
16572
|
+
if (op.changeType === "DELETE") return "skip-failed-noop";
|
|
16573
|
+
const current = stateResources[op.logicalId];
|
|
16574
|
+
if (op.changeType === "CREATE") {
|
|
16575
|
+
if (op.physicalId === void 0) return "skip-failed-unknown";
|
|
16576
|
+
if (!current) return "skip-failed-noop";
|
|
16577
|
+
if (current.physicalId !== op.physicalId) return "skip-failed-noop";
|
|
16578
|
+
return "delete-failed-create";
|
|
16579
|
+
}
|
|
16580
|
+
if (!current || !op.previousState) return "skip-failed-absent";
|
|
16581
|
+
return "revert-failed-update";
|
|
16582
|
+
}
|
|
16583
|
+
/** Build the plan items for a segment's failed ops (issue #1198). */
|
|
16584
|
+
function planFailedOps(failedOps, stateResources) {
|
|
16585
|
+
return failedOps.map((op) => ({
|
|
16586
|
+
op,
|
|
16587
|
+
action: classifyFailedOp(op, stateResources)
|
|
16588
|
+
}));
|
|
16589
|
+
}
|
|
16590
|
+
/**
|
|
16461
16591
|
* Build the full ordered plan for a list of ops (one segment). Mirrors the
|
|
16462
16592
|
* replay order: UPDATE/DELETE first (reverse completion order), then CREATE
|
|
16463
16593
|
* deletions in dependency-aware order.
|
|
@@ -16515,7 +16645,7 @@ async function replayRollback(operations, stateResources, stackName, ctx, option
|
|
|
16515
16645
|
result.interrupted = true;
|
|
16516
16646
|
break;
|
|
16517
16647
|
}
|
|
16518
|
-
await replaySingle(otherOps[i], stateResources, stackName, ctx, orphanLogicalIds, result, options.afterOp);
|
|
16648
|
+
await replaySingle(otherOps[i], stateResources, stackName, ctx, orphanLogicalIds, result, options.afterOp, options.isInterrupted);
|
|
16519
16649
|
}
|
|
16520
16650
|
if (!result.interrupted && createOps.length > 0) {
|
|
16521
16651
|
const sorted = sortRollbackCreates(createOps, stateResources);
|
|
@@ -16524,7 +16654,7 @@ async function replayRollback(operations, stateResources, stackName, ctx, option
|
|
|
16524
16654
|
result.interrupted = true;
|
|
16525
16655
|
break;
|
|
16526
16656
|
}
|
|
16527
|
-
await replaySingle(op, stateResources, stackName, ctx, orphanLogicalIds, result, options.afterOp);
|
|
16657
|
+
await replaySingle(op, stateResources, stackName, ctx, orphanLogicalIds, result, options.afterOp, options.isInterrupted);
|
|
16528
16658
|
}
|
|
16529
16659
|
}
|
|
16530
16660
|
ctx.logger.info("Rollback completed. Some resources may remain if deletion failed.");
|
|
@@ -16534,7 +16664,7 @@ async function replayRollback(operations, stateResources, stackName, ctx, option
|
|
|
16534
16664
|
});
|
|
16535
16665
|
return result;
|
|
16536
16666
|
}
|
|
16537
|
-
async function replaySingle(op, stateResources, stackName, ctx, orphanLogicalIds, result, afterOp) {
|
|
16667
|
+
async function replaySingle(op, stateResources, stackName, ctx, orphanLogicalIds, result, afterOp, isInterrupted) {
|
|
16538
16668
|
const action = classifyRollbackOp(op, stateResources, orphanLogicalIds);
|
|
16539
16669
|
const { logger } = ctx;
|
|
16540
16670
|
try {
|
|
@@ -16607,6 +16737,93 @@ async function replaySingle(op, stateResources, stackName, ctx, orphanLogicalIds
|
|
|
16607
16737
|
});
|
|
16608
16738
|
return;
|
|
16609
16739
|
}
|
|
16740
|
+
case "reverse-replacement-readopt": {
|
|
16741
|
+
const current = stateResources[op.logicalId];
|
|
16742
|
+
const prev = op.previousState;
|
|
16743
|
+
logger.info(` Rollback: Reversing replacement of ${op.logicalId} (${op.resourceType}) — deleting the new resource and re-adopting the retained old one (${prev.physicalId})`);
|
|
16744
|
+
const { provider: newDeleteProvider } = ctx.providerRegistry.getProviderFor({
|
|
16745
|
+
resourceType: op.resourceType,
|
|
16746
|
+
provisionedBy: current.provisionedBy ?? op.provisionedBy
|
|
16747
|
+
});
|
|
16748
|
+
await newDeleteProvider.delete(op.logicalId, current.physicalId, op.resourceType, current.properties, { expectedRegion: ctx.region });
|
|
16749
|
+
stateResources[op.logicalId] = prev;
|
|
16750
|
+
logger.info(` Rollback: ${op.logicalId} restored to the retained old resource`);
|
|
16751
|
+
await afterOp?.(op.logicalId);
|
|
16752
|
+
ctx.recordEvent?.({
|
|
16753
|
+
eventType: "ROLLBACK_RESOURCE_SUCCEEDED",
|
|
16754
|
+
stackName,
|
|
16755
|
+
operation: "UPDATE",
|
|
16756
|
+
logicalId: op.logicalId,
|
|
16757
|
+
resourceType: op.resourceType,
|
|
16758
|
+
...op.provisionedBy && { provisionedBy: op.provisionedBy }
|
|
16759
|
+
});
|
|
16760
|
+
return;
|
|
16761
|
+
}
|
|
16762
|
+
case "reverse-replacement": {
|
|
16763
|
+
const current = stateResources[op.logicalId];
|
|
16764
|
+
const prev = op.previousState;
|
|
16765
|
+
logger.info(` Rollback: Reversing replacement of ${op.logicalId} (${op.resourceType}) — re-creating the old resource and deleting the new one`);
|
|
16766
|
+
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.`);
|
|
16767
|
+
const { provider: createProvider } = ctx.providerRegistry.getProviderFor({
|
|
16768
|
+
resourceType: op.resourceType,
|
|
16769
|
+
provisionedBy: prev.provisionedBy
|
|
16770
|
+
});
|
|
16771
|
+
const { provider: newDeleteProvider } = ctx.providerRegistry.getProviderFor({
|
|
16772
|
+
resourceType: op.resourceType,
|
|
16773
|
+
provisionedBy: current.provisionedBy ?? op.provisionedBy
|
|
16774
|
+
});
|
|
16775
|
+
let deletedNewFirst = false;
|
|
16776
|
+
let createResult;
|
|
16777
|
+
try {
|
|
16778
|
+
createResult = await createProvider.create(op.logicalId, op.resourceType, { ...prev.properties });
|
|
16779
|
+
} catch (createError) {
|
|
16780
|
+
const msg = createError instanceof Error ? createError.message : String(createError);
|
|
16781
|
+
if (!(/already exists/i.test(msg) || msg.includes("AlreadyExists"))) throw createError;
|
|
16782
|
+
logger.info(` Rollback: re-create collided with the new resource's name — deleting the new resource (${current.physicalId}) first...`);
|
|
16783
|
+
await newDeleteProvider.delete(op.logicalId, current.physicalId, op.resourceType, current.properties, { expectedRegion: ctx.region });
|
|
16784
|
+
deletedNewFirst = true;
|
|
16785
|
+
delete stateResources[op.logicalId];
|
|
16786
|
+
await afterOp?.(op.logicalId);
|
|
16787
|
+
try {
|
|
16788
|
+
createResult = await withRetry(() => createProvider.create(op.logicalId, op.resourceType, { ...prev.properties }), op.logicalId, {
|
|
16789
|
+
maxRetries: 5,
|
|
16790
|
+
initialDelayMs: 2e3,
|
|
16791
|
+
maxDelayMs: 1e4,
|
|
16792
|
+
logger,
|
|
16793
|
+
...isInterrupted && {
|
|
16794
|
+
isInterrupted,
|
|
16795
|
+
onInterrupted: () => /* @__PURE__ */ new Error("Rollback interrupted while waiting for the old name to release")
|
|
16796
|
+
},
|
|
16797
|
+
isRetryable: (message) => /already exists/i.test(message) || message.includes("AlreadyExists")
|
|
16798
|
+
});
|
|
16799
|
+
} catch (recreateError) {
|
|
16800
|
+
throw new Error(`Failed to re-create the old ${op.logicalId} after the new resource (${current.physicalId}) was already deleted: ${recreateError instanceof Error ? recreateError.message : String(recreateError)}. The resource is now absent — fix forward with 'cdkd deploy'.`);
|
|
16801
|
+
}
|
|
16802
|
+
}
|
|
16803
|
+
const { observedProperties: _staleObserved, ...prevRecord } = prev;
|
|
16804
|
+
stateResources[op.logicalId] = {
|
|
16805
|
+
...prevRecord,
|
|
16806
|
+
physicalId: createResult.physicalId,
|
|
16807
|
+
attributes: createResult.attributes ?? {}
|
|
16808
|
+
};
|
|
16809
|
+
await afterOp?.(op.logicalId);
|
|
16810
|
+
if (!deletedNewFirst) try {
|
|
16811
|
+
await newDeleteProvider.delete(op.logicalId, current.physicalId, op.resourceType, current.properties, { expectedRegion: ctx.region });
|
|
16812
|
+
} catch (deleteError) {
|
|
16813
|
+
logger.warn(` Rollback: old ${op.logicalId} re-created, but deleting the new resource (${current.physicalId}) failed: ${deleteError instanceof Error ? deleteError.message : String(deleteError)}. Delete it manually — it is no longer tracked in state.`);
|
|
16814
|
+
result.warnings++;
|
|
16815
|
+
}
|
|
16816
|
+
logger.info(` Rollback: ${op.logicalId} replacement reversed (old resource re-created as ${createResult.physicalId})`);
|
|
16817
|
+
ctx.recordEvent?.({
|
|
16818
|
+
eventType: "ROLLBACK_RESOURCE_SUCCEEDED",
|
|
16819
|
+
stackName,
|
|
16820
|
+
operation: "UPDATE",
|
|
16821
|
+
logicalId: op.logicalId,
|
|
16822
|
+
resourceType: op.resourceType,
|
|
16823
|
+
...op.provisionedBy && { provisionedBy: op.provisionedBy }
|
|
16824
|
+
});
|
|
16825
|
+
return;
|
|
16826
|
+
}
|
|
16610
16827
|
case "revert": {
|
|
16611
16828
|
if (!op.previousState) {
|
|
16612
16829
|
logger.warn(` Rollback: Cannot restore ${op.logicalId} — no previous state available`);
|
|
@@ -16654,6 +16871,115 @@ async function replaySingle(op, stateResources, stackName, ctx, orphanLogicalIds
|
|
|
16654
16871
|
});
|
|
16655
16872
|
}
|
|
16656
16873
|
}
|
|
16874
|
+
/**
|
|
16875
|
+
* Revert a segment's FAILED in-flight operations (issue #1198). Opt-in via
|
|
16876
|
+
* `cdkd rollback --revert-failed` — the failed resource's remote state is
|
|
16877
|
+
* unknown (the op died partway), so force-applying `previousState` is a
|
|
16878
|
+
* deliberate user decision, never the default. Runs BEFORE the segment's
|
|
16879
|
+
* completed ops (the failed op is the newest work of the failed deploy).
|
|
16880
|
+
*
|
|
16881
|
+
* Best-effort like {@link replayRollback}: per-op failures are caught,
|
|
16882
|
+
* warned, and counted.
|
|
16883
|
+
*/
|
|
16884
|
+
async function replayFailedOperations(failedOps, stateResources, stackName, ctx, options = {}) {
|
|
16885
|
+
const result = {
|
|
16886
|
+
failures: 0,
|
|
16887
|
+
warnings: 0,
|
|
16888
|
+
interrupted: false,
|
|
16889
|
+
remainingFailedOps: []
|
|
16890
|
+
};
|
|
16891
|
+
const { logger } = ctx;
|
|
16892
|
+
const emitEnvelope = options.emitEnvelope === true && failedOps.length > 0;
|
|
16893
|
+
if (emitEnvelope) ctx.recordEvent?.({
|
|
16894
|
+
eventType: "ROLLBACK_STARTED",
|
|
16895
|
+
stackName
|
|
16896
|
+
});
|
|
16897
|
+
const pending = /* @__PURE__ */ new Set();
|
|
16898
|
+
for (let i = failedOps.length - 1; i >= 0; i--) {
|
|
16899
|
+
if (options.isInterrupted?.()) {
|
|
16900
|
+
result.interrupted = true;
|
|
16901
|
+
for (let j = i; j >= 0; j--) pending.add(failedOps[j]);
|
|
16902
|
+
break;
|
|
16903
|
+
}
|
|
16904
|
+
const op = failedOps[i];
|
|
16905
|
+
const action = classifyFailedOp(op, stateResources);
|
|
16906
|
+
try {
|
|
16907
|
+
switch (action) {
|
|
16908
|
+
case "skip-failed-noop":
|
|
16909
|
+
logger.info(` Rollback: failed ${op.changeType} of ${op.logicalId} (${op.resourceType}) left nothing to revert, skipping`);
|
|
16910
|
+
break;
|
|
16911
|
+
case "skip-failed-unknown":
|
|
16912
|
+
logger.warn(` Rollback: failed CREATE of ${op.logicalId} (${op.resourceType}) recorded no physical id — if it was partially created in AWS, delete it manually`);
|
|
16913
|
+
result.warnings++;
|
|
16914
|
+
break;
|
|
16915
|
+
case "skip-failed-absent":
|
|
16916
|
+
logger.warn(` Rollback: cannot revert failed UPDATE of ${op.logicalId} — no previous state available, skipping`);
|
|
16917
|
+
result.warnings++;
|
|
16918
|
+
break;
|
|
16919
|
+
case "delete-failed-create": {
|
|
16920
|
+
logger.info(` Rollback: deleting partially-created ${op.logicalId} (${op.resourceType}) (--revert-failed)`);
|
|
16921
|
+
const { provider } = ctx.providerRegistry.getProviderFor({
|
|
16922
|
+
resourceType: op.resourceType,
|
|
16923
|
+
provisionedBy: op.provisionedBy
|
|
16924
|
+
});
|
|
16925
|
+
await provider.delete(op.logicalId, op.physicalId, op.resourceType, void 0, { expectedRegion: ctx.region });
|
|
16926
|
+
delete stateResources[op.logicalId];
|
|
16927
|
+
await options.afterOp?.(op.logicalId);
|
|
16928
|
+
ctx.recordEvent?.({
|
|
16929
|
+
eventType: "ROLLBACK_RESOURCE_SUCCEEDED",
|
|
16930
|
+
stackName,
|
|
16931
|
+
operation: "CREATE",
|
|
16932
|
+
logicalId: op.logicalId,
|
|
16933
|
+
resourceType: op.resourceType,
|
|
16934
|
+
...op.provisionedBy && { provisionedBy: op.provisionedBy }
|
|
16935
|
+
});
|
|
16936
|
+
break;
|
|
16937
|
+
}
|
|
16938
|
+
case "revert-failed-update": {
|
|
16939
|
+
const current = stateResources[op.logicalId];
|
|
16940
|
+
const prev = op.previousState;
|
|
16941
|
+
logger.info(` Rollback: force-reverting failed UPDATE of ${op.logicalId} (${op.resourceType}) to its pre-deploy properties (--revert-failed; remote state is unknown)`);
|
|
16942
|
+
const { provider } = ctx.providerRegistry.getProviderFor({
|
|
16943
|
+
resourceType: op.resourceType,
|
|
16944
|
+
provisionedBy: op.provisionedBy ?? current.provisionedBy
|
|
16945
|
+
});
|
|
16946
|
+
await provider.update(op.logicalId, current.physicalId, op.resourceType, prev.properties, op.attemptedProperties ?? current.properties);
|
|
16947
|
+
stateResources[op.logicalId] = prev;
|
|
16948
|
+
logger.info(` Rollback: ${op.logicalId} reverted successfully`);
|
|
16949
|
+
await options.afterOp?.(op.logicalId);
|
|
16950
|
+
ctx.recordEvent?.({
|
|
16951
|
+
eventType: "ROLLBACK_RESOURCE_SUCCEEDED",
|
|
16952
|
+
stackName,
|
|
16953
|
+
operation: "UPDATE",
|
|
16954
|
+
logicalId: op.logicalId,
|
|
16955
|
+
resourceType: op.resourceType,
|
|
16956
|
+
...op.provisionedBy && { provisionedBy: op.provisionedBy }
|
|
16957
|
+
});
|
|
16958
|
+
break;
|
|
16959
|
+
}
|
|
16960
|
+
}
|
|
16961
|
+
} catch (revertError) {
|
|
16962
|
+
logger.warn(` Rollback failed for failed-op ${op.logicalId} (${op.changeType}): ${revertError instanceof Error ? revertError.message : String(revertError)}`);
|
|
16963
|
+
result.failures++;
|
|
16964
|
+
pending.add(op);
|
|
16965
|
+
ctx.recordEvent?.({
|
|
16966
|
+
eventType: "ROLLBACK_RESOURCE_FAILED",
|
|
16967
|
+
stackName,
|
|
16968
|
+
operation: op.changeType,
|
|
16969
|
+
logicalId: op.logicalId,
|
|
16970
|
+
resourceType: op.resourceType,
|
|
16971
|
+
...op.provisionedBy && { provisionedBy: op.provisionedBy },
|
|
16972
|
+
error: extractDeploymentEventError(revertError)
|
|
16973
|
+
});
|
|
16974
|
+
}
|
|
16975
|
+
}
|
|
16976
|
+
if (emitEnvelope) ctx.recordEvent?.({
|
|
16977
|
+
eventType: "ROLLBACK_FINISHED",
|
|
16978
|
+
stackName
|
|
16979
|
+
});
|
|
16980
|
+
result.remainingFailedOps = failedOps.filter((op) => pending.has(op));
|
|
16981
|
+
return result;
|
|
16982
|
+
}
|
|
16657
16983
|
function stateResourcesPolicyLabel(op, stateResources) {
|
|
16658
16984
|
return stateResources[op.logicalId]?.deletionPolicy ?? "Retain";
|
|
16659
16985
|
}
|
|
@@ -16746,7 +17072,7 @@ const FLUSH_INTERVAL_MS = 2e3;
|
|
|
16746
17072
|
const FLUSH_EVENT_THRESHOLD = 50;
|
|
16747
17073
|
/** Build-time cdkd version, with a dev fallback for non-built contexts. */
|
|
16748
17074
|
function getCdkdVersion() {
|
|
16749
|
-
return "0.
|
|
17075
|
+
return "0.264.0";
|
|
16750
17076
|
}
|
|
16751
17077
|
/**
|
|
16752
17078
|
* Generate a time-sortable unique run id, e.g.
|
|
@@ -17303,6 +17629,14 @@ var DeployEngine = class {
|
|
|
17303
17629
|
*/
|
|
17304
17630
|
recordedOutputReads = [];
|
|
17305
17631
|
/**
|
|
17632
|
+
* Per-logical-id snapshot of the intrinsic-RESOLVED desired properties
|
|
17633
|
+
* each CREATE / UPDATE attempted (issue #1198). Written just before the
|
|
17634
|
+
* provider call; read only when the op FAILS, to journal the failed op's
|
|
17635
|
+
* `attemptedProperties` so `cdkd rollback --revert-failed` can generate a
|
|
17636
|
+
* patch that undoes a half-applied update.
|
|
17637
|
+
*/
|
|
17638
|
+
attemptedResolvedProps = /* @__PURE__ */ new Map();
|
|
17639
|
+
/**
|
|
17306
17640
|
* Target region for this stack. Required — load-bearing for the
|
|
17307
17641
|
* region-prefixed S3 state key and recorded in state.json for
|
|
17308
17642
|
* cross-region destroy.
|
|
@@ -17728,6 +18062,7 @@ var DeployEngine = class {
|
|
|
17728
18062
|
skipped: 0
|
|
17729
18063
|
};
|
|
17730
18064
|
const completedOperations = [];
|
|
18065
|
+
const failedOperations = [];
|
|
17731
18066
|
let pendingMigration = migrationPending;
|
|
17732
18067
|
let saveChain = Promise.resolve();
|
|
17733
18068
|
const saveStateAfterResource = (logicalId) => {
|
|
@@ -17789,6 +18124,15 @@ var DeployEngine = class {
|
|
|
17789
18124
|
} catch (provisionError) {
|
|
17790
18125
|
this.interrupted = true;
|
|
17791
18126
|
this.interruptCause ??= "sibling-failure";
|
|
18127
|
+
failedOperations.push({
|
|
18128
|
+
logicalId,
|
|
18129
|
+
changeType: change.changeType,
|
|
18130
|
+
resourceType: change.resourceType,
|
|
18131
|
+
provisionedBy: newResources[logicalId]?.provisionedBy ?? previousState?.provisionedBy,
|
|
18132
|
+
...previousState && { previousState },
|
|
18133
|
+
physicalId: newResources[logicalId]?.physicalId ?? previousState?.physicalId,
|
|
18134
|
+
attemptedProperties: this.attemptedResolvedProps.get(logicalId)
|
|
18135
|
+
});
|
|
17792
18136
|
throw provisionError;
|
|
17793
18137
|
}
|
|
17794
18138
|
completedOperations.push({
|
|
@@ -17827,6 +18171,14 @@ var DeployEngine = class {
|
|
|
17827
18171
|
} catch (provisionError) {
|
|
17828
18172
|
this.interrupted = true;
|
|
17829
18173
|
this.interruptCause ??= "sibling-failure";
|
|
18174
|
+
failedOperations.push({
|
|
18175
|
+
logicalId,
|
|
18176
|
+
changeType: "DELETE",
|
|
18177
|
+
resourceType: change.resourceType,
|
|
18178
|
+
provisionedBy: previousState?.provisionedBy,
|
|
18179
|
+
...previousState && { previousState },
|
|
18180
|
+
physicalId: previousState?.physicalId
|
|
18181
|
+
});
|
|
17830
18182
|
throw provisionError;
|
|
17831
18183
|
}
|
|
17832
18184
|
completedOperations.push({
|
|
@@ -17869,16 +18221,16 @@ var DeployEngine = class {
|
|
|
17869
18221
|
}
|
|
17870
18222
|
let autoRollbackClean = false;
|
|
17871
18223
|
if (error instanceof InterruptedError) {
|
|
17872
|
-
await this.writeRollbackJournalSegment(stackName, completedOperations, "interrupted", initialDeploy);
|
|
18224
|
+
await this.writeRollbackJournalSegment(stackName, completedOperations, failedOperations, "interrupted", initialDeploy);
|
|
17873
18225
|
this.logger.info(`Partial state saved (${Object.keys(newResources).length} resources). Run deploy again to resume, 'cdkd rollback' to revert, or destroy to clean up.`);
|
|
17874
18226
|
throw error;
|
|
17875
18227
|
}
|
|
17876
18228
|
if (this.options.noRollback) {
|
|
17877
|
-
await this.writeRollbackJournalSegment(stackName, completedOperations, "no-rollback-failure", initialDeploy);
|
|
18229
|
+
await this.writeRollbackJournalSegment(stackName, completedOperations, failedOperations, "no-rollback-failure", initialDeploy);
|
|
17878
18230
|
this.logger.warn("Deployment failed. --no-rollback is set, skipping rollback.");
|
|
17879
18231
|
this.logger.warn("Partial state has been saved. Run 'cdkd deploy' to resume, 'cdkd rollback' to revert, or destroy to clean up.");
|
|
17880
18232
|
} else {
|
|
17881
|
-
await this.writeRollbackJournalSegment(stackName, completedOperations, "auto-rollback-started", initialDeploy);
|
|
18233
|
+
await this.writeRollbackJournalSegment(stackName, completedOperations, failedOperations, "auto-rollback-started", initialDeploy);
|
|
17882
18234
|
autoRollbackClean = (await this.performRollback(completedOperations, newResources, stackName)).failures === 0;
|
|
17883
18235
|
}
|
|
17884
18236
|
try {
|
|
@@ -17923,7 +18275,7 @@ var DeployEngine = class {
|
|
|
17923
18275
|
outputs = await this.resolveOutputs(template, newResources, stackName, parameterValues, conditions);
|
|
17924
18276
|
} catch (outputError) {
|
|
17925
18277
|
await this.persistStateAfterOutputFailure(stackName, currentState, newResources, currentEtag, pendingMigration);
|
|
17926
|
-
await this.writeRollbackJournalSegment(stackName, completedOperations, "no-rollback-failure", currentEtag === void 0);
|
|
18278
|
+
await this.writeRollbackJournalSegment(stackName, completedOperations, failedOperations, "no-rollback-failure", currentEtag === void 0);
|
|
17927
18279
|
throw outputError;
|
|
17928
18280
|
}
|
|
17929
18281
|
return {
|
|
@@ -18033,8 +18385,8 @@ var DeployEngine = class {
|
|
|
18033
18385
|
* `cdkd rollback`. Best-effort like the partial-state save, but warns
|
|
18034
18386
|
* LOUDLY on failure — the user just lost the ability to `cdkd rollback`.
|
|
18035
18387
|
*/
|
|
18036
|
-
async writeRollbackJournalSegment(stackName, completedOperations, reason, initialDeploy) {
|
|
18037
|
-
if (completedOperations.length === 0) return;
|
|
18388
|
+
async writeRollbackJournalSegment(stackName, completedOperations, failedOperations, reason, initialDeploy) {
|
|
18389
|
+
if (completedOperations.length === 0 && failedOperations.length === 0) return;
|
|
18038
18390
|
try {
|
|
18039
18391
|
const segment = {
|
|
18040
18392
|
...this.options.eventRecorder?.runId !== void 0 && { runId: this.options.eventRecorder.runId },
|
|
@@ -18043,7 +18395,8 @@ var DeployEngine = class {
|
|
|
18043
18395
|
initialDeploy,
|
|
18044
18396
|
...this.options.roleArn && { roleArn: this.options.roleArn },
|
|
18045
18397
|
cdkdVersion: getCdkdVersion(),
|
|
18046
|
-
operations: completedOperations
|
|
18398
|
+
operations: completedOperations,
|
|
18399
|
+
...failedOperations.length > 0 && { failedOperations }
|
|
18047
18400
|
};
|
|
18048
18401
|
await this.stateBackend.appendRollbackJournalSegment(stackName, this.stackRegion, segment);
|
|
18049
18402
|
this.logger.debug(`Rollback journal segment written (${reason})`);
|
|
@@ -18175,6 +18528,7 @@ var DeployEngine = class {
|
|
|
18175
18528
|
}, stackName);
|
|
18176
18529
|
const resolvedProps = await this.resolver.resolve(desiredProps, context);
|
|
18177
18530
|
this.auditResolvedAssetReferences(logicalId, resourceType, resolvedProps);
|
|
18531
|
+
this.attemptedResolvedProps.set(logicalId, resolvedProps);
|
|
18178
18532
|
const createDecision = this.providerRegistry.getProviderFor({
|
|
18179
18533
|
resourceType,
|
|
18180
18534
|
properties: resolvedProps
|
|
@@ -18215,6 +18569,7 @@ var DeployEngine = class {
|
|
|
18215
18569
|
}, stackName);
|
|
18216
18570
|
const resolvedProps = await this.resolver.resolve(desiredProps, context);
|
|
18217
18571
|
this.auditResolvedAssetReferences(logicalId, resourceType, resolvedProps);
|
|
18572
|
+
this.attemptedResolvedProps.set(logicalId, resolvedProps);
|
|
18218
18573
|
if (JSON.stringify(resolvedProps) === JSON.stringify(currentProps)) {
|
|
18219
18574
|
if (change.attributeChanges && change.attributeChanges.length > 0) {
|
|
18220
18575
|
const attrSummary = change.attributeChanges.map((a) => `${a.attribute}: ${a.oldValue ?? "(unset)"} → ${a.newValue ?? "(unset)"}`).join(", ");
|
|
@@ -18653,5 +19008,5 @@ var DeployEngine = class {
|
|
|
18653
19008
|
};
|
|
18654
19009
|
|
|
18655
19010
|
//#endregion
|
|
18656
|
-
export {
|
|
18657
|
-
//# sourceMappingURL=deploy-engine-
|
|
19011
|
+
export { createAssetRedirectResolver as $, ResourceTimeoutError as $t, CloudControlProvider as A, MIGRATE_TMP_PREFIX as At, assertRegionMatch as B, setAwsClients as Bt, green as C, resolveSkipPrefix as Ct, collectInlinePolicyNamesManagedBySiblings as D, warnDeprecatedNoPrefixCliFlag as Dt, IAMRoleProvider as E, resolveUseCdkBootstrapAssets as Et, cfnRefValueFromPhysicalId as F, clearBucketRegionCache as Ft, LockManager as G, LocalInvokeBuildError as Gt, DiffCalculator as H, CdkdError as Ht, refStateLookupFromResource as I, resolveBucketRegion as It, shouldRetainResource as J, LockError as Jt, S3StateBackend as K, LocalMigrateError as Kt, WAFv2WebACLProvider as L, AwsClients as Lt, disableInstanceApiTermination as M, uploadCfnTemplate as Mt, isTerminationProtectionPropagationError as N, expectedOwnerParam as Nt, ProviderRegistry as O, CFN_TEMPLATE_BODY_LIMIT as Ot, IntrinsicFunctionResolver as P, AssemblyReader as Pt, buildAssetRedirectMap as Q, ProvisioningError as Qt, normalizeAwsTagsToCfn as R, getAwsClients as Rt, gray as S, resolveCaptureObservedState as St, yellow as T, resolveStateBucketWithDefaultAndSource as Tt, DagBuilder as U, ConfigError as Ut, applyRoleArnIfSet as V, AssetError as Vt, TemplateParser as W, DependencyError as Wt, stringifyValue as X, NestedStackChildDirectDestroyError as Xt, AssetPublisher as Y, MissingCdkCliError as Yt, WorkGraph as Z, PartialFailureError as Zt, isStatefulRecreateTargetSync as _, synthesisStatusMessage as _t, DeploymentEventsStore as a, formatError as an, getBootstrapMarkerKey as at, bold as b, resolveApp as bt, replayFailedOperations as c, withErrorHandling as cn, validateContainerRepoName as ct, withRetry as d, getDockerCmd as dt, ResourceUpdateNotSupportedError as en, loadPublishableAssetManifest as et, isRetryableTransientError as f, runDockerForeground as ft, MULTI_REGION_RECREATE_BLOCKED_TYPES as g, Synthesizer as gt, extractDeploymentEventError as h, getDockerImageBySourceHash as ht, DeploymentEventsReader as i, SynthesisError as in, ensureAssetStorage as it, slowCcOperationTimeoutMs as j, findLargeInlineResources as jt, findActionableSilentDrops as k, CFN_TEMPLATE_URL_LIMIT as kt, replayRollback as l, __exportAll as ln, buildDockerImage as lt, computeImplicitDeleteEdges as m, AssetManifestLoader as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, StackTerminationProtectionError as nn, AssetModeResolver as nt, planFailedOps as o, isCdkdError as on, parseBootstrapMarker as ot, IMPLICIT_DELETE_DEPENDENCIES as p, runDockerStreaming as pt, rebuildClientForBucketRegion as q, LocalStartServiceError as qt, DeployEngine as r, StateError as rn, BOOTSTRAP_MARKER_PREFIX as rt, planRollback as s, normalizeAwsError as sn, validateAssetBucketName as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, StackHasActiveImportsError as tn, rewriteTemplateAssetReferences as tt, withResourceDeadline as u, formatDockerLoginError as ut, renderStatefulReason as v, getDefaultStateBucketName as vt, red as w, resolveStateBucketWithDefault as wt, cyan as x, resolveAutoAssetStorage as xt, formatResourceLine as y, getLegacyStateBucketName as yt, resolveExplicitPhysicalId as z, resetAwsClients as zt };
|
|
19012
|
+
//# sourceMappingURL=deploy-engine-hlS6y_-0.js.map
|