@go-to-k/cdkd 0.286.1 → 0.286.3
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-BocQJsjl.js → asg-provider-uaOnRUMe.js} +2 -2
- package/dist/{asg-provider-BocQJsjl.js.map → asg-provider-uaOnRUMe.js.map} +1 -1
- package/dist/cli.js +2 -2
- package/dist/{deploy-engine-D0RCEQ6D.js → deploy-engine--rkIGhow.js} +1061 -67
- package/dist/deploy-engine--rkIGhow.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/{program-x67eZ06V.js → program-CG34-sQd.js} +331 -186
- package/dist/{program-x67eZ06V.js.map → program-CG34-sQd.js.map} +1 -1
- package/dist/{version-CnQK6Jeh.js → version-D3md0Lde.js} +2 -2
- package/dist/{version-CnQK6Jeh.js.map → version-D3md0Lde.js.map} +1 -1
- package/package.json +1 -1
- package/dist/deploy-engine-D0RCEQ6D.js.map +0 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { _ as withStackName, d as applyDefaultNameForFallback, f as generateResourceName, h as looksLikeCdkdGeneratedName, m as getCurrentStackName, n as getLogger, p as generateResourceNameWithFallback, r as isStdoutReservedForPayload, s as getLiveRenderer } from "./logger-Dw-3y48G.js";
|
|
2
2
|
import { t as awsClientDefaults } from "./aws-client-defaults-D-iYiIJ6.js";
|
|
3
|
-
import { t as getCdkdVersion } from "./version-
|
|
3
|
+
import { t as getCdkdVersion } from "./version-D3md0Lde.js";
|
|
4
4
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
|
-
import { CreateBucketCommand, DeleteObjectCommand, DeleteObjectsCommand, GetBucketLocationCommand, GetObjectCommand, HeadBucketCommand, HeadObjectCommand, ListObjectVersionsCommand, ListObjectsV2Command, NoSuchKey, PutBucketEncryptionCommand, PutBucketPolicyCommand, PutObjectCommand, PutPublicAccessBlockCommand, S3Client, S3ServiceException } from "@aws-sdk/client-s3";
|
|
6
|
+
import { CreateBucketCommand, DeleteObjectCommand, DeleteObjectsCommand, GetBucketLocationCommand, GetBucketReplicationCommand, GetObjectCommand, HeadBucketCommand, HeadObjectCommand, ListObjectVersionsCommand, ListObjectsV2Command, NoSuchKey, PutBucketEncryptionCommand, PutBucketPolicyCommand, PutObjectCommand, PutPublicAccessBlockCommand, S3Client, S3ServiceException } from "@aws-sdk/client-s3";
|
|
7
7
|
import { CloudControlClient, CreateResourceCommand, DeleteResourceCommand, GetResourceCommand, GetResourceRequestStatusCommand, ListResourcesCommand, UpdateResourceCommand } from "@aws-sdk/client-cloudcontrol";
|
|
8
8
|
import { AttachRolePolicyCommand, CreateRoleCommand, DeleteRoleCommand, DeleteRolePermissionsBoundaryCommand, DeleteRolePolicyCommand, DetachRolePolicyCommand, GetRoleCommand, GetRolePolicyCommand, IAMClient, ListAttachedRolePoliciesCommand, ListInstanceProfilesForRoleCommand, ListRolePoliciesCommand, ListRoleTagsCommand, NoSuchEntityException, PutRolePermissionsBoundaryCommand, PutRolePolicyCommand, RemoveRoleFromInstanceProfileCommand, TagRoleCommand, UntagRoleCommand, UpdateAssumeRolePolicyCommand, UpdateRoleCommand } from "@aws-sdk/client-iam";
|
|
9
9
|
import { SQSClient } from "@aws-sdk/client-sqs";
|
|
@@ -35,6 +35,7 @@ import { KMSClient, ListAliasesCommand } from "@aws-sdk/client-kms";
|
|
|
35
35
|
import { readFile } from "fs/promises";
|
|
36
36
|
import { join as join$1 } from "path";
|
|
37
37
|
import { CreateRepositoryCommand, DescribeImagesCommand as DescribeImagesCommand$1, DescribeRepositoriesCommand, ECRClient, GetAuthorizationTokenCommand, PutImageTagMutabilityCommand } from "@aws-sdk/client-ecr";
|
|
38
|
+
import { inspect } from "node:util";
|
|
38
39
|
import { hostname } from "os";
|
|
39
40
|
import graphlib from "graphlib";
|
|
40
41
|
import { DescribeDBClustersCommand, DescribeDBInstancesCommand, RDSClient } from "@aws-sdk/client-rds";
|
|
@@ -836,6 +837,116 @@ function isNameCooldownError(message) {
|
|
|
836
837
|
function isRecreateRetryableError(message) {
|
|
837
838
|
return isNameCollisionError(message) || isNameCooldownError(message);
|
|
838
839
|
}
|
|
840
|
+
/**
|
|
841
|
+
* The Cloud Control exception NAME for "this resource type ships no handler
|
|
842
|
+
* for the action you asked for" (issue
|
|
843
|
+
* [#2520](https://github.com/go-to-k/cdkd/issues/2520)).
|
|
844
|
+
*
|
|
845
|
+
* A NAME, not a message fragment: AWS SDK v3 sets `error.name` to the service
|
|
846
|
+
* exception's own identifier, which is a wire-level contract, while the prose
|
|
847
|
+
* beside it is text AWS is free to reword. The sibling READ classifier in
|
|
848
|
+
* `src/cli/commands/drift.ts` (`NO_READ_HANDLER_NAMES`) already keys on this
|
|
849
|
+
* same name for the GET direction.
|
|
850
|
+
*/
|
|
851
|
+
const CC_UNSUPPORTED_ACTION_ERROR_NAME = "UnsupportedActionException";
|
|
852
|
+
/**
|
|
853
|
+
* The AWS prose the update-not-supported classifier accepted before the
|
|
854
|
+
* structured signal existed. Kept as a TOP-LEVEL-only fallback — see
|
|
855
|
+
* {@link isUpdateUnsupportedError} for why it is not walked down the chain.
|
|
856
|
+
*/
|
|
857
|
+
const CC_UPDATE_UNSUPPORTED_MESSAGE_FALLBACK = "does not support UPDATE";
|
|
858
|
+
/**
|
|
859
|
+
* True when a failed `provider.update()` for `logicalId` was rejected because
|
|
860
|
+
* the resource type has no UPDATE handler at all — the signal the deploy
|
|
861
|
+
* engine's update-failure fallback fires on, turning the update into a
|
|
862
|
+
* replacement.
|
|
863
|
+
*
|
|
864
|
+
* ## Why the chain is walked
|
|
865
|
+
*
|
|
866
|
+
* `CloudControlProvider.handleError` WRAPS the raw AWS rejection in a
|
|
867
|
+
* `ProvisioningError` and interpolates `err.message` only; the exception name
|
|
868
|
+
* is never copied into the wrapper's text. Measured 2026-09-04: `aws
|
|
869
|
+
* cloudcontrol update-resource --type-name AWS::DocDB::DBCluster` answers
|
|
870
|
+
* `UnsupportedActionException` with the message `Resource type
|
|
871
|
+
* AWS::DocDB::DBCluster does not support UPDATE action`, which does not repeat
|
|
872
|
+
* the name. That is why the predicate's pre-#2520
|
|
873
|
+
* `message.includes('UnsupportedActionException')` half matched nothing cdkd
|
|
874
|
+
* produces, and why the structured read has to look one link down.
|
|
875
|
+
*
|
|
876
|
+
* Two structured signals, both read off a link rather than out of prose:
|
|
877
|
+
*
|
|
878
|
+
* - `name` — the synchronous `UpdateResource` rejection, one cause link below
|
|
879
|
+
* the provider's wrapper.
|
|
880
|
+
* - `ccErrorCode` PLUS `ccOperation === 'UPDATE'` — the two fields
|
|
881
|
+
* `CloudControlOperationFailedError` carries for an asynchronous
|
|
882
|
+
* progress-event failure, read structurally exactly as
|
|
883
|
+
* `cloud-control-provider.ts` reads the code for `AlreadyExists` /
|
|
884
|
+
* `NotFound`. No async occurrence has been MEASURED
|
|
885
|
+
* (`UnsupportedActionException` is raised synchronously by `UpdateResource`
|
|
886
|
+
* today); the arm exists so the async shape cannot silently fall through to
|
|
887
|
+
* prose, and it is pinned by unit cases built from the real error class.
|
|
888
|
+
* `ccOperation` is required rather than decorative precisely BECAUSE the
|
|
889
|
+
* arm is unmeasured: a CREATE or DELETE sub-operation reporting the same
|
|
890
|
+
* code says nothing about whether the type has an UPDATE handler, and
|
|
891
|
+
* reading the code alone would let it trigger a DELETE + CREATE. The
|
|
892
|
+
* narrowing is not absolute and the gap is stated rather than papered over:
|
|
893
|
+
* such a failure arriving at the TOP level still classifies if its own
|
|
894
|
+
* MESSAGE quotes AWS's prose. Unreachable today —
|
|
895
|
+
* `CloudControlOperationFailedError`'s message is built as
|
|
896
|
+
* `${operation} failed for <id>: <StatusMessage>`, so a CREATE's text
|
|
897
|
+
* cannot contain the UPDATE phrase unless AWS puts it there — and closing
|
|
898
|
+
* it would mean anchoring the prose read on the operation too, which would
|
|
899
|
+
* narrow the retained pre-#2520 reach rather than preserve it.
|
|
900
|
+
*
|
|
901
|
+
* ## Why the walk stops at another resource's error
|
|
902
|
+
*
|
|
903
|
+
* `logicalId` is not decoration — it is the fence that keeps a chain walk from
|
|
904
|
+
* being WIDER than the message read it replaces. `NestedStackProvider.update`
|
|
905
|
+
* runs a whole child deploy inside the PARENT's `provider.update()` call, so a
|
|
906
|
+
* child resource's Cloud Control rejection propagates into the parent's update
|
|
907
|
+
* catch, several cause links down. An unanchored walk would classify that as
|
|
908
|
+
* "the nested stack cannot be updated in place" and DELETE + CREATE the entire
|
|
909
|
+
* child stack. The pre-#2520 message read was immune by accident — the child
|
|
910
|
+
* engine's wrapper is `Failed to update resource <child>`, which quotes no AWS
|
|
911
|
+
* text — and this anchor makes the immunity deliberate: `ProvisioningError`
|
|
912
|
+
* and `ResourceUpdateNotSupportedError` both carry `logicalId`, so the walk
|
|
913
|
+
* stops dead at the first link that names a resource other than the one being
|
|
914
|
+
* updated.
|
|
915
|
+
*
|
|
916
|
+
* The prose fallback is read at the TOP LEVEL ONLY, exactly as the pre-#2520
|
|
917
|
+
* predicate did, and for the same asymmetry: missing the signal fails the
|
|
918
|
+
* deploy (safe), matching it too broadly replaces a resource nobody asked to
|
|
919
|
+
* replace (unsafe). It is read INSIDE the walk, after the anchor, so the
|
|
920
|
+
* anchor governs every route into a `true` — ordered ahead of it, the
|
|
921
|
+
* nested-stack immunity would rest on the child engine's wrapper happening to
|
|
922
|
+
* quote no AWS text, which is a property of another file.
|
|
923
|
+
*
|
|
924
|
+
* ## Codes deliberately NOT matched
|
|
925
|
+
*
|
|
926
|
+
* The Cloud Control handler error code `NotUpdatable` reports "this particular
|
|
927
|
+
* patch is not applicable" (a create-only property, an invalid document)
|
|
928
|
+
* rather than "the type has no UPDATE handler", and cdkd already routes the
|
|
929
|
+
* create-only case through its own property-driven replacement — accepting it
|
|
930
|
+
* here would convert ordinary update rejections into replacements.
|
|
931
|
+
* `TypeNotFoundException`, which `handleError` wraps into the SAME sentence as
|
|
932
|
+
* the unsupported-action case, is not matched either: an unregistered type has
|
|
933
|
+
* no replacement story, so it must keep failing the deploy rather than
|
|
934
|
+
* deleting the resource.
|
|
935
|
+
*/
|
|
936
|
+
function isUpdateUnsupportedError(error, logicalId) {
|
|
937
|
+
let current = error;
|
|
938
|
+
for (let depth = 0; current !== null && current !== void 0 && depth < MAX_CAUSE_CHAIN_DEPTH; depth++) {
|
|
939
|
+
const link = current;
|
|
940
|
+
if (typeof link.logicalId === "string" && link.logicalId !== logicalId) return false;
|
|
941
|
+
if (link.name === "UnsupportedActionException") return true;
|
|
942
|
+
if (link.ccErrorCode === "UnsupportedActionException" && link.ccOperation === "UPDATE") return true;
|
|
943
|
+
if (depth === 0) {
|
|
944
|
+
if ((current instanceof Error ? current.message : String(current)).includes("does not support UPDATE")) return true;
|
|
945
|
+
}
|
|
946
|
+
current = link.cause;
|
|
947
|
+
}
|
|
948
|
+
return false;
|
|
949
|
+
}
|
|
839
950
|
|
|
840
951
|
//#endregion
|
|
841
952
|
//#region src/utils/error-handler.ts
|
|
@@ -3917,16 +4028,402 @@ function displaySafe(value, opts) {
|
|
|
3917
4028
|
}
|
|
3918
4029
|
|
|
3919
4030
|
//#endregion
|
|
3920
|
-
//#region src/state/s3-
|
|
4031
|
+
//#region src/state/s3-replication-purge-gap.ts
|
|
4032
|
+
/**
|
|
4033
|
+
* Tell the user when S3 REPLICATION has silently kept the bodies that
|
|
4034
|
+
* {@link ../state/s3-noncurrent-version-purge.js | the noncurrent-version
|
|
4035
|
+
* purge} just removed (issue
|
|
4036
|
+
* [#2447](https://github.com/go-to-k/cdkd/issues/2447)).
|
|
4037
|
+
*
|
|
4038
|
+
* ## The hole this closes
|
|
4039
|
+
*
|
|
4040
|
+
* `purgeNoncurrentKeyVersions` exists so that "cdkd deleted the object" means
|
|
4041
|
+
* the body is no longer retrievable. It reaches that by issuing
|
|
4042
|
+
* `DeleteObjects` entries that name a specific `VersionId`.
|
|
4043
|
+
*
|
|
4044
|
+
* **S3 never replicates a version-id delete.** Replication propagates PUTs
|
|
4045
|
+
* and — only when `DeleteMarkerReplication` is enabled — delete markers; a
|
|
4046
|
+
* delete naming a version id is deliberately not replicated, so that a delete
|
|
4047
|
+
* on the source cannot destroy data on the destination. On a state bucket
|
|
4048
|
+
* with Cross-Region (CRR) or Same-Region (SRR) replication the purge is
|
|
4049
|
+
* therefore SOURCE-ONLY: the replica keeps its own copy of every version,
|
|
4050
|
+
* `GetObject` with a `VersionId` still returns the secret, and cdkd reports
|
|
4051
|
+
* the purge as complete because on the only bucket it knows about, it was.
|
|
4052
|
+
*
|
|
4053
|
+
* That is exactly the shape the purge was built to remove — an operation that
|
|
4054
|
+
* reports success while the value stays readable — reproduced one bucket over
|
|
4055
|
+
* and invisible from every signal cdkd emitted. The user is not merely
|
|
4056
|
+
* unprotected, they are REASSURED, which is the direction this repo treats as
|
|
4057
|
+
* a defect rather than a gap.
|
|
4058
|
+
*
|
|
4059
|
+
* ## Why a probe and not only a doc line
|
|
4060
|
+
*
|
|
4061
|
+
* Documentation reaches the reader who goes looking. This reaches the one who
|
|
4062
|
+
* does not, at the moment the false belief would be formed. The whole cost is
|
|
4063
|
+
* one bucket-metadata call per process (see the cache below) and one IAM
|
|
4064
|
+
* action the caller may well not hold.
|
|
4065
|
+
*
|
|
4066
|
+
* ## Degrading is the DEFAULT, not the failure
|
|
4067
|
+
*
|
|
4068
|
+
* The purge's contract is that a missing permission produces a warning and
|
|
4069
|
+
* never an abort, and a detector bolted onto it must not be able to break
|
|
4070
|
+
* that. So this module NEVER THROWS, and treats every non-answer as "we do
|
|
4071
|
+
* not know" rather than as either polarity:
|
|
4072
|
+
*
|
|
4073
|
+
* - `ReplicationConfigurationNotFoundError` — the answer "no replication
|
|
4074
|
+
* configuration exists". S3 reports the ABSENCE of a configuration as an
|
|
4075
|
+
* ERROR, not as an empty response body, so this arm is the ordinary case
|
|
4076
|
+
* for almost every bucket and must not read as a failure.
|
|
4077
|
+
* `ReplicationConfiguration` ABSENT from a 200 response is treated the same
|
|
4078
|
+
* way: the SDK models the field as optional, and a modelled field is not a
|
|
4079
|
+
* promise that the API populates it.
|
|
4080
|
+
* - `AccessDenied` and friends — the caller lacks
|
|
4081
|
+
* `s3:GetReplicationConfiguration`. Logged at DEBUG and never at `warn`:
|
|
4082
|
+
* the overwhelming majority of buckets are not replicated, so warning here
|
|
4083
|
+
* would tell almost every user to grant a permission in order to be told
|
|
4084
|
+
* nothing. The doc names the action for the user who wants the check.
|
|
4085
|
+
* - anything else (throttle, 5xx, network) — DEBUG, and deliberately NOT
|
|
4086
|
+
* cached, so the next purge in the same process retries.
|
|
4087
|
+
*
|
|
4088
|
+
* ## What is cached, what is deduped, and why they are different keys
|
|
4089
|
+
*
|
|
4090
|
+
* The PROBE is cached per (bucket, asserted owner) for the process lifetime, as
|
|
4091
|
+
* the sibling `src/provisioning/create-only-properties.ts` caches its
|
|
4092
|
+
* `DescribeType` lookups and for the same reason: the answer cannot change
|
|
4093
|
+
* mid-run, the value stored is the in-flight PROMISE so concurrent purges share
|
|
4094
|
+
* one call, and only non-transient outcomes are kept. Without it a stack with
|
|
4095
|
+
* thirty custom resources would issue thirty identical `GetBucketReplication`
|
|
4096
|
+
* calls.
|
|
4097
|
+
*
|
|
4098
|
+
* The WARNING is deduped too, but on a DIFFERENT and deliberately wider key —
|
|
4099
|
+
* (bucket, object description, destinations) — with the repeats sent to
|
|
4100
|
+
* `debug`. The two keys answer two questions and must not be merged:
|
|
4101
|
+
*
|
|
4102
|
+
* - the bucket alone is not enough, because `lock-manager.ts` routes this
|
|
4103
|
+
* module's `warn` sink to `debug` on the ordinary release path (it purges a
|
|
4104
|
+
* heartbeat record on every command and must not warn about it). Keyed on the
|
|
4105
|
+
* bucket, that demoted line would consume the one warning the rollback
|
|
4106
|
+
* journal or the response sidecar needed;
|
|
4107
|
+
* - adding the description is still not enough, because two rules covering
|
|
4108
|
+
* different prefixes can name DIFFERENT destinations. Purging a `cdkd/dev/…`
|
|
4109
|
+
* key can match only the first rule; a later `cdkd/prod/…` purge under the
|
|
4110
|
+
* same description matches both, and without the destinations in the key the
|
|
4111
|
+
* user would clean the first replica and never learn the second one holds the
|
|
4112
|
+
* production journal.
|
|
4113
|
+
*
|
|
4114
|
+
* Deduping at all is a departure from the sibling purge module, which
|
|
4115
|
+
* deliberately repeats its own failure warning. The difference is that a
|
|
4116
|
+
* failure RECURS — each one is another object cdkd could not remove — while
|
|
4117
|
+
* this is a fact about a BUCKET, identical every time it is re-derived.
|
|
4118
|
+
* Repeating it thirty times in a deploy adds nothing and erodes the failure
|
|
4119
|
+
* warning standing next to it.
|
|
4120
|
+
*/
|
|
3921
4121
|
/**
|
|
3922
4122
|
* Parenthetical used when a caller names nothing.
|
|
3923
4123
|
*
|
|
3924
|
-
* True of ANY object
|
|
3925
|
-
*
|
|
3926
|
-
*
|
|
3927
|
-
*
|
|
4124
|
+
* True of ANY object the purge is pointed at, which is the bar for a default
|
|
4125
|
+
* here: a caller that forgets to describe its object must still emit a warning
|
|
4126
|
+
* that is correct, just less specific. It deliberately does not guess at
|
|
4127
|
+
* content.
|
|
4128
|
+
*
|
|
4129
|
+
* Lives HERE rather than in `s3-noncurrent-version-purge.ts` only to keep the
|
|
4130
|
+
* import one-directional — the purge imports this module, so a shared constant
|
|
4131
|
+
* defined there and read here would be a cycle. It is the same sentence both
|
|
4132
|
+
* warnings fall back to, and one definition is what stops the two drifting.
|
|
4133
|
+
*/
|
|
4134
|
+
const DEFAULT_PURGED_OBJECT_DESCRIPTION = "the body of an object cdkd has just reported as removed";
|
|
4135
|
+
/**
|
|
4136
|
+
* Cache of the replication probe, holding the in-flight promise.
|
|
4137
|
+
*
|
|
4138
|
+
* Keyed on the bucket AND the `ExpectedBucketOwner` the caller asserts. The
|
|
4139
|
+
* SUCCESS answer does not depend on the owner assertion — two callers
|
|
4140
|
+
* differing only in whether they assert it read the same configuration — but
|
|
4141
|
+
* the FAILURE does: a mismatched-owner 403 is an answer about that caller's
|
|
4142
|
+
* request, and caching it under the bare bucket name silenced the detector for
|
|
4143
|
+
* every correctly-scoped caller in the process. cdkd has two shapes today (the
|
|
4144
|
+
* provider probes without an owner, the state backend and lock manager with
|
|
4145
|
+
* one), so the two are distinguishable in practice.
|
|
4146
|
+
*/
|
|
4147
|
+
const replicationProbeCache = /* @__PURE__ */ new Map();
|
|
4148
|
+
/**
|
|
4149
|
+
* (bucket, object description, destinations) triples already warned about.
|
|
4150
|
+
*
|
|
4151
|
+
* The one thing the warning says that VARIES is `objectDescription`; the rest
|
|
4152
|
+
* is a fact about the bucket, identical every time. So a stack with thirty
|
|
4153
|
+
* custom resources repeated a ~600-character warning thirty times while adding
|
|
4154
|
+
* no information after the first — and warning fatigue erodes the purge's own
|
|
4155
|
+
* failure warning standing next to it.
|
|
4156
|
+
*
|
|
4157
|
+
* Keyed on all three precisely so this cannot become a silence hole; the
|
|
4158
|
+
* module doc above gives the two failures a narrower key produces. Entries are
|
|
4159
|
+
* added only after the warning is actually emitted, so a throwing sink does
|
|
4160
|
+
* not burn a slot.
|
|
4161
|
+
*
|
|
4162
|
+
* RESIDUAL, bounded and recorded: cdkd cannot see whether the caller's sink
|
|
4163
|
+
* DEMOTED the line, so a `lock-manager.ts` release (which routes `warn` to
|
|
4164
|
+
* `debug`) claims the lock's slot and a later reap of a stale lock — which
|
|
4165
|
+
* would have warned — is suppressed. It is bounded to the LOCK description,
|
|
4166
|
+
* whose object the docs describe as carrying no secret, and to one process.
|
|
4167
|
+
*/
|
|
4168
|
+
const warnedWarnings = /* @__PURE__ */ new Set();
|
|
4169
|
+
/** How many destinations the warning names before it truncates. */
|
|
4170
|
+
const MAX_NAMED_DESTINATIONS = 3;
|
|
4171
|
+
/** Shown when a rule carries no `Destination.Bucket` for us to name. */
|
|
4172
|
+
const UNNAMED_DESTINATION = "<destination not named by the replication rule>";
|
|
4173
|
+
/**
|
|
4174
|
+
* Error codes meaning "this principal will never get an answer here".
|
|
4175
|
+
*
|
|
4176
|
+
* Cached like a real answer, because unlike a throttle they do not clear
|
|
4177
|
+
* within the process: retrying on every purge would spend one denied API call
|
|
4178
|
+
* per custom resource for a result that cannot change.
|
|
4179
|
+
*/
|
|
4180
|
+
const PERMANENT_DENIALS = /* @__PURE__ */ new Set([
|
|
4181
|
+
"AccessDenied",
|
|
4182
|
+
"AccessDeniedException",
|
|
4183
|
+
"AllAccessDisabled",
|
|
4184
|
+
"MethodNotAllowed",
|
|
4185
|
+
"NotImplemented"
|
|
4186
|
+
]);
|
|
4187
|
+
/**
|
|
4188
|
+
* Credential-shaped failures that ARRIVE AS 403 and are nonetheless transient.
|
|
4189
|
+
*
|
|
4190
|
+
* Checked BEFORE the blanket 403 rule, which is the only way they can win:
|
|
4191
|
+
* `InvalidAccessKeyId` is the classic blip while an IAM principal or an
|
|
4192
|
+
* assumed-role credential propagates — this repo retries that whole class over
|
|
4193
|
+
* a 47.75 s schedule elsewhere — and an expired or skewed token clears on
|
|
4194
|
+
* refresh. Cached as permanent, ONE such blip on the first purge of a long
|
|
4195
|
+
* deploy would silence the detector for the rest of the process, which is the
|
|
4196
|
+
* "false silence leaves them believing a secret is gone" direction this module
|
|
4197
|
+
* forbids.
|
|
4198
|
+
*/
|
|
4199
|
+
const TRANSIENT_CREDENTIAL_CODES = /* @__PURE__ */ new Set([
|
|
4200
|
+
"InvalidAccessKeyId",
|
|
4201
|
+
"ExpiredToken",
|
|
4202
|
+
"ExpiredTokenException",
|
|
4203
|
+
"InvalidToken",
|
|
4204
|
+
"TokenRefreshRequired",
|
|
4205
|
+
"RequestTimeTooSkewed",
|
|
4206
|
+
"RequestExpired"
|
|
4207
|
+
]);
|
|
4208
|
+
const describe$2 = (error) => error instanceof Error ? error.message : String(error);
|
|
4209
|
+
/**
|
|
4210
|
+
* Every wire error code the value offers, `name` and `Code` alike.
|
|
4211
|
+
*
|
|
4212
|
+
* NO PRECEDENCE, deliberately. An earlier revision collapsed the two to one
|
|
4213
|
+
* string and preferred a specific `name`, which had two failure modes in
|
|
4214
|
+
* opposite directions: preferring `name` unconditionally made the `Code`
|
|
4215
|
+
* fallback dead (`{ name: 'Error', Code: 'AccessDenied' }` classified as
|
|
4216
|
+
* retryable, so a denied principal re-probed on every purge), while
|
|
4217
|
+
* preferring a SPECIFIC `name` let a wrapper's own name hide
|
|
4218
|
+
* `Code: 'InvalidAccessKeyId'` and cache a credential blip as a permanent
|
|
4219
|
+
* denial. Membership is tested against every code the error carries, so
|
|
4220
|
+
* neither field can mask the other. (Verified against real S3: a missing
|
|
4221
|
+
* replication configuration arrives with that string on BOTH fields.)
|
|
4222
|
+
*
|
|
4223
|
+
* Null-safe because a `send` that rejects with `null` used to throw a
|
|
4224
|
+
* TypeError INSIDE the probe's own catch, leaving a REJECTED promise in the
|
|
4225
|
+
* cache forever: every later purge on that bucket then fell into the outer
|
|
4226
|
+
* `catch {}` with no warning and no debug line — a silently dead detector,
|
|
4227
|
+
* which is the worst possible failure for this module.
|
|
4228
|
+
*
|
|
4229
|
+
* This guard and `probe`'s rejection arm both serve one invariant — **no
|
|
4230
|
+
* rejection ever reaches the cache** — and the honest statement of their
|
|
4231
|
+
* relationship, measured one mutation at a time on 2026-09-05, is:
|
|
4232
|
+
*
|
|
4233
|
+
* - removing the ARM alone REDS a case (a rejection whose own `toString`
|
|
4234
|
+
* throws: the guards pass that value through and `describe()` is what
|
|
4235
|
+
* raises), so the arm is independently fenced;
|
|
4236
|
+
* - removing these GUARDS alone leaves the CACHING behaviour identical:
|
|
4237
|
+
* `errorCodes` runs inside `runProbe`'s own catch, so a TypeError here is
|
|
4238
|
+
* caught by the arm and produces the same `unknown` / `retry: true` /
|
|
4239
|
+
* evicted outcome. What DOES change is the debug line's reason — guarded it
|
|
4240
|
+
* reports the rejection (`: null`), unguarded it reports the classifier's
|
|
4241
|
+
* own crash — and that difference is what the `throw null` case asserts, so
|
|
4242
|
+
* the guards are fenced after all.
|
|
4243
|
+
*
|
|
4244
|
+
* They also earn their place structurally: classification is inside the catch
|
|
4245
|
+
* TODAY. An edit that hoists it out — or that adds a caller of `errorCodes`
|
|
4246
|
+
* outside a try — reinstates the original defect, in which a `null` rejection
|
|
4247
|
+
* crashed the classifier and left a REJECTED promise cached for the process,
|
|
4248
|
+
* so every later purge on that bucket fell into the outer `catch {}` with no
|
|
4249
|
+
* warning and no debug line: a silently dead detector.
|
|
4250
|
+
*/
|
|
4251
|
+
const errorCodes = (error) => {
|
|
4252
|
+
if (error === null || typeof error !== "object" && typeof error !== "function") return [];
|
|
4253
|
+
const e = error;
|
|
4254
|
+
const codes = [];
|
|
4255
|
+
if (typeof e.name === "string" && e.name.length > 0) codes.push(e.name);
|
|
4256
|
+
if (typeof e.Code === "string" && e.Code.length > 0) codes.push(e.Code);
|
|
4257
|
+
return codes;
|
|
4258
|
+
};
|
|
4259
|
+
/** True when ANY code the error carries is in `set`. */
|
|
4260
|
+
const hasCode = (error, set) => errorCodes(error).some((code) => set.has(code));
|
|
4261
|
+
const httpStatus = (error) => {
|
|
4262
|
+
if (error === null || typeof error !== "object" && typeof error !== "function") return void 0;
|
|
4263
|
+
const meta = error.$metadata;
|
|
4264
|
+
return typeof meta?.httpStatusCode === "number" ? meta.httpStatusCode : void 0;
|
|
4265
|
+
};
|
|
4266
|
+
/**
|
|
4267
|
+
* Strip the ARN wrapper off `Destination.Bucket`, which S3 returns as
|
|
4268
|
+
* `arn:<partition>:s3:::<name>`.
|
|
4269
|
+
*
|
|
4270
|
+
* Partition-agnostic on purpose: `aws-cn` and `aws-us-gov` produce a
|
|
4271
|
+
* different second segment, and a hard-coded `arn:aws:s3:::` would leave the
|
|
4272
|
+
* whole ARN in the message there rather than mis-parse it — survivable, but
|
|
4273
|
+
* the regex costs nothing. A value that is not ARN-shaped is passed through
|
|
4274
|
+
* unchanged rather than rejected; the field is only ever displayed.
|
|
4275
|
+
*/
|
|
4276
|
+
function destinationName(bucketArn, account) {
|
|
4277
|
+
if (bucketArn === void 0 || bucketArn === "") return UNNAMED_DESTINATION;
|
|
4278
|
+
const name = bucketArn.replace(/^arn:[^:]*:s3:::/, "");
|
|
4279
|
+
return account !== void 0 && account !== "" ? `${name} (account ${account})` : name;
|
|
4280
|
+
}
|
|
4281
|
+
/**
|
|
4282
|
+
* Reduce a `ReplicationConfiguration` to the ENABLED rules and their effective
|
|
4283
|
+
* prefixes.
|
|
4284
|
+
*
|
|
4285
|
+
* Exported for its own unit tests: the mapping from the four filter shapes S3
|
|
4286
|
+
* accepts onto one prefix string is the part with real cases in it, and
|
|
4287
|
+
* pinning it through a stubbed client would test the plumbing instead.
|
|
4288
|
+
*
|
|
4289
|
+
* Over-approximates DELIBERATELY, in the direction of warning:
|
|
4290
|
+
*
|
|
4291
|
+
* - a rule filtering on a TAG (`Filter.Tag`, or `Filter.And.Tags` with no
|
|
4292
|
+
* `Prefix`) cannot be evaluated without reading each object's tag set, so
|
|
4293
|
+
* its prefix reduces to `''` and it covers everything. A false warning
|
|
4294
|
+
* sends the reader to look at a replica; a false silence leaves them
|
|
4295
|
+
* believing a secret is gone.
|
|
4296
|
+
* - a `Disabled` rule is KEPT, flagged rather than dropped. Disabling a rule
|
|
4297
|
+
* stops FUTURE replication; it does not remove what the rule already
|
|
4298
|
+
* copied, so a bucket whose rule covered `cdkd/` last month and is disabled
|
|
4299
|
+
* today still holds every body ever purged. Dropping it was the first cut
|
|
4300
|
+
* and is the reassured-user failure this module exists to remove, arriving
|
|
4301
|
+
* inside the module that removes it. A rule with no `Status` at all is kept
|
|
4302
|
+
* as ENABLED: `Status` is required by the model, so an absent one means the
|
|
4303
|
+
* response was not what we assumed — the same reason the sibling purge
|
|
4304
|
+
* treats an absent `IsLatest` as "assume the unsafe thing is true".
|
|
4305
|
+
*
|
|
4306
|
+
* Pure: it reads only its argument, so no ambient locale, clock or
|
|
4307
|
+
* environment can change the answer.
|
|
4308
|
+
*/
|
|
4309
|
+
function normalizeReplicationRules(config) {
|
|
4310
|
+
const targets = [];
|
|
4311
|
+
for (const rule of config.Rules ?? []) {
|
|
4312
|
+
const prefix = rule.Filter !== void 0 ? rule.Filter.Prefix ?? rule.Filter.And?.Prefix ?? "" : rule.Prefix ?? "";
|
|
4313
|
+
targets.push({
|
|
4314
|
+
prefix,
|
|
4315
|
+
destination: destinationName(rule.Destination?.Bucket, rule.Destination?.Account),
|
|
4316
|
+
enabled: rule.Status !== "Disabled"
|
|
4317
|
+
});
|
|
4318
|
+
}
|
|
4319
|
+
return targets;
|
|
4320
|
+
}
|
|
4321
|
+
async function runProbe(s3Client, bucket, requestFields) {
|
|
4322
|
+
try {
|
|
4323
|
+
const config = (await s3Client.send(new GetBucketReplicationCommand({
|
|
4324
|
+
Bucket: bucket,
|
|
4325
|
+
...requestFields
|
|
4326
|
+
}))).ReplicationConfiguration;
|
|
4327
|
+
if (config === void 0) return { kind: "none" };
|
|
4328
|
+
const rules = normalizeReplicationRules(config);
|
|
4329
|
+
return rules.length === 0 ? { kind: "none" } : {
|
|
4330
|
+
kind: "rules",
|
|
4331
|
+
rules
|
|
4332
|
+
};
|
|
4333
|
+
} catch (error) {
|
|
4334
|
+
if (errorCodes(error).includes("ReplicationConfigurationNotFoundError")) return { kind: "none" };
|
|
4335
|
+
if (hasCode(error, TRANSIENT_CREDENTIAL_CODES)) return {
|
|
4336
|
+
kind: "unknown",
|
|
4337
|
+
reason: describe$2(error),
|
|
4338
|
+
retry: true
|
|
4339
|
+
};
|
|
4340
|
+
const permanent = hasCode(error, PERMANENT_DENIALS) || httpStatus(error) === 403;
|
|
4341
|
+
return {
|
|
4342
|
+
kind: "unknown",
|
|
4343
|
+
reason: describe$2(error),
|
|
4344
|
+
retry: !permanent
|
|
4345
|
+
};
|
|
4346
|
+
}
|
|
4347
|
+
}
|
|
4348
|
+
function probe(s3Client, bucket, requestFields) {
|
|
4349
|
+
const key = `${bucket}\u0000${requestFields.ExpectedBucketOwner ?? ""}`;
|
|
4350
|
+
const cached = replicationProbeCache.get(key);
|
|
4351
|
+
if (cached) return cached;
|
|
4352
|
+
const pending = runProbe(s3Client, bucket, requestFields).then((result) => {
|
|
4353
|
+
if (result.kind === "unknown" && result.retry) evictIfOwn(key, pending);
|
|
4354
|
+
return result;
|
|
4355
|
+
}, (error) => {
|
|
4356
|
+
evictIfOwn(key, pending);
|
|
4357
|
+
return {
|
|
4358
|
+
kind: "unknown",
|
|
4359
|
+
reason: describe$2(error),
|
|
4360
|
+
retry: true
|
|
4361
|
+
};
|
|
4362
|
+
});
|
|
4363
|
+
replicationProbeCache.set(key, pending);
|
|
4364
|
+
return pending;
|
|
4365
|
+
}
|
|
4366
|
+
/**
|
|
4367
|
+
* Remove a cache entry only when it is still the one we put there.
|
|
4368
|
+
*
|
|
4369
|
+
* The identity check is UNFENCED by a test, deliberately and stated rather
|
|
4370
|
+
* than left to be discovered: the only way a stale probe can settle against a
|
|
4371
|
+
* newer entry under the same key is for something to have cleared the cache
|
|
4372
|
+
* mid-flight, and the sole clearer is `clearReplicationProbeCache()`, a
|
|
4373
|
+
* test-only helper. Writing a test for it would be writing a test for the test
|
|
4374
|
+
* helper. It is kept because the alternative is an unconditional `delete` that
|
|
4375
|
+
* is wrong the moment a non-test clearer appears.
|
|
3928
4376
|
*/
|
|
3929
|
-
|
|
4377
|
+
function evictIfOwn(key, own) {
|
|
4378
|
+
if (replicationProbeCache.get(key) === own) replicationProbeCache.delete(key);
|
|
4379
|
+
}
|
|
4380
|
+
/**
|
|
4381
|
+
* Warn if replication on `bucket` covers any of `keys`.
|
|
4382
|
+
*
|
|
4383
|
+
* `keys` are the keys whose bodies were actually removed (or could not be
|
|
4384
|
+
* settled) — NOT everything the caller asked about. The purge scopes them; see
|
|
4385
|
+
* its call site for why announcing a survival that never happened is worse
|
|
4386
|
+
* than saying nothing.
|
|
4387
|
+
*
|
|
4388
|
+
* NEVER THROWS and never rejects: the caller runs on a cleanup path that must
|
|
4389
|
+
* not abort, and this is a diagnostic bolted onto it.
|
|
4390
|
+
*/
|
|
4391
|
+
async function warnIfPurgeIsReplicated(s3Client, bucket, keys, options = {}) {
|
|
4392
|
+
if (keys.length === 0) return;
|
|
4393
|
+
const emitDebug = (message) => {
|
|
4394
|
+
const sink = options.logger;
|
|
4395
|
+
if (sink?.debug) sink.debug(message);
|
|
4396
|
+
else getLogger().child("s3-replication-check").debug(message);
|
|
4397
|
+
};
|
|
4398
|
+
try {
|
|
4399
|
+
const safeBucket = displaySafe(bucket, { asciiOnly: true });
|
|
4400
|
+
const result = await probe(s3Client, bucket, options.requestFields ?? {});
|
|
4401
|
+
if (result.kind === "none") return;
|
|
4402
|
+
if (result.kind === "unknown") {
|
|
4403
|
+
emitDebug(`Could not determine whether s3://${safeBucket} is replicated (grant s3:GetReplicationConfiguration to enable the check): ${displaySafe(result.reason)}`);
|
|
4404
|
+
return;
|
|
4405
|
+
}
|
|
4406
|
+
const covered = result.rules.filter((rule) => keys.some((key) => key.startsWith(rule.prefix)));
|
|
4407
|
+
if (covered.length === 0) return;
|
|
4408
|
+
const destinations = [...new Set(covered.map((rule) => rule.enabled ? rule.destination : `${rule.destination} (rule currently Disabled -- it stops FUTURE replication, not what it already copied)`))].sort();
|
|
4409
|
+
const warnKey = [
|
|
4410
|
+
bucket,
|
|
4411
|
+
options.objectDescription ?? "",
|
|
4412
|
+
...destinations
|
|
4413
|
+
].join("\0");
|
|
4414
|
+
if (warnedWarnings.has(warnKey)) {
|
|
4415
|
+
emitDebug(`S3 replication on s3://${safeBucket} still covers purged keys (${options.objectDescription ?? "the body of an object cdkd has just reported as removed"}); already reported once this run for ${destinations.map((d) => displaySafe(d, { asciiOnly: true })).join(", ")}.`);
|
|
4416
|
+
return;
|
|
4417
|
+
}
|
|
4418
|
+
const named = destinations.slice(0, MAX_NAMED_DESTINATIONS).map((d) => displaySafe(d, { asciiOnly: true }));
|
|
4419
|
+
const elided = destinations.length - named.length;
|
|
4420
|
+
(options.logger ?? getLogger().child("s3-replication-check")).warn(`S3 replication is enabled on s3://${safeBucket} and covers the key(s) cdkd just purged. S3 NEVER replicates a version-id delete, so the purge removed those versions from THIS bucket only — the copies in the destination bucket survive and remain readable there via GetObject with a VersionId (${options.objectDescription ?? "the body of an object cdkd has just reported as removed"}). cdkd cannot delete them. Remove them in the destination bucket yourself (aws s3api list-object-versions, then delete-object --version-id), or narrow the replication rule so it excludes the prefixes cdkd purges under. Destination(s): ${named.join(", ")}` + (elided > 0 ? ` (and ${elided} more; aws s3api get-bucket-replication --bucket ${safeBucket} lists them all)` : ""));
|
|
4421
|
+
warnedWarnings.add(warnKey);
|
|
4422
|
+
} catch {}
|
|
4423
|
+
}
|
|
4424
|
+
|
|
4425
|
+
//#endregion
|
|
4426
|
+
//#region src/state/s3-noncurrent-version-purge.ts
|
|
3930
4427
|
/**
|
|
3931
4428
|
* `objectDescription` for the custom-resource response sidecar.
|
|
3932
4429
|
*
|
|
@@ -4000,17 +4497,27 @@ async function purgeNoncurrentKeyVersions(s3Client, bucket, keys, options = {})
|
|
|
4000
4497
|
const prefixes = options.listPrefix !== void 0 ? [options.listPrefix] : keys;
|
|
4001
4498
|
const failed = /* @__PURE__ */ new Map();
|
|
4002
4499
|
const unknown = { n: 0 };
|
|
4500
|
+
const purged = /* @__PURE__ */ new Set();
|
|
4501
|
+
const unsettledBodies = /* @__PURE__ */ new Set();
|
|
4003
4502
|
for (const prefix of prefixes) try {
|
|
4004
|
-
await purgeUnderPrefix(s3Client, bucket, prefix, wanted, requestFields, failed, unknown);
|
|
4503
|
+
await purgeUnderPrefix(s3Client, bucket, prefix, wanted, requestFields, failed, unknown, purged, unsettledBodies);
|
|
4005
4504
|
} catch (error) {
|
|
4006
4505
|
const affected = options.listPrefix !== void 0 ? keys : [prefix];
|
|
4007
|
-
for (const key of affected)
|
|
4506
|
+
for (const key of affected) {
|
|
4507
|
+
recordFailure(failed, key, describe$1(error));
|
|
4508
|
+
unsettledBodies.add(key);
|
|
4509
|
+
}
|
|
4008
4510
|
}
|
|
4009
4511
|
if (failed.size > 0) {
|
|
4010
4512
|
const named = [...failed.entries()].slice(0, MAX_NAMED_FAILURES).map(([key, reasons]) => displaySafe(`${key} (${reasons.join("; ")})`));
|
|
4011
4513
|
const elided = failed.size - named.length;
|
|
4012
|
-
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 ??
|
|
4514
|
+
logger.warn(`Could not purge noncurrent versions of ${failed.size} key(s) in s3://${displaySafe(bucket, { asciiOnly: true })}. Their previous versions survive and remain readable via GetObject with a VersionId (${options.objectDescription ?? "the body of an object cdkd has just reported as removed"}). 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)` : ""));
|
|
4013
4515
|
}
|
|
4516
|
+
await warnIfPurgeIsReplicated(s3Client, bucket, [.../* @__PURE__ */ new Set([...purged, ...unsettledBodies])], {
|
|
4517
|
+
requestFields,
|
|
4518
|
+
logger,
|
|
4519
|
+
...options.objectDescription !== void 0 && { objectDescription: options.objectDescription }
|
|
4520
|
+
});
|
|
4014
4521
|
}
|
|
4015
4522
|
/**
|
|
4016
4523
|
* Paginate `ListObjectVersions` under one prefix and delete every returned
|
|
@@ -4025,7 +4532,7 @@ async function purgeNoncurrentKeyVersions(s3Client, bucket, keys, options = {})
|
|
|
4025
4532
|
* id is NOT filtered out on its own, because a bucket whose versioning was
|
|
4026
4533
|
* SUSPENDED can carry a genuine noncurrent `'null'` version holding the body.
|
|
4027
4534
|
*/
|
|
4028
|
-
async function purgeUnderPrefix(s3Client, bucket, prefix, wanted, requestFields, failed, unknown) {
|
|
4535
|
+
async function purgeUnderPrefix(s3Client, bucket, prefix, wanted, requestFields, failed, unknown, purged, unsettledBodies) {
|
|
4029
4536
|
let keyMarker;
|
|
4030
4537
|
let versionIdMarker;
|
|
4031
4538
|
do {
|
|
@@ -4037,15 +4544,30 @@ async function purgeUnderPrefix(s3Client, bucket, prefix, wanted, requestFields,
|
|
|
4037
4544
|
...versionIdMarker !== void 0 && { VersionIdMarker: versionIdMarker }
|
|
4038
4545
|
}));
|
|
4039
4546
|
const stale = [];
|
|
4040
|
-
|
|
4547
|
+
const entries = [...(resp.Versions ?? []).map((entry) => ({
|
|
4548
|
+
entry,
|
|
4549
|
+
hasBody: true
|
|
4550
|
+
})), ...(resp.DeleteMarkers ?? []).map((entry) => ({
|
|
4551
|
+
entry,
|
|
4552
|
+
hasBody: false
|
|
4553
|
+
}))];
|
|
4554
|
+
for (const { entry, hasBody } of entries) {
|
|
4041
4555
|
if (entry.Key === void 0 || !wanted.has(entry.Key)) continue;
|
|
4042
|
-
if (entry.IsLatest === void 0)
|
|
4556
|
+
if (entry.IsLatest === void 0) {
|
|
4557
|
+
recordFailure(failed, entry.Key, `version ${entry.VersionId ?? "<unknown>"}: listing omitted IsLatest, so the entry was left alone rather than risk deleting a current version`);
|
|
4558
|
+
if (hasBody) unsettledBodies.add(entry.Key);
|
|
4559
|
+
}
|
|
4043
4560
|
if (entry.IsLatest !== false) continue;
|
|
4044
|
-
if (!entry.VersionId)
|
|
4561
|
+
if (!entry.VersionId) {
|
|
4562
|
+
recordFailure(failed, entry.Key, `listing returned a noncurrent entry with no VersionId, so it could not be deleted`);
|
|
4563
|
+
if (hasBody) unsettledBodies.add(entry.Key);
|
|
4564
|
+
continue;
|
|
4565
|
+
}
|
|
4045
4566
|
stale.push({
|
|
4046
4567
|
Key: entry.Key,
|
|
4047
4568
|
VersionId: entry.VersionId
|
|
4048
4569
|
});
|
|
4570
|
+
if (hasBody) purged.add(entry.Key);
|
|
4049
4571
|
}
|
|
4050
4572
|
for (let i = 0; i < stale.length; i += DELETE_BATCH_SIZE) {
|
|
4051
4573
|
const batch = stale.slice(i, i + DELETE_BATCH_SIZE);
|
|
@@ -4072,7 +4594,10 @@ async function purgeUnderPrefix(s3Client, bucket, prefix, wanted, requestFields,
|
|
|
4072
4594
|
}
|
|
4073
4595
|
}
|
|
4074
4596
|
if (resp.IsTruncated === true && resp.NextKeyMarker === void 0) {
|
|
4075
|
-
for (const key of wanted) if (key.startsWith(prefix))
|
|
4597
|
+
for (const key of wanted) if (key.startsWith(prefix)) {
|
|
4598
|
+
recordFailure(failed, key, TRUNCATED_NO_MARKER);
|
|
4599
|
+
unsettledBodies.add(key);
|
|
4600
|
+
}
|
|
4076
4601
|
}
|
|
4077
4602
|
keyMarker = resp.IsTruncated === true ? resp.NextKeyMarker : void 0;
|
|
4078
4603
|
versionIdMarker = keyMarker !== void 0 ? resp.NextVersionIdMarker : void 0;
|
|
@@ -5963,6 +6488,374 @@ function mergeEnv(overrides) {
|
|
|
5963
6488
|
else merged[k] = v;
|
|
5964
6489
|
return merged;
|
|
5965
6490
|
}
|
|
6491
|
+
/** Replaces a redacted argv VALUE wherever this module masks one. */
|
|
6492
|
+
const REDACTED_ARGV_VALUE = "***";
|
|
6493
|
+
/**
|
|
6494
|
+
* `docker` argv flags whose NEXT token is a `KEY=VALUE` pair built from
|
|
6495
|
+
* user-supplied template data, and whose VALUE therefore must not reach a
|
|
6496
|
+
* user-visible error string:
|
|
6497
|
+
*
|
|
6498
|
+
* - `-e` / `--env` — a container's `Environment.Variables` (Lambda) or
|
|
6499
|
+
* `ContainerDefinition.Environment` (ECS), plus `--env-vars` overrides.
|
|
6500
|
+
* Values that cdkd classifies as sensitive never get here at all
|
|
6501
|
+
* ({@link partitionSensitiveEnv} emits those as a value-less `-e KEY`),
|
|
6502
|
+
* but everything else does: connection strings, endpoints, API base URLs.
|
|
6503
|
+
* - `--opt` — `DockerVolumeConfiguration.DriverOpts`, which for the `local`
|
|
6504
|
+
* driver carries mount options (`o=addr=…,username=…,password=…`).
|
|
6505
|
+
* - `--label` — `DockerVolumeConfiguration.Labels`, user-authored metadata.
|
|
6506
|
+
*
|
|
6507
|
+
* A flag NOT in this list keeps its value. For most of the argv that is
|
|
6508
|
+
* because the value is cdkd-authored or infrastructure-shaped — a container
|
|
6509
|
+
* id, an image ref, a `--subnet` CIDR, a `--format` template — and is the
|
|
6510
|
+
* diagnostic.
|
|
6511
|
+
*
|
|
6512
|
+
* That is NOT true of the whole remainder, and the residual is recorded here
|
|
6513
|
+
* rather than quietly implied: `--health-cmd`, `--ulimit`, `--link`,
|
|
6514
|
+
* `--entrypoint`, `--workdir` and the positional image + container command are
|
|
6515
|
+
* all template-supplied and still echoed. They stay unmasked deliberately —
|
|
6516
|
+
* each is a COMMAND or a structural knob whose text IS what an operator reads
|
|
6517
|
+
* the failure for, and none is a documented place to put a secret, unlike
|
|
6518
|
+
* `Environment` / `Secrets` / `DriverOpts`. Revisit per flag if a real leak is
|
|
6519
|
+
* found through one; do not widen the set on suspicion, since every addition
|
|
6520
|
+
* trades away diagnostic text.
|
|
6521
|
+
*/
|
|
6522
|
+
const ARGV_VALUE_BEARING_FLAGS = /* @__PURE__ */ new Set([
|
|
6523
|
+
"-e",
|
|
6524
|
+
"--env",
|
|
6525
|
+
"--opt",
|
|
6526
|
+
"--label"
|
|
6527
|
+
]);
|
|
6528
|
+
/**
|
|
6529
|
+
* Structural, whitespace-delimited form of {@link ARGV_VALUE_BEARING_FLAGS}
|
|
6530
|
+
* for scanning a STRING that embeds a space-joined argv. Keyed on the FLAG's
|
|
6531
|
+
* position — never on the secret's value, so an unrelated literal that merely
|
|
6532
|
+
* coincides with a value is left alone.
|
|
6533
|
+
*
|
|
6534
|
+
* What each piece actually buys, since one of them was mis-credited in review:
|
|
6535
|
+
* the `(^|\s)` lead is what stops `-e` matching inside `--env` (there is no
|
|
6536
|
+
* `m` flag, so `^` is start-of-INPUT), NOT the alternation order — reordering
|
|
6537
|
+
* the branches leaves every test green. The order is kept as belt and braces
|
|
6538
|
+
* only. The mandatory `\s+` after the flag IS load-bearing: it is what stops
|
|
6539
|
+
* `-easy` / `--environment` matching. The KEY is `[^\s=]*` rather than `+` so
|
|
6540
|
+
* an EMPTY key (`-e =value`) is masked too.
|
|
6541
|
+
*/
|
|
6542
|
+
const ARGV_VALUE_TOKEN_RE = /(^|\s)(--env|--label|--opt|-e)(\s+)([^\s=]*)=\S*/g;
|
|
6543
|
+
/**
|
|
6544
|
+
* Return a copy of `args` with the VALUE of every
|
|
6545
|
+
* {@link ARGV_VALUE_BEARING_FLAGS} pair replaced by `***`. The KEY survives —
|
|
6546
|
+
* "which variable" is the diagnostic, "what it was set to" is the disclosure.
|
|
6547
|
+
*
|
|
6548
|
+
* A value-less `-e KEY` (the form {@link partitionSensitiveEnv} emits for a
|
|
6549
|
+
* sensitive key) has nothing to mask and is returned unchanged. `args` is
|
|
6550
|
+
* never mutated: the redacted copy is for DISPLAY only, never for `spawn`.
|
|
6551
|
+
*/
|
|
6552
|
+
function redactDockerArgvValues(args) {
|
|
6553
|
+
const out = [];
|
|
6554
|
+
for (let i = 0; i < args.length; i++) {
|
|
6555
|
+
const cur = args[i];
|
|
6556
|
+
const next = args[i + 1];
|
|
6557
|
+
if (ARGV_VALUE_BEARING_FLAGS.has(cur) && typeof next === "string") {
|
|
6558
|
+
const eqIdx = next.indexOf("=");
|
|
6559
|
+
if (eqIdx >= 0) {
|
|
6560
|
+
out.push(cur, `${next.substring(0, eqIdx)}=${REDACTED_ARGV_VALUE}`);
|
|
6561
|
+
i++;
|
|
6562
|
+
continue;
|
|
6563
|
+
}
|
|
6564
|
+
}
|
|
6565
|
+
out.push(cur);
|
|
6566
|
+
}
|
|
6567
|
+
return out;
|
|
6568
|
+
}
|
|
6569
|
+
/**
|
|
6570
|
+
* Redact argv-borne values inside an error TEXT before it reaches the user.
|
|
6571
|
+
*
|
|
6572
|
+
* **Wrap EVERY `execFile`-derived docker failure text in this**, including one
|
|
6573
|
+
* whose argv carries no user data today — `execFile` puts the WHOLE command
|
|
6574
|
+
* line into `err.message` (`Command failed: <file> <args joined by ' '>\n<stderr>`,
|
|
6575
|
+
* measured on Node 24), so any argv that later gains a `-e` pair starts leaking
|
|
6576
|
+
* with no edit to the error site. That silent-gap shape is exactly what
|
|
6577
|
+
* [#2440](https://github.com/go-to-k/cdkd/issues/2440) reported: the debug log
|
|
6578
|
+
* one screen earlier redacted the same argv and the error path did not.
|
|
6579
|
+
*
|
|
6580
|
+
* Two passes, both structural (positional), never value-based — matching on a
|
|
6581
|
+
* secret's VALUE would also blank an unrelated string that happens to equal it:
|
|
6582
|
+
*
|
|
6583
|
+
* 1. **Exact command-line substitution** (only when `args` is given). The raw
|
|
6584
|
+
* `args.join(' ')` is replaced by {@link redactDockerArgvValues}' rendering
|
|
6585
|
+
* of the same array. This is the pass that matters, because it is the only
|
|
6586
|
+
* one that can mask a value CONTAINING WHITESPACE (`-e CFG={"a": 1}`), which
|
|
6587
|
+
* no token scan of a space-joined string can delimit.
|
|
6588
|
+
* 2. **Token scan** ({@link ARGV_VALUE_TOKEN_RE}) over the result. Catches an
|
|
6589
|
+
* argv echoed in a shape pass 1 cannot see — a future Node message format,
|
|
6590
|
+
* a docker stderr quoting the flag back, or a call site with no `args` to
|
|
6591
|
+
* hand. Deliberately fail-loud rather than fail-open: a `-e X=y` occurring
|
|
6592
|
+
* in unrelated stderr prose loses its value and keeps its key.
|
|
6593
|
+
*
|
|
6594
|
+
* Idempotent (`-e KEY=***` re-masks to itself), so double-wrapping is safe.
|
|
6595
|
+
*
|
|
6596
|
+
* **Call sites should use a composer** ({@link describeDockerFailure} and its
|
|
6597
|
+
* siblings) rather than this function: the composers take a REQUIRED `args`,
|
|
6598
|
+
* which is what makes the redaction impossible to forget. This is the
|
|
6599
|
+
* primitive they are built from, exported so the four passes can be tested
|
|
6600
|
+
* against Node's real message shapes directly — it has no other caller in
|
|
6601
|
+
* `src/`, and that is deliberate rather than an oversight.
|
|
6602
|
+
*/
|
|
6603
|
+
function redactDockerArgvInText(text, args) {
|
|
6604
|
+
let out = text;
|
|
6605
|
+
if (args && args.length > 0) {
|
|
6606
|
+
const maskedArgs = redactDockerArgvValues(args);
|
|
6607
|
+
const raw = args.join(" ");
|
|
6608
|
+
const masked = maskedArgs.join(" ");
|
|
6609
|
+
if (masked !== raw) out = out.split(raw).join(masked);
|
|
6610
|
+
for (let i = 0; i < args.length; i++) {
|
|
6611
|
+
const rawArg = args[i];
|
|
6612
|
+
const maskedArg = maskedArgs[i];
|
|
6613
|
+
if (maskedArg === rawArg) continue;
|
|
6614
|
+
if (!isSubstitutableToken(rawArg)) continue;
|
|
6615
|
+
out = out.split(rawArg).join(maskedArg);
|
|
6616
|
+
const escapedRaw = nodeQuotedRendering(rawArg);
|
|
6617
|
+
const escapedMasked = nodeQuotedRendering(maskedArg);
|
|
6618
|
+
if (escapedRaw !== void 0 && escapedMasked !== void 0 && escapedRaw !== rawArg) out = out.split(escapedRaw).join(escapedMasked);
|
|
6619
|
+
}
|
|
6620
|
+
out = repairSpawnRefusal(out, args, maskedArgs);
|
|
6621
|
+
}
|
|
6622
|
+
return out.replace(ARGV_VALUE_TOKEN_RE, (_match, lead, flag, gap, key) => `${lead}${flag}${gap}${key}=${REDACTED_ARGV_VALUE}`);
|
|
6623
|
+
}
|
|
6624
|
+
/**
|
|
6625
|
+
* Shortest VALUE that pass 1b will substitute as a bare token.
|
|
6626
|
+
*
|
|
6627
|
+
* Pass 1b's needle is the whole `KEY=VALUE` element, and it is replaced
|
|
6628
|
+
* EVERYWHERE in the text — so a tiny needle is a liability rather than a
|
|
6629
|
+
* protection. The empty-key case makes that concrete: a non-sensitive
|
|
6630
|
+
* `{ Name: '', Value: '1' }` produces the two-character token `=1`, and
|
|
6631
|
+
* substituting that rewrites every `=1` in the message (`--cpus=1`,
|
|
6632
|
+
* `status=1`, a path segment). Below this floor the value is not worth
|
|
6633
|
+
* protecting by substring match, and passes 1 and 3 still cover it in the
|
|
6634
|
+
* message shapes that carry a `-e ` prefix or the joined command line.
|
|
6635
|
+
*/
|
|
6636
|
+
const MIN_SUBSTITUTABLE_VALUE_LENGTH = 4;
|
|
6637
|
+
/** Is this `KEY=VALUE` element long enough to substitute as a bare token? */
|
|
6638
|
+
function isSubstitutableToken(arg) {
|
|
6639
|
+
const eqIdx = arg.indexOf("=");
|
|
6640
|
+
if (eqIdx < 0) return false;
|
|
6641
|
+
return arg.length - eqIdx - 1 >= MIN_SUBSTITUTABLE_VALUE_LENGTH;
|
|
6642
|
+
}
|
|
6643
|
+
/**
|
|
6644
|
+
* Render `value` the way Node renders an argv element it quotes back at you —
|
|
6645
|
+
* i.e. with the SAME function Node used, `util.inspect`, minus the quote
|
|
6646
|
+
* characters it chose.
|
|
6647
|
+
*
|
|
6648
|
+
* This started as a hand-rolled `\xNN` escaper and it was WRONG for 16 of the
|
|
6649
|
+
* first 128 code points. Measured against real `execFile` rejections on Node
|
|
6650
|
+
* 24.19.0 (this repo, 2026-09-05): hand-rolled matched 4 of 13 cases,
|
|
6651
|
+
* `inspect` matched 13 of 13.
|
|
6652
|
+
*
|
|
6653
|
+
* input Node emits hand-rolled built
|
|
6654
|
+
* LF `K=a\nb\x00S` `K=a\x0ab\x00S` (Node uses a short escape)
|
|
6655
|
+
* DEL `K=a\x7Fb\x00S` `K=a\x7fb\x00S` (Node uses UPPERCASE hex)
|
|
6656
|
+
* backslash `K=a\\b\x00S` `K=a\b\x00S` (not escaped at all)
|
|
6657
|
+
*
|
|
6658
|
+
* Every miss printed the whole secret. The trigger for this path is a NUL in
|
|
6659
|
+
* a value — binary-ish data, which almost always carries a second control
|
|
6660
|
+
* byte or a backslash — so the table missed the REALISTIC case and hit only
|
|
6661
|
+
* the synthetic NUL-alone one its own fixture happened to use.
|
|
6662
|
+
*
|
|
6663
|
+
* `slice(1, -1)` is safe across the quote Node picks: `inspect` switches
|
|
6664
|
+
* between `'`, `"` and a backtick depending on content and escapes whichever
|
|
6665
|
+
* it chose; because this is the same call Node made, the rendering matches
|
|
6666
|
+
* whatever it picked (verified for a value containing a single quote, a
|
|
6667
|
+
* double quote, both, and a backtick).
|
|
6668
|
+
*
|
|
6669
|
+
* The general lesson, and why this is no longer a table: do not re-implement
|
|
6670
|
+
* another program's formatter — call it.
|
|
6671
|
+
*/
|
|
6672
|
+
function nodeQuotedRendering(value) {
|
|
6673
|
+
const rendered = inspect(value);
|
|
6674
|
+
const quote = rendered[0];
|
|
6675
|
+
if (quote === void 0 || !`'"\``.includes(quote)) return void 0;
|
|
6676
|
+
if (rendered.length < 2 || !rendered.endsWith(quote)) return void 0;
|
|
6677
|
+
const inner = rendered.slice(1, -1);
|
|
6678
|
+
return inner.includes(`${quote} +`) ? void 0 : inner;
|
|
6679
|
+
}
|
|
6680
|
+
/**
|
|
6681
|
+
* Node's refusal to spawn, which QUOTES ONE argv element and names its INDEX:
|
|
6682
|
+
*
|
|
6683
|
+
* The argument 'args[2]' must be a string without null bytes. Received '…'
|
|
6684
|
+
*
|
|
6685
|
+
* The one message shape where the index tells us exactly which of OUR args is
|
|
6686
|
+
* being echoed — so the clause can be REWRITTEN from cdkd's own copy of the
|
|
6687
|
+
* element rather than searched for. That matters because Node does not print
|
|
6688
|
+
* the element whole: it truncates near 200 characters and renders a
|
|
6689
|
+
* newline-bearing value as concatenated chunks, so no needle can match it.
|
|
6690
|
+
*
|
|
6691
|
+
* Global, and scanned to exhaustion rather than bailing on the first match.
|
|
6692
|
+
* Anchoring at `^` was an earlier attempt at forgery resistance and it was
|
|
6693
|
+
* WORSE: it made a refusal anywhere but position 0 unrepairable, which is a
|
|
6694
|
+
* LEAK, and a leak beats a truncated message every time. Pinning Node's
|
|
6695
|
+
* literal prefix keeps the forgery bar high without that trade.
|
|
6696
|
+
*
|
|
6697
|
+
* Pinning the wording has its own cost: if Node rewords the message the repair
|
|
6698
|
+
* silently stops firing. That is why `tests/unit/utils/docker-cmd.test.ts`
|
|
6699
|
+
* drives this path from a REAL rejection — a reword turns CI red instead of
|
|
6700
|
+
* turning the redaction off.
|
|
6701
|
+
*/
|
|
6702
|
+
const SPAWN_REFUSAL_RE = /The argument 'args\[(\d+)\]'[^']*?Received /g;
|
|
6703
|
+
/**
|
|
6704
|
+
* The quoted element Node prints after `Received `.
|
|
6705
|
+
*
|
|
6706
|
+
* Three shapes, and the third is the one that matters. `inspect` renders a
|
|
6707
|
+
* newline-bearing value as chunks joined by ` +`, and Node then SLICES the
|
|
6708
|
+
* whole rendering at 128 characters and appends `...` — so the last chunk is
|
|
6709
|
+
* usually UNTERMINATED:
|
|
6710
|
+
*
|
|
6711
|
+
* Received 'DB_URL=AAA…\n' +
|
|
6712
|
+
* 'hunter2SECRETCCC...
|
|
6713
|
+
*
|
|
6714
|
+
* A pattern that only accepts complete chunks stops after the second `'` and
|
|
6715
|
+
* leaves that tail in place. Measured on a real rejection: 272 of 288 probe
|
|
6716
|
+
* shapes leaked the secret verbatim, and the end-of-string version this
|
|
6717
|
+
* replaced did not — the bound was a regression, not a hardening, until the
|
|
6718
|
+
* trailing open chunk was admitted.
|
|
6719
|
+
*
|
|
6720
|
+
* The separator is Node's own (` +\n `) with HORIZONTAL space only, and an
|
|
6721
|
+
* open chunk runs to end of LINE. Both are deliberate: `\s*\+\s*` and an
|
|
6722
|
+
* unbounded tail let a crafted value swallow the diagnostic lines that follow
|
|
6723
|
+
* the clause.
|
|
6724
|
+
*/
|
|
6725
|
+
const QUOTED_CHUNK = String.raw`'(?:[^'\\\n]|\\.)*'|"(?:[^"\\\n]|\\.)*"|\`(?:[^\`\\\n]|\\.)*\``;
|
|
6726
|
+
/** An opening quote whose closing one was truncated away; bounded to its line. */
|
|
6727
|
+
const OPEN_CHUNK = String.raw`['"\`](?:[^\\\n]|\\.)*`;
|
|
6728
|
+
/** Node's chunk join: horizontal space, `+`, at most one newline, horizontal space. */
|
|
6729
|
+
const CHUNK_SEPARATOR = String.raw`[^\S\n]*\+[^\S\n]*\n?[^\S\n]*`;
|
|
6730
|
+
const QUOTED_ELEMENT_RE = new RegExp(`^(?:(?:${QUOTED_CHUNK})(?:${CHUNK_SEPARATOR}(?:${QUOTED_CHUNK}))*(?:${CHUNK_SEPARATOR}(?:${OPEN_CHUNK}))?|(?:${OPEN_CHUNK}))`);
|
|
6731
|
+
/**
|
|
6732
|
+
* Replace the quoted argv element in a spawn-refusal message with the masked
|
|
6733
|
+
* rendering of the arg its OWN index names.
|
|
6734
|
+
*
|
|
6735
|
+
* Known cosmetic effect: the replacement prints the FULL key where Node had
|
|
6736
|
+
* truncated, so a pathologically long env-var NAME makes the message longer
|
|
6737
|
+
* than Node's ~200 characters. Keys are not secret, and the width is not worth
|
|
6738
|
+
* a second truncation rule with its own edge cases.
|
|
6739
|
+
*/
|
|
6740
|
+
function repairSpawnRefusal(text, args, maskedArgs) {
|
|
6741
|
+
const scanner = new RegExp(SPAWN_REFUSAL_RE.source, "g");
|
|
6742
|
+
let out = "";
|
|
6743
|
+
let cursor = 0;
|
|
6744
|
+
for (let match = scanner.exec(text); match !== null; match = scanner.exec(text)) {
|
|
6745
|
+
if (match.index < cursor) continue;
|
|
6746
|
+
const index = Number(match[1]);
|
|
6747
|
+
const rawArg = args[index];
|
|
6748
|
+
const maskedArg = maskedArgs[index];
|
|
6749
|
+
if (rawArg !== void 0 && maskedArg === rawArg) continue;
|
|
6750
|
+
const clauseEnd = match.index + match[0].length;
|
|
6751
|
+
const quoted = QUOTED_ELEMENT_RE.exec(text.slice(clauseEnd));
|
|
6752
|
+
if (quoted === null) continue;
|
|
6753
|
+
const replacement = maskedArg === void 0 ? `'${REDACTED_ARGV_VALUE}'` : inspect(maskedArg);
|
|
6754
|
+
out += text.slice(cursor, clauseEnd) + replacement;
|
|
6755
|
+
cursor = clauseEnd + quoted[0].length;
|
|
6756
|
+
scanner.lastIndex = cursor;
|
|
6757
|
+
}
|
|
6758
|
+
return out + text.slice(cursor);
|
|
6759
|
+
}
|
|
6760
|
+
/**
|
|
6761
|
+
* Compose a user-visible description of a `child_process` rejection from a
|
|
6762
|
+
* docker call, ALREADY REDACTED against that call's own argv.
|
|
6763
|
+
*
|
|
6764
|
+
* Moved here from `src/local/invoke-agentcore-watch-loop.ts` in issue #2440's
|
|
6765
|
+
* review round 3, and the move is the point: `args` is REQUIRED, so a call
|
|
6766
|
+
* site cannot obtain the text without handing over the argv to redact it
|
|
6767
|
+
* with. That is a stronger guarantee than any text fence over the call sites
|
|
6768
|
+
* — which is what the round-2 reviewers demonstrated, by writing four
|
|
6769
|
+
* spellings of an unredacted read that the fence could not see.
|
|
6770
|
+
*
|
|
6771
|
+
* Shape differs from the `stderr || message` composition the other sites use,
|
|
6772
|
+
* and deliberately so: `err.stderr` is where docker writes its actionable
|
|
6773
|
+
* diagnostics, while `err.message` carries the exit status, so the AgentCore
|
|
6774
|
+
* soft-reload path APPENDS rather than prefers — without stderr the wrapped
|
|
6775
|
+
* error would only say "Command failed with exit code N".
|
|
6776
|
+
*/
|
|
6777
|
+
function describeDockerExecFailure(error, args) {
|
|
6778
|
+
const message = thrownMessageText(error) || safeStringify(error);
|
|
6779
|
+
const stderrText = capturedStreamText(error, "stderr");
|
|
6780
|
+
return redactDockerArgvInText(stderrText ? `${message}\n${stderrText}` : message, args);
|
|
6781
|
+
}
|
|
6782
|
+
/**
|
|
6783
|
+
* A captured stream of a `child_process` rejection as trimmed text, or `''`.
|
|
6784
|
+
*
|
|
6785
|
+
* Takes `unknown`, not `Error`, and duck-types the field. Every composer here
|
|
6786
|
+
* is called from a `catch`, where the value is whatever was thrown — and the
|
|
6787
|
+
* shapes the call sites actually see are plain objects
|
|
6788
|
+
* (`{ stderr, message }`), `SpawnError`, and cross-realm `Error`s that
|
|
6789
|
+
* `instanceof` misses. An earlier revision narrowed this to `Error` and the
|
|
6790
|
+
* standard composer then wrapped non-Errors in a FRESH `Error`, which has no
|
|
6791
|
+
* `.stderr` at all — so the whole diagnostic silently became
|
|
6792
|
+
* `'[object Object]'` for exactly the shape most of the call sites throw.
|
|
6793
|
+
*
|
|
6794
|
+
* `execFile` hands back a string under the default encoding and a `Buffer`
|
|
6795
|
+
* under `encoding: 'buffer'`; `ArrayBuffer.isView` covers a plain
|
|
6796
|
+
* `Uint8Array` too.
|
|
6797
|
+
*/
|
|
6798
|
+
function capturedStreamText(error, field) {
|
|
6799
|
+
try {
|
|
6800
|
+
const raw = error?.[field];
|
|
6801
|
+
if (typeof raw === "string") return raw.trim();
|
|
6802
|
+
if (ArrayBuffer.isView(raw)) return Buffer.from(raw.buffer, raw.byteOffset, raw.byteLength).toString("utf8").trim();
|
|
6803
|
+
} catch {}
|
|
6804
|
+
return "";
|
|
6805
|
+
}
|
|
6806
|
+
/** A thrown value's `message` as text, or `''`. Duck-typed, for the same reason. */
|
|
6807
|
+
function thrownMessageText(error) {
|
|
6808
|
+
try {
|
|
6809
|
+
const raw = error?.["message"];
|
|
6810
|
+
return typeof raw === "string" ? raw : "";
|
|
6811
|
+
} catch {
|
|
6812
|
+
return "";
|
|
6813
|
+
}
|
|
6814
|
+
}
|
|
6815
|
+
/**
|
|
6816
|
+
* `String(value)` that cannot itself throw. A rejection with a null prototype
|
|
6817
|
+
* (or a `toString` that throws) would otherwise blow up INSIDE a `catch` — and
|
|
6818
|
+
* two of these call sites are in `cleanupEcsRun`, where an exception aborts
|
|
6819
|
+
* the remaining volume / network teardown and leaks real Docker resources.
|
|
6820
|
+
*/
|
|
6821
|
+
function safeStringify(error) {
|
|
6822
|
+
try {
|
|
6823
|
+
return String(error);
|
|
6824
|
+
} catch {
|
|
6825
|
+
return "[unstringifiable rejection]";
|
|
6826
|
+
}
|
|
6827
|
+
}
|
|
6828
|
+
/**
|
|
6829
|
+
* The STANDARD composer for a docker failure text: the captured stderr if
|
|
6830
|
+
* there is any, else the error's own message, else its string form —
|
|
6831
|
+
* ALREADY REDACTED against that call's argv.
|
|
6832
|
+
*
|
|
6833
|
+
* Use this at every site that wraps a docker `execFile` / spawn rejection.
|
|
6834
|
+
* `args` is REQUIRED, which is the whole point: a call site cannot obtain the
|
|
6835
|
+
* text without handing over the argv to redact it with, so the guarantee is
|
|
6836
|
+
* type-checked rather than fenced. Issue #2440's review spent three rounds
|
|
6837
|
+
* showing that a text fence over hand-composed sites is evadable — an
|
|
6838
|
+
* intermediate variable, an inline cast, a destructure, a computed member, a
|
|
6839
|
+
* concat — while this shape has nothing to evade.
|
|
6840
|
+
*
|
|
6841
|
+
* Prefer stderr over message because docker writes its actionable diagnostic
|
|
6842
|
+
* there, and `execFile`'s message is mostly the command line plus an exit
|
|
6843
|
+
* status. {@link describeDockerExecFailure} keeps BOTH, for a caller whose
|
|
6844
|
+
* wrapper text needs the status as well.
|
|
6845
|
+
*/
|
|
6846
|
+
function describeDockerFailure(error, args) {
|
|
6847
|
+
return redactDockerArgvInText(capturedStreamText(error, "stderr") || thrownMessageText(error) || safeStringify(error), args);
|
|
6848
|
+
}
|
|
6849
|
+
/**
|
|
6850
|
+
* Composer for a captured-output failure where the diagnostic may be on
|
|
6851
|
+
* STDOUT rather than stderr (`runDockerStreaming`'s non-zero-exit path, whose
|
|
6852
|
+
* `SpawnError` carries both). `fallback` is used when neither stream said
|
|
6853
|
+
* anything. Redacted, and `args` is required, for the same reason as
|
|
6854
|
+
* {@link describeDockerFailure}.
|
|
6855
|
+
*/
|
|
6856
|
+
function describeDockerCapturedOutput(error, args, fallback) {
|
|
6857
|
+
return redactDockerArgvInText(capturedStreamText(error, "stderr") || capturedStreamText(error, "stdout") || fallback, args);
|
|
6858
|
+
}
|
|
5966
6859
|
|
|
5967
6860
|
//#endregion
|
|
5968
6861
|
//#region src/assets/docker-build.ts
|
|
@@ -21642,7 +22535,8 @@ function ccProtectionProperty(resourceType) {
|
|
|
21642
22535
|
const NON_PROVISIONABLE_TYPES = /* @__PURE__ */ new Set([
|
|
21643
22536
|
"Alexa::ASK::Skill",
|
|
21644
22537
|
"AWS::AmazonMQ::ConfigurationAssociation",
|
|
21645
|
-
"AWS::
|
|
22538
|
+
"AWS::Amplify::Jobs",
|
|
22539
|
+
"AWS::AmplifyUIBuilder::CodegenJob",
|
|
21646
22540
|
"AWS::AppMesh::GatewayRoute",
|
|
21647
22541
|
"AWS::AppMesh::Mesh",
|
|
21648
22542
|
"AWS::AppMesh::Route",
|
|
@@ -21651,64 +22545,90 @@ const NON_PROVISIONABLE_TYPES = /* @__PURE__ */ new Set([
|
|
|
21651
22545
|
"AWS::AppMesh::VirtualRouter",
|
|
21652
22546
|
"AWS::AppMesh::VirtualService",
|
|
21653
22547
|
"AWS::AppStream::Fleet",
|
|
22548
|
+
"AWS::AppStream::StackFleetAssociation",
|
|
21654
22549
|
"AWS::AppSync::ApiCache",
|
|
21655
22550
|
"AWS::Artifact::Report",
|
|
21656
22551
|
"AWS::Athena::Session",
|
|
21657
22552
|
"AWS::AutoScalingPlans::ScalingPlan",
|
|
21658
22553
|
"AWS::BackupSearch::SearchJob",
|
|
22554
|
+
"AWS::BackupSearch::SearchResultExportJob",
|
|
21659
22555
|
"AWS::BCMDataExports::Table",
|
|
22556
|
+
"AWS::Bedrock::AsyncInvoke",
|
|
21660
22557
|
"AWS::Bedrock::DefaultPromptRouter",
|
|
22558
|
+
"AWS::Bedrock::FlowExecution",
|
|
22559
|
+
"AWS::Bedrock::FoundationModel",
|
|
22560
|
+
"AWS::Bedrock::ImportedModel",
|
|
22561
|
+
"AWS::Bedrock::ModelImportJob",
|
|
21661
22562
|
"AWS::Bedrock::ModelInvocationJob",
|
|
22563
|
+
"AWS::BedrockAgentCore::ConfigurationBundleVersion",
|
|
22564
|
+
"AWS::BedrockAgentCore::HarnessVersion",
|
|
22565
|
+
"AWS::BedrockAgentCore::PolicyGeneration",
|
|
21662
22566
|
"AWS::BedrockAgentCore::TokenVault",
|
|
22567
|
+
"AWS::Braket::Job",
|
|
22568
|
+
"AWS::Cassandra::Stream",
|
|
21663
22569
|
"AWS::Cloud9::EnvironmentEC2",
|
|
21664
22570
|
"AWS::CloudFormation::Macro",
|
|
21665
22571
|
"AWS::CloudFormation::ResourceScan",
|
|
21666
22572
|
"AWS::CloudFormation::WaitCondition",
|
|
21667
22573
|
"AWS::CloudFront::StreamingDistribution",
|
|
21668
22574
|
"AWS::CodeArtifact::Package",
|
|
21669
|
-
"AWS::CodeBuild::
|
|
22575
|
+
"AWS::CodeBuild::Build",
|
|
22576
|
+
"AWS::CodeBuild::BuildBatch",
|
|
21670
22577
|
"AWS::CodeBuild::Sandbox",
|
|
21671
|
-
"AWS::CodeBuild::SourceCredential",
|
|
21672
22578
|
"AWS::CodeStar::GitHubRepository",
|
|
21673
22579
|
"AWS::CognitoSync::Dataset",
|
|
22580
|
+
"AWS::Comprehend::DocumentClassificationJob",
|
|
22581
|
+
"AWS::Comprehend::DominantLanguageDetectionJob",
|
|
22582
|
+
"AWS::Comprehend::EntitiesDetectionJob",
|
|
22583
|
+
"AWS::Comprehend::FlywheelDataset",
|
|
22584
|
+
"AWS::Comprehend::SentimentDetectionJob",
|
|
22585
|
+
"AWS::Comprehend::TargetedSentimentDetectionJob",
|
|
21674
22586
|
"AWS::Config::ConfigurationRecorder",
|
|
21675
22587
|
"AWS::Config::DeliveryChannel",
|
|
21676
22588
|
"AWS::Config::OrganizationConfigRule",
|
|
22589
|
+
"AWS::ControlCatalog::CommonControl",
|
|
22590
|
+
"AWS::ControlCatalog::Control",
|
|
21677
22591
|
"AWS::ControlCatalog::Objective",
|
|
22592
|
+
"AWS::DataExchange::Assets",
|
|
22593
|
+
"AWS::DataExchange::EntitledDataSets",
|
|
22594
|
+
"AWS::DataExchange::Job",
|
|
22595
|
+
"AWS::DataSync::TaskExecution",
|
|
21678
22596
|
"AWS::DAX::Cluster",
|
|
21679
|
-
"AWS::DAX::ParameterGroup",
|
|
21680
22597
|
"AWS::DAX::SubnetGroup",
|
|
22598
|
+
"AWS::Deadline::Job",
|
|
21681
22599
|
"AWS::DirectoryService::MicrosoftAD",
|
|
21682
|
-
"AWS::DMS::EventSubscription",
|
|
21683
22600
|
"AWS::DMS::ReplicationInstance",
|
|
21684
|
-
"AWS::
|
|
21685
|
-
"AWS::DMS::ReplicationTask",
|
|
21686
|
-
"AWS::DocDB::DBClusterParameterGroup",
|
|
22601
|
+
"AWS::DRS::RecoveryInstance",
|
|
21687
22602
|
"AWS::DynamoDB::Export",
|
|
22603
|
+
"AWS::DynamoDB::Stream",
|
|
21688
22604
|
"AWS::EC2::ClientVpnAuthorizationRule",
|
|
21689
22605
|
"AWS::EC2::ClientVpnEndpoint",
|
|
21690
22606
|
"AWS::EC2::ClientVpnRoute",
|
|
21691
22607
|
"AWS::EC2::ClientVpnTargetNetworkAssociation",
|
|
22608
|
+
"AWS::EC2::ExportInstanceTask",
|
|
21692
22609
|
"AWS::EC2::NetworkInterfacePermission",
|
|
22610
|
+
"AWS::EC2::ReplaceRootVolumeTask",
|
|
22611
|
+
"AWS::EC2::VpnConnectionDeviceType",
|
|
21693
22612
|
"AWS::EC2::VPNGatewayRoutePropagation",
|
|
22613
|
+
"AWS::ECRPublic::Registry",
|
|
22614
|
+
"AWS::ECS::ContainerInstance",
|
|
22615
|
+
"AWS::ECS::Task",
|
|
21694
22616
|
"AWS::ElastiCache::ReservedCacheNode",
|
|
21695
22617
|
"AWS::ElastiCache::SecurityGroup",
|
|
21696
22618
|
"AWS::ElastiCache::SecurityGroupIngress",
|
|
21697
22619
|
"AWS::ElasticLoadBalancingV2::ListenerCertificate",
|
|
21698
22620
|
"AWS::Elasticsearch::Domain",
|
|
21699
22621
|
"AWS::EMR::NotebookExecution",
|
|
22622
|
+
"AWS::EMRContainers::JobRun",
|
|
22623
|
+
"AWS::EMRServerless::JobRun",
|
|
21700
22624
|
"AWS::Events::Replay",
|
|
22625
|
+
"AWS::FIS::Experiment",
|
|
21701
22626
|
"AWS::FIS::SafetyLever",
|
|
21702
22627
|
"AWS::FSx::Snapshot",
|
|
21703
22628
|
"AWS::FSx::StorageVirtualMachine",
|
|
21704
|
-
"AWS::FSx::Volume",
|
|
21705
|
-
"AWS::Glue::Classifier",
|
|
21706
|
-
"AWS::Glue::CustomEntityType",
|
|
21707
|
-
"AWS::Glue::DataQualityRuleset",
|
|
21708
22629
|
"AWS::Glue::DevEndpoint",
|
|
21709
|
-
"AWS::Glue::MLTransform",
|
|
21710
22630
|
"AWS::Glue::Partition",
|
|
21711
|
-
"AWS::Glue::
|
|
22631
|
+
"AWS::Glue::TableVersion",
|
|
21712
22632
|
"AWS::Greengrass::ConnectorDefinition",
|
|
21713
22633
|
"AWS::Greengrass::ConnectorDefinitionVersion",
|
|
21714
22634
|
"AWS::Greengrass::CoreDefinition",
|
|
@@ -21730,11 +22650,18 @@ const NON_PROVISIONABLE_TYPES = /* @__PURE__ */ new Set([
|
|
|
21730
22650
|
"AWS::IdentityStore::AllGroupMemberships",
|
|
21731
22651
|
"AWS::ImageBuilder::AllImageBuildVersions",
|
|
21732
22652
|
"AWS::ImageBuilder::AllWorkflowBuildVersions",
|
|
22653
|
+
"AWS::ImageBuilder::LifecycleExecution",
|
|
21733
22654
|
"AWS::ImageBuilder::WorkflowExecution",
|
|
21734
22655
|
"AWS::ImageBuilder::WorkflowStepExecution",
|
|
22656
|
+
"AWS::InternetMonitor::InternetEvent",
|
|
22657
|
+
"AWS::IoT::Index",
|
|
21735
22658
|
"AWS::IoT::PolicyPrincipalAttachment",
|
|
21736
22659
|
"AWS::IoT::ThingPrincipalAttachment",
|
|
22660
|
+
"AWS::IoTDeviceAdvisor::SuiteRun",
|
|
21737
22661
|
"AWS::IoTThingsGraph::FlowTemplate",
|
|
22662
|
+
"AWS::IoTTwinMaker::MetadataTransferJob",
|
|
22663
|
+
"AWS::IVS::Composition",
|
|
22664
|
+
"AWS::KafkaConnect::ConnectorOperation",
|
|
21738
22665
|
"AWS::KinesisAnalytics::Application",
|
|
21739
22666
|
"AWS::KinesisAnalytics::ApplicationOutput",
|
|
21740
22667
|
"AWS::KinesisAnalytics::ApplicationReferenceDataSource",
|
|
@@ -21744,22 +22671,32 @@ const NON_PROVISIONABLE_TYPES = /* @__PURE__ */ new Set([
|
|
|
21744
22671
|
"AWS::LakeFormation::DataLakeSettings",
|
|
21745
22672
|
"AWS::LakeFormation::Permissions",
|
|
21746
22673
|
"AWS::LakeFormation::Resource",
|
|
22674
|
+
"AWS::Lambda::DurableExecution",
|
|
22675
|
+
"AWS::Lightsail::ExportSnapshotRecord",
|
|
22676
|
+
"AWS::Location::Job",
|
|
22677
|
+
"AWS::Macie2::ClassificationJob",
|
|
21747
22678
|
"AWS::ManagedBlockchain::Member",
|
|
21748
22679
|
"AWS::ManagedBlockchain::Node",
|
|
21749
22680
|
"AWS::MediaConnect::Offering",
|
|
21750
22681
|
"AWS::MediaConnect::Reservation",
|
|
21751
22682
|
"AWS::MediaConvert::JobTemplate",
|
|
21752
|
-
"AWS::MediaConvert::Preset",
|
|
21753
22683
|
"AWS::MediaConvert::Queue",
|
|
21754
22684
|
"AWS::MediaLive::Channel",
|
|
21755
22685
|
"AWS::MediaLive::Input",
|
|
21756
22686
|
"AWS::MediaLive::InputSecurityGroup",
|
|
21757
22687
|
"AWS::MediaLive::Offering",
|
|
21758
22688
|
"AWS::MediaPackage::HarvestJob",
|
|
22689
|
+
"AWS::MediaPackageV2::HarvestJob",
|
|
21759
22690
|
"AWS::MediaStore::Container",
|
|
22691
|
+
"AWS::MedicalImaging::ImageSet",
|
|
21760
22692
|
"AWS::MemoryDB::MultiRegionParameterGroup",
|
|
21761
22693
|
"AWS::MemoryDB::ReservedNode",
|
|
22694
|
+
"AWS::NeptuneGraph::ExportTask",
|
|
22695
|
+
"AWS::NovaAct::WorkflowRun",
|
|
22696
|
+
"AWS::Omics::ReadSet",
|
|
21762
22697
|
"AWS::Omics::Reference",
|
|
22698
|
+
"AWS::Omics::Run",
|
|
22699
|
+
"AWS::Omics::Task",
|
|
21763
22700
|
"AWS::OpsWorks::App",
|
|
21764
22701
|
"AWS::OpsWorks::ElasticLoadBalancerAttachment",
|
|
21765
22702
|
"AWS::OpsWorks::Instance",
|
|
@@ -21767,8 +22704,14 @@ const NON_PROVISIONABLE_TYPES = /* @__PURE__ */ new Set([
|
|
|
21767
22704
|
"AWS::OpsWorks::Stack",
|
|
21768
22705
|
"AWS::OpsWorks::UserProfile",
|
|
21769
22706
|
"AWS::OpsWorks::Volume",
|
|
22707
|
+
"AWS::Organizations::Root",
|
|
21770
22708
|
"AWS::OSIS::PipelineBlueprint",
|
|
22709
|
+
"AWS::PartnerCentral::ConnectionPreferences",
|
|
22710
|
+
"AWS::PartnerCentral::Partner",
|
|
22711
|
+
"AWS::Personalize::BatchInferenceJob",
|
|
22712
|
+
"AWS::Personalize::BatchSegmentJob",
|
|
21771
22713
|
"AWS::Personalize::DataDeletionJob",
|
|
22714
|
+
"AWS::Personalize::DatasetExportJob",
|
|
21772
22715
|
"AWS::Personalize::Recipe",
|
|
21773
22716
|
"AWS::Pinpoint::ADMChannel",
|
|
21774
22717
|
"AWS::Pinpoint::APNSChannel",
|
|
@@ -21793,33 +22736,50 @@ const NON_PROVISIONABLE_TYPES = /* @__PURE__ */ new Set([
|
|
|
21793
22736
|
"AWS::PinpointEmail::DedicatedIpPool",
|
|
21794
22737
|
"AWS::PinpointEmail::Identity",
|
|
21795
22738
|
"AWS::QLDB::Ledger",
|
|
22739
|
+
"AWS::QuickSight::AssetBundleExportJob",
|
|
22740
|
+
"AWS::QuickSight::AssetBundleImportJob",
|
|
21796
22741
|
"AWS::RDS::DBSecurityGroup",
|
|
21797
22742
|
"AWS::RDS::DBSecurityGroupIngress",
|
|
22743
|
+
"AWS::RDS::ReservedDBInstance",
|
|
21798
22744
|
"AWS::Redshift::ClusterSecurityGroup",
|
|
21799
22745
|
"AWS::Redshift::ClusterSecurityGroupIngress",
|
|
22746
|
+
"AWS::Redshift::DataShare",
|
|
21800
22747
|
"AWS::RedshiftServerless::RecoveryPoint",
|
|
22748
|
+
"AWS::ResilienceHub::RecommendationTemplate",
|
|
21801
22749
|
"AWS::Route53::RecordSetGroup",
|
|
22750
|
+
"AWS::Route53Resolver::FirewallConfig",
|
|
22751
|
+
"AWS::SageMaker::AutoMLJob",
|
|
21802
22752
|
"AWS::SageMaker::CodeRepository",
|
|
21803
|
-
"AWS::SageMaker::
|
|
22753
|
+
"AWS::SageMaker::ExperimentTrialComponent",
|
|
22754
|
+
"AWS::SageMaker::HubContentVersion",
|
|
22755
|
+
"AWS::SageMaker::HyperParameterTuningJob",
|
|
21804
22756
|
"AWS::SageMaker::ModelCardExportJob",
|
|
21805
22757
|
"AWS::SageMaker::MonitoringScheduleAlert",
|
|
21806
22758
|
"AWS::SageMaker::NotebookInstance",
|
|
21807
22759
|
"AWS::SageMaker::NotebookInstanceLifecycleConfig",
|
|
22760
|
+
"AWS::SageMaker::OptimizationJob",
|
|
22761
|
+
"AWS::SageMaker::PipelineExecution",
|
|
22762
|
+
"AWS::SageMaker::TrainingJob",
|
|
21808
22763
|
"AWS::SageMaker::TransformJob",
|
|
21809
22764
|
"AWS::SageMaker::Workteam",
|
|
21810
|
-
"AWS::
|
|
22765
|
+
"AWS::SavingsPlans::SavingsPlan",
|
|
22766
|
+
"AWS::SecurityAgent::PentestTask",
|
|
21811
22767
|
"AWS::ServiceDiscovery::Instance",
|
|
21812
|
-
"AWS::
|
|
21813
|
-
"AWS::SES::ReceiptRule",
|
|
21814
|
-
"AWS::SES::ReceiptRuleSet",
|
|
22768
|
+
"AWS::ServiceQuotas::Quota",
|
|
21815
22769
|
"AWS::Signer::SigningJob",
|
|
22770
|
+
"AWS::SSM::AutomationExecution",
|
|
22771
|
+
"AWS::SSM::ManagedInstance",
|
|
21816
22772
|
"AWS::SSM::Session",
|
|
21817
22773
|
"AWS::SSO::ApplicationProvider",
|
|
21818
22774
|
"AWS::States::Execution",
|
|
21819
22775
|
"AWS::StepFunctions::MapRun",
|
|
21820
22776
|
"AWS::ThinClient::SoftwareSet",
|
|
22777
|
+
"AWS::Transcribe::CallAnalyticsJob",
|
|
22778
|
+
"AWS::Transcribe::MedicalScribeJob",
|
|
21821
22779
|
"AWS::Transcribe::MedicalTranscriptionJob",
|
|
22780
|
+
"AWS::Transcribe::TranscriptionJob",
|
|
21822
22781
|
"AWS::UserNotifications::ManagedNotificationConfiguration",
|
|
22782
|
+
"AWS::UserNotifications::NotificationEvent",
|
|
21823
22783
|
"AWS::WAF::ByteMatchSet",
|
|
21824
22784
|
"AWS::WAF::IPSet",
|
|
21825
22785
|
"AWS::WAF::Rule",
|
|
@@ -21837,7 +22797,9 @@ const NON_PROVISIONABLE_TYPES = /* @__PURE__ */ new Set([
|
|
|
21837
22797
|
"AWS::WAFRegional::SqlInjectionMatchSet",
|
|
21838
22798
|
"AWS::WAFRegional::WebACL",
|
|
21839
22799
|
"AWS::WAFRegional::WebACLAssociation",
|
|
21840
|
-
"AWS::WAFRegional::XssMatchSet"
|
|
22800
|
+
"AWS::WAFRegional::XssMatchSet",
|
|
22801
|
+
"AWS::Wisdom::Session",
|
|
22802
|
+
"AWS::WorkSpaces::WorkSpaceApplication"
|
|
21841
22803
|
]);
|
|
21842
22804
|
|
|
21843
22805
|
//#endregion
|
|
@@ -22346,7 +23308,7 @@ var CloudControlProvider = class {
|
|
|
22346
23308
|
const indeterminateGuard = await this.confirmDeleteTargetIdentity(logicalId, resourceType, physicalId, context);
|
|
22347
23309
|
if (context?.removeProtection === true && resourceType === "AWS::AutoScaling::AutoScalingGroup") {
|
|
22348
23310
|
this.logger.debug(`Delegating protected AutoScalingGroup ${logicalId} delete to the SDK ASGProvider (Cloud Control cannot force-delete a protected ASG)`);
|
|
22349
|
-
const { ASGProvider } = await import("./asg-provider-
|
|
23311
|
+
const { ASGProvider } = await import("./asg-provider-uaOnRUMe.js").then((n) => n.n);
|
|
22350
23312
|
return withIndeterminateGuard(await new ASGProvider().delete(logicalId, physicalId, resourceType, _properties, context), indeterminateGuard);
|
|
22351
23313
|
}
|
|
22352
23314
|
const isProtectedEc2Instance = context?.removeProtection === true && resourceType === "AWS::EC2::Instance";
|
|
@@ -29274,6 +30236,7 @@ const STATEFUL_TYPES = /* @__PURE__ */ new Set([
|
|
|
29274
30236
|
"AWS::Cognito::UserPool",
|
|
29275
30237
|
"AWS::SecretsManager::Secret",
|
|
29276
30238
|
"AWS::SSM::Parameter",
|
|
30239
|
+
"AWS::CloudHSM::Cluster",
|
|
29277
30240
|
"AWS::KMS::Key",
|
|
29278
30241
|
"AWS::KMS::ReplicaKey",
|
|
29279
30242
|
"AWS::CodeCommit::Repository",
|
|
@@ -29294,6 +30257,9 @@ const STATEFUL_TYPES = /* @__PURE__ */ new Set([
|
|
|
29294
30257
|
"AWS::RedshiftServerless::Snapshot",
|
|
29295
30258
|
"AWS::NeptuneGraph::Graph",
|
|
29296
30259
|
"AWS::NeptuneGraph::GraphSnapshot",
|
|
30260
|
+
"AWS::RDS::DBSnapshot",
|
|
30261
|
+
"AWS::RDS::ClusterSnapshot",
|
|
30262
|
+
"AWS::DynamoDB::Backup",
|
|
29297
30263
|
"AWS::MemoryDB::Cluster",
|
|
29298
30264
|
"AWS::MemoryDB::MultiRegionCluster",
|
|
29299
30265
|
"AWS::ElastiCache::ServerlessCache",
|
|
@@ -29319,6 +30285,7 @@ const STATEFUL_TYPES = /* @__PURE__ */ new Set([
|
|
|
29319
30285
|
"AWS::S3Outposts::Bucket",
|
|
29320
30286
|
"AWS::HealthImaging::Datastore",
|
|
29321
30287
|
"AWS::HealthLake::FHIRDatastore",
|
|
30288
|
+
"AWS::FSx::Volume",
|
|
29322
30289
|
"AWS::SES::MailManagerArchive",
|
|
29323
30290
|
"AWS::WorkspacesInstances::Volume",
|
|
29324
30291
|
"AWS::IoTAnalytics::Channel",
|
|
@@ -29341,6 +30308,7 @@ const STATEFUL_TYPES = /* @__PURE__ */ new Set([
|
|
|
29341
30308
|
"AWS::Connect::DataTable",
|
|
29342
30309
|
"AWS::AppConfig::ConfigurationProfile",
|
|
29343
30310
|
"AWS::AIOps::InvestigationGroup",
|
|
30311
|
+
"AWS::IoTSiteWise::Workspace",
|
|
29344
30312
|
"AWS::Rbin::Rule",
|
|
29345
30313
|
"AWS::SMSVOICE::PhoneNumber",
|
|
29346
30314
|
"AWS::SMSVOICE::SenderId"
|
|
@@ -29429,7 +30397,7 @@ function isStatefulRecreateTargetForReplace(resourceType, recordedProperties) {
|
|
|
29429
30397
|
function renderStatefulReason(reason) {
|
|
29430
30398
|
switch (reason) {
|
|
29431
30399
|
case "always": return "destroy loses all data in the resource";
|
|
29432
|
-
case "has-objects": return "S3 bucket is
|
|
30400
|
+
case "has-objects": return "S3 bucket is not provably empty";
|
|
29433
30401
|
case "has-retention": return "log group retains data (RetentionInDays > 0)";
|
|
29434
30402
|
case "has-log-events": return "log group is not provably empty";
|
|
29435
30403
|
case null: return "(not stateful)";
|
|
@@ -34099,42 +35067,64 @@ var DeployEngine = class {
|
|
|
34099
35067
|
expectedRegion: this.stackRegion
|
|
34100
35068
|
})), logicalId, void 0, void 0, updateProvider);
|
|
34101
35069
|
} catch (updateError) {
|
|
34102
|
-
const
|
|
34103
|
-
const ccUnsupported = msg.includes("UnsupportedActionException") || msg.includes("does not support UPDATE");
|
|
35070
|
+
const ccUnsupported = isUpdateUnsupportedError(updateError, logicalId);
|
|
34104
35071
|
const replaceOptIn = updateError instanceof ResourceUpdateNotSupportedError && this.options.replace === true;
|
|
34105
35072
|
if (ccUnsupported || replaceOptIn) {
|
|
34106
|
-
const
|
|
34107
|
-
|
|
34108
|
-
|
|
34109
|
-
|
|
34110
|
-
|
|
34111
|
-
|
|
34112
|
-
|
|
34113
|
-
|
|
34114
|
-
|
|
34115
|
-
|
|
34116
|
-
|
|
34117
|
-
|
|
34118
|
-
|
|
34119
|
-
})
|
|
34120
|
-
|
|
34121
|
-
|
|
34122
|
-
|
|
34123
|
-
|
|
35073
|
+
const retainOldOnReplace = updateReplacePolicy === "Retain";
|
|
35074
|
+
const statefulReason = retainOldOnReplace ? null : isStatefulRecreateTargetForReplace(resourceType, currentProps);
|
|
35075
|
+
if (statefulReason && this.options.forceStatefulRecreation !== true) throw markNonRetryable(new CdkdError(replaceOptIn ? `--replace would DELETE + CREATE the stateful resource ${logicalId} (${resourceType}) — ${renderStatefulReason(statefulReason)}. Re-run with --force-stateful-recreation to confirm the data loss, or change the resource definition to avoid the immutable-property change.` : `${logicalId} (${resourceType}) cannot be updated in place by the provisioning layer it routes through, so applying this change would DELETE + CREATE it — but it is a stateful resource: ${renderStatefulReason(statefulReason)}. Re-run with --force-stateful-recreation to confirm the data loss, or change the resource definition to avoid the update.`, "STATEFUL_REPLACE_BLOCKED", updateError instanceof Error ? updateError : void 0));
|
|
35076
|
+
this.logger.info(retainOldOnReplace ? `UPDATE not supported for ${logicalId} (${resourceType}), replacing (CREATE only — UpdateReplacePolicy: Retain keeps the old resource)` : `UPDATE not supported for ${logicalId} (${resourceType}), replacing (DELETE → CREATE)`);
|
|
35077
|
+
if (!retainOldOnReplace) {
|
|
35078
|
+
const fallbackFinalSnapshotId = await this.prepareFinalSnapshotForDelete(logicalId, resourceType, currentResource, template?.Resources?.[logicalId]?.UpdateReplacePolicy ?? currentResource.updateReplacePolicy);
|
|
35079
|
+
let fallbackDeleteResult = void 0;
|
|
35080
|
+
try {
|
|
35081
|
+
fallbackDeleteResult = await updateProvider.delete(logicalId, currentResource.physicalId, resourceType, currentProps, {
|
|
35082
|
+
expectedRegion: this.stackRegion,
|
|
35083
|
+
forceDataDelete: this.options.forceStatefulRecreation === true,
|
|
35084
|
+
...fallbackFinalSnapshotId !== void 0 && { finalSnapshotIdentifier: fallbackFinalSnapshotId }
|
|
35085
|
+
});
|
|
35086
|
+
} catch (deleteError) {
|
|
35087
|
+
const deleteMsg = deleteError instanceof Error ? deleteError.message : String(deleteError);
|
|
35088
|
+
if (deleteMsg.includes("does not exist") || deleteMsg.includes("not found") || deleteMsg.includes("NotFound")) this.logger.debug(`Old resource ${logicalId} already gone, proceeding with CREATE`);
|
|
35089
|
+
else throw deleteError;
|
|
35090
|
+
}
|
|
35091
|
+
const fallbackSkipReason = deleteSkipReason(fallbackDeleteResult);
|
|
35092
|
+
if (fallbackSkipReason !== void 0) throw new Error(deleteSkippedMessage(logicalId, currentResource.physicalId, fallbackSkipReason, "during the UPDATE-not-supported replacement"));
|
|
34124
35093
|
}
|
|
34125
|
-
const fallbackSkipReason = deleteSkipReason(fallbackDeleteResult);
|
|
34126
|
-
if (fallbackSkipReason !== void 0) throw new Error(deleteSkippedMessage(logicalId, currentResource.physicalId, fallbackSkipReason, "during the UPDATE-not-supported replacement"));
|
|
34127
35094
|
const replDecision = this.providerRegistry.getProviderFor({
|
|
34128
35095
|
resourceType,
|
|
34129
35096
|
properties: resolvedProps
|
|
34130
35097
|
});
|
|
34131
35098
|
const replProvider = replDecision.provider;
|
|
34132
35099
|
const replProps = replDecision.provisionedBy === "cc-api" ? this.preparePropertiesForCcApi(resourceType, resolvedProps, logicalId) : resolvedProps;
|
|
34133
|
-
|
|
35100
|
+
let retainedSurvivorReason;
|
|
35101
|
+
let createResult;
|
|
35102
|
+
try {
|
|
35103
|
+
createResult = await this.withRetry(() => withCurrentResourceSecrets(updateSecrets, () => replProvider.create(logicalId, resourceType, replProps, { maskSecrets: createSecretMasker(updateSecrets) })), logicalId, void 0, void 0, replProvider);
|
|
35104
|
+
} catch (createError) {
|
|
35105
|
+
if (!retainOldOnReplace) throw createError;
|
|
35106
|
+
if (!isNameCollisionError(createError instanceof Error ? createError.message : String(createError))) throw createError;
|
|
35107
|
+
const nameOrigin = this.replacementNameOrigin(logicalId, currentResource.physicalId);
|
|
35108
|
+
throw markNonRetryable(new CdkdError(`${logicalId} (${resourceType}) requires replacement because the provisioning layer cannot update it in place — but its physical name is still held by the existing resource AND UpdateReplacePolicy: Retain pins that resource in place. ${nameOrigin.descriptor}. ${nameOrigin.remedy} — with Retain, the old resource keeps the name, so a same-name replacement can never proceed. Removing UpdateReplacePolicy: Retain lets cdkd delete the old resource first, which destroys it and any data it holds.`, "NAMED_REPLACEMENT_COLLISION", createError instanceof Error ? createError : void 0));
|
|
35109
|
+
}
|
|
35110
|
+
if (retainOldOnReplace) {
|
|
35111
|
+
if (createResult.physicalId === currentResource.physicalId) {
|
|
35112
|
+
const idempotentNameOrigin = this.replacementNameOrigin(logicalId, currentResource.physicalId);
|
|
35113
|
+
throw markNonRetryable(new CdkdError(`${logicalId} (${resourceType}) requires replacement, but its Create API is name-idempotent: the create returned the existing resource (${currentResource.physicalId}) instead of creating a new one, and UpdateReplacePolicy: Retain pins that resource in place, so the new properties were not applied. ${idempotentNameOrigin.descriptor}. ${idempotentNameOrigin.remedy} — with Retain, the old resource keeps the name, so a same-name replacement can never proceed.`, "NAMED_REPLACEMENT_IDEMPOTENT_CREATE"));
|
|
35114
|
+
}
|
|
35115
|
+
this.logger.warn(` ⚠ ${logicalId} has UpdateReplacePolicy: Retain — the old physical resource (${currentResource.physicalId}) is RETAINED and is no longer tracked by cdkd: it keeps running and incurring cost, and \`cdkd destroy\` will not remove it. Delete it yourself once you no longer need its data.`);
|
|
35116
|
+
retainedSurvivorReason = `UpdateReplacePolicy: Retain kept the old ${resourceType} (${currentResource.physicalId}), now untracked by cdkd`;
|
|
35117
|
+
}
|
|
34134
35118
|
const replacementResult = {
|
|
34135
35119
|
physicalId: createResult.physicalId,
|
|
34136
35120
|
wasReplaced: true,
|
|
34137
|
-
...createResult.attributes && { attributes: createResult.attributes }
|
|
35121
|
+
...createResult.attributes && { attributes: createResult.attributes },
|
|
35122
|
+
...createResult.noEchoAttributes === true && { noEchoAttributes: true },
|
|
35123
|
+
...createResult.noEchoAttributeNames && { noEchoAttributeNames: createResult.noEchoAttributeNames },
|
|
35124
|
+
...retainedSurvivorReason !== void 0 ? {
|
|
35125
|
+
outcome: "partial",
|
|
35126
|
+
reason: retainedSurvivorReason
|
|
35127
|
+
} : { outcome: "updated" }
|
|
34138
35128
|
};
|
|
34139
35129
|
if (createResult.effectiveProperties) replacementResult.effectiveProperties = createResult.effectiveProperties;
|
|
34140
35130
|
result = replacementResult;
|
|
@@ -34280,8 +35270,12 @@ var DeployEngine = class {
|
|
|
34280
35270
|
* connect the message to their code at all.
|
|
34281
35271
|
*
|
|
34282
35272
|
* `descriptor` names WHERE the name came from; `remedy` is the accurate
|
|
34283
|
-
* first option.
|
|
34284
|
-
*
|
|
35273
|
+
* first option. What each caller appends after it differs: the two
|
|
35274
|
+
* property-driven create-first sites append the shared `--replace`
|
|
35275
|
+
* alternative, while the update-failure fallback's two `Retain` refusals
|
|
35276
|
+
* (issue #2518) append the Retain clause instead — under `Retain` no flag
|
|
35277
|
+
* frees the name, so offering `--replace` there would send the user to a
|
|
35278
|
+
* flag that changes nothing.
|
|
34285
35279
|
*
|
|
34286
35280
|
* Classification is best-effort by construction (see
|
|
34287
35281
|
* {@link looksLikeCdkdGeneratedName}) and falls back to the pre-#1636
|
|
@@ -34607,5 +35601,5 @@ var DeployEngine = class {
|
|
|
34607
35601
|
};
|
|
34608
35602
|
|
|
34609
35603
|
//#endregion
|
|
34610
|
-
export { beginCommandInterruptScope as $,
|
|
34611
|
-
//# sourceMappingURL=deploy-engine
|
|
35604
|
+
export { beginCommandInterruptScope as $, synthesisStatusMessage as $n, withErrorHandling as $r, maskSecretsInError as $t, formatResourceLine as A, ensureAssetStorage as An, AssetError as Ar, requireConfigString as At, isExportAliasCollision as B, describeDockerCapturedOutput as Bn, LockError as Br, INTRINSIC_KEYS as Bt, unsupportedFinalSnapshotError as C, loadPublishableAssetManifest as Cn, processStackMessages as Cr, coerceCfnBoolean as Ct, isStatefulRecreateTargetForReplace as D, AssetModeResolver as Dn, getAwsClients as Dr, replayWarn as Dt, MULTI_REGION_RECREATE_BLOCKED_TYPES as E, stripControlChars as En, AwsClients as Er, readConfigString as Et, red as F, validateAssetBucketName as Fn, DeployCancelledError as Fr, s3BucketDualStackDomainName as Ft, collectInlinePolicyNamesManagedBySiblings as G, getDockerCmd as Gn, ResourceUpdateNotSupportedError as Gr, STATE_SOURCED_CROSS_GENERATION_RULES as Gt, stateKeySecretExposure as H, describeDockerFailure as Hn, PartialFailureError as Hr, withRetry as Ht, yellow as I, validateContainerRepoName as In, DynamicReferenceRegionAmbiguousError as Ir, s3BucketRegionalDomainName as It, findActionableSilentDrops as J, runDockerForeground as Jn, StateError as Jr, carriesSecretMask as Jt, clearOnUpdateRemoval as K, partitionSensitiveEnv as Kn, StackHasActiveImportsError as Kr, STATE_SOURCED_READBACK_RULES as Kt, collectDeclaredOutputNames as L, buildDenyExternalAccessPolicy as Ln, IntrinsicResolutionRefusalError as Lr, s3BucketWebsiteUrl as Lt, cyan as M, isCrossRegionRedirect as Mn, ConfigError as Mr, producerRegionsFromState as Mt, gray as N, parseBootstrapMarker as Nn, CrossAccountSecretRefusalError as Nr, s3BucketArn as Nt, isStatefulRecreateTargetSync as O, BOOTSTRAP_MARKER_PREFIX as On, resetAwsClients as Or, requireConfigArray as Ot, green as P, readBootstrapMarkerBody as Pn, DependencyError as Pr, s3BucketDomainName as Pt, maskerOrIdentity as Q, Synthesizer as Qn, normalizeAwsError as Qr, isSingleDynamicReferenceToken as Qt, collectPublishedOutputNames as R, describeAwsFailure as Rn, LocalInvokeBuildError as Rr, applyRoleArnIfSet as Rt, refusesFinalSnapshot as S, createAssetRedirectResolver as Sn, AssemblyReader as Sr, assertRegionMatch as St, extractDeploymentEventError as T, escapeRegExp$1 as Tn, resolveBucketRegion as Tr, configStringRefusal as Tt, getCurrentResourceSecrets as U, dockerSpawnEnvWithSensitive as Un, ProvisioningError as Ur, DagBuilder as Ut, secretBearingStateKeyWarning as V, describeDockerExecFailure as Vn, NestedStackChildDirectDestroyError as Vr, describeTypeWithThrottleRetry as Vt, IAMRoleProvider as W, formatDockerLoginError as Wn, ResourceTimeoutError as Wr, TemplateParser as Wt, createMaskedRetryLogger as X, AssetManifestLoader as Xn, formatError as Xr, dynamicReferenceTokens as Xt, findSilentDropProperties as Y, runDockerStreaming as Yn, SynthesisError as Yr, createSecretMasker as Yt, maskDeep as Z, getDockerImageBySourceHash as Zn, isCdkdError as Zr, errorCauseChain as Zt, PRE_DELETE_SNAPSHOT_TYPES as _, shouldRetainResource as _n, displaySafe as _r, parameterTypeMayLoseSecretIdentity as _t, DeploymentEventsStore as a, markRedactedCause as ai, LockManager as an, resolveSkipPrefix as ar, slowCcOperationTimeoutMs as at, createPreDeleteFinalSnapshot as b, WorkGraph as bn, canonicalizeRegion as br, normalizeAwsTagsToCfn as bt, replayFailedOperations as c, UNRENDERABLE as cn, resolveUseCdkBootstrapAssets as cr, deleteSkipReason as ct, updatePartialReason as d, forceQuitRecoveryClause as dn, CFN_TEMPLATE_BODY_LIMIT as dr, IntrinsicFunctionResolver as dt, isMarkedNonRetryable as ei, maskSecretsInText as en, getDefaultStateBucketName as er, endCommandInterruptScope as et, withResourceDeadline as f, CUSTOM_RESOURCE_RESPONSE_PREFIX as fn, CFN_TEMPLATE_URL_LIMIT as fr, carriesDynamicReference as ft, ATOMIC_FINAL_SNAPSHOT_TYPES as g, importableOutputs as gn, CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION as gr, isUnboundTemplateParameter as gt, computeImplicitDeleteEdges as h, importableOutputKeys as hn, uploadCfnTemplate as hr, getAccountInfo as ht, DeploymentEventsReader as i, markNonRetryable as ii, scrubResourceRecord as in, resolveCaptureObservedState as ir, CloudControlProvider as it, bold as j, getBootstrapMarkerKey as jn, CdkdError as jr, classifyReplaySecretRegion as jt, renderStatefulReason as k, assertAssetBucketRegion as kn, setAwsClients as kr, requireConfigObject as kt, replayRollback as l, buildForceUnlockCommand as ln, stateBucketExistenceConfirmed as lr, disableInstanceApiTermination as lt, IMPLICIT_DELETE_DEPENDENCIES as m, exportNamesCarriedFrom as mn, findLargeInlineResources as mr, coerceParameterTypedValue as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, isThrottlingError as ni, recoverMaskedOutput as nn, resolveApp as nr, isInterruptedWaitError as nt, planFailedOps as o, retryClassificationText as oi, S3StateBackend as on, resolveStateBucketWithDefault as or, UNSPECIFIED_SKIP_REASON as ot, maskingRetryLogger as p, DEFAULT_STATE_PREFIX as pn, MIGRATE_TMP_PREFIX as pr, cfnRefValueFromPhysicalId as pt, ProviderRegistry as q, redactDockerArgvValues as qn, StackTerminationProtectionError as qr, TEMPLATE_SOURCED_RULES as qt, DeployEngine as r, isTransientServerError as ri, redactSecretsForState as rn, resolveAutoAssetStorage as rr, startInterruptWatch as rt, planRollback as s, __exportAll as si, rebuildClientForBucketRegion as sn, resolveStateBucketWithDefaultAndSource as sr, deleteIndeterminateGuards as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, isRetryableTransientError as ti, recordMaskOnlyValue as tn, getLegacyStateBucketName as tr, interruptWatchListenerCount as tt, updatePartialMessage as u, buildLockContentionMessage as un, warnDeprecatedNoPrefixCliFlag as ur, isTerminationProtectionPropagationError as ut, buildFinalSnapshotIdentifier as v, AssetPublisher as vn, expectedOwnerParam as vr, refStateLookupFromResource as vt, makeCanonicalizePropertiesFn as w, rewriteTemplateAssetReferences as wn, clearBucketRegionCache as wr, configBooleanRefusal as wt, isFinalSnapshotError as x, buildAssetRedirectMap as xn, derivePartitionAndUrlSuffix as xr, resolveExplicitPhysicalId as xt, ccRoutedFinalSnapshotError as y, stringifyValue as yn, PARTITION_TABLE as yr, WAFv2WebACLProvider as yt, exportAliasCollisionScrubWarning as z, buildDockerImage as zn, LocalStartServiceError as zr, DiffCalculator as zt };
|
|
35605
|
+
//# sourceMappingURL=deploy-engine--rkIGhow.js.map
|