@go-to-k/cdkd 0.219.6 → 0.219.8

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.
@@ -9,7 +9,7 @@ import { GetFunctionUrlConfigCommand, InvokeCommand, LambdaClient, UpdateFunctio
9
9
  import { AssumeRoleCommand, GetCallerIdentityCommand, STSClient } from "@aws-sdk/client-sts";
10
10
  import { DescribeAvailabilityZonesCommand, DescribeImagesCommand, DescribeLaunchTemplatesCommand, DescribeRouteTablesCommand, DescribeSecurityGroupsCommand, DescribeSubnetsCommand, DescribeVpcsCommand, DescribeVpnGatewaysCommand, EC2Client } from "@aws-sdk/client-ec2";
11
11
  import { DescribeTableCommand } from "@aws-sdk/client-dynamodb";
12
- import { CloudFormationClient, CreateChangeSetCommand, DeleteStackCommand, DescribeChangeSetCommand, GetTemplateCommand, waitUntilChangeSetCreateComplete } from "@aws-sdk/client-cloudformation";
12
+ import { CloudFormationClient, CreateChangeSetCommand, DeleteStackCommand, DescribeChangeSetCommand, DescribeTypeCommand, GetTemplateCommand, waitUntilChangeSetCreateComplete } from "@aws-sdk/client-cloudformation";
13
13
  import { GetRestApiCommand } from "@aws-sdk/client-api-gateway";
14
14
  import { GetSecretValueCommand } from "@aws-sdk/client-secrets-manager";
15
15
  import { GetParameterCommand, SSMClient } from "@aws-sdk/client-ssm";
@@ -3763,6 +3763,22 @@ var TemplateParser = class {
3763
3763
  return dependencies;
3764
3764
  }
3765
3765
  /**
3766
+ * Extract resource references (Ref / Fn::GetAtt / Fn::Sub / Fn::Join /
3767
+ * Fn::Select / Fn::Split) from an arbitrary template value.
3768
+ *
3769
+ * Public wrapper over the same recursive extraction `extractDependencies`
3770
+ * uses for Properties — exposed so the diff phase can compute
3771
+ * per-property reference edges (issue #807 replacement propagation)
3772
+ * without duplicating the intrinsic walk. Unlike `extractDependencies`
3773
+ * this does NOT include `DependsOn` (which is pure ordering, not a data
3774
+ * reference — a replaced dependency carries no new value to propagate).
3775
+ */
3776
+ extractReferences(value) {
3777
+ const references = /* @__PURE__ */ new Set();
3778
+ this.extractRefsFromValue(value, references);
3779
+ return references;
3780
+ }
3781
+ /**
3766
3782
  * Recursively extract Ref and Fn::GetAtt from a value
3767
3783
  */
3768
3784
  extractRefsFromValue(value, dependencies) {
@@ -4585,6 +4601,7 @@ var ReplacementRulesRegistry = class {
4585
4601
  var DiffCalculator = class DiffCalculator {
4586
4602
  logger = getLogger().child("DiffCalculator");
4587
4603
  replacementRules = new ReplacementRulesRegistry();
4604
+ parser = new TemplateParser();
4588
4605
  /**
4589
4606
  * Calculate changes needed to reach desired state
4590
4607
  *
@@ -4674,11 +4691,103 @@ var DiffCalculator = class DiffCalculator {
4674
4691
  });
4675
4692
  this.logger.debug(`DELETE: ${logicalId} (${currentResource.resourceType})`);
4676
4693
  }
4694
+ this.promoteReplacementDependents(changes, desiredTemplate);
4677
4695
  const summary = this.getSummary(changes);
4678
4696
  this.logger.debug(`Diff calculated: ${summary.create} CREATE, ${summary.update} UPDATE, ${summary.delete} DELETE, ${summary.noChange} NO_CHANGE`);
4679
4697
  return changes;
4680
4698
  }
4681
4699
  /**
4700
+ * Promote transitive dependents of to-be-replaced resources from
4701
+ * NO_CHANGE to UPDATE (issue #807).
4702
+ *
4703
+ * Diff-time intrinsic resolution runs against the CURRENT state, so a
4704
+ * dependent referencing a resource that will be REPLACED (new physical
4705
+ * ID — e.g. an `AWS::ECS::TaskDefinition` revision) compares equal and
4706
+ * stays NO_CHANGE; the deploy engine then never re-points it at the new
4707
+ * physical resource (for ECS: `UpdateService` is never issued and the
4708
+ * service keeps running the old, now-deregistered revision).
4709
+ * CloudFormation propagates the new physical ID to dependents — mirror
4710
+ * that here by walking reverse reference edges (`Ref` / `Fn::GetAtt` /
4711
+ * `Fn::Sub` and intrinsics nesting them — the same extraction the DAG
4712
+ * builder uses) from every replacement-triggering UPDATE and promoting
4713
+ * NO_CHANGE dependents to UPDATE.
4714
+ *
4715
+ * Promotion is safe even when speculative: the deploy engine re-resolves
4716
+ * the promoted resource's properties against the in-flight state map
4717
+ * (which the DAG guarantees already carries the dependency's new
4718
+ * physical ID) and skips the provider call if nothing actually changed.
4719
+ *
4720
+ * Promotion is transitive: each referencing property of a promoted
4721
+ * dependent is re-evaluated against the replacement rules, and if the
4722
+ * dependent is itself replacement-triggering (the referencing property
4723
+ * is immutable for its type), its own dependents are promoted in turn.
4724
+ * The same re-evaluation also applies to dependents that already had
4725
+ * their own (non-replacement) property changes — their referencing
4726
+ * property gains a synthetic PropertyChange so a replacement cascade is
4727
+ * not masked by an unrelated in-place change.
4728
+ */
4729
+ promoteReplacementDependents(changes, desiredTemplate) {
4730
+ const queue = [];
4731
+ for (const [logicalId, change] of changes) if (change.changeType === "UPDATE" && change.propertyChanges?.some((pc) => pc.requiresReplacement)) queue.push(logicalId);
4732
+ if (queue.length === 0) return;
4733
+ const dependentsOf = /* @__PURE__ */ new Map();
4734
+ for (const [logicalId, resource] of Object.entries(desiredTemplate.Resources)) {
4735
+ if (resource.Type === "AWS::CDK::Metadata") continue;
4736
+ for (const [propKey, propValue] of Object.entries(resource.Properties ?? {})) for (const referencedId of this.parser.extractReferences(propValue)) {
4737
+ if (referencedId === logicalId) continue;
4738
+ let dependents = dependentsOf.get(referencedId);
4739
+ if (!dependents) {
4740
+ dependents = /* @__PURE__ */ new Map();
4741
+ dependentsOf.set(referencedId, dependents);
4742
+ }
4743
+ let propKeys = dependents.get(logicalId);
4744
+ if (!propKeys) {
4745
+ propKeys = /* @__PURE__ */ new Set();
4746
+ dependents.set(logicalId, propKeys);
4747
+ }
4748
+ propKeys.add(propKey);
4749
+ }
4750
+ }
4751
+ const enqueued = new Set(queue);
4752
+ while (queue.length > 0) {
4753
+ const replacedId = queue.shift();
4754
+ const dependents = dependentsOf.get(replacedId);
4755
+ if (!dependents) continue;
4756
+ for (const [dependentId, refPropKeys] of dependents) {
4757
+ const change = changes.get(dependentId);
4758
+ if (!change) continue;
4759
+ if (change.changeType !== "NO_CHANGE" && change.changeType !== "UPDATE") continue;
4760
+ const existingPaths = new Set((change.propertyChanges ?? []).map((pc) => pc.path));
4761
+ const syntheticChanges = [];
4762
+ for (const propKey of refPropKeys) {
4763
+ if (existingPaths.has(propKey)) continue;
4764
+ const oldValue = change.currentProperties?.[propKey];
4765
+ const newValue = change.desiredProperties?.[propKey];
4766
+ syntheticChanges.push({
4767
+ path: propKey,
4768
+ oldValue,
4769
+ newValue,
4770
+ replacementPropagated: true,
4771
+ requiresReplacement: this.replacementRules.requiresReplacement(change.resourceType, propKey, void 0, void 0)
4772
+ });
4773
+ }
4774
+ if (syntheticChanges.length === 0) continue;
4775
+ if (change.changeType === "NO_CHANGE") {
4776
+ change.changeType = "UPDATE";
4777
+ change.propertyChanges = syntheticChanges;
4778
+ this.logger.debug(`UPDATE (promoted): ${dependentId} references replaced resource ${replacedId} via ${[...refPropKeys].join(", ")}`);
4779
+ } else {
4780
+ change.propertyChanges = [...change.propertyChanges ?? [], ...syntheticChanges];
4781
+ this.logger.debug(`UPDATE (augmented): ${dependentId} references replaced resource ${replacedId} via ${[...refPropKeys].join(", ")}`);
4782
+ }
4783
+ if (!enqueued.has(dependentId) && change.propertyChanges.some((pc) => pc.requiresReplacement)) {
4784
+ enqueued.add(dependentId);
4785
+ queue.push(dependentId);
4786
+ }
4787
+ }
4788
+ }
4789
+ }
4790
+ /**
4682
4791
  * Best-effort resolution of template property intrinsics against current state.
4683
4792
  *
4684
4793
  * Iterates top-level properties and resolves each independently: if resolution
@@ -6388,6 +6497,100 @@ var JsonPatchGenerator = class {
6388
6497
  }
6389
6498
  };
6390
6499
 
6500
+ //#endregion
6501
+ //#region src/provisioning/write-only-properties.ts
6502
+ /**
6503
+ * Write-only property resolution for Cloud Control API updates (issue #809).
6504
+ *
6505
+ * Cloud Control API applies UPDATE patch documents with read-modify-write
6506
+ * semantics: the type's read handler returns the current model, the patch is
6507
+ * applied on top, and the result becomes the desired state handed to the
6508
+ * update handler. Read handlers cannot return write-only properties, so any
6509
+ * write-only property that is not explicitly present in the patch document
6510
+ * vanishes from the desired state on every CC-routed UPDATE (e.g.
6511
+ * `AWS::ECS::Service` loses `VolumeConfigurations` and the update hard-fails;
6512
+ * for other types the loss is silent).
6513
+ *
6514
+ * This module resolves each resource type's `writeOnlyProperties` from the
6515
+ * CloudFormation registry schema via `cloudformation:DescribeType`, reduced
6516
+ * to the TOP-LEVEL containing property names (a nested path like
6517
+ * `/properties/Foo/Bar` strips to `Foo` — re-adding the whole containing
6518
+ * property is sufficient and matches terraform-provider-awscc's approach of
6519
+ * clearing write-only attribute paths in the prior state so the patch
6520
+ * generator always emits `add` ops for them).
6521
+ *
6522
+ * Only SUCCESSFUL lookups are cached per resource type for the process
6523
+ * (deploy) lifetime — `cloudformation:DescribeType` is throttled per-account,
6524
+ * and the schema cannot change mid-deploy. A FAILED lookup (missing IAM
6525
+ * permission, transient throttle / 5xx) is logged as a warning and resolves
6526
+ * to an empty set WITHOUT poisoning the cache, so the caller gracefully falls
6527
+ * back to the minimal patch for THIS update while a later update of the same
6528
+ * type retries `DescribeType`. Caching failures would let a single transient
6529
+ * throttle on the first CC-routed UPDATE silently disable write-only
6530
+ * re-inclusion for every CC-routed type for the rest of the deploy —
6531
+ * reintroducing the exact hard-fail this module fixes. No regression for
6532
+ * callers permanently without the `cloudformation:DescribeType` permission:
6533
+ * each update simply re-warns and re-falls-back.
6534
+ */
6535
+ /**
6536
+ * Per-type cache of SUCCESSFUL lookups only. The value is the in-flight (or
6537
+ * settled) promise so that concurrent updates of the same resource type share
6538
+ * a single DescribeType call. A failed lookup removes its own entry so a
6539
+ * later call retries instead of being permanently poisoned by a transient
6540
+ * throttle / 5xx.
6541
+ */
6542
+ const writeOnlyPropertiesCache = /* @__PURE__ */ new Map();
6543
+ /**
6544
+ * Resolve the TOP-LEVEL write-only property names for a resource type.
6545
+ *
6546
+ * Never throws: a DescribeType failure logs a warning and resolves to an
6547
+ * empty set (graceful fallback to the minimal patch). Only SUCCESSFUL
6548
+ * lookups are cached per resource type for the process lifetime; a failed
6549
+ * lookup is NOT cached, so a later call for the same type retries
6550
+ * DescribeType (a transient throttle must not poison the deploy).
6551
+ */
6552
+ function getTopLevelWriteOnlyProperties(resourceType) {
6553
+ const cached = writeOnlyPropertiesCache.get(resourceType);
6554
+ if (cached) return cached;
6555
+ const entry = fetchTopLevelWriteOnlyProperties(resourceType).catch((error) => {
6556
+ writeOnlyPropertiesCache.delete(resourceType);
6557
+ const message = error instanceof Error ? error.message : String(error);
6558
+ getLogger().child("WriteOnlyProperties").warn(`Failed to resolve write-only properties for ${resourceType} via cloudformation:DescribeType (${message}). Falling back to a minimal update patch — write-only properties (if any) may be dropped by the Cloud Control read-modify-write update. Grant cloudformation:DescribeType to enable write-only property re-inclusion.`);
6559
+ return /* @__PURE__ */ new Set();
6560
+ });
6561
+ writeOnlyPropertiesCache.set(resourceType, entry);
6562
+ return entry;
6563
+ }
6564
+ /**
6565
+ * Fetch + parse the type's write-only properties. THROWS on a DescribeType
6566
+ * failure — the caller (`getTopLevelWriteOnlyProperties`) catches, warns, and
6567
+ * declines to cache so the lookup can be retried later.
6568
+ */
6569
+ async function fetchTopLevelWriteOnlyProperties(resourceType) {
6570
+ const logger = getLogger().child("WriteOnlyProperties");
6571
+ const response = await getAwsClients().cloudFormation.send(new DescribeTypeCommand({
6572
+ Type: "RESOURCE",
6573
+ TypeName: resourceType
6574
+ }));
6575
+ const result = /* @__PURE__ */ new Set();
6576
+ if (response.Schema) {
6577
+ const writeOnly = JSON.parse(response.Schema).writeOnlyProperties;
6578
+ if (Array.isArray(writeOnly)) for (const path of writeOnly) {
6579
+ if (typeof path !== "string") continue;
6580
+ const match = /^\/properties\/([^/]+)/.exec(path);
6581
+ if (match?.[1]) result.add(unescapeJsonPointerSegment(match[1]));
6582
+ }
6583
+ }
6584
+ logger.debug(`Resolved ${result.size} top-level write-only properties for ${resourceType}` + (result.size > 0 ? `: ${[...result].join(", ")}` : ""));
6585
+ return result;
6586
+ }
6587
+ /**
6588
+ * Unescape an RFC 6901 JSON Pointer segment (`~1` -> `/`, `~0` -> `~`).
6589
+ */
6590
+ function unescapeJsonPointerSegment(segment) {
6591
+ return segment.replace(/~1/g, "/").replace(/~0/g, "~");
6592
+ }
6593
+
6391
6594
  //#endregion
6392
6595
  //#region src/provisioning/unsupported-types.generated.ts
6393
6596
  /**
@@ -6700,7 +6903,7 @@ var CloudControlProvider = class {
6700
6903
  try {
6701
6904
  const cleanPreviousProperties = stringifyJsonProperties(resourceType, stripNullValues(previousProperties));
6702
6905
  const cleanProperties = stringifyJsonProperties(resourceType, stripNullValues(properties));
6703
- const patch = this.patchGenerator.generatePatch(cleanPreviousProperties, cleanProperties);
6906
+ let patch = this.patchGenerator.generatePatch(cleanPreviousProperties, cleanProperties);
6704
6907
  if (patch.length === 0) {
6705
6908
  this.logger.debug(`No property changes detected for ${logicalId}, skipping update`);
6706
6909
  return {
@@ -6708,6 +6911,19 @@ var CloudControlProvider = class {
6708
6911
  wasReplaced: false
6709
6912
  };
6710
6913
  }
6914
+ const writeOnlyProperties = await getTopLevelWriteOnlyProperties(resourceType);
6915
+ if (writeOnlyProperties.size > 0) {
6916
+ const previousWithoutWriteOnly = { ...cleanPreviousProperties };
6917
+ for (const propertyName of writeOnlyProperties) delete previousWithoutWriteOnly[propertyName];
6918
+ patch = this.patchGenerator.generatePatch(previousWithoutWriteOnly, cleanProperties);
6919
+ if (patch.length === 0) {
6920
+ this.logger.debug(`Only removed write-only properties detected for ${logicalId}, skipping update`);
6921
+ return {
6922
+ physicalId,
6923
+ wasReplaced: false
6924
+ };
6925
+ }
6926
+ }
6711
6927
  this.logger.debug(`Generated ${patch.length} patch operations for ${logicalId}: ${JSON.stringify(patch)}`);
6712
6928
  const updateResponse = await this.cloudControlClient.send(new UpdateResourceCommand({
6713
6929
  TypeName: resourceType,
@@ -12272,4 +12488,4 @@ var DeployEngine = class {
12272
12488
 
12273
12489
  //#endregion
12274
12490
  export { WorkGraph as A, resolveCaptureObservedState as B, DagBuilder as C, shouldRetainResource as D, S3StateBackend as E, runDockerStreaming as F, CFN_TEMPLATE_BODY_LIMIT as G, resolveStateBucketWithDefault as H, Synthesizer as I, findLargeInlineResources as J, CFN_TEMPLATE_URL_LIMIT as K, getDefaultStateBucketName as L, formatDockerLoginError as M, getDockerCmd as N, AssetPublisher as O, runDockerForeground as P, resolveBucketRegion as Q, getLegacyStateBucketName as R, DiffCalculator as S, LockManager as T, resolveStateBucketWithDefaultAndSource as U, resolveSkipPrefix as V, warnDeprecatedNoPrefixCliFlag as W, AssemblyReader as X, uploadCfnTemplate as Y, clearBucketRegionCache as Z, ProviderRegistry as _, withRetry as a, IntrinsicFunctionResolver as b, formatResourceLine as c, gray as d, green as f, collectInlinePolicyNamesManagedBySiblings as g, IAMRoleProvider as h, withResourceDeadline as i, buildDockerImage as j, stringifyValue as k, bold as l, yellow as m, DEFAULT_RESOURCE_WARN_AFTER_MS as n, isRetryableTransientError as o, red as p, MIGRATE_TMP_PREFIX as q, DeployEngine as r, IMPLICIT_DELETE_DEPENDENCIES as s, DEFAULT_RESOURCE_TIMEOUT_MS as t, cyan as u, findActionableSilentDrops as v, TemplateParser as w, applyRoleArnIfSet as x, CloudControlProvider as y, resolveApp as z };
12275
- //# sourceMappingURL=deploy-engine-CwB8POpC.js.map
12491
+ //# sourceMappingURL=deploy-engine-Bl8wyosc.js.map