@go-to-k/cdkd 0.230.16 → 0.230.17

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.
@@ -4867,6 +4867,24 @@ var ReplacementRulesRegistry = class {
4867
4867
  return false;
4868
4868
  }
4869
4869
  /**
4870
+ * Whether the registry has an EXPLICIT opinion about a property's
4871
+ * replacement behavior — i.e. the type has a rule AND the property is listed
4872
+ * in its `replacementProperties`, `updateableProperties`, OR
4873
+ * `conditionalReplacements`. Returns false for an unknown type, or for a
4874
+ * known type whose rule does not mention the property (the "default to
4875
+ * updateable" fall-through inside {@link requiresReplacement}).
4876
+ *
4877
+ * The diff calculator uses this to decide whether the CFn-schema
4878
+ * `createOnlyProperties` fallback may apply: the schema only fills the GAP
4879
+ * where the registry has no opinion, so a deliberate `updateableProperties`
4880
+ * (or conditional) classification is never overridden by the fallback.
4881
+ */
4882
+ isClassified(resourceType, propertyPath) {
4883
+ const rule = this.rules.get(resourceType);
4884
+ if (!rule) return false;
4885
+ return rule.replacementProperties.has(propertyPath) || (rule.updateableProperties?.has(propertyPath) ?? false) || (rule.conditionalReplacements?.has(propertyPath) ?? false);
4886
+ }
4887
+ /**
4870
4888
  * Initialize replacement rules for common AWS resource types
4871
4889
  */
4872
4890
  initializeRules() {
@@ -5117,6 +5135,98 @@ var ReplacementRulesRegistry = class {
5117
5135
  }
5118
5136
  };
5119
5137
 
5138
+ //#endregion
5139
+ //#region src/provisioning/create-only-properties.ts
5140
+ /**
5141
+ * Create-only (immutable) property resolution for replacement detection.
5142
+ *
5143
+ * CloudFormation marks some properties "Update requires: Replacement" — the
5144
+ * resource type's registry schema lists them under `createOnlyProperties`.
5145
+ * Changing such a property cannot be an in-place UPDATE; CloudFormation
5146
+ * transparently DELETE+CREATEs the resource. cdkd's diff classifier
5147
+ * (`ReplacementRulesRegistry`) only knows the ~25 types with a hand-authored
5148
+ * rule, so for every other type an immutable-property change was previously
5149
+ * mis-classified as an in-place UPDATE (the provider's `update()` then either
5150
+ * rejected it with a typed error — at best — or silently dropped it).
5151
+ *
5152
+ * This module resolves each resource type's `createOnlyProperties` from the
5153
+ * CloudFormation registry schema via `cloudformation:DescribeType`, reduced to
5154
+ * the TOP-LEVEL containing property names (a nested path like
5155
+ * `/properties/Foo/Bar` strips to `Foo`) so the diff's top-level property keys
5156
+ * match. The diff calculator consults it as a fallback for any property the
5157
+ * registry does not explicitly classify, so a createOnly change on ANY type now
5158
+ * correctly drives a replacement.
5159
+ *
5160
+ * Caching / failure semantics are identical to {@link
5161
+ * ./write-only-properties.ts} (the sibling DescribeType-backed resolver): only
5162
+ * SUCCESSFUL lookups are cached per resource type for the process (deploy)
5163
+ * lifetime — `cloudformation:DescribeType` is throttled per-account and the
5164
+ * schema cannot change mid-deploy. A FAILED lookup (missing IAM permission,
5165
+ * transient throttle / 5xx) is logged as a warning and resolves to an empty set
5166
+ * WITHOUT poisoning the cache, so the caller gracefully falls back to the
5167
+ * registry-only classification for THIS resource while a later resource of the
5168
+ * same type retries. A caller permanently without `cloudformation:DescribeType`
5169
+ * simply keeps the pre-existing registry-only behavior — no regression.
5170
+ */
5171
+ /**
5172
+ * Per-type cache of SUCCESSFUL lookups only. The value is the in-flight (or
5173
+ * settled) promise so concurrent diffs of the same resource type share a
5174
+ * single DescribeType call. A failed lookup removes its own entry so a later
5175
+ * call retries instead of being permanently poisoned by a transient throttle.
5176
+ */
5177
+ const createOnlyPropertiesCache = /* @__PURE__ */ new Map();
5178
+ /**
5179
+ * Resolve the TOP-LEVEL create-only (immutable) property names for a resource
5180
+ * type.
5181
+ *
5182
+ * Never throws: a DescribeType failure logs a warning and resolves to an empty
5183
+ * set (graceful fallback to the registry-only classification). Only SUCCESSFUL
5184
+ * lookups are cached per resource type for the process lifetime; a failed
5185
+ * lookup is NOT cached, so a later call for the same type retries DescribeType
5186
+ * (a transient throttle must not poison the deploy's replacement detection).
5187
+ */
5188
+ function getTopLevelCreateOnlyProperties(resourceType) {
5189
+ const cached = createOnlyPropertiesCache.get(resourceType);
5190
+ if (cached) return cached;
5191
+ const entry = fetchTopLevelCreateOnlyProperties(resourceType).catch((error) => {
5192
+ createOnlyPropertiesCache.delete(resourceType);
5193
+ const message = error instanceof Error ? error.message : String(error);
5194
+ getLogger().child("CreateOnlyProperties").warn(`Failed to resolve create-only properties for ${resourceType} via cloudformation:DescribeType (${message}). Falling back to the registry-only replacement classification for this resource — an immutable-property change may be mis-classified as an in-place update. Grant cloudformation:DescribeType to enable schema-driven replacement detection.`);
5195
+ return /* @__PURE__ */ new Set();
5196
+ });
5197
+ createOnlyPropertiesCache.set(resourceType, entry);
5198
+ return entry;
5199
+ }
5200
+ /**
5201
+ * Fetch + parse the type's create-only properties. THROWS on a DescribeType
5202
+ * failure — the caller ({@link getTopLevelCreateOnlyProperties}) catches,
5203
+ * warns, and declines to cache so the lookup can be retried later.
5204
+ */
5205
+ async function fetchTopLevelCreateOnlyProperties(resourceType) {
5206
+ const logger = getLogger().child("CreateOnlyProperties");
5207
+ const response = await getAwsClients().cloudFormation.send(new DescribeTypeCommand({
5208
+ Type: "RESOURCE",
5209
+ TypeName: resourceType
5210
+ }));
5211
+ const result = /* @__PURE__ */ new Set();
5212
+ if (response.Schema) {
5213
+ const createOnly = JSON.parse(response.Schema).createOnlyProperties;
5214
+ if (Array.isArray(createOnly)) for (const path of createOnly) {
5215
+ if (typeof path !== "string") continue;
5216
+ const match = /^\/properties\/([^/]+)/.exec(path);
5217
+ if (match?.[1]) result.add(unescapeJsonPointerSegment$1(match[1]));
5218
+ }
5219
+ }
5220
+ logger.debug(`Resolved ${result.size} top-level create-only properties for ${resourceType}` + (result.size > 0 ? `: ${[...result].join(", ")}` : ""));
5221
+ return result;
5222
+ }
5223
+ /**
5224
+ * Unescape an RFC 6901 JSON Pointer segment (`~1` -> `/`, `~0` -> `~`).
5225
+ */
5226
+ function unescapeJsonPointerSegment$1(segment) {
5227
+ return segment.replace(/~1/g, "/").replace(/~0/g, "~");
5228
+ }
5229
+
5120
5230
  //#endregion
