@go-to-k/cdkd 0.290.33 → 0.290.34
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-BCpZ70Ws.js → asg-provider-BTxYWlwu.js} +2 -2
- package/dist/{asg-provider-BCpZ70Ws.js.map → asg-provider-BTxYWlwu.js.map} +1 -1
- package/dist/cli.js +2 -2
- package/dist/{deploy-engine-B6TcGQSU.js → deploy-engine-efiVJ-rX.js} +597 -71
- package/dist/deploy-engine-efiVJ-rX.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/{program-D75-DVWT.js → program-Dh1J573K.js} +415 -50
- package/dist/{program-D75-DVWT.js.map → program-Dh1J573K.js.map} +1 -1
- package/dist/{version-xHGBnQbJ.js → version-CpyoQ3PO.js} +2 -2
- package/dist/{version-xHGBnQbJ.js.map → version-CpyoQ3PO.js.map} +1 -1
- package/package.json +1 -1
- package/dist/deploy-engine-B6TcGQSU.js.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { C as displayAwsMessage, D as truncateCodePoints, E as isPasteableIdent, S as UNRENDERABLE, T as displaySafe, d as applyDefaultNameForFallback, f as explicitNamePropertyFor, g as looksLikeCdkdGeneratedName, h as getCurrentStackName, m as generateResourceNameWithFallback, n as getLogger, p as generateResourceName, r as isStdoutReservedForPayload, s as getLiveRenderer, v as withStackName, w as displayIdent, x as STACK_REF_MAX_CODE_POINTS, y as withoutGeneratedFallbackName } from "./logger-BX4pJk0T.js";
|
|
2
2
|
import { a as setAssumedRoleCredentials, i as getPreAssumeEnvCredentials, n as awsClientDefaults, o as setPreAssumeEnvCredentials, r as getAssumedRoleCredentials, t as SECOND_ROLE_PUBLISH_REFUSAL } from "./aws-client-defaults-BoIxyCir.js";
|
|
3
|
-
import { t as getCdkdVersion } from "./version-
|
|
3
|
+
import { t as getCdkdVersion } from "./version-CpyoQ3PO.js";
|
|
4
4
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
5
5
|
import { createHash, randomUUID } from "node:crypto";
|
|
6
6
|
import { CreateBucketCommand, DeleteObjectCommand, DeleteObjectsCommand, GetBucketLocationCommand, GetBucketReplicationCommand, GetObjectCommand, HeadBucketCommand, HeadObjectCommand, ListObjectVersionsCommand, ListObjectsV2Command, NoSuchKey, PutBucketEncryptionCommand, PutBucketPolicyCommand, PutObjectCommand, PutPublicAccessBlockCommand, S3Client, S3ServiceException } from "@aws-sdk/client-s3";
|
|
@@ -9802,6 +9802,7 @@ function refuseMalformedOperation(shownStack, where, op) {
|
|
|
9802
9802
|
const attempted = o["attemptedProperties"];
|
|
9803
9803
|
if (attempted !== void 0 && (typeof attempted !== "object" || attempted === null || Array.isArray(attempted))) fail("attemptedProperties", attempted, "an object when present");
|
|
9804
9804
|
if (o["oldResourceRetained"] !== void 0 && typeof o["oldResourceRetained"] !== "boolean") fail("oldResourceRetained", o["oldResourceRetained"], "a boolean when present");
|
|
9805
|
+
if (o["previousResourceType"] !== void 0 && typeof o["previousResourceType"] !== "string") fail("previousResourceType", o["previousResourceType"], "a string when present");
|
|
9805
9806
|
}
|
|
9806
9807
|
/** The TYPE of a JSON-derived value, for a refusal that must not echo the value. */
|
|
9807
9808
|
function kind(v) {
|
|
@@ -13071,7 +13072,7 @@ function storeAssociation(associations, key, expression, plaintext) {
|
|
|
13071
13072
|
* matters: an AWS resource type string is fixed by AWS, and a typo in either
|
|
13072
13073
|
* copy makes that copy's gate simply never fire (no-op), never fire wrongly.
|
|
13073
13074
|
*/
|
|
13074
|
-
const NESTED_STACK_RESOURCE_TYPE$
|
|
13075
|
+
const NESTED_STACK_RESOURCE_TYPE$4 = "AWS::CloudFormation::Stack";
|
|
13075
13076
|
/**
|
|
13076
13077
|
* What a PARENT stack's resolution proved about each `Parameters` entry of an
|
|
13077
13078
|
* `AWS::CloudFormation::Stack` row it is about to provision, keyed by the
|
|
@@ -13410,7 +13411,7 @@ const UNFRAMED_SPELLING = Symbol("cdkd.nested-parameter.unframed-spelling");
|
|
|
13410
13411
|
* it either -- it walks the child's stored bag with no inherited bag.
|
|
13411
13412
|
*/
|
|
13412
13413
|
function recordNestedStackParameterExpressions(secrets, resourceType, resolvedProperties, sourceProperties, rules = TEMPLATE_DERIVED_RULES) {
|
|
13413
|
-
if (resourceType !== NESTED_STACK_RESOURCE_TYPE$
|
|
13414
|
+
if (resourceType !== NESTED_STACK_RESOURCE_TYPE$4) return;
|
|
13414
13415
|
if (secrets.size === 0) return;
|
|
13415
13416
|
if (!isPlainObject$2(resolvedProperties) || !isPlainObject$2(sourceProperties)) return;
|
|
13416
13417
|
if (!Object.hasOwn(resolvedProperties, "Parameters")) return;
|
|
@@ -18705,6 +18706,7 @@ const PROPERTY_COVERAGE_BY_TYPE = /* @__PURE__ */ new Map([
|
|
|
18705
18706
|
silentDrop: /* @__PURE__ */ new Map([
|
|
18706
18707
|
["CapacityProviderConfiguration", "not yet implemented by cdkd"],
|
|
18707
18708
|
["FilesystemConfigurations", "not yet implemented by cdkd"],
|
|
18709
|
+
["PlatformVersion", "not yet implemented by cdkd"],
|
|
18708
18710
|
["RequestHeaderConfiguration", "not yet implemented by cdkd"],
|
|
18709
18711
|
["Tags", "not yet implemented by cdkd"]
|
|
18710
18712
|
]),
|
|
@@ -24103,6 +24105,88 @@ function classifyReplaySecretRegion(expression, consumerRegion, importedProducer
|
|
|
24103
24105
|
};
|
|
24104
24106
|
}
|
|
24105
24107
|
|
|
24108
|
+
//#endregion
|
|
24109
|
+
//#region src/deployment/stale-attribute-heal.ts
|
|
24110
|
+
/**
|
|
24111
|
+
* Control-flow signal from the physical-id fallback to `resolveGetAtt`'s heal
|
|
24112
|
+
* wrapper. It never escapes that wrapper: it is raised only under a `probe`
|
|
24113
|
+
* context, which only the wrapper builds, and the wrapper catches it on the
|
|
24114
|
+
* same `await`. An `Error` subclass so a lint rule or a stray `catch` that
|
|
24115
|
+
* assumes `Error` still behaves.
|
|
24116
|
+
*/
|
|
24117
|
+
var StaleAttributeMissSignal = class StaleAttributeMissSignal extends Error {
|
|
24118
|
+
constructor() {
|
|
24119
|
+
super("stale attribute miss (internal control flow — must not escape resolveGetAtt)");
|
|
24120
|
+
this.name = "StaleAttributeMissSignal";
|
|
24121
|
+
Object.setPrototypeOf(this, StaleAttributeMissSignal.prototype);
|
|
24122
|
+
}
|
|
24123
|
+
};
|
|
24124
|
+
const NESTED_STACK_RESOURCE_TYPE$3 = "AWS::CloudFormation::Stack";
|
|
24125
|
+
/**
|
|
24126
|
+
* Types whose `attributes` are NOT an AWS read-back: a custom resource's are its
|
|
24127
|
+
* handler's response `Data` (possibly `NoEcho`, and re-invoking the handler is
|
|
24128
|
+
* not a read), a nested stack's are its child's outputs.
|
|
24129
|
+
*/
|
|
24130
|
+
function isHealExcludedType(resourceType) {
|
|
24131
|
+
return resourceType === NESTED_STACK_RESOURCE_TYPE$3 || resourceType === "AWS::CloudFormation::CustomResource" || resourceType.startsWith("Custom::");
|
|
24132
|
+
}
|
|
24133
|
+
/**
|
|
24134
|
+
* Drop every member that is not a value: `undefined`, `null` and `''`.
|
|
24135
|
+
*
|
|
24136
|
+
* Empty-to-absent is the point for the `--no-wait` `DBInstance` row
|
|
24137
|
+
* (go-to-k/cdkd#3077): an instance still `creating` has no endpoint, and an
|
|
24138
|
+
* empty value persisted into the record would be SERVED by the resolver's
|
|
24139
|
+
* cached read forever, shadowing every later heal.
|
|
24140
|
+
*/
|
|
24141
|
+
function normalizeHealedAttributes(attributes) {
|
|
24142
|
+
const out = {};
|
|
24143
|
+
if (attributes === void 0 || attributes === null || typeof attributes !== "object") return out;
|
|
24144
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
24145
|
+
if (value === void 0 || value === null || value === "") continue;
|
|
24146
|
+
if (key === "__proto__") continue;
|
|
24147
|
+
out[key] = value;
|
|
24148
|
+
}
|
|
24149
|
+
return out;
|
|
24150
|
+
}
|
|
24151
|
+
/**
|
|
24152
|
+
* Read `attributeName` out of a healed map the way `resolveGetAtt` reads the
|
|
24153
|
+
* recorded one: flat key first (SDK providers record `Endpoint.Port` flat), then
|
|
24154
|
+
* a dot-path walk (Cloud Control records nested objects). `Object.hasOwn` at
|
|
24155
|
+
* every step — `attributeName` is template text (issue #2767).
|
|
24156
|
+
*/
|
|
24157
|
+
function readHealedAttribute(attributes, attributeName) {
|
|
24158
|
+
if (Object.hasOwn(attributes, attributeName)) {
|
|
24159
|
+
const flat = attributes[attributeName];
|
|
24160
|
+
if (flat !== void 0 && flat !== null && flat !== "") return flat;
|
|
24161
|
+
}
|
|
24162
|
+
if (!attributeName.includes(".")) return void 0;
|
|
24163
|
+
let cursor = attributes;
|
|
24164
|
+
for (const part of attributeName.split(".")) if (cursor !== null && typeof cursor === "object" && Object.hasOwn(cursor, part)) cursor = cursor[part];
|
|
24165
|
+
else return;
|
|
24166
|
+
return cursor === null || cursor === "" ? void 0 : cursor;
|
|
24167
|
+
}
|
|
24168
|
+
/**
|
|
24169
|
+
* Merge a healed map INTO a record's attributes: a healed key is added only
|
|
24170
|
+
* where the record holds NO value for it (or holds one `isStaleValue` declares
|
|
24171
|
+
* unusable — the pre-#1681 placeholder ARN). A recorded value always wins
|
|
24172
|
+
* otherwise: create-time attributes the read does not report must survive, and
|
|
24173
|
+
* a read must never rewrite what a provider recorded.
|
|
24174
|
+
*
|
|
24175
|
+
* Returns the SAME `recorded` reference when nothing was added, so a caller can
|
|
24176
|
+
* tell "healed" from "nothing to do" by identity.
|
|
24177
|
+
*/
|
|
24178
|
+
function mergeHealedAttributes(recorded, healed, isStaleValue) {
|
|
24179
|
+
let merged;
|
|
24180
|
+
for (const [key, value] of Object.entries(healed)) {
|
|
24181
|
+
if (value === void 0 || value === null || value === "") continue;
|
|
24182
|
+
if (key === "__proto__") continue;
|
|
24183
|
+
if (recorded !== void 0 && Object.hasOwn(recorded, key) && !isStaleValue(key, recorded[key])) continue;
|
|
24184
|
+
merged ??= { ...recorded };
|
|
24185
|
+
merged[key] = value;
|
|
24186
|
+
}
|
|
24187
|
+
return merged ?? recorded;
|
|
24188
|
+
}
|
|
24189
|
+
|
|
24106
24190
|
//#endregion
|
|
24107
24191
|
//#region src/provisioning/config-shape.ts
|
|
24108
24192
|
/**
|
|
@@ -25757,6 +25841,20 @@ const REF_RETURNS_ARN_FROM_STATE = /* @__PURE__ */ new Map([
|
|
|
25757
25841
|
["AWS::AppSync::Resolver", ["ResolverArn"]]
|
|
25758
25842
|
]);
|
|
25759
25843
|
/**
|
|
25844
|
+
* Is `value`, recorded under `attributeName` of a `resourceType` record, a
|
|
25845
|
+
* pre-#1681 PLACEHOLDER ARN — a value that is present but knowably unusable?
|
|
25846
|
+
*
|
|
25847
|
+
* The one predicate behind `rejectPlaceholderArnAttribute`'s refusal and the
|
|
25848
|
+
* #1852 heal's "this recorded value may be overwritten" exception
|
|
25849
|
+
* (`mergeHealedAttributes`), so the two cannot disagree about which records are
|
|
25850
|
+
* stale. Scoped to the {@link REF_RETURNS_ARN_FROM_STATE} types AND their
|
|
25851
|
+
* declared ARN attribute names — see that refusal's note for why no wider.
|
|
25852
|
+
*/
|
|
25853
|
+
function isStalePlaceholderArnAttribute(resourceType, attributeName, value) {
|
|
25854
|
+
if (!REF_RETURNS_ARN_FROM_STATE.get(resourceType)?.includes(attributeName)) return false;
|
|
25855
|
+
return typeof value === "string" && isPlaceholderArn(value);
|
|
25856
|
+
}
|
|
25857
|
+
/**
|
|
25760
25858
|
* Build a {@link RefStateLookup} from a resource's stored state maps, checking
|
|
25761
25859
|
* `properties` first (the template value CFn `Ref` mirrors) then `attributes`.
|
|
25762
25860
|
* Only non-empty string values qualify — an intrinsic-shaped or empty value is
|
|
@@ -26074,7 +26172,7 @@ function carriesDynamicReference(value) {
|
|
|
26074
26172
|
return false;
|
|
26075
26173
|
}
|
|
26076
26174
|
/** The nested-stack resource type, whose `Outputs.<Name>` attributes are re-resolved (issue #2055). */
|
|
26077
|
-
const NESTED_STACK_RESOURCE_TYPE$
|
|
26175
|
+
const NESTED_STACK_RESOURCE_TYPE$2 = "AWS::CloudFormation::Stack";
|
|
26078
26176
|
/** Prefix `NestedStackProvider` records a child stack output under. */
|
|
26079
26177
|
const NESTED_STACK_OUTPUT_ATTRIBUTE_PREFIX = "Outputs.";
|
|
26080
26178
|
/**
|
|
@@ -28070,14 +28168,19 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
28070
28168
|
const storedValue = Object.hasOwn(resource.attributes, attributeName) ? resource.attributes[attributeName] : void 0;
|
|
28071
28169
|
const flatValue = isImpossibleEmptyStoredAttribute(resource.resourceType, attributeName, storedValue) ? void 0 : storedValue;
|
|
28072
28170
|
if (flatValue !== void 0) {
|
|
28073
|
-
|
|
28171
|
+
if (isStalePlaceholderArnAttribute(resource.resourceType, attributeName, flatValue)) {
|
|
28172
|
+
const outcome = await this.healStaleAttributes(logicalId, resource, context);
|
|
28173
|
+
const healed = this.usableHealedAttribute(outcome, attributeName);
|
|
28174
|
+
if (healed !== void 0 && !isStalePlaceholderArnAttribute(resource.resourceType, attributeName, healed)) return this.serveHealedAttribute(logicalId, attributeName, healed, context);
|
|
28175
|
+
this.rejectPlaceholderArnAttribute(resource, attributeName, flatValue, logicalId, context, outcome);
|
|
28176
|
+
}
|
|
28074
28177
|
if (resource.resourceType === "AWS::Route53::HostedZone" && attributeName === "NameServers" && typeof flatValue === "string") {
|
|
28075
28178
|
const nameServers = flatValue === "" ? [] : flatValue.split(",");
|
|
28076
28179
|
this.logger.debug(`Normalized legacy Fn::GetAtt attribute: ${this.displayMasked(logicalId, context)}.${this.displayMasked(attributeName, context)} -> ${this.displayMasked(stringifyAttributeForLog(attributeName, this.maskValueLeaves(nameServers, context)), context)}`);
|
|
28077
28180
|
return this.noteAttributeSecrecy(logicalId, attributeName, nameServers, context);
|
|
28078
28181
|
}
|
|
28079
28182
|
this.logger.debug(`Resolved Fn::GetAtt from attributes: ${this.displayMasked(logicalId, context)}.${this.displayMasked(attributeName, context)} -> ${this.displayMasked(stringifyAttributeForLog(attributeName, this.maskValueLeaves(flatValue, context)), context)}`);
|
|
28080
|
-
if (resource.resourceType === NESTED_STACK_RESOURCE_TYPE$
|
|
28183
|
+
if (resource.resourceType === NESTED_STACK_RESOURCE_TYPE$2 && attributeName.startsWith(NESTED_STACK_OUTPUT_ATTRIBUTE_PREFIX) && carriesDynamicReference(flatValue)) return await this.reresolveCrossStackValue(flatValue, nestedStackChildRegionFromLocalArn(resource.physicalId), context, `nested stack ${logicalId} ${this.displayLeaf(attributeName, context)}`, crossStackSourceKey({ "Fn::GetAtt": getAtt }));
|
|
28081
28184
|
return this.noteAttributeSecrecy(logicalId, attributeName, flatValue, context);
|
|
28082
28185
|
}
|
|
28083
28186
|
if (attributeName.includes(".")) {
|
|
@@ -28094,13 +28197,13 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
28094
28197
|
}
|
|
28095
28198
|
}
|
|
28096
28199
|
}
|
|
28097
|
-
if (resource.resourceType === NESTED_STACK_RESOURCE_TYPE$
|
|
28200
|
+
if (resource.resourceType === NESTED_STACK_RESOURCE_TYPE$2 && attributeName.startsWith(NESTED_STACK_OUTPUT_ATTRIBUTE_PREFIX)) {
|
|
28098
28201
|
const declared = Object.keys(resource.attributes ?? {}).filter((k) => k.startsWith(NESTED_STACK_OUTPUT_ATTRIBUTE_PREFIX)).sort().map((k) => this.displayMasked(this.outputNameLogText(k, context), context));
|
|
28099
28202
|
const declaredText = declared.length > 0 ? declared.join(", ") : "(none)";
|
|
28100
28203
|
const loggedLogicalId = this.displayMasked(logicalId, context);
|
|
28101
28204
|
throw markNonRetryable(new IntrinsicResolutionRefusalError(`Cannot resolve Fn::GetAtt [${loggedLogicalId}, ${this.displayMasked(attributeName, context)}]: the nested stack '${loggedLogicalId}' declares no output named '${this.displayMasked(this.outputNameLogText(attributeName, context), context)}'. Its outputs are ${this.displayMasked(declaredText, context)}. Check the output name in the nested stack's template, and deploy the child stack again if you have just added it.`));
|
|
28102
28205
|
}
|
|
28103
|
-
const value = await this.
|
|
28206
|
+
const value = await this.constructWithStaleRecordHeal(resource, attributeName, context, logicalId);
|
|
28104
28207
|
this.logger.debug(`Resolved Fn::GetAtt: ${this.displayMasked(logicalId, context)}.${this.displayMasked(attributeName, context)} -> ${this.displayMasked(stringifyAttributeForLog(attributeName, this.maskValueLeaves(value, context)), context)}`);
|
|
28105
28208
|
return value;
|
|
28106
28209
|
}
|
|
@@ -28191,10 +28294,143 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
28191
28294
|
* honest answer is to say so and name the remedy: the record heals on the
|
|
28192
28295
|
* resource's next in-place update (#1727).
|
|
28193
28296
|
*/
|
|
28194
|
-
rejectPlaceholderArnAttribute(resource, attributeName, value, logicalId, context) {
|
|
28195
|
-
if (!
|
|
28196
|
-
|
|
28197
|
-
|
|
28297
|
+
rejectPlaceholderArnAttribute(resource, attributeName, value, logicalId, context, healOutcome) {
|
|
28298
|
+
if (!isStalePlaceholderArnAttribute(resource.resourceType, attributeName, value)) return;
|
|
28299
|
+
throw markNonRetryable(new IntrinsicResolutionRefusalError(`Cannot resolve Fn::GetAtt [${this.displayMasked(logicalId, context)}, ${this.displayMasked(attributeName, context)}] for ${resource.resourceType}: the recorded value "${stringifyValue(this.maskValueLeaves(value, context))}" is a placeholder written by a cdkd version older than issue #1681 — its region and account fields are literal wildcards, so it is not a usable ARN. ` + this.staleRecordRemedy(healOutcome, context)));
|
|
28300
|
+
}
|
|
28301
|
+
/**
|
|
28302
|
+
* The remedy half of a STALE-RECORD refusal (issue
|
|
28303
|
+
* [#1852](https://github.com/go-to-k/cdkd/issues/1852)), worded from what the
|
|
28304
|
+
* heal observed so it is true on the path taken.
|
|
28305
|
+
*
|
|
28306
|
+
* The sentence it replaces — "deploy the stack again so the resource's next
|
|
28307
|
+
* update heals the record" — was false for the commonest case: a deploy that
|
|
28308
|
+
* changes none of the resource's own properties takes the no-change skip and
|
|
28309
|
+
* never runs `update()`, so "again" healed nothing.
|
|
28310
|
+
*
|
|
28311
|
+
* Every clause is cdkd-authored; AWS's text stays behind `--verbose`
|
|
28312
|
+
* ({@link describeFailureObserved}), because a denied read quotes the
|
|
28313
|
+
* caller's account, role and session.
|
|
28314
|
+
*/
|
|
28315
|
+
staleRecordRemedy(outcome, context) {
|
|
28316
|
+
const touch = "change any property of the resource so its next update re-records the attributes, or re-import it with 'cdkd import'";
|
|
28317
|
+
if (outcome === void 0) return `Run 'cdkd deploy': it re-reads a stale record's attributes from AWS and heals the record. Otherwise ${touch}.`;
|
|
28318
|
+
switch (outcome.kind) {
|
|
28319
|
+
case "failed": return `cdkd tried to re-read the attributes from AWS to heal the record, but ${this.describeFailureObserved("the provider read", outcome.error, context)}. Fix that (a missing read permission is the usual cause) and deploy again — cdkd retries the read on every deploy until the record is healed — or ${touch}.`;
|
|
28320
|
+
case "not-found": return "cdkd tried to re-read the attributes from AWS to heal the record, but AWS reports no resource behind the recorded physical id — it was probably deleted outside cdkd. Check it with 'cdkd drift', then re-create it (change the resource so it is replaced) or remove it from state.";
|
|
28321
|
+
case "read":
|
|
28322
|
+
if ((outcome.withheldKeys?.length ?? 0) > 0) return this.withheldRemedy();
|
|
28323
|
+
return `cdkd re-read the resource from AWS and the read reports no usable value for this attribute either, so there is nothing to heal the record with; ${touch}.`;
|
|
28324
|
+
case "not-attempted": return `cdkd did not re-read it from AWS (the record was written by this deploy, or this resource type has no read-only lookup); ${touch}.`;
|
|
28325
|
+
}
|
|
28326
|
+
}
|
|
28327
|
+
/**
|
|
28328
|
+
* The remedy half of the "not enriched" refusal. The pre-#1852 sentence,
|
|
28329
|
+
* plus what the heal established: a completed re-read that reports no such
|
|
28330
|
+
* attribute CONFIRMS the type does not supply it; a context with no healer
|
|
28331
|
+
* (`cdkd diff`, `cdkd drift`, ...) re-read nothing, so the record may merely
|
|
28332
|
+
* predate the enrichment and `cdkd deploy` is what heals it.
|
|
28333
|
+
*/
|
|
28334
|
+
unenrichedRemedy(resourceType, attributeName, outcome, context) {
|
|
28335
|
+
const fileIssue = `Avoid this Fn::GetAtt, or file an issue at https://github.com/go-to-k/cdkd/issues so cdkd can enrich ${resourceType}.${this.displayMasked(attributeName, context)}.`;
|
|
28336
|
+
if (outcome === void 0) return `If this record was written by an older cdkd that did not record the attribute yet, 'cdkd deploy' re-reads it from AWS and heals the record. ${fileIssue}`;
|
|
28337
|
+
if (outcome.kind === "read") {
|
|
28338
|
+
if (this.healWithheld(outcome, attributeName)) return this.withheldRemedy();
|
|
28339
|
+
return `cdkd re-read the resource's attributes from AWS and the read reports none by that name. ${fileIssue}`;
|
|
28340
|
+
}
|
|
28341
|
+
return fileIssue;
|
|
28342
|
+
}
|
|
28343
|
+
/** Did the heal's read report this attribute (by its top-level key) and cdkd withhold it as masked? */
|
|
28344
|
+
healWithheld(outcome, attributeName) {
|
|
28345
|
+
const head = attributeName.split(".")[0];
|
|
28346
|
+
return outcome.withheldKeys?.some((key) => key === attributeName || key === head) ?? false;
|
|
28347
|
+
}
|
|
28348
|
+
/**
|
|
28349
|
+
* The remedy when the re-read DID report a value and cdkd withheld it: Cloud
|
|
28350
|
+
* Control's read-back is masked wherever cdkd cannot certify a key as a
|
|
28351
|
+
* read-only attribute, which is every key when `DescribeType` is unavailable.
|
|
28352
|
+
* "The read reports none ... file an issue" would be false here.
|
|
28353
|
+
*/
|
|
28354
|
+
withheldRemedy() {
|
|
28355
|
+
return "cdkd re-read the resource through Cloud Control, but withheld the value: it could not confirm that this is a read-only attribute of the type, and an unconfirmed value is never used. Grant the deploy role cloudformation:DescribeType and deploy again; if the role already has it, the name is a writable property rather than an attribute — reference the value the template sets instead.";
|
|
28356
|
+
}
|
|
28357
|
+
/**
|
|
28358
|
+
* Ask the context's healer (issue #1852) — `undefined` when it has none. The
|
|
28359
|
+
* healer is contracted never to throw; the `catch` makes that a property of
|
|
28360
|
+
* THIS call site rather than of every supplier, because a throw here would
|
|
28361
|
+
* fail a deploy the pre-#1852 code passed (the warn-and-return fallback).
|
|
28362
|
+
*/
|
|
28363
|
+
async healStaleAttributes(logicalId, resource, context) {
|
|
28364
|
+
if (context.attributeHealer === void 0) return void 0;
|
|
28365
|
+
try {
|
|
28366
|
+
return await context.attributeHealer(logicalId, resource);
|
|
28367
|
+
} catch (error) {
|
|
28368
|
+
return {
|
|
28369
|
+
kind: "failed",
|
|
28370
|
+
error
|
|
28371
|
+
};
|
|
28372
|
+
}
|
|
28373
|
+
}
|
|
28374
|
+
/**
|
|
28375
|
+
* The attribute out of a heal's read-back, or `undefined` when the read does
|
|
28376
|
+
* not supply a USABLE one. A value carrying `SECRET_MASK` is not usable:
|
|
28377
|
+
* `CloudControlProvider.import` masks what it cannot certify, and serving
|
|
28378
|
+
* that would send the literal mask to AWS.
|
|
28379
|
+
*/
|
|
28380
|
+
usableHealedAttribute(outcome, attributeName) {
|
|
28381
|
+
if (outcome?.kind !== "read") return void 0;
|
|
28382
|
+
const value = readHealedAttribute(outcome.attributes, attributeName);
|
|
28383
|
+
return value === void 0 || carriesSecretMask(value) ? void 0 : value;
|
|
28384
|
+
}
|
|
28385
|
+
/** Serve a value the #1852 heal just read from AWS, logging it like a cached read. */
|
|
28386
|
+
serveHealedAttribute(logicalId, attributeName, value, context) {
|
|
28387
|
+
this.logger.debug(`Resolved Fn::GetAtt from a re-read of AWS (the state record lacked it): ${this.displayMasked(logicalId, context)}.${this.displayMasked(attributeName, context)} -> ${this.displayMasked(stringifyAttributeForLog(attributeName, this.maskValueLeaves(value, context)), context)}`);
|
|
28388
|
+
return this.noteAttributeSecrecy(logicalId, attributeName, value, context);
|
|
28389
|
+
}
|
|
28390
|
+
/**
|
|
28391
|
+
* {@link constructGuardedAttribute}, healing a STALE state record first when
|
|
28392
|
+
* the construction is about to take {@link guardedPhysicalIdFallback} (issue
|
|
28393
|
+
* [#1852](https://github.com/go-to-k/cdkd/issues/1852)).
|
|
28394
|
+
*
|
|
28395
|
+
* Three passes at most, and an AWS call on none of them unless the fallback
|
|
28396
|
+
* is actually reached:
|
|
28397
|
+
*
|
|
28398
|
+
* 1. PROBE — construct under a derived context whose fallback raises
|
|
28399
|
+
* {@link StaleAttributeMissSignal} instead of deciding. Every per-type arm
|
|
28400
|
+
* that CAN answer (the ~40 constructed ARNs, the live-read arms) returns
|
|
28401
|
+
* here exactly as before.
|
|
28402
|
+
* 2. HEAL — only on the signal: ask the context's healer, which re-reads the
|
|
28403
|
+
* record's attributes through its provider once per deploy. A value for
|
|
28404
|
+
* this attribute is served from that read.
|
|
28405
|
+
* 3. SETTLE — otherwise construct again under a context carrying the heal's
|
|
28406
|
+
* outcome, so the fallback decides exactly as it always has (refuse an
|
|
28407
|
+
* `*Arn` / `*Url` shape, refuse under `--strict-getatt`, else warn and
|
|
28408
|
+
* return the physical id) and words a refusal from what was observed.
|
|
28409
|
+
*
|
|
28410
|
+
* The phase rides a DERIVED context, never a field of this resolver: one
|
|
28411
|
+
* resolver instance serves every concurrently resolving resource of a stack.
|
|
28412
|
+
* A context with no healer skips all of it.
|
|
28413
|
+
*/
|
|
28414
|
+
async constructWithStaleRecordHeal(resource, attributeName, context, logicalId) {
|
|
28415
|
+
if (context.attributeHealer === void 0) return this.constructGuardedAttribute(resource, attributeName, context, logicalId);
|
|
28416
|
+
try {
|
|
28417
|
+
return await this.constructGuardedAttribute(resource, attributeName, {
|
|
28418
|
+
...context,
|
|
28419
|
+
staleAttributeHeal: { phase: "probe" }
|
|
28420
|
+
}, logicalId);
|
|
28421
|
+
} catch (error) {
|
|
28422
|
+
if (!(error instanceof StaleAttributeMissSignal)) throw error;
|
|
28423
|
+
}
|
|
28424
|
+
const outcome = await this.healStaleAttributes(logicalId, resource, context) ?? { kind: "not-attempted" };
|
|
28425
|
+
const healed = this.usableHealedAttribute(outcome, attributeName);
|
|
28426
|
+
if (healed !== void 0) return this.serveHealedAttribute(logicalId, attributeName, healed, context);
|
|
28427
|
+
return this.constructGuardedAttribute(resource, attributeName, {
|
|
28428
|
+
...context,
|
|
28429
|
+
staleAttributeHeal: {
|
|
28430
|
+
phase: "settled",
|
|
28431
|
+
outcome
|
|
28432
|
+
}
|
|
28433
|
+
}, logicalId);
|
|
28198
28434
|
}
|
|
28199
28435
|
/**
|
|
28200
28436
|
* Construct resource attribute value based on resource type, refusing to
|
|
@@ -28667,7 +28903,11 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
28667
28903
|
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
28668
28904
|
}
|
|
28669
28905
|
if (resourceType === "AWS::RDS::DBProxy" || resourceType === "AWS::RDS::DBProxyEndpoint") {
|
|
28670
|
-
if (attributeName === "VpcId")
|
|
28906
|
+
if (attributeName === "VpcId") {
|
|
28907
|
+
if (context.staleAttributeHeal?.phase === "probe") throw new StaleAttributeMissSignal();
|
|
28908
|
+
const vpcIdHealOutcome = context.staleAttributeHeal?.phase === "settled" ? context.staleAttributeHeal.outcome : void 0;
|
|
28909
|
+
throw markNonRetryable(new IntrinsicResolutionRefusalError(`Cannot resolve Fn::GetAtt [${this.displayMasked(logicalId, context)}, ${this.displayMasked(attributeName, context)}] for ${resourceType}: the state record holds no VpcId for it (the read-back that would have recorded it reported none), and the physical id "${this.displayMasked(physicalId, context)}" is a name, not a VPC id, so cdkd refuses to substitute it. ${this.staleRecordRemedy(vpcIdHealOutcome, context)} Referencing the VPC directly also works.`));
|
|
28910
|
+
}
|
|
28671
28911
|
return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
28672
28912
|
}
|
|
28673
28913
|
return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context);
|
|
@@ -28769,15 +29009,23 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
28769
29009
|
* per-type handlers.
|
|
28770
29010
|
*/
|
|
28771
29011
|
guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId, context) {
|
|
29012
|
+
if (context?.staleAttributeHeal?.phase === "probe") throw new StaleAttributeMissSignal();
|
|
29013
|
+
const healOutcome = context?.staleAttributeHeal?.phase === "settled" ? context.staleAttributeHeal.outcome : void 0;
|
|
29014
|
+
const recordMayBeStale = healOutcome?.kind === "failed" || healOutcome?.kind === "not-found";
|
|
28772
29015
|
const expectsArnShape = attributeName.endsWith("Arn") && !physicalId.startsWith("arn:");
|
|
28773
29016
|
const expectsUrlShape = attributeName.endsWith("Url") && !/^https?:\/\//.test(physicalId);
|
|
28774
29017
|
if (expectsArnShape || expectsUrlShape) {
|
|
28775
29018
|
const expectedShape = expectsArnShape ? "an ARN (arn:...)" : "a URL (http(s)://...)";
|
|
28776
|
-
throw markNonRetryable(new IntrinsicResolutionRefusalError(`Cannot resolve Fn::GetAtt [${this.displayMasked(logicalId, context)}, ${this.displayMasked(attributeName, context)}] for ${resourceType}: attributes are not enriched for this resource type, and the physical ID fallback "${this.displayMasked(physicalId, context)}" is not ${expectedShape}. CloudFormation would return a different value here, so falling back to the physical ID would silently produce a wrong value (e.g. in stack Outputs).
|
|
29019
|
+
throw markNonRetryable(new IntrinsicResolutionRefusalError(`Cannot resolve Fn::GetAtt [${this.displayMasked(logicalId, context)}, ${this.displayMasked(attributeName, context)}] for ${resourceType}: ` + (recordMayBeStale ? `the state record holds no value for it, and the physical ID ` : `attributes are not enriched for this resource type, and the physical ID `) + `fallback "${this.displayMasked(physicalId, context)}" is not ${expectedShape}. CloudFormation would return a different value here, so falling back to the physical ID would silently produce a wrong value (e.g. in stack Outputs). ` + (recordMayBeStale ? this.staleRecordRemedy(healOutcome, context) : this.unenrichedRemedy(resourceType, attributeName, healOutcome, context))));
|
|
28777
29020
|
}
|
|
28778
|
-
if (this.strictGetAtt) throw markNonRetryable(new IntrinsicResolutionRefusalError(`Cannot resolve Fn::GetAtt [${this.displayMasked(logicalId, context)}, ${this.displayMasked(attributeName, context)}] for ${resourceType}: attributes are not enriched for this resource type, and --strict-getatt rejects the physical ID fallback "${this.displayMasked(physicalId, context)}" (which may not be the value CloudFormation would return). Drop --strict-getatt to fall back with a warning
|
|
29021
|
+
if (this.strictGetAtt) throw markNonRetryable(new IntrinsicResolutionRefusalError(`Cannot resolve Fn::GetAtt [${this.displayMasked(logicalId, context)}, ${this.displayMasked(attributeName, context)}] for ${resourceType}: ` + (recordMayBeStale ? `the state record holds no value for it, and --strict-getatt ` : `attributes are not enriched for this resource type, and --strict-getatt `) + `rejects the physical ID fallback "${this.displayMasked(physicalId, context)}" (which may not be the value CloudFormation would return). Drop --strict-getatt to fall back with a warning. ` + (recordMayBeStale ? this.staleRecordRemedy(healOutcome, context) : this.unenrichedRemedy(resourceType, attributeName, healOutcome, context))));
|
|
28779
29022
|
this.physicalIdFallbackCount++;
|
|
28780
|
-
|
|
29023
|
+
if (recordMayBeStale) {
|
|
29024
|
+
this.logger.warn(`The state record for ${this.displayMasked(logicalId, context)} (${resourceType}) holds no ${this.displayMasked(attributeName, context)}, returning physical ID. ` + this.staleRecordRemedy(healOutcome, context));
|
|
29025
|
+
return physicalId;
|
|
29026
|
+
}
|
|
29027
|
+
const withheld = healOutcome?.kind === "read" && this.healWithheld(healOutcome, attributeName) ? `. ${this.withheldRemedy()}` : "";
|
|
29028
|
+
this.logger.warn(`Unknown attribute ${this.displayMasked(attributeName, context)} for resource type ${resourceType}, returning physical ID${withheld}`);
|
|
28781
29029
|
return physicalId;
|
|
28782
29030
|
}
|
|
28783
29031
|
/**
|
|
@@ -32887,7 +33135,7 @@ var CloudControlProvider = class {
|
|
|
32887
33135
|
const indeterminateGuard = await this.confirmDeleteTargetIdentity(logicalId, resourceType, physicalId, context);
|
|
32888
33136
|
if (context?.removeProtection === true && resourceType === "AWS::AutoScaling::AutoScalingGroup") {
|
|
32889
33137
|
this.logger.debug(`Delegating protected AutoScalingGroup ${logicalId} delete to the SDK ASGProvider (Cloud Control cannot force-delete a protected ASG)`);
|
|
32890
|
-
const { ASGProvider } = await import("./asg-provider-
|
|
33138
|
+
const { ASGProvider } = await import("./asg-provider-BTxYWlwu.js").then((n) => n.n);
|
|
32891
33139
|
const asgProvider = new ASGProvider();
|
|
32892
33140
|
return withIndeterminateGuard(await asgProvider.delete(logicalId, physicalId, resourceType, _properties, context), indeterminateGuard);
|
|
32893
33141
|
}
|
|
@@ -36889,6 +37137,11 @@ var ProviderRegistry = class {
|
|
|
36889
37137
|
return hits;
|
|
36890
37138
|
}
|
|
36891
37139
|
};
|
|
37140
|
+
/**
|
|
37141
|
+
* True for the types `CustomResourceProvider` serves. Exported for
|
|
37142
|
+
* `type-change-guard.ts`'s `equalIdNamesSameResource`: these types are the one
|
|
37143
|
+
* family whose physical ids share ONE namespace across types.
|
|
37144
|
+
*/
|
|
36892
37145
|
function isCustomResource(resourceType) {
|
|
36893
37146
|
return resourceType.startsWith("Custom::") || resourceType === "AWS::CloudFormation::CustomResource";
|
|
36894
37147
|
}
|
|
@@ -37590,6 +37843,35 @@ function getCurrentResourceSecrets() {
|
|
|
37590
37843
|
//#endregion
|
|
37591
37844
|
//#region src/deployment/type-change-guard.ts
|
|
37592
37845
|
/**
|
|
37846
|
+
* Does an EQUAL physical id on the two halves of a replacement name the SAME
|
|
37847
|
+
* resource? (issue [#2668](https://github.com/go-to-k/cdkd/issues/2668))
|
|
37848
|
+
*
|
|
37849
|
+
* Within one type, always — which is what the engine's name-idempotent-create
|
|
37850
|
+
* guards and the rollback's "adopted the live new resource" shortcut assume.
|
|
37851
|
+
* Across a `Type` change an equal id is a coincidence of two namespaces (an SSM
|
|
37852
|
+
* parameter and a log group, an IAM user and an IAM group, can share a bare
|
|
37853
|
+
* name): the create was genuine, and the other half still has to be deleted
|
|
37854
|
+
* through its own type's provider.
|
|
37855
|
+
*
|
|
37856
|
+
* The ONE exception is the custom-resource family. Every `Custom::*` type and
|
|
37857
|
+
* `AWS::CloudFormation::CustomResource` are served by the user's handler, which
|
|
37858
|
+
* picks the id, so `Custom::Foo` -> `Custom::Bar` returning the same
|
|
37859
|
+
* `PhysicalResourceId` names the SAME resource — and "deleting the other half"
|
|
37860
|
+
* would send `Delete` for what the create just built. That holds only on the
|
|
37861
|
+
* SDK layer: Cloud Control addresses a resource by type AND identifier.
|
|
37862
|
+
*
|
|
37863
|
+
* Keyed on the TYPES, deliberately not on "both halves resolved to one provider
|
|
37864
|
+
* instance": `register-providers.ts` shares one instance across many types
|
|
37865
|
+
* whose namespaces are disjoint (`AWS::IAM::User` / `AWS::IAM::Group`, the EC2,
|
|
37866
|
+
* RDS, Glue, ECS families ...), and reading those as one namespace refuses a
|
|
37867
|
+
* genuine create on the deploy side and strands the new resource on the
|
|
37868
|
+
* rollback side.
|
|
37869
|
+
*/
|
|
37870
|
+
function equalIdNamesSameResource(input) {
|
|
37871
|
+
if (input.oldType === input.newType) return true;
|
|
37872
|
+
return isCustomResource(input.oldType) && isCustomResource(input.newType) && input.createLayer !== "cc-api";
|
|
37873
|
+
}
|
|
37874
|
+
/**
|
|
37593
37875
|
* The CFn type of a nested stack's row in its PARENT's template.
|
|
37594
37876
|
*
|
|
37595
37877
|
* Spelled locally rather than imported, matching
|
|
@@ -37597,14 +37879,14 @@ function getCurrentResourceSecrets() {
|
|
|
37597
37879
|
* `src/cli/commands/retire-cfn-stack.ts`, and importing a CLI command module
|
|
37598
37880
|
* from the deployment layer would invert the dependency direction.
|
|
37599
37881
|
*/
|
|
37600
|
-
const NESTED_STACK_RESOURCE_TYPE = "AWS::CloudFormation::Stack";
|
|
37882
|
+
const NESTED_STACK_RESOURCE_TYPE$1 = "AWS::CloudFormation::Stack";
|
|
37601
37883
|
/**
|
|
37602
37884
|
* Find every planned change whose recorded type and template type differ with
|
|
37603
37885
|
* `AWS::CloudFormation::Stack` on one side.
|
|
37604
37886
|
*
|
|
37605
|
-
* Reads exactly the two values
|
|
37606
|
-
*
|
|
37607
|
-
*
|
|
37887
|
+
* Reads exactly the two values a replacement's halves route on:
|
|
37888
|
+
* `change.resourceType` (the create) and the state record's `resourceType`
|
|
37889
|
+
* (the resource that actually exists, and the delete). That
|
|
37608
37890
|
* is deliberate — deriving the "desired" type from the template again would be
|
|
37609
37891
|
* a second implementation of the diff's own Type-change rule (metadata skip,
|
|
37610
37892
|
* condition pruning) which could drift away from the routing decision this
|
|
@@ -37624,8 +37906,8 @@ function findNestedStackTypeChanges(input) {
|
|
|
37624
37906
|
const currentType = currentResource.resourceType;
|
|
37625
37907
|
const desiredType = change.resourceType;
|
|
37626
37908
|
if (currentType === desiredType) continue;
|
|
37627
|
-
const intoNested = desiredType === NESTED_STACK_RESOURCE_TYPE;
|
|
37628
|
-
if (!intoNested && !(currentType === NESTED_STACK_RESOURCE_TYPE)) continue;
|
|
37909
|
+
const intoNested = desiredType === NESTED_STACK_RESOURCE_TYPE$1;
|
|
37910
|
+
if (!intoNested && !(currentType === NESTED_STACK_RESOURCE_TYPE$1)) continue;
|
|
37629
37911
|
found.push({
|
|
37630
37912
|
logicalId,
|
|
37631
37913
|
currentType,
|
|
@@ -37637,18 +37919,18 @@ function findNestedStackTypeChanges(input) {
|
|
|
37637
37919
|
return found;
|
|
37638
37920
|
}
|
|
37639
37921
|
/**
|
|
37640
|
-
* Render the refusal. Names the logical id, BOTH types, the resource
|
|
37641
|
-
*
|
|
37922
|
+
* Render the refusal. Names the logical id, BOTH types, the existing resource,
|
|
37923
|
+
* the child stack involved, and what to do instead.
|
|
37642
37924
|
*
|
|
37643
|
-
* `stackName` is the stack being deployed, so
|
|
37644
|
-
*
|
|
37645
|
-
*
|
|
37925
|
+
* `stackName` is the stack being deployed, so each arm can print the child
|
|
37926
|
+
* stack name `<stackName>~<logicalId>` — the one piece the user cannot read off
|
|
37927
|
+
* their own template.
|
|
37646
37928
|
*/
|
|
37647
37929
|
function renderNestedStackTypeChangeRefusal(typeChanges, stackName) {
|
|
37648
37930
|
const rows = typeChanges.map((tc) => {
|
|
37649
|
-
return `${` - ${tc.logicalId}: Type changes from ${tc.currentType} to ${tc.desiredType} (the existing ${tc.currentType} is ${tc.physicalId}).`}\n${tc.direction === "into-nested-stack" ? `
|
|
37931
|
+
return `${` - ${tc.logicalId}: Type changes from ${tc.currentType} to ${tc.desiredType} (the existing ${tc.currentType} is ${tc.physicalId}).`}\n${tc.direction === "into-nested-stack" ? ` Replacing a single resource BY a nested stack is not supported: the create half of the replacement would deploy a whole child stack "${stackName}~${tc.logicalId}" as one row, while ${tc.physicalId} is still recorded under this logical id.` : ` Replacing a nested stack BY a single resource is not supported: a replacement deletes its old half as a best-effort cleanup step whose failure is only a warning, so the child stack "${stackName}~${tc.logicalId}" and every resource it owns could be left behind, untracked, under a deploy that reports success.`}`;
|
|
37650
37932
|
});
|
|
37651
|
-
return `Refusing to deploy ${stackName}: ` + (typeChanges.length === 1 ? `a resource changes its Type ` : `${typeChanges.length} resources change their Type `) + `into or out of ${NESTED_STACK_RESOURCE_TYPE}, which cdkd
|
|
37933
|
+
return `Refusing to deploy ${stackName}: ` + (typeChanges.length === 1 ? `a resource changes its Type ` : `${typeChanges.length} resources change their Type `) + `into or out of ${NESTED_STACK_RESOURCE_TYPE$1}, which cdkd does not replace in place (issue #2668).\n${rows.join("\n")}\n Deploy this as two changes instead: give the new resource a DIFFERENT logical id (in CDK, rename the construct) so the existing row is deleted through its own type's provider and the new one is created under its own — or remove the resource in one deploy and add its replacement in the next. There is no flag that overrides this refusal.`;
|
|
37652
37934
|
}
|
|
37653
37935
|
|
|
37654
37936
|
//#endregion
|
|
@@ -40927,13 +41209,86 @@ const RECREATE_RETRY_SCHEDULE = {
|
|
|
40927
41209
|
initialDelayMs: 2e3,
|
|
40928
41210
|
maxDelayMs: 1e4
|
|
40929
41211
|
};
|
|
41212
|
+
/** Spelled locally: importing the CLI's copy would invert the layer direction. */
|
|
41213
|
+
const NESTED_STACK_RESOURCE_TYPE = "AWS::CloudFormation::Stack";
|
|
41214
|
+
const nonEmptyString = (v) => typeof v === "string" && v.length > 0;
|
|
41215
|
+
/** The two places a journal can name the old resource's type, each `undefined` when unusable. */
|
|
41216
|
+
function journaledOldTypes(op) {
|
|
41217
|
+
return {
|
|
41218
|
+
stamped: nonEmptyString(op.previousResourceType) ? op.previousResourceType : void 0,
|
|
41219
|
+
recorded: nonEmptyString(op.previousState?.resourceType) ? op.previousState.resourceType : void 0
|
|
41220
|
+
};
|
|
41221
|
+
}
|
|
40930
41222
|
/**
|
|
40931
|
-
*
|
|
40932
|
-
*
|
|
40933
|
-
*
|
|
41223
|
+
* Which type the OLD half of a replacement op routes on, or why that cannot be
|
|
41224
|
+
* decided (issue [#2668](https://github.com/go-to-k/cdkd/issues/2668)).
|
|
41225
|
+
*
|
|
41226
|
+
* Two sources name it: {@link CompletedOperation.previousResourceType} (written
|
|
41227
|
+
* by a binary that knows about Type changes) and `previousState.resourceType`
|
|
41228
|
+
* (journaled by every binary, inside the previous record). A legacy journal has
|
|
41229
|
+
* only the second, which is enough. The verdict is REFUSED rather than guessed
|
|
41230
|
+
* in three shapes, because a wrong answer dispatches a create and a delete at
|
|
41231
|
+
* the wrong service:
|
|
41232
|
+
*
|
|
41233
|
+
* - neither source names a type (a hand-edited or torn op) — falling back to
|
|
41234
|
+
* `op.resourceType` is exactly the single-type assumption this replaces;
|
|
41235
|
+
* - the two sources disagree — one of them was edited, and nothing says which;
|
|
41236
|
+
* - the types differ with `AWS::CloudFormation::Stack` on either side. The
|
|
41237
|
+
* deploy refuses that pair at plan time (`type-change-guard.ts`), so only a
|
|
41238
|
+
* journal from a binary older than that guard carries one, and what that
|
|
41239
|
+
* deploy left behind is not knowable from the journal: its mis-routed
|
|
41240
|
+
* delete may have destroyed the child it had just created.
|
|
41241
|
+
*/
|
|
41242
|
+
function resolveReplacementOldType(op) {
|
|
41243
|
+
const { stamped, recorded } = journaledOldTypes(op);
|
|
41244
|
+
if (stamped !== void 0 && recorded !== void 0 && stamped !== recorded) return {
|
|
41245
|
+
ok: false,
|
|
41246
|
+
reason: `the journal names two different types for the old resource (previousResourceType ${safe(stamped)}, previousState.resourceType ${safe(recorded)})`
|
|
41247
|
+
};
|
|
41248
|
+
const oldType = stamped ?? recorded;
|
|
41249
|
+
if (oldType === void 0) return {
|
|
41250
|
+
ok: false,
|
|
41251
|
+
reason: "the journal does not record the old resource's type"
|
|
41252
|
+
};
|
|
41253
|
+
if (oldType !== op.resourceType && (oldType === NESTED_STACK_RESOURCE_TYPE || op.resourceType === NESTED_STACK_RESOURCE_TYPE)) return {
|
|
41254
|
+
ok: false,
|
|
41255
|
+
reason: `it is a Type change between ${safe(oldType)} and ${safe(op.resourceType)}, and cdkd does not replace a nested stack with, or by, a single resource`
|
|
41256
|
+
};
|
|
41257
|
+
return {
|
|
41258
|
+
ok: true,
|
|
41259
|
+
oldType
|
|
41260
|
+
};
|
|
41261
|
+
}
|
|
41262
|
+
/**
|
|
41263
|
+
* The one refusal both the `refuse-replacement-routing` arm and the
|
|
41264
|
+
* `reverse-replacement` arm's own guard raise. `markNonRetryable`: the verdict
|
|
41265
|
+
* is read off the journal alone, so no retry can change it.
|
|
41266
|
+
*/
|
|
41267
|
+
function unroutableReplacementError(op, reason) {
|
|
41268
|
+
return markNonRetryable(new CdkdError(`Cannot reverse the replacement of ${safe(op.logicalId)} (${safe(op.resourceType)}): ${reason}, so cdkd will not guess which provider re-creates the old resource. Nothing was changed. The journal is kept: fix forward with \`cdkd deploy\`, or re-run with ` + (typeof op.logicalId === "string" && PASTEABLE_LOGICAL_ID.test(op.logicalId) ? `\`cdkd rollback --orphan ${op.logicalId}\`` : `\`cdkd rollback --orphan <id>\` (read the id from \`cdkd events\`)`) + ` to leave this resource as it is and let the rest of the rollback proceed.`, "ROLLBACK_REPLACEMENT_UNROUTABLE"));
|
|
41269
|
+
}
|
|
41270
|
+
/**
|
|
41271
|
+
* True when the op changed the resource's `Type`. `false` when the old type is
|
|
41272
|
+
* not recorded at all: that shape is refused by {@link resolveReplacementOldType}
|
|
41273
|
+
* where it matters, and must not by itself turn an in-place op into a
|
|
41274
|
+
* replacement.
|
|
41275
|
+
*/
|
|
41276
|
+
function isTypeChangeOp(op) {
|
|
41277
|
+
const { stamped, recorded } = journaledOldTypes(op);
|
|
41278
|
+
return stamped !== void 0 && stamped !== op.resourceType || recorded !== void 0 && recorded !== op.resourceType;
|
|
41279
|
+
}
|
|
41280
|
+
/**
|
|
41281
|
+
* True when the op recorded a replacement: the old physical id differs from the
|
|
41282
|
+
* new one, OR the resource's `Type` changed (issue #2668). The second arm is
|
|
41283
|
+
* not redundant — two types' physical-id namespaces can overlap (a log group
|
|
41284
|
+
* and a Lambda function are both addressed by a bare name), so a Type change
|
|
41285
|
+
* can keep the id, and classified as an in-place `revert` it would hand the
|
|
41286
|
+
* NEW resource's id to an `update()` of either type. The old physical resource
|
|
41287
|
+
* is already gone / orphaned, so an in-place revert is best-effort — the plan
|
|
41288
|
+
* labels these explicitly.
|
|
40934
41289
|
*/
|
|
40935
41290
|
function isReplacementOp(op) {
|
|
40936
|
-
return op.changeType === "UPDATE" && op.previousState?.physicalId !== void 0 && op.previousState.physicalId !== op.physicalId;
|
|
41291
|
+
return op.changeType === "UPDATE" && op.previousState?.physicalId !== void 0 && (op.previousState.physicalId !== op.physicalId || isTypeChangeOp(op));
|
|
40937
41292
|
}
|
|
40938
41293
|
function deepEqual(a, b) {
|
|
40939
41294
|
if (a === b) return true;
|
|
@@ -40976,11 +41331,15 @@ function classifyRollbackOp(op, stateResources, orphanLogicalIds) {
|
|
|
40976
41331
|
const current = stateResources[op.logicalId];
|
|
40977
41332
|
if (!current) return "skip-absent";
|
|
40978
41333
|
if (replacement) {
|
|
40979
|
-
|
|
41334
|
+
const oldTypes = journaledOldTypes(op);
|
|
41335
|
+
const oldTypeForCompare = isTypeChangeOp(op) ? oldTypes.stamped ?? oldTypes.recorded : void 0;
|
|
41336
|
+
const sameTypeAsOld = oldTypeForCompare === void 0 || !nonEmptyString(current.resourceType) || current.resourceType === oldTypeForCompare;
|
|
41337
|
+
if (current.physicalId === op.previousState.physicalId && sameTypeAsOld) return "skip-already-done";
|
|
40980
41338
|
if (op.physicalId !== void 0 && current.physicalId !== op.physicalId) {
|
|
40981
|
-
if (deepEqual(current.properties, op.previousState.properties)) return "skip-already-done";
|
|
41339
|
+
if (sameTypeAsOld && deepEqual(current.properties, op.previousState.properties)) return "skip-already-done";
|
|
40982
41340
|
return "skip-mismatch";
|
|
40983
41341
|
}
|
|
41342
|
+
if (!resolveReplacementOldType(op).ok) return "refuse-replacement-routing";
|
|
40984
41343
|
return op.oldResourceRetained ?? op.previousState.updateReplacePolicy === "Retain" ? "reverse-replacement-readopt" : "reverse-replacement";
|
|
40985
41344
|
}
|
|
40986
41345
|
if (op.previousState && deepEqual(current.properties, op.previousState.properties)) return "skip-already-done";
|
|
@@ -41004,6 +41363,7 @@ function classifyFailedOp(op, stateResources) {
|
|
|
41004
41363
|
return "delete-failed-create";
|
|
41005
41364
|
}
|
|
41006
41365
|
if (!current || !op.previousState) return "skip-failed-absent";
|
|
41366
|
+
if (isTypeChangeOp(op)) return "skip-failed-type-change";
|
|
41007
41367
|
return "revert-failed-update";
|
|
41008
41368
|
}
|
|
41009
41369
|
/** Build the plan items for a segment's failed ops (issue #1198). */
|
|
@@ -41625,6 +41985,10 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
|
|
|
41625
41985
|
logger.warn(` Rollback: Cannot restore ${safe(op.logicalId)} — resource no longer in state, skipping`);
|
|
41626
41986
|
result.warnings++;
|
|
41627
41987
|
return;
|
|
41988
|
+
case "refuse-replacement-routing": {
|
|
41989
|
+
const routing = resolveReplacementOldType(op);
|
|
41990
|
+
throw unroutableReplacementError(op, routing.ok ? "its old type could not be routed" : routing.reason);
|
|
41991
|
+
}
|
|
41628
41992
|
case "orphan-flag":
|
|
41629
41993
|
if (op.changeType === "CREATE") {
|
|
41630
41994
|
const record = stateResources[op.logicalId];
|
|
@@ -41770,6 +42134,10 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
|
|
|
41770
42134
|
case "reverse-replacement": {
|
|
41771
42135
|
const current = stateResources[op.logicalId];
|
|
41772
42136
|
const prev = op.previousState;
|
|
42137
|
+
const oldTypeRouting = resolveReplacementOldType(op);
|
|
42138
|
+
if (!oldTypeRouting.ok) throw unroutableReplacementError(op, oldTypeRouting.reason);
|
|
42139
|
+
const oldType = oldTypeRouting.oldType;
|
|
42140
|
+
const typeChanged = oldType !== op.resourceType;
|
|
41773
42141
|
if (!requireRestorableBaseline(prev.properties, logger, {
|
|
41774
42142
|
logicalId: op.logicalId,
|
|
41775
42143
|
consequence: "create a default-configured resource and then delete the live one",
|
|
@@ -41781,14 +42149,14 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
|
|
|
41781
42149
|
}
|
|
41782
42150
|
const resolvedPrevProps = await resolveReplayProps(prev.properties, resolver, secrets, ctx, op.logicalId) ?? {};
|
|
41783
42151
|
refuseMaskedReplayBaseline(resolvedPrevProps, op.logicalId);
|
|
41784
|
-
recordNestedStackParameterExpressions(secrets,
|
|
41785
|
-
logger.info(` Rollback: Reversing replacement of ${safe(op.logicalId)} (${safe(op.resourceType)}) — re-creating the old resource and deleting the new one`);
|
|
41786
|
-
if (STATEFUL_TYPES.has(
|
|
42152
|
+
recordNestedStackParameterExpressions(secrets, oldType, resolvedPrevProps, prev.properties, STATE_DERIVED_RULES);
|
|
42153
|
+
logger.info(` Rollback: Reversing replacement of ${safe(op.logicalId)} (${typeChanged ? `${safe(op.resourceType)} -> ${safe(oldType)}` : safe(op.resourceType)}) — re-creating the old resource and deleting the new one`);
|
|
42154
|
+
if (STATEFUL_TYPES.has(oldType)) logger.warn(` ⚠ ${safe(op.logicalId)} (${safe(oldType)}) is a stateful type — the old physical resource's data was destroyed by the replacement and is NOT recovered by this rollback; the re-created resource starts empty.`);
|
|
41787
42155
|
const { provider: createProvider, provisionedBy: createProvisionedBy } = ctx.providerRegistry.getProviderFor({
|
|
41788
|
-
resourceType:
|
|
42156
|
+
resourceType: oldType,
|
|
41789
42157
|
provisionedBy: prev.provisionedBy
|
|
41790
42158
|
});
|
|
41791
|
-
const replayCreateProps = () => ({ ...createProvisionedBy === "cc-api" ? applyDefaultNameForFallback(op.logicalId,
|
|
42159
|
+
const replayCreateProps = () => ({ ...createProvisionedBy === "cc-api" ? applyDefaultNameForFallback(op.logicalId, oldType, resolvedPrevProps) : resolvedPrevProps });
|
|
41792
42160
|
const resolveNewDeleteProvider = () => ctx.providerRegistry.getProviderFor({
|
|
41793
42161
|
resourceType: op.resourceType,
|
|
41794
42162
|
provisionedBy: current.provisionedBy ?? op.provisionedBy
|
|
@@ -41796,7 +42164,7 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
|
|
|
41796
42164
|
let deletedNewFirst = false;
|
|
41797
42165
|
let createResult;
|
|
41798
42166
|
try {
|
|
41799
|
-
createResult = await createWithRollbackRetry(createProvider, () => withCurrentResourceSecrets(secrets, () => createProvider.create(op.logicalId,
|
|
42167
|
+
createResult = await createWithRollbackRetry(createProvider, () => withCurrentResourceSecrets(secrets, () => createProvider.create(op.logicalId, oldType, replayCreateProps(), replayingStateCreateContext(secrets))), op.logicalId, logger, isInterrupted, secrets, {
|
|
41800
42168
|
isRetryable: isNameCooldownError,
|
|
41801
42169
|
interruptedMessage: "Rollback interrupted while waiting out the name cooldown"
|
|
41802
42170
|
});
|
|
@@ -41804,7 +42172,7 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
|
|
|
41804
42172
|
const msg = createError instanceof Error ? createError.message : String(createError);
|
|
41805
42173
|
if (!isNameCollisionErrorFrom(createError, op.logicalId)) throw createError;
|
|
41806
42174
|
if (rollbackRetainsNewResource(current)) throw markNonRetryable(new CdkdError(maskSecretsInText(`Cannot reverse the replacement of ${safe(op.logicalId)} (${safe(op.resourceType)}): the re-create of the old resource (${safe(prev.physicalId)}) collided with the name still held by the new one (${safe(current.physicalId)}), and UpdateReplacePolicy: Retain pins that new resource in place, so cdkd will not delete it to free the name. Delete the new resource yourself, or remove UpdateReplacePolicy: Retain, then re-run \`cdkd rollback\` — the journal is kept, so the revert resumes from here. To leave THIS resource alone and let the rest of the rollback proceed, re-run with ` + (typeof op.logicalId === "string" && PASTEABLE_LOGICAL_ID.test(op.logicalId) ? `\`cdkd rollback --orphan ${op.logicalId}\`: one op failure stops the ` : "`cdkd rollback --orphan <id>` (the id is withheld: it is not a plain CloudFormation logical id, so a pasted command could be reshaped by the shell or name a different resource -- read it from `cdkd events`): one op failure stops the ") + `segment loop, so a single pinned resource otherwise halts every OLDER segment too. Underlying collision: ${displaySafe(msg)}`, secrets), "NAMED_REPLACEMENT_COLLISION", maskSecretsInError(createError instanceof Error ? createError : void 0, secrets)));
|
|
41807
|
-
logger.info(` Rollback: re-create collided with the new resource's name — deleting the new resource (${displaySafe(current.physicalId)}) first...`);
|
|
42175
|
+
logger.info(` Rollback: re-create collided with the new resource's name — deleting the new resource (${displaySafe(current.physicalId)}) first...` + (typeChanged ? ` (this op changed the resource's Type, ${safe(op.resourceType)} -> ${safe(oldType)}: if those types do not share a name space the name is held by an unrelated resource and the re-create will collide again)` : ""));
|
|
41808
42176
|
{
|
|
41809
42177
|
const finalSnapshotIdentifier = rollbackFinalSnapshotId(op.resourceType, current, op.provisionedBy);
|
|
41810
42178
|
throwIfDeleteSkipped(await resolveNewDeleteProvider().delete(op.logicalId, current.physicalId, op.resourceType, current.properties, {
|
|
@@ -41816,7 +42184,7 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
|
|
|
41816
42184
|
delete stateResources[op.logicalId];
|
|
41817
42185
|
await afterOp?.(op.logicalId);
|
|
41818
42186
|
try {
|
|
41819
|
-
createResult = await createWithRollbackRetry(createProvider, () => withCurrentResourceSecrets(secrets, () => createProvider.create(op.logicalId,
|
|
42187
|
+
createResult = await createWithRollbackRetry(createProvider, () => withCurrentResourceSecrets(secrets, () => createProvider.create(op.logicalId, oldType, replayCreateProps(), replayingStateCreateContext(secrets))), op.logicalId, logger, isInterrupted, secrets, {
|
|
41820
42188
|
isRetryable: isRecreateRetryableError,
|
|
41821
42189
|
interruptedMessage: "Rollback interrupted while waiting for the old name to release"
|
|
41822
42190
|
});
|
|
@@ -41824,7 +42192,11 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
|
|
|
41824
42192
|
throw new Error(maskSecretsInText(`Failed to re-create the old ${safe(op.logicalId)} after the new resource (${current.physicalId}) was already deleted: ${displaySafe(recreateError instanceof Error ? recreateError.message : String(recreateError))}. The resource is now absent — fix forward with 'cdkd deploy'.`, secrets), { cause: maskSecretsInError(recreateError instanceof Error ? recreateError : void 0, secrets) });
|
|
41825
42193
|
}
|
|
41826
42194
|
}
|
|
41827
|
-
const adoptedLiveNewResource =
|
|
42195
|
+
const adoptedLiveNewResource = equalIdNamesSameResource({
|
|
42196
|
+
oldType,
|
|
42197
|
+
newType: op.resourceType,
|
|
42198
|
+
createLayer: createProvisionedBy
|
|
42199
|
+
}) && !deletedNewFirst && createResult.physicalId === current.physicalId;
|
|
41828
42200
|
if (adoptedLiveNewResource) {
|
|
41829
42201
|
logger.warn(` ⚠ ${safe(op.logicalId)} (${safe(op.resourceType)}): the re-create returned the LIVE new resource (${displaySafe(current.physicalId)}) instead of re-creating the old one — its Create API is name-idempotent and the new resource still holds the same user-supplied name. Skipping the delete-new step (it would delete that very resource). The old resource's ORIGINAL properties may NOT have been re-applied; state now records the pre-replacement properties, so run 'cdkd drift ${stackName}' to inspect and 'cdkd deploy' to reconcile, or rename the resource to make the replacement reversible.`);
|
|
41830
42202
|
result.warnings++;
|
|
@@ -42004,6 +42376,10 @@ async function replayFailedOperations(failedOps, stateResources, stackName, ctx,
|
|
|
42004
42376
|
logger.warn(` Rollback: cannot revert failed UPDATE of ${safe(op.logicalId)} — no previous state available, skipping`);
|
|
42005
42377
|
result.warnings++;
|
|
42006
42378
|
break;
|
|
42379
|
+
case "skip-failed-type-change":
|
|
42380
|
+
logger.warn(` Rollback: cannot revert failed UPDATE of ${safe(op.logicalId)} in place — it was a Type change (${safe(op.previousState?.resourceType)} -> ${safe(op.resourceType)}), which is a replacement, and its remote state is unknown. Inspect it with \`cdkd drift\` and re-converge with \`cdkd deploy\`. Skipping.`);
|
|
42381
|
+
result.warnings++;
|
|
42382
|
+
break;
|
|
42007
42383
|
case "orphan-failed-create-retain": {
|
|
42008
42384
|
const failedCreateRecord = stateResources[op.logicalId];
|
|
42009
42385
|
const orphanProvisionedBy = effectiveProvisionedBy(failedCreateRecord, op.provisionedBy);
|
|
@@ -43110,6 +43486,40 @@ var DeployEngine = class DeployEngine {
|
|
|
43110
43486
|
*/
|
|
43111
43487
|
retainedOldOnReplacement = /* @__PURE__ */ new Set();
|
|
43112
43488
|
/**
|
|
43489
|
+
* The pre-deploy state records, as loaded — the #1852 heal's eligibility
|
|
43490
|
+
* baseline. A record is healed only while it is still the one this deploy
|
|
43491
|
+
* LOADED (same physical id, same `attributes` object): once a provider has
|
|
43492
|
+
* (re)written it this run, a missing attribute is the provider's answer, not
|
|
43493
|
+
* staleness, and re-reading would cost an AWS call to learn nothing.
|
|
43494
|
+
*
|
|
43495
|
+
* Compared by the `attributes` REFERENCE rather than the record's, because
|
|
43496
|
+
* the metadata-only arm re-spreads a record (`{ ...currentResource }`) without
|
|
43497
|
+
* a provider call — its attributes object survives the spread, a create /
|
|
43498
|
+
* update result's does not.
|
|
43499
|
+
*/
|
|
43500
|
+
healBaseline = {};
|
|
43501
|
+
/**
|
|
43502
|
+
* Single-flight + per-deploy memo of the #1852 heal, keyed by logical id and
|
|
43503
|
+
* physical id. ENGINE-instance state, never module-global: one engine deploys
|
|
43504
|
+
* one stack, and `--stack-concurrency` runs several engines at once. Holding
|
|
43505
|
+
* the PROMISE is what makes it single-flight — every concurrent resolution of
|
|
43506
|
+
* the same record awaits the one read — and never deleting an entry is what
|
|
43507
|
+
* bounds it: one read per record per deploy, success or failure, no retry.
|
|
43508
|
+
*/
|
|
43509
|
+
attributeHeals = /* @__PURE__ */ new Map();
|
|
43510
|
+
/**
|
|
43511
|
+
* What the heals of this deploy read, waiting for the next state save.
|
|
43512
|
+
*
|
|
43513
|
+
* Applied at {@link redactStateForPersist} — the choke point EVERY save
|
|
43514
|
+
* passes through — rather than written into the in-memory record, so a heal
|
|
43515
|
+
* survives every exit path that saves at all (success, a failed resource, a
|
|
43516
|
+
* failed output, the no-change path), whichever copy-on-write record map the
|
|
43517
|
+
* save happens to hold, and is scrubbed by the same redaction every other
|
|
43518
|
+
* attribute goes through. A path that saves NOTHING (`--dry-run`) persists
|
|
43519
|
+
* nothing and the next deploy re-heals.
|
|
43520
|
+
*/
|
|
43521
|
+
healedAttributes = /* @__PURE__ */ new Map();
|
|
43522
|
+
/**
|
|
43113
43523
|
* Target region for this stack. Required — load-bearing for the
|
|
43114
43524
|
* region-prefixed S3 state key and recorded in state.json for
|
|
43115
43525
|
* cross-region destroy.
|
|
@@ -43153,6 +43563,9 @@ var DeployEngine = class DeployEngine {
|
|
|
43153
43563
|
this.outputsSourceUsable = true;
|
|
43154
43564
|
this.skippedOutputs = void 0;
|
|
43155
43565
|
this.retainedOldOnReplacement = /* @__PURE__ */ new Set();
|
|
43566
|
+
this.healBaseline = {};
|
|
43567
|
+
this.attributeHeals = /* @__PURE__ */ new Map();
|
|
43568
|
+
this.healedAttributes = /* @__PURE__ */ new Map();
|
|
43156
43569
|
this.resolver.resetPhysicalIdFallbackCount();
|
|
43157
43570
|
return withStackName(stackName, () => this.doDeploy(stackName, template));
|
|
43158
43571
|
}
|
|
@@ -43175,6 +43588,7 @@ var DeployEngine = class DeployEngine {
|
|
|
43175
43588
|
...this.exportIndexStore && { exportIndex: this.exportIndexStore },
|
|
43176
43589
|
recordedImports: this.recordedImports,
|
|
43177
43590
|
recordedOutputReads: this.recordedOutputReads,
|
|
43591
|
+
attributeHealer: (logicalId, resource) => this.healStaleAttributes(logicalId, resource, stackName),
|
|
43178
43592
|
...this.options.inheritedSecrets && this.options.inheritedSecrets.size > 0 && { inheritedSecrets: this.options.inheritedSecrets },
|
|
43179
43593
|
recordedSecretValues,
|
|
43180
43594
|
noEchoAttributeResources: this.noEchoAttributeResources,
|
|
@@ -43618,12 +44032,111 @@ var DeployEngine = class DeployEngine {
|
|
|
43618
44032
|
return next;
|
|
43619
44033
|
});
|
|
43620
44034
|
}
|
|
44035
|
+
/**
|
|
44036
|
+
* Re-read a STALE record's attributes from AWS (issue
|
|
44037
|
+
* [#1852](https://github.com/go-to-k/cdkd/issues/1852)) — the resolver calls
|
|
44038
|
+
* this, through `ResolverContext.attributeHealer`, only when `Fn::GetAtt` is
|
|
44039
|
+
* about to take the physical-id fallback.
|
|
44040
|
+
*
|
|
44041
|
+
* The read is the provider's `import()` with `knownPhysicalId` — the same
|
|
44042
|
+
* primitive `orphan-adoption.ts` verifies a record with. It is READ-ONLY by
|
|
44043
|
+
* contract ("verify the resource exists and fetch attributes, do NOT
|
|
44044
|
+
* search"), it returns the map `create()` records (so a healed record looks
|
|
44045
|
+
* like a fresh one), and it is routed by the record's own `resourceType` +
|
|
44046
|
+
* `provisionedBy`, so a Cloud-Control-routed record is read through Cloud
|
|
44047
|
+
* Control. `getAttribute()` was the alternative and is implemented by about
|
|
44048
|
+
* half the providers — neither of the two types this issue names.
|
|
44049
|
+
*
|
|
44050
|
+
* NEVER throws and never retries: every failure is an outcome the resolver
|
|
44051
|
+
* words its refusal from, and the memo makes one read per record per deploy
|
|
44052
|
+
* the ceiling.
|
|
44053
|
+
*/
|
|
44054
|
+
healStaleAttributes(logicalId, resource, stackName) {
|
|
44055
|
+
if (!this.isHealEligible(logicalId, resource)) return Promise.resolve({ kind: "not-attempted" });
|
|
44056
|
+
const key = `${logicalId}\u0000${resource.physicalId}`;
|
|
44057
|
+
const inFlight = this.attributeHeals.get(key);
|
|
44058
|
+
if (inFlight) return inFlight;
|
|
44059
|
+
const heal = this.readStaleAttributes(logicalId, resource, stackName).catch((error) => ({
|
|
44060
|
+
kind: "failed",
|
|
44061
|
+
error
|
|
44062
|
+
}));
|
|
44063
|
+
this.attributeHeals.set(key, heal);
|
|
44064
|
+
return heal;
|
|
44065
|
+
}
|
|
44066
|
+
/**
|
|
44067
|
+
* Is `resource` still the record this deploy LOADED — same physical id, same
|
|
44068
|
+
* `attributes` object — and of a type whose attributes are an AWS read-back?
|
|
44069
|
+
* See {@link healBaseline}. Asked by the read AND by the persist merge: a
|
|
44070
|
+
* read taken before this deploy rewrote the record must reach neither a later
|
|
44071
|
+
* resolution nor the rewritten record.
|
|
44072
|
+
*/
|
|
44073
|
+
isHealEligible(logicalId, resource) {
|
|
44074
|
+
const loaded = Object.hasOwn(this.healBaseline, logicalId) ? this.healBaseline[logicalId] : void 0;
|
|
44075
|
+
return loaded !== void 0 && loaded.physicalId === resource.physicalId && loaded.attributes === resource.attributes && !isHealExcludedType(resource.resourceType);
|
|
44076
|
+
}
|
|
44077
|
+
async readStaleAttributes(logicalId, resource, stackName) {
|
|
44078
|
+
const { provider } = this.providerRegistry.getProviderFor({
|
|
44079
|
+
resourceType: resource.resourceType,
|
|
44080
|
+
properties: resource.properties,
|
|
44081
|
+
provisionedBy: resource.provisionedBy
|
|
44082
|
+
});
|
|
44083
|
+
if (!provider.import) return { kind: "not-attempted" };
|
|
44084
|
+
const found = await provider.import({
|
|
44085
|
+
logicalId,
|
|
44086
|
+
resourceType: resource.resourceType,
|
|
44087
|
+
stackName,
|
|
44088
|
+
region: this.stackRegion,
|
|
44089
|
+
properties: resource.properties,
|
|
44090
|
+
knownPhysicalId: resource.physicalId
|
|
44091
|
+
});
|
|
44092
|
+
if (found === null) return { kind: "not-found" };
|
|
44093
|
+
if (found.physicalId !== resource.physicalId) throw new Error(`the provider answered for a different resource (${found.physicalId}) than the one asked about (${resource.physicalId})`);
|
|
44094
|
+
const reported = Object.entries(normalizeHealedAttributes(found.attributes));
|
|
44095
|
+
const attributes = Object.fromEntries(reported.filter(([, value]) => !carriesSecretMask(value)));
|
|
44096
|
+
const withheldKeys = reported.filter(([, value]) => carriesSecretMask(value)).map(([key]) => key);
|
|
44097
|
+
if (Object.keys(attributes).length > 0) this.healedAttributes.set(logicalId, {
|
|
44098
|
+
physicalId: resource.physicalId,
|
|
44099
|
+
resourceType: resource.resourceType,
|
|
44100
|
+
attributes
|
|
44101
|
+
});
|
|
44102
|
+
this.logger.debug(`Re-read the attributes of ${displaySafe(logicalId)} (${displaySafe(resource.resourceType)}) from AWS — its state record lacked one a Fn::GetAtt asked for (#1852): ${Object.keys(attributes).length} attribute(s) read`);
|
|
44103
|
+
return {
|
|
44104
|
+
kind: "read",
|
|
44105
|
+
attributes,
|
|
44106
|
+
...withheldKeys.length > 0 && { withheldKeys }
|
|
44107
|
+
};
|
|
44108
|
+
}
|
|
44109
|
+
/**
|
|
44110
|
+
* The record to persist for `logicalId`: `record` itself, or a copy whose
|
|
44111
|
+
* `attributes` gained what this deploy's heal read. MERGED, never replaced —
|
|
44112
|
+
* only `attributes` is touched, and within it only keys the record does not
|
|
44113
|
+
* hold (or holds as a pre-#1681 placeholder ARN). Skipped when the record no
|
|
44114
|
+
* longer describes the resource that was read: a replacement or a Type change
|
|
44115
|
+
* this deploy made.
|
|
44116
|
+
*/
|
|
44117
|
+
withHealedAttributes(logicalId, record) {
|
|
44118
|
+
const healed = this.healedAttributes.get(logicalId);
|
|
44119
|
+
if (healed === void 0 || healed.physicalId !== record.physicalId || healed.resourceType !== record.resourceType || !this.isHealEligible(logicalId, record)) return record;
|
|
44120
|
+
const merged = mergeHealedAttributes(record.attributes, healed.attributes, (key, value) => isStalePlaceholderArnAttribute(record.resourceType, key, value));
|
|
44121
|
+
return merged === record.attributes || merged === void 0 ? record : {
|
|
44122
|
+
...record,
|
|
44123
|
+
attributes: merged
|
|
44124
|
+
};
|
|
44125
|
+
}
|
|
44126
|
+
/** Would a save of `resources` persist something a heal read? The no-change path's trigger. */
|
|
44127
|
+
hasUnpersistedHeals(resources) {
|
|
44128
|
+
for (const logicalId of this.healedAttributes.keys()) {
|
|
44129
|
+
const record = Object.hasOwn(resources, logicalId) ? resources[logicalId] : void 0;
|
|
44130
|
+
if (record !== void 0 && this.withHealedAttributes(logicalId, record) !== record) return true;
|
|
44131
|
+
}
|
|
44132
|
+
return false;
|
|
44133
|
+
}
|
|
43621
44134
|
redactStateForPersist(state) {
|
|
43622
44135
|
const resources = {};
|
|
43623
44136
|
for (const [logicalId, record] of Object.entries(state.resources)) {
|
|
43624
44137
|
const secrets = this.perResourceSecrets.get(logicalId);
|
|
43625
44138
|
const templateProps = this.perResourceTemplateProps.get(logicalId);
|
|
43626
|
-
resources[logicalId] = scrubResourceRecord(record, secrets ?? /* @__PURE__ */ new Map(), templateProps);
|
|
44139
|
+
resources[logicalId] = scrubResourceRecord(this.withHealedAttributes(logicalId, record), secrets ?? /* @__PURE__ */ new Map(), templateProps);
|
|
43627
44140
|
}
|
|
43628
44141
|
const orphans = state.orphans?.map((entry) => {
|
|
43629
44142
|
const sameResource = this.perResourceResolvedType.get(entry.logicalId) === entry.state.resourceType;
|
|
@@ -43975,6 +44488,7 @@ var DeployEngine = class DeployEngine {
|
|
|
43975
44488
|
lastModified: Date.now()
|
|
43976
44489
|
};
|
|
43977
44490
|
const currentEtag = currentStateData?.etag;
|
|
44491
|
+
this.healBaseline = currentState.resources ?? {};
|
|
43978
44492
|
refuseMalformedOutputs(currentState, stackName, this.stackRegion);
|
|
43979
44493
|
refuseMalformedResourcesForDeploy(currentState, stackName, this.stackRegion);
|
|
43980
44494
|
const migrationPending = currentStateData?.migrationPending ?? false;
|
|
@@ -44067,7 +44581,8 @@ var DeployEngine = class DeployEngine {
|
|
|
44067
44581
|
if (merge?.kind === "kept" && !outputMapsEqual(persistedOutputs, resolvedOutputs)) this.logger.warn(`Outputs changed but one or more could not be resolved; keeping the previously persisted outputs. ${keptWholeReasonText(merge.reason)} A downstream Fn::ImportValue may fail until the next deploy.`);
|
|
44068
44582
|
else if (merge?.kind === "merged" && merge.carriedKeys.length > 0) this.logger.debug(`Kept the previously persisted value of ${merge.carriedKeys.length} output key(s), carried export aliases included, that could not be resolved (no-change path, #2771)`);
|
|
44069
44583
|
const skippedOutputsChanged = !skippedOutputsEqual(currentState.skippedOutputs, this.skippedOutputs);
|
|
44070
|
-
|
|
44584
|
+
const healedAttributesPending = this.hasUnpersistedHeals(currentState.resources);
|
|
44585
|
+
if (observedRefresh || outputsChanged || exportSetChanged || skippedOutputsChanged || orphansChanged || healedAttributesPending) try {
|
|
44071
44586
|
const refreshedState = {
|
|
44072
44587
|
version: 10,
|
|
44073
44588
|
region: this.stackRegion,
|
|
@@ -44093,6 +44608,7 @@ var DeployEngine = class DeployEngine {
|
|
|
44093
44608
|
outputs: this.redactOutputs(refreshedState.outputs)
|
|
44094
44609
|
}));
|
|
44095
44610
|
} else if (observedRefresh) this.logger.debug("Persisted refreshed observedProperties (no-change path)");
|
|
44611
|
+
else if (healedAttributesPending) this.logger.debug("Persisted attributes re-read from AWS for a stale record (no-change path, #1852)");
|
|
44096
44612
|
else this.logger.debug("Persisted skipped-outputs record (no outputs-value diff, no-change path, #2740)");
|
|
44097
44613
|
} catch (saveError) {
|
|
44098
44614
|
this.logger.warn(`Failed to persist no-change state update: ${saveError instanceof Error ? saveError.message : String(saveError)} — drift baseline / outputs will be re-resolved on next deploy.`);
|
|
@@ -44275,7 +44791,8 @@ var DeployEngine = class DeployEngine {
|
|
|
44275
44791
|
previousState,
|
|
44276
44792
|
physicalId: newResources[logicalId]?.physicalId,
|
|
44277
44793
|
properties: newResources[logicalId]?.properties,
|
|
44278
|
-
...change.changeType === "UPDATE" && { oldResourceRetained: this.retainedOldOnReplacement.has(logicalId) }
|
|
44794
|
+
...change.changeType === "UPDATE" && { oldResourceRetained: this.retainedOldOnReplacement.has(logicalId) },
|
|
44795
|
+
...change.changeType === "UPDATE" && previousState !== void 0 && { previousResourceType: previousState.resourceType }
|
|
44279
44796
|
});
|
|
44280
44797
|
saveStateAfterResource(logicalId);
|
|
44281
44798
|
}, () => this.interrupted);
|
|
@@ -44656,7 +45173,8 @@ var DeployEngine = class DeployEngine {
|
|
|
44656
45173
|
const resourceType = change.resourceType;
|
|
44657
45174
|
const renderer = getLiveRenderer();
|
|
44658
45175
|
const labelRecreateDirection = this.recreateDirectionFor(stackName, logicalId);
|
|
44659
|
-
const
|
|
45176
|
+
const labelRecordedType = stateResources[logicalId]?.resourceType;
|
|
45177
|
+
const needsReplacement = change.changeType === "UPDATE" && ((change.propertyChanges?.some((pc) => pc.requiresReplacement) ?? false) || labelRecordedType !== void 0 && labelRecordedType !== resourceType) || labelRecreateDirection !== void 0;
|
|
44660
45178
|
const verb = change.changeType === "CREATE" ? "Creating" : change.changeType === "DELETE" ? "Deleting" : needsReplacement ? "Replacing" : "Updating";
|
|
44661
45179
|
const labelRouting = this.peekRoutingForLabel(change, stateResources[logicalId], stackName, logicalId, needsReplacement, labelRecreateDirection);
|
|
44662
45180
|
const baseLabel = `${verb} ${logicalId} (${resourceType})${labelRouting === "cc-api" ? " [CC API]" : ""}`;
|
|
@@ -44883,12 +45401,12 @@ var DeployEngine = class DeployEngine {
|
|
|
44883
45401
|
* #960 follow-up) and the name-idempotent same-id guard (issue #1238) so
|
|
44884
45402
|
* the two --replace escape hatches cannot drift apart.
|
|
44885
45403
|
*/
|
|
44886
|
-
async replaceDeleteFirstAndRecreate(logicalId, resourceType, currentResource, oldDeleteProvider, replaceProvider, replaceProps, secrets, updateReplacePolicy) {
|
|
45404
|
+
async replaceDeleteFirstAndRecreate(logicalId, resourceType, oldResourceType, currentResource, oldDeleteProvider, replaceProvider, replaceProps, secrets, updateReplacePolicy) {
|
|
44887
45405
|
const createContext = { maskSecrets: createSecretMasker(secrets) };
|
|
44888
|
-
const finalSnapshotIdentifier = await this.prepareFinalSnapshotForDelete(logicalId,
|
|
45406
|
+
const finalSnapshotIdentifier = await this.prepareFinalSnapshotForDelete(logicalId, oldResourceType, currentResource, updateReplacePolicy);
|
|
44889
45407
|
let deleteResult;
|
|
44890
45408
|
try {
|
|
44891
|
-
deleteResult = await oldDeleteProvider.delete(logicalId, currentResource.physicalId,
|
|
45409
|
+
deleteResult = await oldDeleteProvider.delete(logicalId, currentResource.physicalId, oldResourceType, currentResource.properties, {
|
|
44892
45410
|
expectedRegion: this.stackRegion,
|
|
44893
45411
|
forceDataDelete: this.options.forceStatefulRecreation === true,
|
|
44894
45412
|
...finalSnapshotIdentifier !== void 0 && { finalSnapshotIdentifier }
|
|
@@ -44974,6 +45492,8 @@ var DeployEngine = class DeployEngine {
|
|
|
44974
45492
|
case "UPDATE": {
|
|
44975
45493
|
const currentResource = existingState;
|
|
44976
45494
|
if (!currentResource) throw new Error(`Cannot update ${logicalId}: resource not found in state`);
|
|
45495
|
+
const oldResourceType = currentResource.resourceType;
|
|
45496
|
+
const typeChanged = oldResourceType !== resourceType;
|
|
44977
45497
|
const desiredProps = change.desiredProperties || {};
|
|
44978
45498
|
const currentProps = change.currentProperties || {};
|
|
44979
45499
|
const currentPropsAsWritten = currentResource.provisionedBy === "cc-api" ? currentProps : withoutSilentDropProperties(resourceType, currentProps);
|
|
@@ -44996,7 +45516,7 @@ var DeployEngine = class DeployEngine {
|
|
|
44996
45516
|
const desiredForSkipCheck = redactSecretsForState(markSameGenerationBag({ ...resolvedProps }), updateSecrets, desiredProps);
|
|
44997
45517
|
const allowedSilentDrops = this.providerRegistry.getAllowedUnsupportedProperties?.();
|
|
44998
45518
|
const desiredForSkipCheckAsWritten = currentResource.provisionedBy !== "cc-api" && allowedSilentDrops ? withoutAcceptedSilentDropProperties(resourceType, desiredForSkipCheck, allowedSilentDrops) : desiredForSkipCheck;
|
|
44999
|
-
if (JSON.stringify(desiredForSkipCheckAsWritten) === JSON.stringify(currentPropsAsWritten)) {
|
|
45519
|
+
if (!typeChanged && JSON.stringify(desiredForSkipCheckAsWritten) === JSON.stringify(currentPropsAsWritten)) {
|
|
45000
45520
|
if (change.attributeChanges && change.attributeChanges.length > 0) {
|
|
45001
45521
|
const attrSummary = change.attributeChanges.map((a) => `${a.attribute}: ${a.oldValue ?? "(unset)"} → ${a.newValue ?? "(unset)"}`).join(", ");
|
|
45002
45522
|
this.logger.info(` ↻ ${logicalId} (${resourceType}) attribute update: ${attrSummary}`);
|
|
@@ -45015,7 +45535,7 @@ var DeployEngine = class DeployEngine {
|
|
|
45015
45535
|
if (counts) counts.skipped++;
|
|
45016
45536
|
break;
|
|
45017
45537
|
}
|
|
45018
|
-
const propertyDrivenReplacement = change.propertyChanges?.some((pc) => pc.requiresReplacement);
|
|
45538
|
+
const propertyDrivenReplacement = typeChanged || change.propertyChanges?.some((pc) => pc.requiresReplacement);
|
|
45019
45539
|
const recreateTargets = this.options.recreateTargets?.stackName === stackName ? this.options.recreateTargets : void 0;
|
|
45020
45540
|
const recreateViaCcApi = recreateTargets?.viaCcApi.has(logicalId) ?? false;
|
|
45021
45541
|
const recreateViaSdkProvider = recreateTargets?.viaSdkProvider.has(logicalId) ?? false;
|
|
@@ -45025,17 +45545,17 @@ var DeployEngine = class DeployEngine {
|
|
|
45025
45545
|
const updateReplacePolicy = template?.Resources?.[logicalId]?.UpdateReplacePolicy;
|
|
45026
45546
|
if (needsReplacement) {
|
|
45027
45547
|
if (propertyDrivenReplacement && !recreateFlagged && updateReplacePolicy !== "Retain") {
|
|
45028
|
-
const statefulReason = isStatefulRecreateTargetForReplace(
|
|
45548
|
+
const statefulReason = isStatefulRecreateTargetForReplace(oldResourceType, currentProps, currentResource.observedProperties);
|
|
45029
45549
|
if (statefulReason && this.options.forceStatefulRecreation !== true) {
|
|
45030
|
-
const immutableProps = change.propertyChanges?.filter((pc) => pc.requiresReplacement).map((pc) => pc.path).join(", ");
|
|
45031
|
-
throw markNonRetryable(new CdkdError(`${logicalId} (${
|
|
45550
|
+
const immutableProps = change.propertyChanges?.filter((pc) => pc.requiresReplacement).map((pc) => pc.path).join(", ") || "Type";
|
|
45551
|
+
throw markNonRetryable(new CdkdError(`${logicalId} (${oldResourceType}) requires replacement (immutable property changed: ${immutableProps}${typeChanged ? `, to ${resourceType}` : ""}) 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 immutable-property change.`, "STATEFUL_REPLACE_BLOCKED"));
|
|
45032
45552
|
}
|
|
45033
45553
|
}
|
|
45034
45554
|
let replacementReason;
|
|
45035
45555
|
if (recreateViaCcApi) replacementReason = "--recreate-via-cc-api flag (mid-life SDK→CC migration)";
|
|
45036
45556
|
else if (recreateViaSdkProvider) replacementReason = "--recreate-via-sdk-provider flag (mid-life CC→SDK migration)";
|
|
45037
45557
|
else replacementReason = `immutable properties changed: ${change.propertyChanges?.filter((pc) => pc.requiresReplacement).map((pc) => pc.path).join(", ")}`;
|
|
45038
|
-
this.logger.info(`Replacing ${logicalId} (${resourceType}) - ${replacementReason}`);
|
|
45558
|
+
this.logger.info(`Replacing ${logicalId} (${typeChanged ? `${oldResourceType} -> ${resourceType}` : resourceType}) - ${replacementReason}`);
|
|
45039
45559
|
const recreateDirectionHint = recreateViaCcApi ? "cc-api" : recreateViaSdkProvider ? "sdk" : void 0;
|
|
45040
45560
|
const replaceDecision = this.providerRegistry.getProviderFor({
|
|
45041
45561
|
resourceType,
|
|
@@ -45046,9 +45566,14 @@ var DeployEngine = class DeployEngine {
|
|
|
45046
45566
|
const replaceProvider = replaceDecision.provider;
|
|
45047
45567
|
const replaceProps = replaceDecision.provisionedBy === "cc-api" ? this.preparePropertiesForCcApi(resourceType, resolvedProps, logicalId) : resolvedProps;
|
|
45048
45568
|
const oldDeleteProvider = this.providerRegistry.getProviderFor({
|
|
45049
|
-
resourceType,
|
|
45569
|
+
resourceType: oldResourceType,
|
|
45050
45570
|
provisionedBy: currentResource.provisionedBy
|
|
45051
45571
|
}).provider;
|
|
45572
|
+
const equalIdIsSameResource = equalIdNamesSameResource({
|
|
45573
|
+
oldType: oldResourceType,
|
|
45574
|
+
newType: resourceType,
|
|
45575
|
+
createLayer: replaceDecision.provisionedBy
|
|
45576
|
+
});
|
|
45052
45577
|
let createResult;
|
|
45053
45578
|
if (recreateFlagged) {
|
|
45054
45579
|
const recreateFlagName = recreateViaCcApi ? "--recreate-via-cc-api" : "--recreate-via-sdk-provider";
|
|
@@ -45057,10 +45582,10 @@ var DeployEngine = class DeployEngine {
|
|
|
45057
45582
|
this.logger.warn(` ⚠ ${logicalId} has UpdateReplacePolicy: Retain — ${recreateFlagName} will leak the old physical resource (${currentResource.physicalId}). The new resource shares the same name where applicable; if the type has user-supplied names (e.g. functionName, bucketName), the create will deterministically collide with the retained orphan.`);
|
|
45058
45583
|
} else {
|
|
45059
45584
|
this.logger.info(` Destroying old ${logicalId} (${currentResource.physicalId}) before recreate...`);
|
|
45060
|
-
const recreateFinalSnapshotId = await this.prepareFinalSnapshotForDelete(logicalId,
|
|
45585
|
+
const recreateFinalSnapshotId = await this.prepareFinalSnapshotForDelete(logicalId, oldResourceType, currentResource, updateReplacePolicy);
|
|
45061
45586
|
let recreateDeleteResult;
|
|
45062
45587
|
try {
|
|
45063
|
-
recreateDeleteResult = await oldDeleteProvider.delete(logicalId, currentResource.physicalId,
|
|
45588
|
+
recreateDeleteResult = await oldDeleteProvider.delete(logicalId, currentResource.physicalId, oldResourceType, currentResource.properties, {
|
|
45064
45589
|
expectedRegion: this.stackRegion,
|
|
45065
45590
|
forceDataDelete: this.options.forceStatefulRecreation === true,
|
|
45066
45591
|
...recreateFinalSnapshotId !== void 0 && { finalSnapshotIdentifier: recreateFinalSnapshotId }
|
|
@@ -45082,7 +45607,7 @@ var DeployEngine = class DeployEngine {
|
|
|
45082
45607
|
onInterrupted: () => new InterruptedError(this.interruptCause ?? "user"),
|
|
45083
45608
|
isRetryable: isRecreateRetryableError
|
|
45084
45609
|
});
|
|
45085
|
-
if (updateReplacePolicy === "Retain" && createResult.physicalId === currentResource.physicalId) throw new CdkdError(`${logicalId} (${resourceType}) recreate returned the existing resource (${currentResource.physicalId}) instead of creating a new one — its Create API is name-idempotent — and UpdateReplacePolicy: Retain means the old resource was never destroyed, so the new properties were not applied. Rename the resource in your CDK code (or remove the explicit physical name) so the recreate can produce a genuinely new resource.`, "NAMED_REPLACEMENT_IDEMPOTENT_CREATE");
|
|
45610
|
+
if (equalIdIsSameResource && updateReplacePolicy === "Retain" && createResult.physicalId === currentResource.physicalId) throw new CdkdError(`${logicalId} (${resourceType}) recreate returned the existing resource (${currentResource.physicalId}) instead of creating a new one — its Create API is name-idempotent — and UpdateReplacePolicy: Retain means the old resource was never destroyed, so the new properties were not applied. Rename the resource in your CDK code (or remove the explicit physical name) so the recreate can produce a genuinely new resource.`, "NAMED_REPLACEMENT_IDEMPOTENT_CREATE");
|
|
45086
45611
|
} else {
|
|
45087
45612
|
this.logger.info(` Creating new ${logicalId}...`);
|
|
45088
45613
|
let deletedOldFirst = false;
|
|
@@ -45092,19 +45617,20 @@ var DeployEngine = class DeployEngine {
|
|
|
45092
45617
|
const createMsg = createError instanceof Error ? createError.message : String(createError);
|
|
45093
45618
|
if (!isNameCollisionErrorFrom(createError, logicalId)) throw createError;
|
|
45094
45619
|
const nameOrigin = this.replacementNameOrigin(logicalId, currentResource.physicalId);
|
|
45095
|
-
|
|
45096
|
-
if (
|
|
45620
|
+
const typeChangeCollisionNote = typeChanged ? ` Note: this replacement changes the resource's Type (${oldResourceType} -> ${resourceType}). Unless those two types share one name space, the name is held by an unrelated existing ${resourceType}, not by the ${oldResourceType} being replaced — then deleting the old resource first cannot free it, and the fix is a different name.` : "";
|
|
45621
|
+
if (updateReplacePolicy === "Retain") throw new CdkdError(`${logicalId} (${resourceType}) requires replacement, 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.${typeChangeCollisionNote}`, "NAMED_REPLACEMENT_COLLISION");
|
|
45622
|
+
if (this.options.replace !== true) throw new CdkdError(`${logicalId} (${resourceType}) requires replacement, but the create-first attempt collided with the existing resource: ${createMsg}. ${nameOrigin.descriptor}, so the CloudFormation-style safe replacement order (create the new resource before deleting the old) cannot reuse the occupied name — CloudFormation refuses this shape with "cannot update a stack when a custom-named resource requires replacing". ${nameOrigin.remedy}, or re-run with \`cdkd deploy --replace\` to delete the old resource FIRST and recreate it under the same name (the resource is briefly unavailable while it is recreated).${typeChangeCollisionNote}`, "NAMED_REPLACEMENT_COLLISION");
|
|
45097
45623
|
this.logger.info(` Create-first collided with the existing resource's name and --replace is set — deleting old ${logicalId} (${currentResource.physicalId}) first...`);
|
|
45098
45624
|
deletedOldFirst = true;
|
|
45099
|
-
createResult = await this.replaceDeleteFirstAndRecreate(logicalId, resourceType, currentResource, oldDeleteProvider, replaceProvider, replaceProps, updateSecrets, updateReplacePolicy);
|
|
45625
|
+
createResult = await this.replaceDeleteFirstAndRecreate(logicalId, resourceType, oldResourceType, currentResource, oldDeleteProvider, replaceProvider, replaceProps, updateSecrets, updateReplacePolicy);
|
|
45100
45626
|
}
|
|
45101
|
-
if (!deletedOldFirst && createResult.physicalId === currentResource.physicalId) {
|
|
45627
|
+
if (equalIdIsSameResource && !deletedOldFirst && createResult.physicalId === currentResource.physicalId) {
|
|
45102
45628
|
const idempotentNameOrigin = this.replacementNameOrigin(logicalId, currentResource.physicalId);
|
|
45103
45629
|
if (updateReplacePolicy === "Retain") throw new CdkdError(`${logicalId} (${resourceType}) requires replacement, but its Create API is name-idempotent: the create-first attempt returned the existing resource (${currentResource.physicalId}) instead of creating a new one, and UpdateReplacePolicy: Retain pins that resource in place. ${idempotentNameOrigin.descriptor}. ${idempotentNameOrigin.remedy} — with Retain, the old resource keeps the name, so a same-name replacement can never proceed.`, "NAMED_REPLACEMENT_IDEMPOTENT_CREATE");
|
|
45104
45630
|
if (this.options.replace !== true) throw new CdkdError(`${logicalId} (${resourceType}) requires replacement, but its Create API is name-idempotent: the create-first attempt returned the EXISTING resource (${currentResource.physicalId}) instead of creating a new one, so deleting the "old" resource would silently destroy the resource the deploy just reported as created. ${idempotentNameOrigin.descriptor}; ${idempotentNameOrigin.remedy}, or re-run with \`cdkd deploy --replace\` to delete the old resource FIRST and recreate it under the same name (the resource is briefly unavailable while it is recreated). Note: this branch is also reached when the old resource was deleted out-of-band and the physical id is name-derived — there the create was a genuine fresh create; \`--replace\` converges that case too.`, "NAMED_REPLACEMENT_IDEMPOTENT_CREATE");
|
|
45105
45631
|
this.logger.info(` Create-first returned the existing resource (name-idempotent Create API) and --replace is set — deleting old ${logicalId} (${currentResource.physicalId}) first...`);
|
|
45106
45632
|
deletedOldFirst = true;
|
|
45107
|
-
createResult = await this.replaceDeleteFirstAndRecreate(logicalId, resourceType, currentResource, oldDeleteProvider, replaceProvider, replaceProps, updateSecrets, updateReplacePolicy);
|
|
45633
|
+
createResult = await this.replaceDeleteFirstAndRecreate(logicalId, resourceType, oldResourceType, currentResource, oldDeleteProvider, replaceProvider, replaceProps, updateSecrets, updateReplacePolicy);
|
|
45108
45634
|
}
|
|
45109
45635
|
if (deletedOldFirst) {} else if (updateReplacePolicy === "Retain") {
|
|
45110
45636
|
this.retainedOldOnReplacement.add(logicalId);
|
|
@@ -45114,7 +45640,7 @@ var DeployEngine = class DeployEngine {
|
|
|
45114
45640
|
let cleanupFinalSnapshotId;
|
|
45115
45641
|
let snapshotBlockedDelete = false;
|
|
45116
45642
|
try {
|
|
45117
|
-
cleanupFinalSnapshotId = await this.prepareFinalSnapshotForDelete(logicalId,
|
|
45643
|
+
cleanupFinalSnapshotId = await this.prepareFinalSnapshotForDelete(logicalId, oldResourceType, currentResource, updateReplacePolicy);
|
|
45118
45644
|
} catch (snapshotError) {
|
|
45119
45645
|
if (snapshotError instanceof CdkdError && snapshotError.code === "FINAL_SNAPSHOT_UNSUPPORTED") throw snapshotError;
|
|
45120
45646
|
snapshotBlockedDelete = true;
|
|
@@ -45124,7 +45650,7 @@ var DeployEngine = class DeployEngine {
|
|
|
45124
45650
|
let cleanupDeleteResult = void 0;
|
|
45125
45651
|
let cleanupDeleteFailed = false;
|
|
45126
45652
|
try {
|
|
45127
|
-
cleanupDeleteResult = await oldDeleteProvider.delete(logicalId, currentResource.physicalId,
|
|
45653
|
+
cleanupDeleteResult = await oldDeleteProvider.delete(logicalId, currentResource.physicalId, oldResourceType, currentResource.properties, {
|
|
45128
45654
|
expectedRegion: this.stackRegion,
|
|
45129
45655
|
forceDataDelete: this.options.forceStatefulRecreation === true,
|
|
45130
45656
|
...cleanupFinalSnapshotId !== void 0 && { finalSnapshotIdentifier: cleanupFinalSnapshotId }
|
|
@@ -45980,5 +46506,5 @@ var DeployEngine = class DeployEngine {
|
|
|
45980
46506
|
};
|
|
45981
46507
|
|
|
45982
46508
|
//#endregion
|
|
45983
|
-
export {
|
|
45984
|
-
//# sourceMappingURL=deploy-engine-
|
|
46509
|
+
export { isExportAliasCollision as $, StateError as $i, maskSecretsInText as $n, stripControlChars as $r, withSharedDrainBudget as $t, makeCanonicalizePropertiesFn as A, AwsClients as Ai, refuseMalformedResourcesForDestroy as An, AssetModeResolver as Ar, getAccountInfo as At, isWarmThroughputDecrease as B, DynamicReferenceRegionAmbiguousError as Bi, TemplateParser as Bn, buildDenyExternalAccessPolicy as Br, assertRegionMatch as Bt, PRE_DELETE_SNAPSHOT_TYPES as C, PARTITION_TABLE as Ci, malformedStateRefusalMessage as Cn, AssetPublisher as Cr, disableInstanceApiTermination as Ct, isFinalSnapshotError as D, processStackMessages as Di, refuseMalformedOutputs as Dn, createAssetRedirectResolver as Dr, carriesFetchableDynamicReference as Dt, createPreDeleteFinalSnapshot as E, AssemblyReader as Ei, refuseMalformedNestedChildOutputs as En, buildAssetRedirectMap as Er, carriesDynamicReference as Et, isStatefulRecreateTargetForReplace as F, CdkdError as Fi, findActionableSilentDrops as Fn, isCrossRegionRedirect as Fr, refStateLookupFromResource as Ft, cyan as G, MalformedProducerRecordRefusalError as Gi, carriesSecretMask as Gn, dockerSpawnEnvWithSensitive as Gr, configStringRefusal as Gt, toFiniteNumber as H, LocalInvokeBuildError as Hi, STATE_SOURCED_CROSS_GENERATION_RULES as Hn, describeDockerCapturedOutput as Hr, coerceCfnInteger as Ht, isStatefulRecreateTargetSync as I, ConfigError as Ii, findSilentDropProperties as In, parseBootstrapMarker as Ir, isSettledInstanceState as It, red as J, ProvisioningError as Ji, errorCauseChain as Jn, partitionSensitiveEnv as Jr, requireConfigArray as Jt, gray as K, NestedStackChildDirectDestroyError as Ki, createSecretMasker as Kn, formatDockerLoginError as Kr, readConfigString as Kt, renderStatefulReason as L, CrossAccountSecretRefusalError as Li, describeTypeWithThrottleRetry as Ln, readBootstrapMarkerBody as Lr, WAFv2WebACLProvider as Lt, makeSiblingClaimReader as M, resetAwsClients as Mi, repairMalformedOutputsForReadOnly as Mn, assertAssetBucketRegion as Mr, isNamelessDynamicReferenceError as Mt, planOrphanAdoption as N, setAwsClients as Ni, repairMalformedResourcePropertiesForReadOnly as Nn, ensureAssetStorage as Nr, isUnboundTemplateParameter as Nt, refusesFinalSnapshot as O, clearBucketRegionCache as Oi, refuseMalformedOutputsForDestroy as On, loadPublishableAssetManifest as Or, cfnRefValueFromPhysicalId as Ot, MULTI_REGION_RECREATE_BLOCKED_TYPES as P, AssetError as Pi, repairMalformedResourcesForReadOnly as Pn, getBootstrapMarkerKey as Pr, parameterTypeMayLoseSecretIdentity as Pt, exportAliasCollisionScrubWarning as Q, StackTerminationProtectionError as Qi, maskSecretsInError as Qn, escapeRegExp$1 as Qr, producerRegionsFromState as Qt, WARM_THROUGHPUT_MEMBERS as R, DependencyError as Ri, withRetry as Rn, validateAssetBucketName as Rr, normalizeAwsTagsToCfn as Rt, ATOMIC_FINAL_SNAPSHOT_TYPES as S, expectedOwnerParam as Si, malformedResourcesWarning as Sn, shouldRetainResource as Sr, deleteSkipReason as St, ccRoutedFinalSnapshotError as T, derivePartitionAndUrlSuffix as Ti, refuseDivergentRecordRegionForDestroy as Tn, WorkGraph as Tr, IntrinsicFunctionResolver as Tt, formatResourceLine as U, LocalStartServiceError as Ui, STATE_SOURCED_READBACK_RULES as Un, describeDockerExecFailure as Ur, configBooleanRefusal as Ut, toCfnInteger as V, IntrinsicResolutionRefusalError as Vi, STATE_SOURCED_BASELINE_RULES as Vn, buildDockerImage as Vr, coerceCfnBoolean as Vt, bold as W, LockError as Wi, TEMPLATE_SOURCED_RULES as Wn, describeDockerFailure as Wr, configIntegerRefusal as Wt, collectDeclaredOutputNames as X, ResourceUpdateNotSupportedError as Xi, isSingleDynamicReferenceToken as Xn, runDockerForeground as Xr, requireConfigString as Xt, yellow as Y, ResourceTimeoutError as Yi, identityKeyFor as Yn, redactDockerArgvValues as Yr, requireConfigObject as Yt, collectPublishedOutputNames as Z, StackHasActiveImportsError as Zi, markSameGenerationBag as Zn, runDockerStreaming as Zr, classifyReplaySecretRegion as Zt, bagHoldsSecretExpression as _, MIGRATE_TMP_PREFIX as _i, malformedLocalOutputsWarning as _n, importableOutputKeys as _r, CloudControlProvider as _t, DeploymentEventsStore as a, isMarkedNonRetryable as aa, getLegacyStateBucketName as ai, applyRoleArnIfSet as an, S3StateBackend as ar, collectInlinePolicyNamesManagedBySiblings as at, mergeNoChangeOutputs as b, CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION as bi, malformedRenderedContainersWarning as bn, orphansAfterRollback as br, UNSPECIFIED_SKIP_REASON as bt, replayFailedOperations as c, isTransientServerError as ca, resolveCaptureObservedState as ci, callerEnvCredentials as cn, buildForceUnlockCommand as cr, wouldReturnToSdkProvider as ct, updatePartialMessage as d, retryClassificationText as da, resolveStateBucketWithDefaultAndSource as di, INTRINSIC_KEYS as dn, formatLockExpiry as dr, maskerOrIdentity as dt, SynthesisError as ea, AssetManifestLoader as ei, s3BucketArn as en, recordMaskOnlyValue as er, secretBearing as et, updatePartialReason as f, describeAwsFailure as fa, resolveUseCdkBootstrapAssets as fi, STATE_RESOURCES_MALFORMED as fn, shellQuote as fr, beginCommandInterruptScope as ft, computeImplicitDeleteEdges as g, CFN_TEMPLATE_URL_LIMIT as gi, malformedExportSourceWarning as gn, hasReadableExportSet as gr, startInterruptWatch as gt, IMPLICIT_DELETE_DEPENDENCIES as h, CFN_TEMPLATE_BODY_LIMIT as hi, malformedExportNamesWarning as hn, exportNamesCarriedFrom as hr, isInterruptedWaitError as ht, DeploymentEventsReader as i, withErrorHandling as ia, getDefaultStateBucketName as ii, s3BucketWebsiteUrl as in, LockManager as ir, IAMRoleProvider as it, extractDeploymentEventError as j, getAwsClients as ji, refuseMalformedState as jn, BOOTSTRAP_MARKER_PREFIX as jr, isImpossibleEmptyStoredAttribute as jt, unsupportedFinalSnapshotError as k, resolveBucketRegion as ki, refuseMalformedResourcePropertiesForOrphan as kn, rewriteTemplateAssetReferences as kr, coerceParameterTypedValue as kt, replayRollback as l, markNonRetryable as la, resolveSkipPrefix as li, envCredentialsAreAssumedRole as ln, buildLockContentionMessage as lr, createMaskedRetryLogger as lt, maskingRetryLogger as m, __exportAll as ma, warnDeprecatedNoPrefixCliFlag as mi, hasReadableResources as mn, DEFAULT_STATE_PREFIX as mr, interruptWatchListenerCount as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, isCdkdError as na, Synthesizer as ni, s3BucketDualStackDomainName as nn, redactSecretsForState as nr, secretSafeKeyDisplay as nt, planFailedOps as o, isRetryableTransientError as oa, resolveApp as oi, AWS_CREDENTIAL_ENV_KEYS as on, rebuildClientForBucketRegion as or, clearOnUpdateRemoval as ot, withResourceDeadline as p, safeStringify$1 as pa, stateBucketExistenceConfirmed as pi, hasReadableOutputs as pn, CUSTOM_RESOURCE_RESPONSE_PREFIX as pr, endCommandInterruptScope as pt, green as q, PartialFailureError as qi, dynamicReferenceTokens as qn, getDockerCmd as qr, replayWarn as qt, DeployEngine as r, normalizeAwsError as ra, synthesisStatusMessage as ri, s3BucketRegionalDomainName as rn, scrubResourceRecord as rr, getCurrentResourceSecrets as rt, planRollback as s, isThrottlingError as sa, resolveAutoAssetStorage as si, applyCallerIdentityCredentials as sn, UNREPRODUCIBLE_LOCK_CLAUSE as sr, ProviderRegistry as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, formatError as ta, getDockerImageBySourceHash as ti, s3BucketDomainName as tn, recoverMaskedOutput as tr, secretBearingStateKeyWarning as tt, resolveReplacementOldType as u, markRedactedCause as ua, resolveStateBucketWithDefault as ui, DiffCalculator as un, forceQuitRecoveryClause as ur, maskDeep as ut, isSecretBearingReferenceString as v, findLargeInlineResources as vi, malformedOutputsRefusalMessage as vn, importableOutputs as vr, isWaitAbandonedError as vt, buildFinalSnapshotIdentifier as w, canonicalizeRegion as wi, producerRecordKey as wn, stringifyValue as wr, isTerminationProtectionPropagationError as wt, bindingSkippedOutputs as x, decodeListingKey as xi, malformedResourcePropertiesWarning as xn, orphansCarriedFrom as xr, deleteIndeterminateGuards as xt, keptWholeReasonText as y, uploadCfnTemplate as yi, malformedOutputsWarning as yn, isReadableBag as yr, slowCcOperationTimeoutMs as yt, coerceWarmThroughput as z, DeployCancelledError as zi, DagBuilder as zn, validateContainerRepoName as zr, resolveExplicitPhysicalId as zt };
|
|
46510
|
+
//# sourceMappingURL=deploy-engine-efiVJ-rX.js.map
|