@go-to-k/cdkd 0.284.82 → 0.284.84
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-BoJz-x3Q.js → asg-provider-BeELAzdu.js} +2 -2
- package/dist/{asg-provider-BoJz-x3Q.js.map → asg-provider-BeELAzdu.js.map} +1 -1
- package/dist/cli.js +2 -2
- package/dist/{deploy-engine-Dh4Jid_H.js → deploy-engine-Du2CDZop.js} +339 -171
- package/dist/deploy-engine-Du2CDZop.js.map +1 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/{program-c230yTpa.js → program-C3e88Igf.js} +16 -8
- package/dist/{program-c230yTpa.js.map → program-C3e88Igf.js.map} +1 -1
- package/dist/{version-D_b3uJus.js → version-BJFNb-b3.js} +2 -2
- package/dist/{version-D_b3uJus.js.map → version-BJFNb-b3.js.map} +1 -1
- package/package.json +1 -1
- package/dist/deploy-engine-Dh4Jid_H.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { d as generateResourceName, f as generateResourceNameWithFallback, g as withStackName, m as looksLikeCdkdGeneratedName, n as getLogger, o as getLiveRenderer, p as getCurrentStackName, u as applyDefaultNameForFallback } from "./logger-C9-E73FI.js";
|
|
2
|
-
import { t as getCdkdVersion } from "./version-
|
|
2
|
+
import { t as getCdkdVersion } from "./version-BJFNb-b3.js";
|
|
3
3
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
5
|
import { CreateBucketCommand, DeleteObjectCommand, DeleteObjectsCommand, GetBucketLocationCommand, GetObjectCommand, HeadBucketCommand, HeadObjectCommand, ListObjectVersionsCommand, ListObjectsV2Command, NoSuchKey, PutBucketEncryptionCommand, PutBucketPolicyCommand, PutObjectCommand, PutPublicAccessBlockCommand, S3Client, S3ServiceException } from "@aws-sdk/client-s3";
|
|
@@ -3868,6 +3868,168 @@ async function expectedOwnerParam(client) {
|
|
|
3868
3868
|
return owner ? { ExpectedBucketOwner: owner } : {};
|
|
3869
3869
|
}
|
|
3870
3870
|
|
|
3871
|
+
//#endregion
|
|
3872
|
+
//#region src/state/s3-noncurrent-version-purge.ts
|
|
3873
|
+
/**
|
|
3874
|
+
* Parenthetical used when a caller names nothing.
|
|
3875
|
+
*
|
|
3876
|
+
* True of ANY object this function is pointed at, which is the bar for a
|
|
3877
|
+
* default here: a caller that forgets to describe its object must still emit a
|
|
3878
|
+
* warning that is correct, just less specific. It deliberately does not guess
|
|
3879
|
+
* at content.
|
|
3880
|
+
*/
|
|
3881
|
+
const DEFAULT_OBJECT_DESCRIPTION = "the body of an object cdkd has just reported as removed";
|
|
3882
|
+
/**
|
|
3883
|
+
* `objectDescription` for the custom-resource response sidecar.
|
|
3884
|
+
*
|
|
3885
|
+
* A SHARED CONSTANT rather than the same literal at both sites, for the reason
|
|
3886
|
+
* this whole module is shared: the provider's own `cleanupResponseObject` and
|
|
3887
|
+
* `cdkd gc`'s sweep of the abandoned placeholders delete the SAME object, so a
|
|
3888
|
+
* reader must not be able to tell from the warning which of the two produced
|
|
3889
|
+
* it. Two literals are how one of them drifts — and it is not hypothetical:
|
|
3890
|
+
* review probed it by editing `gc.ts`'s string alone and the suite stayed
|
|
3891
|
+
* green, because nothing tied the two together. One binding cannot drift, and
|
|
3892
|
+
* needs no test to say so.
|
|
3893
|
+
*/
|
|
3894
|
+
const CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION = "a custom-resource response object, which is the handler's full cfn-response body including `Data`";
|
|
3895
|
+
/**
|
|
3896
|
+
* `DeleteObjects` is capped at 1000 entries per call.
|
|
3897
|
+
*
|
|
3898
|
+
* DEFENCE IN DEPTH, and unreachable today: `stale` is accumulated from a
|
|
3899
|
+
* SINGLE `ListObjectVersions` page, whose `Versions` + `DeleteMarkers` are
|
|
3900
|
+
* capped at 1000 COMBINED by `MaxKeys`, so the chunking below never takes its
|
|
3901
|
+
* second iteration. It is kept because the invariant it guards ("never hand
|
|
3902
|
+
* DeleteObjects more than 1000") is one a future change accumulating across
|
|
3903
|
+
* pages would silently break.
|
|
3904
|
+
*
|
|
3905
|
+
* Mutation coverage of this constant is ASYMMETRIC, which is worth stating
|
|
3906
|
+
* because the obvious summary is wrong in one direction: RAISING it is green
|
|
3907
|
+
* (nothing ever reaches the second chunk, so a bigger ceiling changes
|
|
3908
|
+
* nothing), while LOWERING it to 500 is RED — the multi-page fixture's
|
|
3909
|
+
* thousand-entry pages then split and the asserted batch shape changes. So the
|
|
3910
|
+
* value is fenced from below and not from above.
|
|
3911
|
+
*/
|
|
3912
|
+
const DELETE_BATCH_SIZE = 1e3;
|
|
3913
|
+
/** How many failing keys the warning names before it truncates. */
|
|
3914
|
+
const MAX_NAMED_FAILURES = 5;
|
|
3915
|
+
/**
|
|
3916
|
+
* Label for a `DeleteObjects` error entry that carries no `Key`.
|
|
3917
|
+
*
|
|
3918
|
+
* S3 always populates it in practice; the point is that an unnameable failure
|
|
3919
|
+
* must still COUNT, because the alternative measured here was `failed.size`
|
|
3920
|
+
* reaching 0 and the whole warning disappearing.
|
|
3921
|
+
*
|
|
3922
|
+
* Each keyless entry gets its OWN slot (`<unknown key #1>`, `#2`, ...) rather
|
|
3923
|
+
* than sharing one. Collapsing them was defended as "the honest reading", but
|
|
3924
|
+
* it is honest about NAMING and not about COUNTING: N keyless failures then
|
|
3925
|
+
* reported `1 key(s)`, which is the same prefixes-not-keys under-count this
|
|
3926
|
+
* change was raised to fix, arriving through the branch that fixed it. One
|
|
3927
|
+
* slot per failure can over-count if S3 ever returns two entries for one
|
|
3928
|
+
* object, which is the direction that errs toward reporting too much.
|
|
3929
|
+
*
|
|
3930
|
+
* The slot name is SYNTHETIC and its uniqueness is not enforced: a real key
|
|
3931
|
+
* literally called `<unknown key #1>` would merge with the first keyless
|
|
3932
|
+
* entry and under-count by one. Unreachable here — every caller passes
|
|
3933
|
+
* `custom-resource-responses/<requestId>.json` — and stated rather than left
|
|
3934
|
+
* implied, because "the name cannot collide" is the kind of unstated
|
|
3935
|
+
* invariant this module exists to stop asserting.
|
|
3936
|
+
*/
|
|
3937
|
+
const UNKNOWN_KEY_PREFIX = "<unknown key #";
|
|
3938
|
+
/** Reason recorded when a page says it is truncated but names no next key. */
|
|
3939
|
+
const TRUNCATED_NO_MARKER = "listing reported IsTruncated with no NextKeyMarker; the walk stopped early and versions may remain";
|
|
3940
|
+
/** Record a per-key failure reason without losing an earlier one. */
|
|
3941
|
+
function recordFailure(failed, key, reason) {
|
|
3942
|
+
const existing = failed.get(key);
|
|
3943
|
+
if (existing) existing.push(reason);
|
|
3944
|
+
else failed.set(key, [reason]);
|
|
3945
|
+
}
|
|
3946
|
+
const describe$1 = (error) => error instanceof Error ? error.message : String(error);
|
|
3947
|
+
async function purgeNoncurrentKeyVersions(s3Client, bucket, keys, options = {}) {
|
|
3948
|
+
if (keys.length === 0) return;
|
|
3949
|
+
const logger = options.logger ?? getLogger().child("s3-version-purge");
|
|
3950
|
+
const requestFields = options.requestFields ?? {};
|
|
3951
|
+
const wanted = new Set(keys);
|
|
3952
|
+
const prefixes = options.listPrefix !== void 0 ? [options.listPrefix] : keys;
|
|
3953
|
+
const failed = /* @__PURE__ */ new Map();
|
|
3954
|
+
const unknown = { n: 0 };
|
|
3955
|
+
for (const prefix of prefixes) try {
|
|
3956
|
+
await purgeUnderPrefix(s3Client, bucket, prefix, wanted, requestFields, failed, unknown);
|
|
3957
|
+
} catch (error) {
|
|
3958
|
+
const affected = options.listPrefix !== void 0 ? keys : [prefix];
|
|
3959
|
+
for (const key of affected) recordFailure(failed, key, describe$1(error));
|
|
3960
|
+
}
|
|
3961
|
+
if (failed.size > 0) {
|
|
3962
|
+
const named = [...failed.entries()].slice(0, MAX_NAMED_FAILURES).map(([key, reasons]) => `${key} (${reasons.join("; ")})`);
|
|
3963
|
+
const elided = failed.size - named.length;
|
|
3964
|
+
logger.warn(`Could not purge noncurrent versions of ${failed.size} key(s) in s3://${bucket}. Their previous versions survive and remain readable via GetObject with a VersionId (${options.objectDescription ?? DEFAULT_OBJECT_DESCRIPTION}). Grant s3:ListBucketVersions and s3:DeleteObjectVersion on the state bucket, or purge the key(s) by hand. Failures: ${named.join(", ")}` + (elided > 0 ? ` (and ${elided} more)` : ""));
|
|
3965
|
+
}
|
|
3966
|
+
}
|
|
3967
|
+
/**
|
|
3968
|
+
* Paginate `ListObjectVersions` under one prefix and delete every returned
|
|
3969
|
+
* entry that is in `wanted` and is not the current version.
|
|
3970
|
+
*
|
|
3971
|
+
* Throws only when the LISTING fails; per-key delete failures are recorded in
|
|
3972
|
+
* `failed` and do not stop the walk.
|
|
3973
|
+
*
|
|
3974
|
+
* Safe on an UNVERSIONED bucket: S3 answers there with the single live object
|
|
3975
|
+
* carrying `VersionId: 'null'` and `IsLatest: true`, which the `IsLatest`
|
|
3976
|
+
* filter drops — so nothing is deleted and nothing throws. A `'null'` version
|
|
3977
|
+
* id is NOT filtered out on its own, because a bucket whose versioning was
|
|
3978
|
+
* SUSPENDED can carry a genuine noncurrent `'null'` version holding the body.
|
|
3979
|
+
*/
|
|
3980
|
+
async function purgeUnderPrefix(s3Client, bucket, prefix, wanted, requestFields, failed, unknown) {
|
|
3981
|
+
let keyMarker;
|
|
3982
|
+
let versionIdMarker;
|
|
3983
|
+
do {
|
|
3984
|
+
const resp = await s3Client.send(new ListObjectVersionsCommand({
|
|
3985
|
+
Bucket: bucket,
|
|
3986
|
+
...requestFields,
|
|
3987
|
+
Prefix: prefix,
|
|
3988
|
+
...keyMarker !== void 0 && { KeyMarker: keyMarker },
|
|
3989
|
+
...versionIdMarker !== void 0 && { VersionIdMarker: versionIdMarker }
|
|
3990
|
+
}));
|
|
3991
|
+
const stale = [];
|
|
3992
|
+
for (const entry of [...resp.Versions ?? [], ...resp.DeleteMarkers ?? []]) {
|
|
3993
|
+
if (entry.Key === void 0 || !wanted.has(entry.Key)) continue;
|
|
3994
|
+
if (entry.IsLatest === void 0) recordFailure(failed, entry.Key, `version ${entry.VersionId ?? "<unknown>"}: listing omitted IsLatest, so the entry was left alone rather than risk deleting a current version`);
|
|
3995
|
+
if (entry.IsLatest !== false) continue;
|
|
3996
|
+
if (!entry.VersionId) continue;
|
|
3997
|
+
stale.push({
|
|
3998
|
+
Key: entry.Key,
|
|
3999
|
+
VersionId: entry.VersionId
|
|
4000
|
+
});
|
|
4001
|
+
}
|
|
4002
|
+
for (let i = 0; i < stale.length; i += DELETE_BATCH_SIZE) {
|
|
4003
|
+
const batch = stale.slice(i, i + DELETE_BATCH_SIZE);
|
|
4004
|
+
try {
|
|
4005
|
+
const deleted = await s3Client.send(new DeleteObjectsCommand({
|
|
4006
|
+
Bucket: bucket,
|
|
4007
|
+
...requestFields,
|
|
4008
|
+
Delete: {
|
|
4009
|
+
Objects: batch,
|
|
4010
|
+
Quiet: true
|
|
4011
|
+
}
|
|
4012
|
+
}));
|
|
4013
|
+
for (const err of deleted.Errors ?? []) {
|
|
4014
|
+
const reason = `version ${err.VersionId ?? "<unknown>"}: ${err.Code ?? "Error"}` + (err.Message ? ` - ${err.Message}` : "");
|
|
4015
|
+
if (err.Key !== void 0) recordFailure(failed, err.Key, reason);
|
|
4016
|
+
else {
|
|
4017
|
+
unknown.n += 1;
|
|
4018
|
+
recordFailure(failed, `${UNKNOWN_KEY_PREFIX}${unknown.n}>`, reason);
|
|
4019
|
+
}
|
|
4020
|
+
}
|
|
4021
|
+
} catch (error) {
|
|
4022
|
+
for (const object of batch) recordFailure(failed, object.Key, describe$1(error));
|
|
4023
|
+
}
|
|
4024
|
+
}
|
|
4025
|
+
if (resp.IsTruncated === true && resp.NextKeyMarker === void 0) {
|
|
4026
|
+
for (const key of wanted) if (key.startsWith(prefix)) recordFailure(failed, key, TRUNCATED_NO_MARKER);
|
|
4027
|
+
}
|
|
4028
|
+
keyMarker = resp.IsTruncated === true ? resp.NextKeyMarker : void 0;
|
|
4029
|
+
versionIdMarker = keyMarker !== void 0 ? resp.NextVersionIdMarker : void 0;
|
|
4030
|
+
} while (keyMarker !== void 0);
|
|
4031
|
+
}
|
|
4032
|
+
|
|
3871
4033
|
//#endregion
|
|
3872
4034
|
//#region src/cli/upload-cfn-template.ts
|
|
3873
4035
|
/**
|
|
@@ -3900,8 +4062,9 @@ const MIGRATE_TMP_PREFIX = "cdkd-migrate-tmp";
|
|
|
3900
4062
|
/**
|
|
3901
4063
|
* Upload a CFn template body to the cdkd state bucket and return both a
|
|
3902
4064
|
* virtual-hosted-style HTTPS URL CloudFormation can fetch via
|
|
3903
|
-
* `TemplateURL` and a `cleanup` callback that deletes the object
|
|
3904
|
-
*
|
|
4065
|
+
* `TemplateURL` and a `cleanup` callback that deletes the object, PURGES its
|
|
4066
|
+
* noncurrent versions (the state bucket is versioned — see the callback), and
|
|
4067
|
+
* destroys the S3 client.
|
|
3905
4068
|
*
|
|
3906
4069
|
* The state bucket's actual region is resolved via `GetBucketLocation`
|
|
3907
4070
|
* (cached per-process) so the upload client and the URL match the
|
|
@@ -3953,7 +4116,17 @@ async function uploadCfnTemplate(args) {
|
|
|
3953
4116
|
...await expectedOwnerParam(s3)
|
|
3954
4117
|
}));
|
|
3955
4118
|
} finally {
|
|
3956
|
-
|
|
4119
|
+
try {
|
|
4120
|
+
await purgeNoncurrentKeyVersions(s3, bucket, [key], {
|
|
4121
|
+
requestFields: await expectedOwnerParam(s3),
|
|
4122
|
+
logger: getLogger(),
|
|
4123
|
+
objectDescription: "the transient CloudFormation template body uploaded for this command"
|
|
4124
|
+
});
|
|
4125
|
+
} catch (purgeError) {
|
|
4126
|
+
getLogger().warn(`Could not purge noncurrent versions of the transient template s3://${bucket}/${key}; its previous versions survive and remain readable via GetObject with a VersionId. Grant s3:ListBucketVersions and s3:DeleteObjectVersion on the state bucket, or purge the key by hand. Underlying error: ${purgeError instanceof Error ? purgeError.message : String(purgeError)}`);
|
|
4127
|
+
} finally {
|
|
4128
|
+
s3.destroy();
|
|
4129
|
+
}
|
|
3957
4130
|
}
|
|
3958
4131
|
};
|
|
3959
4132
|
return {
|
|
@@ -7600,145 +7773,6 @@ async function rebuildClientForBucketRegion(client, bucket, opts = {}) {
|
|
|
7600
7773
|
return replacement;
|
|
7601
7774
|
}
|
|
7602
7775
|
|
|
7603
|
-
//#endregion
|
|
7604
|
-
//#region src/state/s3-noncurrent-version-purge.ts
|
|
7605
|
-
/**
|
|
7606
|
-
* `DeleteObjects` is capped at 1000 entries per call.
|
|
7607
|
-
*
|
|
7608
|
-
* DEFENCE IN DEPTH, and unreachable today: `stale` is accumulated from a
|
|
7609
|
-
* SINGLE `ListObjectVersions` page, whose `Versions` + `DeleteMarkers` are
|
|
7610
|
-
* capped at 1000 COMBINED by `MaxKeys`, so the chunking below never takes its
|
|
7611
|
-
* second iteration. It is kept because the invariant it guards ("never hand
|
|
7612
|
-
* DeleteObjects more than 1000") is one a future change accumulating across
|
|
7613
|
-
* pages would silently break.
|
|
7614
|
-
*
|
|
7615
|
-
* Mutation coverage of this constant is ASYMMETRIC, which is worth stating
|
|
7616
|
-
* because the obvious summary is wrong in one direction: RAISING it is green
|
|
7617
|
-
* (nothing ever reaches the second chunk, so a bigger ceiling changes
|
|
7618
|
-
* nothing), while LOWERING it to 500 is RED — the multi-page fixture's
|
|
7619
|
-
* thousand-entry pages then split and the asserted batch shape changes. So the
|
|
7620
|
-
* value is fenced from below and not from above.
|
|
7621
|
-
*/
|
|
7622
|
-
const DELETE_BATCH_SIZE = 1e3;
|
|
7623
|
-
/** How many failing keys the warning names before it truncates. */
|
|
7624
|
-
const MAX_NAMED_FAILURES = 5;
|
|
7625
|
-
/**
|
|
7626
|
-
* Label for a `DeleteObjects` error entry that carries no `Key`.
|
|
7627
|
-
*
|
|
7628
|
-
* S3 always populates it in practice; the point is that an unnameable failure
|
|
7629
|
-
* must still COUNT, because the alternative measured here was `failed.size`
|
|
7630
|
-
* reaching 0 and the whole warning disappearing.
|
|
7631
|
-
*
|
|
7632
|
-
* Each keyless entry gets its OWN slot (`<unknown key #1>`, `#2`, ...) rather
|
|
7633
|
-
* than sharing one. Collapsing them was defended as "the honest reading", but
|
|
7634
|
-
* it is honest about NAMING and not about COUNTING: N keyless failures then
|
|
7635
|
-
* reported `1 key(s)`, which is the same prefixes-not-keys under-count this
|
|
7636
|
-
* change was raised to fix, arriving through the branch that fixed it. One
|
|
7637
|
-
* slot per failure can over-count if S3 ever returns two entries for one
|
|
7638
|
-
* object, which is the direction that errs toward reporting too much.
|
|
7639
|
-
*
|
|
7640
|
-
* The slot name is SYNTHETIC and its uniqueness is not enforced: a real key
|
|
7641
|
-
* literally called `<unknown key #1>` would merge with the first keyless
|
|
7642
|
-
* entry and under-count by one. Unreachable here — every caller passes
|
|
7643
|
-
* `custom-resource-responses/<requestId>.json` — and stated rather than left
|
|
7644
|
-
* implied, because "the name cannot collide" is the kind of unstated
|
|
7645
|
-
* invariant this module exists to stop asserting.
|
|
7646
|
-
*/
|
|
7647
|
-
const UNKNOWN_KEY_PREFIX = "<unknown key #";
|
|
7648
|
-
/** Reason recorded when a page says it is truncated but names no next key. */
|
|
7649
|
-
const TRUNCATED_NO_MARKER = "listing reported IsTruncated with no NextKeyMarker; the walk stopped early and versions may remain";
|
|
7650
|
-
/** Record a per-key failure reason without losing an earlier one. */
|
|
7651
|
-
function recordFailure(failed, key, reason) {
|
|
7652
|
-
const existing = failed.get(key);
|
|
7653
|
-
if (existing) existing.push(reason);
|
|
7654
|
-
else failed.set(key, [reason]);
|
|
7655
|
-
}
|
|
7656
|
-
const describe$1 = (error) => error instanceof Error ? error.message : String(error);
|
|
7657
|
-
async function purgeNoncurrentKeyVersions(s3Client, bucket, keys, options = {}) {
|
|
7658
|
-
if (keys.length === 0) return;
|
|
7659
|
-
const logger = options.logger ?? getLogger().child("s3-version-purge");
|
|
7660
|
-
const requestFields = options.requestFields ?? {};
|
|
7661
|
-
const wanted = new Set(keys);
|
|
7662
|
-
const prefixes = options.listPrefix !== void 0 ? [options.listPrefix] : keys;
|
|
7663
|
-
const failed = /* @__PURE__ */ new Map();
|
|
7664
|
-
const unknown = { n: 0 };
|
|
7665
|
-
for (const prefix of prefixes) try {
|
|
7666
|
-
await purgeUnderPrefix(s3Client, bucket, prefix, wanted, requestFields, failed, unknown);
|
|
7667
|
-
} catch (error) {
|
|
7668
|
-
const affected = options.listPrefix !== void 0 ? keys : [prefix];
|
|
7669
|
-
for (const key of affected) recordFailure(failed, key, describe$1(error));
|
|
7670
|
-
}
|
|
7671
|
-
if (failed.size > 0) {
|
|
7672
|
-
const named = [...failed.entries()].slice(0, MAX_NAMED_FAILURES).map(([key, reasons]) => `${key} (${reasons.join("; ")})`);
|
|
7673
|
-
const elided = failed.size - named.length;
|
|
7674
|
-
logger.warn(`Could not purge noncurrent versions of ${failed.size} key(s) in s3://${bucket}. Their previous versions survive and remain readable via GetObject with a VersionId (for a custom-resource response object that is the handler's full response body, including \`Data\`). Grant s3:ListBucketVersions and s3:DeleteObjectVersion on the state bucket, or purge the key(s) by hand. Failures: ${named.join(", ")}` + (elided > 0 ? ` (and ${elided} more)` : ""));
|
|
7675
|
-
}
|
|
7676
|
-
}
|
|
7677
|
-
/**
|
|
7678
|
-
* Paginate `ListObjectVersions` under one prefix and delete every returned
|
|
7679
|
-
* entry that is in `wanted` and is not the current version.
|
|
7680
|
-
*
|
|
7681
|
-
* Throws only when the LISTING fails; per-key delete failures are recorded in
|
|
7682
|
-
* `failed` and do not stop the walk.
|
|
7683
|
-
*
|
|
7684
|
-
* Safe on an UNVERSIONED bucket: S3 answers there with the single live object
|
|
7685
|
-
* carrying `VersionId: 'null'` and `IsLatest: true`, which the `IsLatest`
|
|
7686
|
-
* filter drops — so nothing is deleted and nothing throws. A `'null'` version
|
|
7687
|
-
* id is NOT filtered out on its own, because a bucket whose versioning was
|
|
7688
|
-
* SUSPENDED can carry a genuine noncurrent `'null'` version holding the body.
|
|
7689
|
-
*/
|
|
7690
|
-
async function purgeUnderPrefix(s3Client, bucket, prefix, wanted, requestFields, failed, unknown) {
|
|
7691
|
-
let keyMarker;
|
|
7692
|
-
let versionIdMarker;
|
|
7693
|
-
do {
|
|
7694
|
-
const resp = await s3Client.send(new ListObjectVersionsCommand({
|
|
7695
|
-
Bucket: bucket,
|
|
7696
|
-
...requestFields,
|
|
7697
|
-
Prefix: prefix,
|
|
7698
|
-
...keyMarker !== void 0 && { KeyMarker: keyMarker },
|
|
7699
|
-
...versionIdMarker !== void 0 && { VersionIdMarker: versionIdMarker }
|
|
7700
|
-
}));
|
|
7701
|
-
const stale = [];
|
|
7702
|
-
for (const entry of [...resp.Versions ?? [], ...resp.DeleteMarkers ?? []]) {
|
|
7703
|
-
if (entry.Key === void 0 || !wanted.has(entry.Key)) continue;
|
|
7704
|
-
if (entry.IsLatest !== false) continue;
|
|
7705
|
-
if (!entry.VersionId) continue;
|
|
7706
|
-
stale.push({
|
|
7707
|
-
Key: entry.Key,
|
|
7708
|
-
VersionId: entry.VersionId
|
|
7709
|
-
});
|
|
7710
|
-
}
|
|
7711
|
-
for (let i = 0; i < stale.length; i += DELETE_BATCH_SIZE) {
|
|
7712
|
-
const batch = stale.slice(i, i + DELETE_BATCH_SIZE);
|
|
7713
|
-
try {
|
|
7714
|
-
const deleted = await s3Client.send(new DeleteObjectsCommand({
|
|
7715
|
-
Bucket: bucket,
|
|
7716
|
-
...requestFields,
|
|
7717
|
-
Delete: {
|
|
7718
|
-
Objects: batch,
|
|
7719
|
-
Quiet: true
|
|
7720
|
-
}
|
|
7721
|
-
}));
|
|
7722
|
-
for (const err of deleted.Errors ?? []) {
|
|
7723
|
-
const reason = `version ${err.VersionId ?? "<unknown>"}: ${err.Code ?? "Error"}` + (err.Message ? ` - ${err.Message}` : "");
|
|
7724
|
-
if (err.Key !== void 0) recordFailure(failed, err.Key, reason);
|
|
7725
|
-
else {
|
|
7726
|
-
unknown.n += 1;
|
|
7727
|
-
recordFailure(failed, `${UNKNOWN_KEY_PREFIX}${unknown.n}>`, reason);
|
|
7728
|
-
}
|
|
7729
|
-
}
|
|
7730
|
-
} catch (error) {
|
|
7731
|
-
for (const object of batch) recordFailure(failed, object.Key, describe$1(error));
|
|
7732
|
-
}
|
|
7733
|
-
}
|
|
7734
|
-
if (resp.IsTruncated === true && resp.NextKeyMarker === void 0) {
|
|
7735
|
-
for (const key of wanted) if (key.startsWith(prefix)) recordFailure(failed, key, TRUNCATED_NO_MARKER);
|
|
7736
|
-
}
|
|
7737
|
-
keyMarker = resp.IsTruncated === true ? resp.NextKeyMarker : void 0;
|
|
7738
|
-
versionIdMarker = keyMarker !== void 0 ? resp.NextVersionIdMarker : void 0;
|
|
7739
|
-
} while (keyMarker !== void 0);
|
|
7740
|
-
}
|
|
7741
|
-
|
|
7742
7776
|
//#endregion
|
|
7743
7777
|
//#region src/state/s3-state-backend.ts
|
|
7744
7778
|
/**
|
|
@@ -8060,7 +8094,6 @@ var S3StateBackend = class {
|
|
|
8060
8094
|
}));
|
|
8061
8095
|
this.logger.debug(`Deleted legacy state for stack: ${stackName}`);
|
|
8062
8096
|
}
|
|
8063
|
-
await this.deleteRollbackJournal(stackName, region);
|
|
8064
8097
|
this.logger.debug(`State deleted: ${stackName} (${region})`);
|
|
8065
8098
|
} catch (error) {
|
|
8066
8099
|
const normalized = normalizeAwsError(error, {
|
|
@@ -8068,6 +8101,8 @@ var S3StateBackend = class {
|
|
|
8068
8101
|
operation: "DeleteObject"
|
|
8069
8102
|
});
|
|
8070
8103
|
throw new StateError(`Failed to delete state for stack '${stackName}' (${region}): ${normalized.message}`, normalized);
|
|
8104
|
+
} finally {
|
|
8105
|
+
await this.deleteRollbackJournal(stackName, region);
|
|
8071
8106
|
}
|
|
8072
8107
|
}
|
|
8073
8108
|
/**
|
|
@@ -8314,7 +8349,8 @@ var S3StateBackend = class {
|
|
|
8314
8349
|
logger: this.logger
|
|
8315
8350
|
});
|
|
8316
8351
|
} catch (error) {
|
|
8317
|
-
|
|
8352
|
+
const describing = options.objectDescription ? ` (${options.objectDescription})` : "";
|
|
8353
|
+
this.logger.warn(`Could not purge noncurrent versions of ${keys.length} key(s) in bucket '${this.config.bucket}': the purge could not be started. Their previous versions survive and remain readable via GetObject with a VersionId${describing}. Grant s3:ListBucketVersions and s3:DeleteObjectVersion on the state bucket, or purge the key(s) by hand. Underlying error: ${error instanceof Error ? error.message : String(error)}`);
|
|
8318
8354
|
}
|
|
8319
8355
|
}
|
|
8320
8356
|
/**
|
|
@@ -8387,19 +8423,48 @@ var S3StateBackend = class {
|
|
|
8387
8423
|
* Delete the stack's rollback journal object (idempotent). Called on the
|
|
8388
8424
|
* deploy success path, after a clean rollback, and via {@link deleteState}
|
|
8389
8425
|
* so `cdkd destroy` / `cdkd state destroy` sweep it too.
|
|
8426
|
+
*
|
|
8427
|
+
* TWO steps, and the second is not housekeeping (issue
|
|
8428
|
+
* [#2346](https://github.com/go-to-k/cdkd/issues/2346) site 4). `cdkd
|
|
8429
|
+
* bootstrap` turns VERSIONING ON for the state bucket, so the
|
|
8430
|
+
* `DeleteObject` above only writes a DELETE MARKER and leaves every prior
|
|
8431
|
+
* version of the journal readable through `GetObject` with a `VersionId`.
|
|
8432
|
+
* The journal's `failedOperations[].attemptedProperties` is the PROPERTIES
|
|
8433
|
+
* OF THE FAILED WRITE, verbatim — measured 2026-08-20 on
|
|
8434
|
+
* `CdkdDeletionPolicySnapshotHeavyExample` as four surviving versions each
|
|
8435
|
+
* carrying a literal `"MasterUserPassword": "Cdkdcf2f..."` after cdkd
|
|
8436
|
+
* reported the state deleted (recorded in the `s3_stack_prefix` comment of
|
|
8437
|
+
* `tests/integration/s3-versions.sh`, ~line 270). A
|
|
8438
|
+
* delete-only cleanup therefore reports success while the credential stays
|
|
8439
|
+
* retrievable by anyone holding `s3:GetObjectVersion`.
|
|
8440
|
+
*
|
|
8441
|
+
* Unlike `state.json` — whose noncurrent versions ARE the state-recovery
|
|
8442
|
+
* capability versioning is enabled FOR, which is why {@link deleteState}
|
|
8443
|
+
* deliberately does NOT purge — the journal is TRANSIENT by design: it
|
|
8444
|
+
* exists only between a failed / interrupted deploy and its `cdkd
|
|
8445
|
+
* rollback`. There is no recovery capability to weigh against the purge.
|
|
8446
|
+
*
|
|
8447
|
+
* The purge runs UNCONDITIONALLY, including on the arm where the delete
|
|
8448
|
+
* failed. `purgeNoncurrentKeyVersions` filters on `IsLatest`, so a key
|
|
8449
|
+
* whose delete failed keeps its current version intact and only its history
|
|
8450
|
+
* goes — the worst case is that the object survives while its old bodies do
|
|
8451
|
+
* not, which is the safe direction. Skipping the purge when the delete
|
|
8452
|
+
* threw would leave every readable version behind with no warning at all,
|
|
8453
|
+
* the same partial-failure gap `cdkd gc` closed with its `finally`.
|
|
8390
8454
|
*/
|
|
8391
8455
|
async deleteRollbackJournal(stackName, region) {
|
|
8392
8456
|
await this.ensureClientForBucket();
|
|
8457
|
+
const key = this.getRollbackJournalKey(stackName, region);
|
|
8393
8458
|
try {
|
|
8394
8459
|
await this.s3Client.send(new DeleteObjectCommand({
|
|
8395
8460
|
Bucket: this.config.bucket,
|
|
8396
8461
|
...await this.ownerParam(),
|
|
8397
|
-
Key:
|
|
8462
|
+
Key: key
|
|
8398
8463
|
}));
|
|
8399
8464
|
} catch (error) {
|
|
8400
|
-
if (isNoSuchKey(error)
|
|
8401
|
-
this.logger.warn(`Failed to delete rollback journal for '${stackName}' (${region}): ${error instanceof Error ? error.message : String(error)}`);
|
|
8465
|
+
if (!isNoSuchKey(error) && error.name !== "NotFound") this.logger.warn(`Failed to delete rollback journal for '${stackName}' (${region}): ${error instanceof Error ? error.message : String(error)}`);
|
|
8402
8466
|
}
|
|
8467
|
+
await this.purgeNoncurrentVersions([key], { objectDescription: "the rollback journal, whose `failedOperations[].attemptedProperties` records the properties of the failed write verbatim" });
|
|
8403
8468
|
}
|
|
8404
8469
|
/**
|
|
8405
8470
|
* HeadObject probe — returns true on 200, false on NotFound. Other errors
|
|
@@ -14907,8 +14972,10 @@ function describe(value) {
|
|
|
14907
14972
|
//#region src/provisioning/region-check.ts
|
|
14908
14973
|
/**
|
|
14909
14974
|
* Verify that the AWS client's region matches the region the resource is
|
|
14910
|
-
* expected to live in before treating a `NotFound` error as idempotent
|
|
14911
|
-
* delete success
|
|
14975
|
+
* expected to live in — before treating a `NotFound` error as idempotent
|
|
14976
|
+
* delete success (`phase: 'not-found'`), or before issuing a mutating call
|
|
14977
|
+
* against a state-recorded physical id at all (`'pre-delete'` /
|
|
14978
|
+
* `'pre-update'`, issue #2301).
|
|
14912
14979
|
*
|
|
14913
14980
|
* Why: a destroy run with the wrong region would otherwise receive
|
|
14914
14981
|
* `*NotFound` for every resource and silently strip them all from state,
|
|
@@ -14917,13 +14984,24 @@ function describe(value) {
|
|
|
14917
14984
|
* `us-west-2` removed from state by a destroy that ran with a `us-east-1`
|
|
14918
14985
|
* client.
|
|
14919
14986
|
*
|
|
14920
|
-
*
|
|
14987
|
+
* And a `NotFound` is not the only way that ends badly, which is what the
|
|
14988
|
+
* pre-flight phases add: many physical ids are names rather than ARNs, so the
|
|
14989
|
+
* same name usually EXISTS in the client's region too (the same stack deployed
|
|
14990
|
+
* twice, or cdkd's own `resource-name.ts` deriving an identical name from an
|
|
14991
|
+
* identical stack + logical id). Then the wrong-region call never errors — it
|
|
14992
|
+
* succeeds against the wrong resource. That path is unrecoverable on delete
|
|
14993
|
+
* and a misapplied configuration on update, and neither ever reaches the
|
|
14994
|
+
* `NotFound` branch this helper originally lived on.
|
|
14995
|
+
*
|
|
14996
|
+
* Behavior (identical in every phase):
|
|
14921
14997
|
* - If `expectedRegion` is unset, this is a no-op (back-compat: existing
|
|
14922
14998
|
* idempotent semantics preserved for callers that have not been
|
|
14923
|
-
* threaded with state region).
|
|
14999
|
+
* threaded with state region). An EMPTY string counts as unset — a caller
|
|
15000
|
+
* typed `region: string` can hand one over, and refusing on it would make
|
|
15001
|
+
* this guard reject its own default.
|
|
14924
15002
|
* - If `clientRegion` matches `expectedRegion`, returns silently.
|
|
14925
15003
|
* - Otherwise throws `ProvisioningError` so the caller surfaces the
|
|
14926
|
-
* mismatch instead of swallowing the NotFound.
|
|
15004
|
+
* mismatch instead of swallowing the NotFound / issuing the call.
|
|
14927
15005
|
*
|
|
14928
15006
|
* @param clientRegion Region resolved from the AWS SDK client config
|
|
14929
15007
|
* (typically `await client.config.region()`).
|
|
@@ -14933,13 +15011,27 @@ function describe(value) {
|
|
|
14933
15011
|
* message and on the thrown ProvisioningError.
|
|
14934
15012
|
* @param logicalId Logical ID of the resource, used in the error message
|
|
14935
15013
|
* and on the thrown ProvisioningError.
|
|
14936
|
-
* @param physicalId Optional physical ID,
|
|
14937
|
-
*
|
|
15014
|
+
* @param physicalId Optional physical ID, carried on the thrown
|
|
15015
|
+
* ProvisioningError.
|
|
15016
|
+
* @param phase Which call is being guarded — see {@link RegionCheckPhase}.
|
|
15017
|
+
* Defaults to the historical `'not-found'` so every pre-#2301 call site
|
|
15018
|
+
* keeps its exact wording.
|
|
14938
15019
|
*/
|
|
14939
|
-
function assertRegionMatch(clientRegion, expectedRegion, resourceType, logicalId, physicalId) {
|
|
15020
|
+
function assertRegionMatch(clientRegion, expectedRegion, resourceType, logicalId, physicalId, phase = "not-found") {
|
|
14940
15021
|
if (!expectedRegion) return;
|
|
14941
|
-
if (!clientRegion) throw new ProvisioningError(`Refusing to treat NotFound as idempotent delete success for ${logicalId} (${resourceType}): AWS client region is unknown but stack state expects ${expectedRegion}. The resource may exist in ${expectedRegion} and would be silently removed from state if this NotFound were trusted.`, resourceType, logicalId, physicalId);
|
|
14942
|
-
if (clientRegion !== expectedRegion) throw new ProvisioningError(`Refusing to treat NotFound as idempotent delete success for ${logicalId} (${resourceType}): AWS client region ${clientRegion} does not match stack state region ${expectedRegion}. The resource likely still exists in ${expectedRegion}; rerun the destroy with the correct region (e.g. --region ${expectedRegion}).`, resourceType, logicalId, physicalId);
|
|
15022
|
+
if (!clientRegion) throw new ProvisioningError(phase === "not-found" ? `Refusing to treat NotFound as idempotent delete success for ${logicalId} (${resourceType}): AWS client region is unknown but stack state expects ${expectedRegion}. The resource may exist in ${expectedRegion} and would be silently removed from state if this NotFound were trusted.` : `Refusing to ${phaseVerb(phase)} ${logicalId} (${resourceType}): AWS client region is unknown but stack state records the resource in ${expectedRegion}. cdkd cannot confirm that the physical id recorded in state names the resource this client would act on, so the ${phaseVerb(phase)} is not issued. Point the AWS client at ${expectedRegion} (AWS_REGION or your AWS profile) and re-run.`, resourceType, logicalId, physicalId);
|
|
15023
|
+
if (clientRegion !== expectedRegion) throw new ProvisioningError(phase === "not-found" ? `Refusing to treat NotFound as idempotent delete success for ${logicalId} (${resourceType}): AWS client region ${clientRegion} does not match stack state region ${expectedRegion}. The resource likely still exists in ${expectedRegion}; rerun the destroy with the correct region (e.g. --region ${expectedRegion}).` : `Refusing to ${phaseVerb(phase)} ${logicalId} (${resourceType}): AWS client region ${clientRegion} does not match stack state region ${expectedRegion}. The physical id recorded in cdkd state names a resource in ${expectedRegion}, so this ${phaseVerb(phase)} would act on whatever carries that id in ${clientRegion} instead — which for a name-shaped physical id is a different resource that usually exists. Point the AWS client at ${expectedRegion} (AWS_REGION or your AWS profile) and re-run; when the run spans several regions at once (cdkd drift --all), select the stacks in one region per run, because no single client region is correct for all of them. If the recorded region is the wrong one, correct the state record (cdkd state show).`, resourceType, logicalId, physicalId);
|
|
15024
|
+
}
|
|
15025
|
+
/**
|
|
15026
|
+
* The operation a pre-flight phase is about to issue, for the message.
|
|
15027
|
+
*
|
|
15028
|
+
* `'not-found'` is EXCLUDED from the parameter type rather than mapped to a
|
|
15029
|
+
* verb: both call sites already sit in the `else` of a
|
|
15030
|
+
* `phase === 'not-found' ? ... : ...`, so an arm answering for it would be
|
|
15031
|
+
* unreachable, and an unreachable arm is a claim no test can hold to account.
|
|
15032
|
+
*/
|
|
15033
|
+
function phaseVerb(phase) {
|
|
15034
|
+
return phase === "pre-update" ? "update" : "delete";
|
|
14943
15035
|
}
|
|
14944
15036
|
|
|
14945
15037
|
//#endregion
|
|
@@ -20413,8 +20505,9 @@ var CloudControlProvider = class {
|
|
|
20413
20505
|
/**
|
|
20414
20506
|
* Update a resource using Cloud Control API
|
|
20415
20507
|
*/
|
|
20416
|
-
async update(logicalId, physicalId, resourceType, properties, previousProperties) {
|
|
20508
|
+
async update(logicalId, physicalId, resourceType, properties, previousProperties, context) {
|
|
20417
20509
|
this.logger.debug(`Updating resource ${logicalId} (${resourceType}), physical ID: ${physicalId}`);
|
|
20510
|
+
await this.assertRecordedRegionAgainstClient("pre-update", context?.expectedRegion, resourceType, logicalId, physicalId);
|
|
20418
20511
|
try {
|
|
20419
20512
|
const cleanPreviousProperties = stringifyJsonProperties(resourceType, stripNullValues(previousProperties));
|
|
20420
20513
|
const cleanProperties = stringifyJsonProperties(resourceType, stripNullValues(properties));
|
|
@@ -20473,10 +20566,11 @@ var CloudControlProvider = class {
|
|
|
20473
20566
|
async delete(logicalId, physicalId, resourceType, _properties, context) {
|
|
20474
20567
|
this.logger.debug(`Deleting resource ${logicalId} (${resourceType}), physical ID: ${physicalId}`);
|
|
20475
20568
|
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);
|
|
20569
|
+
await this.assertRecordedRegionAgainstClient("pre-delete", context?.expectedRegion, resourceType, logicalId, physicalId);
|
|
20476
20570
|
await this.confirmDeleteTargetIdentity(logicalId, resourceType, physicalId, context);
|
|
20477
20571
|
if (context?.removeProtection === true && resourceType === "AWS::AutoScaling::AutoScalingGroup") {
|
|
20478
20572
|
this.logger.debug(`Delegating protected AutoScalingGroup ${logicalId} delete to the SDK ASGProvider (Cloud Control cannot force-delete a protected ASG)`);
|
|
20479
|
-
const { ASGProvider } = await import("./asg-provider-
|
|
20573
|
+
const { ASGProvider } = await import("./asg-provider-BeELAzdu.js").then((n) => n.n);
|
|
20480
20574
|
return await new ASGProvider().delete(logicalId, physicalId, resourceType, _properties, context);
|
|
20481
20575
|
}
|
|
20482
20576
|
const isProtectedEc2Instance = context?.removeProtection === true && resourceType === "AWS::EC2::Instance";
|
|
@@ -20499,7 +20593,7 @@ var CloudControlProvider = class {
|
|
|
20499
20593
|
} catch (error) {
|
|
20500
20594
|
const err = error;
|
|
20501
20595
|
if (error instanceof CloudControlOperationFailedError && error.ccOperation === "DELETE" && error.ccErrorCode === "NotFound" || err.name === "ResourceNotFoundException" || err.message?.includes("does not exist") || err.message?.includes("not found") || err.message?.includes("NotFound")) {
|
|
20502
|
-
|
|
20596
|
+
await this.assertRecordedRegionAgainstClient("not-found", context?.expectedRegion, resourceType, logicalId, physicalId);
|
|
20503
20597
|
this.logger.debug(`Resource ${logicalId} already deleted (not found), treating as success`);
|
|
20504
20598
|
return;
|
|
20505
20599
|
}
|
|
@@ -20513,15 +20607,77 @@ var CloudControlProvider = class {
|
|
|
20513
20607
|
}
|
|
20514
20608
|
}
|
|
20515
20609
|
/**
|
|
20610
|
+
* Refuse a Cloud Control call whose target region cannot be shown to be the
|
|
20611
|
+
* one the state record was written in (issue #2301).
|
|
20612
|
+
*
|
|
20613
|
+
* The ONE place this comparison happens, for all three phases: the
|
|
20614
|
+
* pre-flights at the top of `delete()` and `update()`, and the reactive
|
|
20615
|
+
* `not-found` arm inside `delete()`'s catch block. They can therefore never
|
|
20616
|
+
* disagree about what "unknown region" means, nor about how a region is
|
|
20617
|
+
* SPELLED -- the second one was live before this became shared: the reactive
|
|
20618
|
+
* arm compared raw while the pre-flight folded case, so one correct call
|
|
20619
|
+
* could pass the first and be refused by the second. THREE inputs, THREE outcomes, and they are
|
|
20620
|
+
* deliberately not two:
|
|
20621
|
+
*
|
|
20622
|
+
* - NO recorded region (`undefined`, or an empty / whitespace-only string)
|
|
20623
|
+
* -> PROCEED, and do not even resolve the client region. This is the
|
|
20624
|
+
* guard's OWN default: a `version: 1` state record predates the
|
|
20625
|
+
* region-scoped key layout and carries no region at all, and callers
|
|
20626
|
+
* typed `region: string` (`deploy-engine.ts`'s `stackRegion`) can hand
|
|
20627
|
+
* over `''`. Refusing on the absence would break every ordinary
|
|
20628
|
+
* destroy / update of a pre-v2 record, which is the over-tightening
|
|
20629
|
+
* failure a one-directional fence never sees.
|
|
20630
|
+
* - A recorded region that MATCHES the client -> proceed silently. This is
|
|
20631
|
+
* the ordinary path and it must stay free of new refusals: the whole
|
|
20632
|
+
* fleet of same-region deletes and updates runs through here.
|
|
20633
|
+
* - A recorded region that DIFFERS, or a client region that cannot be
|
|
20634
|
+
* resolved at all -> REFUSE before issuing anything.
|
|
20635
|
+
*
|
|
20636
|
+
* The unresolvable-client-region arm is the one asymmetry worth naming:
|
|
20637
|
+
* {@link CloudControlProvider.confirmDeleteTargetIdentity} PROCEEDS when it
|
|
20638
|
+
* cannot establish a region, and this helper refuses. The two are answering
|
|
20639
|
+
* different questions. That probe asks a remote service where a globally
|
|
20640
|
+
* unique NAME lives, and a least-privilege role that was never granted
|
|
20641
|
+
* `s3:GetBucketLocation` would be stranded by a refusal. Here the caller has
|
|
20642
|
+
* positively recorded a region, the comparison is local and free, and a
|
|
20643
|
+
* client that cannot say where it points cannot be shown to point at that
|
|
20644
|
+
* region -- the same answer `assertRegionMatch` has always given on its
|
|
20645
|
+
* `not-found` phase.
|
|
20646
|
+
*
|
|
20647
|
+
* The refusal is marked non-retryable because it is deterministic: both
|
|
20648
|
+
* loops that wrap these calls -- the destroy runner's own attempt loop and
|
|
20649
|
+
* the deploy engine's / rollback executor's `withRetry` -- would otherwise
|
|
20650
|
+
* spend their full budget re-deriving the same verdict, which reads to a
|
|
20651
|
+
* user as flaky AWS rather than as a refusal.
|
|
20652
|
+
*/
|
|
20653
|
+
async assertRecordedRegionAgainstClient(phase, expectedRegion, resourceType, logicalId, physicalId) {
|
|
20654
|
+
const recordedRegion = canonicalizeRegion(expectedRegion?.trim());
|
|
20655
|
+
if (recordedRegion === void 0 || recordedRegion === "") return;
|
|
20656
|
+
let clientRegion;
|
|
20657
|
+
try {
|
|
20658
|
+
clientRegion = canonicalizeRegion((await this.cloudControlClient.config.region())?.trim());
|
|
20659
|
+
} catch (error) {
|
|
20660
|
+
this.logger.debug(`Could not resolve the Cloud Control client region before the ${phase} region check for ${logicalId} (${resourceType}): ${error instanceof Error ? error.message : String(error)}`);
|
|
20661
|
+
clientRegion = void 0;
|
|
20662
|
+
}
|
|
20663
|
+
try {
|
|
20664
|
+
assertRegionMatch(clientRegion, recordedRegion, resourceType, logicalId, physicalId, phase);
|
|
20665
|
+
} catch (error) {
|
|
20666
|
+
throw markNonRetryable(error);
|
|
20667
|
+
}
|
|
20668
|
+
}
|
|
20669
|
+
/**
|
|
20516
20670
|
* Confirm that the resource `physicalId` names actually lives in the region
|
|
20517
20671
|
* this destroy is targeting, for the types in
|
|
20518
20672
|
* {@link CC_DELETE_IDENTITY_CHECKED_TYPES}. No-op for every other type.
|
|
20519
20673
|
*
|
|
20520
20674
|
* WHAT THIS GUARDS THAT `assertRegionMatch` DOES NOT
|
|
20521
20675
|
* ---------------------------------------------------
|
|
20522
|
-
* The
|
|
20523
|
-
*
|
|
20524
|
-
*
|
|
20676
|
+
* The `assertRegionMatch` comparison — which since issue #2301 runs both as
|
|
20677
|
+
* an unconditional pre-flight and on the `NotFound` arm below — compares the
|
|
20678
|
+
* CLIENT's region against the STATE's. That misses this hazard however often
|
|
20679
|
+
* it runs: both of its inputs can agree while the bucket the physical id
|
|
20680
|
+
* names sits somewhere else entirely. An `AWS::S3::Bucket` physical id is
|
|
20525
20681
|
* a GLOBALLY unique name, so a state record written before the issue #2227 /
|
|
20526
20682
|
* #2245 guards existed can name a bucket that is ours but lives elsewhere --
|
|
20527
20683
|
* a cdkd-GENERATED bucket name carries no region or account: for a name cdkd
|
|
@@ -23388,7 +23544,10 @@ var CustomResourceProvider = class CustomResourceProvider {
|
|
|
23388
23544
|
} catch (error) {
|
|
23389
23545
|
this.logger.debug(`Failed to delete custom-resource response object s3://${bucket}/${responseKey}; it remains as a current object. Underlying error: ${error instanceof Error ? error.message : String(error)}`);
|
|
23390
23546
|
}
|
|
23391
|
-
await purgeNoncurrentKeyVersions(this.s3Client, bucket, [responseKey], {
|
|
23547
|
+
await purgeNoncurrentKeyVersions(this.s3Client, bucket, [responseKey], {
|
|
23548
|
+
logger: this.logger,
|
|
23549
|
+
objectDescription: CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION
|
|
23550
|
+
});
|
|
23392
23551
|
}
|
|
23393
23552
|
/**
|
|
23394
23553
|
* Convert property values to strings for CloudFormation compatibility
|
|
@@ -29430,7 +29589,10 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
|
|
|
29430
29589
|
op.resourceType,
|
|
29431
29590
|
desiredProps ?? {},
|
|
29432
29591
|
currentProps ?? {},
|
|
29433
|
-
{
|
|
29592
|
+
{
|
|
29593
|
+
maskSecrets: createSecretMasker(secrets),
|
|
29594
|
+
expectedRegion: ctx.region
|
|
29595
|
+
}
|
|
29434
29596
|
], op.logicalId, logger, isInterrupted, secrets);
|
|
29435
29597
|
stateResources[op.logicalId] = redactRollbackRecord(recordAfterRollbackUpdate(previousState, revertResult), secrets, previousState.properties);
|
|
29436
29598
|
const rollbackPartial = updatePartialReason(revertResult);
|
|
@@ -29583,7 +29745,10 @@ async function replayFailedOperations(failedOps, stateResources, stackName, ctx,
|
|
|
29583
29745
|
op.resourceType,
|
|
29584
29746
|
desiredProps ?? {},
|
|
29585
29747
|
attemptedProps ?? {},
|
|
29586
|
-
{
|
|
29748
|
+
{
|
|
29749
|
+
maskSecrets: createSecretMasker(secrets),
|
|
29750
|
+
expectedRegion: ctx.region
|
|
29751
|
+
}
|
|
29587
29752
|
], op.logicalId, logger, options.isInterrupted, secrets);
|
|
29588
29753
|
stateResources[op.logicalId] = redactRollbackRecord(recordAfterRollbackUpdate(prev, revertFailedResult), secrets, prev.properties);
|
|
29589
29754
|
const revertFailedPartial = updatePartialReason(revertFailedResult);
|
|
@@ -31889,7 +32054,10 @@ var DeployEngine = class {
|
|
|
31889
32054
|
let result;
|
|
31890
32055
|
let resultProvisionedBy = updateDecision.provisionedBy;
|
|
31891
32056
|
try {
|
|
31892
|
-
result = await this.withRetry(() => withCurrentResourceSecrets(updateSecrets, () => updateProvider.update(logicalId, currentResource.physicalId, resourceType, updateProps, currentProps, {
|
|
32057
|
+
result = await this.withRetry(() => withCurrentResourceSecrets(updateSecrets, () => updateProvider.update(logicalId, currentResource.physicalId, resourceType, updateProps, currentProps, {
|
|
32058
|
+
maskSecrets: createSecretMasker(updateSecrets),
|
|
32059
|
+
expectedRegion: this.stackRegion
|
|
32060
|
+
})), logicalId, void 0, void 0, updateProvider);
|
|
31893
32061
|
} catch (updateError) {
|
|
31894
32062
|
const msg = updateError instanceof Error ? updateError.message : String(updateError);
|
|
31895
32063
|
const ccUnsupported = msg.includes("UnsupportedActionException") || msg.includes("does not support UPDATE");
|
|
@@ -31982,7 +32150,7 @@ var DeployEngine = class {
|
|
|
31982
32150
|
}), logicalId, 3, 5e3, deleteProvider);
|
|
31983
32151
|
} catch (deleteError) {
|
|
31984
32152
|
const msg = deleteError instanceof Error ? deleteError.message : String(deleteError);
|
|
31985
|
-
if (!isInterruptedWaitError(deleteError) && (msg.includes("does not exist") || msg.includes("was not found") || msg.includes("not found") || msg.includes("No policy found") || msg.includes("NoSuchEntity") || msg.includes("NotFoundException") || msg.includes("ResourceNotFoundException"))) this.logger.debug(`Resource ${logicalId} already deleted (${msg}), removing from state`);
|
|
32153
|
+
if (!isInterruptedWaitError(deleteError) && !isMarkedNonRetryable(deleteError) && (msg.includes("does not exist") || msg.includes("was not found") || msg.includes("not found") || msg.includes("No policy found") || msg.includes("NoSuchEntity") || msg.includes("NotFoundException") || msg.includes("ResourceNotFoundException"))) this.logger.debug(`Resource ${logicalId} already deleted (${msg}), removing from state`);
|
|
31986
32154
|
else throw deleteError;
|
|
31987
32155
|
}
|
|
31988
32156
|
const deleteSkipped = deleteSkipReason(deleteResult);
|
|
@@ -32390,5 +32558,5 @@ var DeployEngine = class {
|
|
|
32390
32558
|
};
|
|
32391
32559
|
|
|
32392
32560
|
//#endregion
|
|
32393
|
-
export { maskerOrIdentity as $, CFN_TEMPLATE_BODY_LIMIT as $n, maskSecretsInText as $t, renderStatefulReason as A, describeAwsFailure as An,
|
|
32394
|
-
//# sourceMappingURL=deploy-engine-
|
|
32561
|
+
export { maskerOrIdentity as $, CFN_TEMPLATE_BODY_LIMIT as $n, maskSecretsInText as $t, renderStatefulReason as A, describeAwsFailure as An, PartialFailureError as Ar, requireConfigString as At, exportAliasCollisionScrubWarning as B, Synthesizer as Bn, normalizeAwsError as Br, INTRINSIC_KEYS as Bt, isFinalSnapshotError as C, getBootstrapMarkerKey as Cn, DynamicReferenceRegionAmbiguousError as Cr, coerceCfnBoolean as Ct, extractDeploymentEventError as D, validateAssetBucketName as Dn, LockError as Dr, replayWarn as Dt, makeCanonicalizePropertiesFn as E, readBootstrapMarkerBody as En, LocalStartServiceError as Er, readConfigString as Et, green as F, partitionSensitiveEnv as Fn, StackTerminationProtectionError as Fr, s3BucketDualStackDomainName as Ft, IAMRoleProvider as G, resolveAutoAssetStorage as Gn, markNonRetryable as Gr, STATE_SOURCED_CROSS_GENERATION_RULES as Gt, secretBearingStateKeyWarning as H, getDefaultStateBucketName as Hn, isMarkedNonRetryable as Hr, withRetry as Ht, red as I, runDockerForeground as In, StateError as Ir, s3BucketRegionalDomainName as It, ProviderRegistry as J, resolveStateBucketWithDefault as Jn, __exportAll as Jr, createSecretMasker as Jt, collectInlinePolicyNamesManagedBySiblings as K, resolveCaptureObservedState as Kn, markRedactedCause as Kr, STATE_SOURCED_READBACK_RULES as Kt, yellow as L, runDockerStreaming as Ln, SynthesisError as Lr, s3BucketWebsiteUrl as Lt, bold as M, dockerSpawnEnvWithSensitive as Mn, ResourceTimeoutError as Mr, producerRegionsFromState as Mt, cyan as N, formatDockerLoginError as Nn, ResourceUpdateNotSupportedError as Nr, s3BucketArn as Nt, MULTI_REGION_RECREATE_BLOCKED_TYPES as O, validateContainerRepoName as On, MissingCdkCliError as Or, requireConfigArray as Ot, gray as P, getDockerCmd as Pn, StackHasActiveImportsError as Pr, s3BucketDomainName as Pt, maskDeep as Q, warnDeprecatedNoPrefixCliFlag as Qn, maskSecretsInError as Qt, collectDeclaredOutputNames as R, AssetManifestLoader as Rn, formatError as Rr, applyRoleArnIfSet as Rt, createPreDeleteFinalSnapshot as S, ensureAssetStorage as Sn, DeployCancelledError as Sr, assertRegionMatch as St, unsupportedFinalSnapshotError as T, parseBootstrapMarker as Tn, LocalMigrateError as Tr, configStringRefusal as Tt, stateKeySecretExposure as U, getLegacyStateBucketName as Un, isRetryableTransientError as Ur, DagBuilder as Ut, isExportAliasCollision as V, synthesisStatusMessage as Vn, withErrorHandling as Vr, describeTypeWithThrottleRetry as Vt, getCurrentResourceSecrets as W, resolveApp as Wn, isThrottlingError as Wr, TemplateParser as Wt, findSilentDropProperties as X, resolveUseCdkBootstrapAssets as Xn, errorCauseChain as Xt, findActionableSilentDrops as Y, resolveStateBucketWithDefaultAndSource as Yn, dynamicReferenceTokens as Yt, createMaskedRetryLogger as Z, stateBucketExistenceConfirmed as Zn, isSingleDynamicReferenceToken as Zt, computeImplicitDeleteEdges as _, escapeRegExp$1 as _n, AssetError as _r, parameterTypeMayLoseSecretIdentity as _t, DeploymentEventsStore as a, rebuildClientForBucketRegion as an, expectedOwnerParam as ar, isInterruptedWaitError as at, buildFinalSnapshotIdentifier as b, BOOTSTRAP_MARKER_PREFIX as bn, CrossAccountSecretRefusalError as br, normalizeAwsTagsToCfn as bt, replayFailedOperations as c, importableOutputs as cn, derivePartitionAndUrlSuffix as cr, slowCcOperationTimeoutMs as ct, updatePartialReason as d, stringifyValue as dn, clearBucketRegionCache as dr, IntrinsicFunctionResolver as dt, redactSecretsForState as en, CFN_TEMPLATE_URL_LIMIT as er, CUSTOM_RESOURCE_RESPONSE_PREFIX as et, UNSPECIFIED_SKIP_REASON as f, WorkGraph as fn, resolveBucketRegion as fr, carriesDynamicReference as ft, IMPLICIT_DELETE_DEPENDENCIES as g, rewriteTemplateAssetReferences as gn, setAwsClients as gr, isUnboundTemplateParameter as gt, maskingRetryLogger as h, loadPublishableAssetManifest as hn, resetAwsClients as hr, getAccountInfo as ht, DeploymentEventsReader as i, S3StateBackend as in, CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION as ir, interruptWatchListenerCount as it, formatResourceLine as j, buildDockerImage as jn, ProvisioningError as jr, classifyReplaySecretRegion as jt, isStatefulRecreateTargetSync as k, buildDenyExternalAccessPolicy as kn, NestedStackChildDirectDestroyError as kr, requireConfigObject as kt, replayRollback as l, shouldRetainResource as ln, AssemblyReader as lr, disableInstanceApiTermination as lt, withResourceDeadline as m, createAssetRedirectResolver as mn, getAwsClients as mr, coerceParameterTypedValue as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, LockManager as nn, findLargeInlineResources as nr, beginCommandInterruptScope as nt, planFailedOps as o, exportNamesCarriedFrom as on, PARTITION_TABLE as or, startInterruptWatch as ot, deleteSkipReason as p, buildAssetRedirectMap as pn, AwsClients as pr, cfnRefValueFromPhysicalId as pt, clearOnUpdateRemoval as q, resolveSkipPrefix as qn, retryClassificationText as qr, TEMPLATE_SOURCED_RULES as qt, DeployEngine as r, displaySafe as rn, uploadCfnTemplate as rr, endCommandInterruptScope as rt, planRollback as s, importableOutputKeys as sn, canonicalizeRegion as sr, CloudControlProvider as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, scrubResourceRecord as tn, MIGRATE_TMP_PREFIX as tr, DEFAULT_STATE_PREFIX as tt, updatePartialMessage as u, AssetPublisher as un, processStackMessages as ur, isTerminationProtectionPropagationError as ut, ATOMIC_FINAL_SNAPSHOT_TYPES as v, stripControlChars as vn, CdkdError as vr, refStateLookupFromResource as vt, refusesFinalSnapshot as w, isCrossRegionRedirect as wn, LocalInvokeBuildError as wr, configBooleanRefusal as wt, ccRoutedFinalSnapshotError as x, assertAssetBucketRegion as xn, DependencyError as xr, resolveExplicitPhysicalId as xt, PRE_DELETE_SNAPSHOT_TYPES as y, AssetModeResolver as yn, ConfigError as yr, WAFv2WebACLProvider as yt, collectPublishedOutputNames as z, getDockerImageBySourceHash as zn, isCdkdError as zr, DiffCalculator as zt };
|
|
32562
|
+
//# sourceMappingURL=deploy-engine-Du2CDZop.js.map
|