5121
5231
  //#region src/analyzer/diff-calculator.ts
5122
5232
  /**
@@ -5191,7 +5301,7 @@ var DiffCalculator = class DiffCalculator {
5191
5301
  } else {
5192
5302
  const rawDesiredProps = desiredResource.Properties || {};
5193
5303
  const desiredPropsForCompare = resolveFn ? await this.resolveBestEffort(rawDesiredProps, resolveFn) : rawDesiredProps;
5194
- const propertyChanges = this.compareProperties(desiredResource.Type, currentResource.properties, desiredPropsForCompare);
5304
+ const propertyChanges = await this.compareProperties(desiredResource.Type, currentResource.properties, desiredPropsForCompare);
5195
5305
  const attributeChanges = this.compareAttributes(currentResource, desiredResource);
5196
5306
  if (propertyChanges.length > 0 || attributeChanges.length > 0) {
5197
5307
  changes.set(logicalId, {
@@ -5497,17 +5607,25 @@ var DiffCalculator = class DiffCalculator {
5497
5607
  * Uses ReplacementRulesRegistry to determine which property changes require replacement.
5498
5608
  * Reference: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html
5499
5609
  */
5500
- compareProperties(resourceType, currentProperties, desiredProperties) {
5610
+ async compareProperties(resourceType, currentProperties, desiredProperties) {
5501
5611
  const changes = [];
5502
5612
  const allKeys = new Set([...Object.keys(currentProperties), ...Object.keys(desiredProperties)]);
5503
5613
  const ignoredProperties = /* @__PURE__ */ new Set();
5504
5614
  if (resourceType === "AWS::CloudFormation::CustomResource" || resourceType.startsWith("Custom::")) ignoredProperties.add("Timestamp");
5615
+ let createOnlyProps;
5505
5616
  for (const key of allKeys) {
5506
5617
  if (ignoredProperties.has(key)) continue;
5507
5618
  const oldValue = currentProperties[key];
5508
5619
  const newValue = desiredProperties[key];
5509
5620
  if (!this.valuesEqual(oldValue, newValue)) {
5510
- const requiresReplacement = this.replacementRules.requiresReplacement(resourceType, key, oldValue, newValue);
5621
+ let requiresReplacement = this.replacementRules.requiresReplacement(resourceType, key, oldValue, newValue);
5622
+ if (!requiresReplacement && !this.replacementRules.isClassified(resourceType, key)) {
5623
+ if (createOnlyProps === void 0) createOnlyProps = await getTopLevelCreateOnlyProperties(resourceType);
5624
+ if (createOnlyProps.has(key)) {
5625
+ requiresReplacement = true;
5626
+ this.logger.debug(`Property ${key} of ${resourceType} is createOnly per the CFn schema — requires replacement`);
5627
+ }
5628
+ }
5511
5629
  changes.push({
5512
5630
  path: key,
5513
5631
  oldValue,
@@ -13676,6 +13794,13 @@ var DeployEngine = class {
13676
13794
  const needsReplacement = propertyDrivenReplacement || recreateFlagged;
13677
13795
  const dependencies = this.extractAllDependencies(template, logicalId);
13678
13796
  if (needsReplacement) {
13797
+ if (propertyDrivenReplacement && !recreateFlagged) {
13798
+ const statefulReason = isStatefulRecreateTargetForReplace(resourceType, currentProps);
13799
+ if (statefulReason && this.options.forceStatefulRecreation !== true) {
13800
+ const immutableProps = change.propertyChanges?.filter((pc) => pc.requiresReplacement).map((pc) => pc.path).join(", ");
13801
+ throw new CdkdError(`${logicalId} (${resourceType}) requires replacement (immutable property changed: ${immutableProps}) 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");
13802
+ }
13803
+ }
13679
13804
  let replacementReason;
13680
13805
  if (recreateViaCcApi) replacementReason = "--recreate-via-cc-api flag (mid-life SDK→CC migration)";
13681
13806
  else if (recreateViaSdkProvider) replacementReason = "--recreate-via-sdk-provider flag (mid-life CC→SDK migration)";
@@ -14036,4 +14161,4 @@ var DeployEngine = class {
14036
14161
 
14037
14162
  //#endregion
14038
14163
  export { CFN_TEMPLATE_BODY_LIMIT as $, LockManager as A, runDockerForeground as B, findActionableSilentDrops as C, DiffCalculator as D, applyRoleArnIfSet as E, stringifyValue as F, getDefaultStateBucketName as G, AssetManifestLoader as H, WorkGraph as I, resolveCaptureObservedState as J, getLegacyStateBucketName as K, buildDockerImage as L, rebuildClientForBucketRegion as M, shouldRetainResource as N, DagBuilder as O, AssetPublisher as P, warnDeprecatedNoPrefixCliFlag as Q, formatDockerLoginError as R, ProviderRegistry as S, IntrinsicFunctionResolver as T, getDockerImageBySourceHash as U, runDockerStreaming as V, Synthesizer as W, resolveStateBucketWithDefault as X, resolveSkipPrefix as Y, resolveStateBucketWithDefaultAndSource as Z, green as _, withRetry as a, clearBucketRegionCache as at, IAMRoleProvider as b, computeImplicitDeleteEdges as c, isStatefulRecreateTargetSync as d, CFN_TEMPLATE_URL_LIMIT as et, renderStatefulReason as f, gray as g, cyan as h, withResourceDeadline as i, AssemblyReader as it, S3StateBackend as j, TemplateParser as k, extractDeploymentEventError as l, bold as m, DEFAULT_RESOURCE_WARN_AFTER_MS as n, findLargeInlineResources as nt, isRetryableTransientError as o, resolveBucketRegion as ot, formatResourceLine as p, resolveApp as q, DeployEngine as r, uploadCfnTemplate as rt, IMPLICIT_DELETE_DEPENDENCIES as s, DEFAULT_RESOURCE_TIMEOUT_MS as t, MIGRATE_TMP_PREFIX as tt, MULTI_REGION_RECREATE_BLOCKED_TYPES as u, red as v, CloudControlProvider as w, collectInlinePolicyNamesManagedBySiblings as x, yellow as y, getDockerCmd as z };
14039
- //# sourceMappingURL=deploy-engine-BqUZUty9.js.map
14164
+ //# sourceMappingURL=deploy-engine-CAG2RhWS.js.map