@go-to-k/cdkd 0.230.16 → 0.230.18

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.
@@ -1992,6 +1992,21 @@ async function bucketExists(client, bucketName) {
1992
1992
  //#endregion
1993
1993
  //#region src/synthesis/synthesizer.ts
1994
1994
  /**
1995
+ * CDK CLI compatibility: a `--app` value pointing at an existing directory is
1996
+ * treated as a pre-synthesized cloud assembly — synthesis (the subprocess
1997
+ * execution) is skipped and the manifest is read directly. A `--app` value that
1998
+ * is a command (e.g. "node app.ts") or any path that is not an existing
1999
+ * directory is synthesized normally.
2000
+ *
2001
+ * Exported so callers can pick an accurate status message
2002
+ * ("Reading cloud assembly..." vs "Synthesizing CDK app...") BEFORE invoking
2003
+ * {@link Synthesizer.synthesize}, which is the single place that branches on it.
2004
+ */
2005
+ function isPreSynthesizedAssembly(app) {
2006
+ const appPath = resolve(app);
2007
+ return existsSync(appPath) && statSync(appPath).isDirectory();
2008
+ }
2009
+ /**
1995
2010
  * CDK app synthesizer
1996
2011
  *
1997
2012
  * Replaces @aws-cdk/toolkit-lib with self-implemented:
@@ -2016,7 +2031,7 @@ var Synthesizer = class {
2016
2031
  */
2017
2032
  async synthesize(options) {
2018
2033
  const appPath = resolve(options.app);
2019
- if (existsSync(appPath) && statSync(appPath).isDirectory()) {
2034
+ if (isPreSynthesizedAssembly(options.app)) {
2020
2035
  this.logger.debug(`Using pre-synthesized cloud assembly at ${appPath}`);
2021
2036
  const manifest = this.assemblyReader.readManifest(appPath);
2022
2037
  const stacks = this.assemblyReader.getAllStacks(appPath, manifest);
@@ -4867,6 +4882,24 @@ var ReplacementRulesRegistry = class {
4867
4882
  return false;
4868
4883
  }
4869
4884
  /**
4885
+ * Whether the registry has an EXPLICIT opinion about a property's
4886
+ * replacement behavior — i.e. the type has a rule AND the property is listed
4887
+ * in its `replacementProperties`, `updateableProperties`, OR
4888
+ * `conditionalReplacements`. Returns false for an unknown type, or for a
4889
+ * known type whose rule does not mention the property (the "default to
4890
+ * updateable" fall-through inside {@link requiresReplacement}).
4891
+ *
4892
+ * The diff calculator uses this to decide whether the CFn-schema
4893
+ * `createOnlyProperties` fallback may apply: the schema only fills the GAP
4894
+ * where the registry has no opinion, so a deliberate `updateableProperties`
4895
+ * (or conditional) classification is never overridden by the fallback.
4896
+ */
4897
+ isClassified(resourceType, propertyPath) {
4898
+ const rule = this.rules.get(resourceType);
4899
+ if (!rule) return false;
4900
+ return rule.replacementProperties.has(propertyPath) || (rule.updateableProperties?.has(propertyPath) ?? false) || (rule.conditionalReplacements?.has(propertyPath) ?? false);
4901
+ }
4902
+ /**
4870
4903
  * Initialize replacement rules for common AWS resource types
4871
4904
  */
4872
4905
  initializeRules() {
@@ -5117,6 +5150,98 @@ var ReplacementRulesRegistry = class {
5117
5150
  }
5118
5151
  };
5119
5152
 
5153
+ //#endregion
5154
+ //#region src/provisioning/create-only-properties.ts
5155
+ /**
5156
+ * Create-only (immutable) property resolution for replacement detection.
5157
+ *
5158
+ * CloudFormation marks some properties "Update requires: Replacement" — the
5159
+ * resource type's registry schema lists them under `createOnlyProperties`.
5160
+ * Changing such a property cannot be an in-place UPDATE; CloudFormation
5161
+ * transparently DELETE+CREATEs the resource. cdkd's diff classifier
5162
+ * (`ReplacementRulesRegistry`) only knows the ~25 types with a hand-authored
5163
+ * rule, so for every other type an immutable-property change was previously
5164
+ * mis-classified as an in-place UPDATE (the provider's `update()` then either
5165
+ * rejected it with a typed error — at best — or silently dropped it).
5166
+ *
5167
+ * This module resolves each resource type's `createOnlyProperties` from the
5168
+ * CloudFormation registry schema via `cloudformation:DescribeType`, reduced to
5169
+ * the TOP-LEVEL containing property names (a nested path like
5170
+ * `/properties/Foo/Bar` strips to `Foo`) so the diff's top-level property keys
5171
+ * match. The diff calculator consults it as a fallback for any property the
5172
+ * registry does not explicitly classify, so a createOnly change on ANY type now
5173
+ * correctly drives a replacement.
5174
+ *
5175
+ * Caching / failure semantics are identical to {@link
5176
+ * ./write-only-properties.ts} (the sibling DescribeType-backed resolver): only
5177
+ * SUCCESSFUL lookups are cached per resource type for the process (deploy)
5178
+ * lifetime — `cloudformation:DescribeType` is throttled per-account and the
5179
+ * schema cannot change mid-deploy. A FAILED lookup (missing IAM permission,
5180
+ * transient throttle / 5xx) is logged as a warning and resolves to an empty set
5181
+ * WITHOUT poisoning the cache, so the caller gracefully falls back to the
5182
+ * registry-only classification for THIS resource while a later resource of the
5183
+ * same type retries. A caller permanently without `cloudformation:DescribeType`
5184
+ * simply keeps the pre-existing registry-only behavior — no regression.
5185
+ */
5186
+ /**
5187
+ * Per-type cache of SUCCESSFUL lookups only. The value is the in-flight (or
5188
+ * settled) promise so concurrent diffs of the same resource type share a
5189
+ * single DescribeType call. A failed lookup removes its own entry so a later
5190
+ * call retries instead of being permanently poisoned by a transient throttle.
5191
+ */
5192
+ const createOnlyPropertiesCache = /* @__PURE__ */ new Map();
5193
+ /**
5194
+ * Resolve the TOP-LEVEL create-only (immutable) property names for a resource
5195
+ * type.
5196
+ *
5197
+ * Never throws: a DescribeType failure logs a warning and resolves to an empty
5198
+ * set (graceful fallback to the registry-only classification). Only SUCCESSFUL
5199
+ * lookups are cached per resource type for the process lifetime; a failed
5200
+ * lookup is NOT cached, so a later call for the same type retries DescribeType
5201
+ * (a transient throttle must not poison the deploy's replacement detection).
5202
+ */
5203
+ function getTopLevelCreateOnlyProperties(resourceType) {
5204
+ const cached = createOnlyPropertiesCache.get(resourceType);
5205
+ if (cached) return cached;
5206
+ const entry = fetchTopLevelCreateOnlyProperties(resourceType).catch((error) => {
5207
+ createOnlyPropertiesCache.delete(resourceType);
5208
+ const message = error instanceof Error ? error.message : String(error);
5209
+ 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.`);
5210
+ return /* @__PURE__ */ new Set();
5211
+ });
5212
+ createOnlyPropertiesCache.set(resourceType, entry);
5213
+ return entry;
5214
+ }
5215
+ /**
5216
+ * Fetch + parse the type's create-only properties. THROWS on a DescribeType
5217
+ * failure — the caller ({@link getTopLevelCreateOnlyProperties}) catches,
5218
+ * warns, and declines to cache so the lookup can be retried later.
5219
+ */
5220
+ async function fetchTopLevelCreateOnlyProperties(resourceType) {
5221
+ const logger = getLogger().child("CreateOnlyProperties");
5222
+ const response = await getAwsClients().cloudFormation.send(new DescribeTypeCommand({
5223
+ Type: "RESOURCE",
5224
+ TypeName: resourceType
5225
+ }));
5226
+ const result = /* @__PURE__ */ new Set();
5227
+ if (response.Schema) {
5228
+ const createOnly = JSON.parse(response.Schema).createOnlyProperties;
5229
+ if (Array.isArray(createOnly)) for (const path of createOnly) {
5230
+ if (typeof path !== "string") continue;
5231
+ const match = /^\/properties\/([^/]+)/.exec(path);
5232
+ if (match?.[1]) result.add(unescapeJsonPointerSegment$1(match[1]));
5233
+ }
5234
+ }
5235
+ logger.debug(`Resolved ${result.size} top-level create-only properties for ${resourceType}` + (result.size > 0 ? `: ${[...result].join(", ")}` : ""));
5236
+ return result;
5237
+ }
5238
+ /**
5239
+ * Unescape an RFC 6901 JSON Pointer segment (`~1` -> `/`, `~0` -> `~`).
5240
+ */
5241
+ function unescapeJsonPointerSegment$1(segment) {
5242
+ return segment.replace(/~1/g, "/").replace(/~0/g, "~");
5243
+ }
5244
+
5120
5245
  //#endregion
5121
5246
  //#region src/analyzer/diff-calculator.ts
5122
5247
  /**
@@ -5191,7 +5316,7 @@ var DiffCalculator = class DiffCalculator {
5191
5316
  } else {
5192
5317
  const rawDesiredProps = desiredResource.Properties || {};
5193
5318
  const desiredPropsForCompare = resolveFn ? await this.resolveBestEffort(rawDesiredProps, resolveFn) : rawDesiredProps;
5194
- const propertyChanges = this.compareProperties(desiredResource.Type, currentResource.properties, desiredPropsForCompare);
5319
+ const propertyChanges = await this.compareProperties(desiredResource.Type, currentResource.properties, desiredPropsForCompare);
5195
5320
  const attributeChanges = this.compareAttributes(currentResource, desiredResource);
5196
5321
  if (propertyChanges.length > 0 || attributeChanges.length > 0) {
5197
5322
  changes.set(logicalId, {
@@ -5497,17 +5622,25 @@ var DiffCalculator = class DiffCalculator {
5497
5622
  * Uses ReplacementRulesRegistry to determine which property changes require replacement.
5498
5623
  * Reference: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html
5499
5624
  */
5500
- compareProperties(resourceType, currentProperties, desiredProperties) {
5625
+ async compareProperties(resourceType, currentProperties, desiredProperties) {
5501
5626
  const changes = [];
5502
5627
  const allKeys = new Set([...Object.keys(currentProperties), ...Object.keys(desiredProperties)]);
5503
5628
  const ignoredProperties = /* @__PURE__ */ new Set();
5504
5629
  if (resourceType === "AWS::CloudFormation::CustomResource" || resourceType.startsWith("Custom::")) ignoredProperties.add("Timestamp");
5630
+ let createOnlyProps;
5505
5631
  for (const key of allKeys) {
5506
5632
  if (ignoredProperties.has(key)) continue;
5507
5633
  const oldValue = currentProperties[key];
5508
5634
  const newValue = desiredProperties[key];
5509
5635
  if (!this.valuesEqual(oldValue, newValue)) {
5510
- const requiresReplacement = this.replacementRules.requiresReplacement(resourceType, key, oldValue, newValue);
5636
+ let requiresReplacement = this.replacementRules.requiresReplacement(resourceType, key, oldValue, newValue);
5637
+ if (!requiresReplacement && !this.replacementRules.isClassified(resourceType, key)) {
5638
+ if (createOnlyProps === void 0) createOnlyProps = await getTopLevelCreateOnlyProperties(resourceType);
5639
+ if (createOnlyProps.has(key)) {
5640
+ requiresReplacement = true;
5641
+ this.logger.debug(`Property ${key} of ${resourceType} is createOnly per the CFn schema — requires replacement`);
5642
+ }
5643
+ }
5511
5644
  changes.push({
5512
5645
  path: key,
5513
5646
  oldValue,
@@ -13676,6 +13809,13 @@ var DeployEngine = class {
13676
13809
  const needsReplacement = propertyDrivenReplacement || recreateFlagged;
13677
13810
  const dependencies = this.extractAllDependencies(template, logicalId);
13678
13811
  if (needsReplacement) {
13812
+ if (propertyDrivenReplacement && !recreateFlagged) {
13813
+ const statefulReason = isStatefulRecreateTargetForReplace(resourceType, currentProps);
13814
+ if (statefulReason && this.options.forceStatefulRecreation !== true) {
13815
+ const immutableProps = change.propertyChanges?.filter((pc) => pc.requiresReplacement).map((pc) => pc.path).join(", ");
13816
+ 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");
13817
+ }
13818
+ }
13679
13819
  let replacementReason;
13680
13820
  if (recreateViaCcApi) replacementReason = "--recreate-via-cc-api flag (mid-life SDK→CC migration)";
13681
13821
  else if (recreateViaSdkProvider) replacementReason = "--recreate-via-sdk-provider flag (mid-life CC→SDK migration)";
@@ -14035,5 +14175,5 @@ var DeployEngine = class {
14035
14175
  };
14036
14176
 
14037
14177
  //#endregion
14038
- 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
14178
+ export { warnDeprecatedNoPrefixCliFlag as $, LockManager as A, runDockerForeground as B, findActionableSilentDrops as C, DiffCalculator as D, applyRoleArnIfSet as E, stringifyValue as F, isPreSynthesizedAssembly as G, AssetManifestLoader as H, WorkGraph as I, resolveApp as J, getDefaultStateBucketName as K, buildDockerImage as L, rebuildClientForBucketRegion as M, shouldRetainResource as N, DagBuilder as O, AssetPublisher as P, resolveStateBucketWithDefaultAndSource as Q, formatDockerLoginError as R, ProviderRegistry as S, IntrinsicFunctionResolver as T, getDockerImageBySourceHash as U, runDockerStreaming as V, Synthesizer as W, resolveSkipPrefix as X, resolveCaptureObservedState as Y, resolveStateBucketWithDefault as Z, green as _, withRetry as a, AssemblyReader as at, IAMRoleProvider as b, computeImplicitDeleteEdges as c, isStatefulRecreateTargetSync as d, CFN_TEMPLATE_BODY_LIMIT as et, renderStatefulReason as f, gray as g, cyan as h, withResourceDeadline as i, uploadCfnTemplate as it, S3StateBackend as j, TemplateParser as k, extractDeploymentEventError as l, bold as m, DEFAULT_RESOURCE_WARN_AFTER_MS as n, MIGRATE_TMP_PREFIX as nt, isRetryableTransientError as o, clearBucketRegionCache as ot, formatResourceLine as p, getLegacyStateBucketName as q, DeployEngine as r, findLargeInlineResources as rt, IMPLICIT_DELETE_DEPENDENCIES as s, resolveBucketRegion as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, CFN_TEMPLATE_URL_LIMIT as tt, MULTI_REGION_RECREATE_BLOCKED_TYPES as u, red as v, CloudControlProvider as w, collectInlinePolicyNamesManagedBySiblings as x, yellow as y, getDockerCmd as z };
14179
+ //# sourceMappingURL=deploy-engine-CXgQSFYL.js.map