@go-to-k/cdkd 0.280.26 → 0.280.28

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.
@@ -7985,9 +7985,43 @@ function isIamPropagationError(message) {
7985
7985
  * and the rollback executor's reverse-replacement) — everywhere else it is a
7986
7986
  * genuine conflict that must fail fast. Shared by those sites' collision
7987
7987
  * detection + retry filters so a signature extension lands in one place.
7988
+ *
7989
+ * The optional `s` is load-bearing, not defensive spelling (issue #1625):
7990
+ * Lambda's `CreateFunction` raises `ResourceConflictException: Function
7991
+ * already exist: <name>` — SINGULAR — so the `already exists` form missed it
7992
+ * entirely and NO Lambda function could take the collision path. The
7993
+ * consequence was not a cosmetic message: a property-driven replacement of a
7994
+ * Lambda (dropping `DurableConfig`, changing `TenancyConfig`) create-firsts
7995
+ * into its own still-live name, the raw `ResourceConflictException` escaped
7996
+ * instead of the actionable `NAMED_REPLACEMENT_COLLISION` error, and
7997
+ * `cdkd deploy --replace`'s delete-first fallback never fired — so the
7998
+ * replacement was unperformable by any flag. Verified against real AWS
7999
+ * (us-east-1, 2026-08-12) by creating one function name twice.
8000
+ *
8001
+ * Two fences keep the widened arm from crediting a NON-collision, which
8002
+ * matters because the sites that consult it react DESTRUCTIVELY (the
8003
+ * `--replace` delete-first fallback deletes the old resource):
8004
+ * - `\b` after `exists?` rejects a participle ("already existed as a draft");
8005
+ * - the lookbehind rejects a NEGATED or MODAL phrase — "the bucket does NOT
8006
+ * already exist", "the destination bucket MUST already exist" — which the
8007
+ * bare pattern matched. The modal form is the one that bites: a create
8008
+ * rejected for a missing PREREQUISITE would be reported to the user as a
8009
+ * name collision pointing at `--replace`, and following that advice
8010
+ * deletes the live old resource before the re-create fails again for the
8011
+ * same reason, leaving it absent from AWS with state still recording it.
8012
+ * The error-CODE arm stays exact (`AlreadyExists`): the singular
8013
+ * `AlreadyExist` is not an AWS code spelling, and loosening it would match
8014
+ * inside unrelated identifiers.
8015
+ *
8016
+ * Classification stays MESSAGE-based rather than moving to the exception
8017
+ * NAME, and that is load-bearing here rather than inherited: Lambda raises
8018
+ * `ResourceConflictException` for a function in a PENDING state too (see
8019
+ * `lambda-function-provider.ts`), so keying on the name would classify a
8020
+ * transient state conflict as a collision and delete a live function under
8021
+ * `--replace`.
7988
8022
  */
7989
8023
  function isNameCollisionError(message) {
7990
- return /already exists/i.test(message) || message.includes("AlreadyExists");
8024
+ return /(?<!\b(?:must|not|should|may|cannot)\s)already exists?\b/i.test(message) || message.includes("AlreadyExists");
7991
8025
  }
7992
8026
  /**
7993
8027
  * Match the SQS same-name re-creation cooldown: after `DeleteQueue`, creating
@@ -12836,7 +12870,7 @@ var CloudControlProvider = class {
12836
12870
  if (context?.finalSnapshotIdentifier !== void 0) throw new ProvisioningError(`${logicalId} (${resourceType}) requires a final snapshot (DeletionPolicy: Snapshot), but the Cloud Control API delete route has no final-snapshot parameter. Re-run with --skip-final-snapshot after snapshotting manually, or retain the resource.`, resourceType, logicalId, physicalId);
12837
12871
  if (context?.removeProtection === true && resourceType === "AWS::AutoScaling::AutoScalingGroup") {
12838
12872
  this.logger.debug(`Delegating protected AutoScalingGroup ${logicalId} delete to the SDK ASGProvider (Cloud Control cannot force-delete a protected ASG)`);
12839
- const { ASGProvider } = await import("./asg-provider-BJsjMRaA.js").then((n) => n.n);
12873
+ const { ASGProvider } = await import("./asg-provider-C3rBE_Ni.js").then((n) => n.n);
12840
12874
  await new ASGProvider().delete(logicalId, physicalId, resourceType, _properties, context);
12841
12875
  return;
12842
12876
  }
@@ -16440,6 +16474,111 @@ function findActionableSilentDrops(resourceType, templateProperties, allowedKeys
16440
16474
  return drops.filter(({ property }) => !allowedKeys.has(`${resourceType}:${property}`));
16441
16475
  }
16442
16476
 
16477
+ //#endregion
16478
+ //#region src/provisioning/mutually-exclusive-properties.ts
16479
+ /**
16480
+ * The rule table, keyed by CFn resource type.
16481
+ *
16482
+ * Deliberately seeded with the ONE combination this repo has verified end to
16483
+ * end (`AWS::EC2::Route`, refused by `EC2Provider.createRoute` since #1566 and
16484
+ * normalized on both diff sides since #1591). The mechanism is general; the
16485
+ * data is not speculative. See "Adding a rule" above.
16486
+ */
16487
+ const MUTUALLY_EXCLUSIVE_PROPERTIES = /* @__PURE__ */ new Map([["AWS::EC2::Route", [{
16488
+ properties: [
16489
+ "DestinationCidrBlock",
16490
+ "DestinationIpv6CidrBlock",
16491
+ "DestinationPrefixListId"
16492
+ ],
16493
+ rationale: "CloudFormation and the EC2 CreateRoute API accept exactly one destination per route.",
16494
+ firstDeclaredWins: true
16495
+ }]]]);
16496
+ /**
16497
+ * True for a single-key object whose key is `Ref` or `Fn::*` — the shape of an
16498
+ * unresolved CloudFormation intrinsic. Mirrors `isIntrinsicShaped` in
16499
+ * {@link ./create-only-properties}; kept local because the two modules answer
16500
+ * different questions about the shape and should not drift into one another's
16501
+ * behavior by accident.
16502
+ */
16503
+ function isUnresolvedIntrinsic(value) {
16504
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
16505
+ const keys = Object.keys(value);
16506
+ return keys.length === 1 && (keys[0] === "Ref" || keys[0].startsWith("Fn::"));
16507
+ }
16508
+ /**
16509
+ * True when `value` is unconditionally present in the template.
16510
+ *
16511
+ * The predicate is `Boolean`, not a hand-listed `undefined | null | ''` set,
16512
+ * matching `narrowRouteDestinations`' truthiness narrowing in
16513
+ * `ec2-provider.ts`: the property bag is `unknown`-valued at runtime, so an
16514
+ * unquoted YAML `0` must be skipped here exactly as the provider's `||` chain
16515
+ * skips it — otherwise pre-flight refuses a template the provider would have
16516
+ * accepted. An unresolved intrinsic is truthy but its resolved presence is
16517
+ * NOT yet known, so it is excluded ahead of the truthiness test.
16518
+ */
16519
+ function isDeclared(value) {
16520
+ if (isUnresolvedIntrinsic(value)) return false;
16521
+ return Boolean(value);
16522
+ }
16523
+ /**
16524
+ * Find every mutually-exclusive rule this resource violates.
16525
+ *
16526
+ * Returns `[]` for a type with no rules, an absent property bag, and any
16527
+ * combination that is only reached through an unresolved intrinsic.
16528
+ */
16529
+ function findMutuallyExclusiveViolations(resourceType, templateProperties) {
16530
+ if (!templateProperties) return [];
16531
+ const rules = MUTUALLY_EXCLUSIVE_PROPERTIES.get(resourceType);
16532
+ if (!rules) return [];
16533
+ return findViolationsForRules(resourceType, rules, templateProperties);
16534
+ }
16535
+ /**
16536
+ * The table-free core of {@link findMutuallyExclusiveViolations}, taking the
16537
+ * rules explicitly.
16538
+ *
16539
+ * Exported so the multi-rule and multi-violation paths are reachable from
16540
+ * tests: the shipped table has a single type with a single rule, so those
16541
+ * branches would otherwise be dead code that no assertion can exercise.
16542
+ */
16543
+ function findViolationsForRules(resourceType, rules, templateProperties) {
16544
+ const violations = [];
16545
+ for (const rule of rules) {
16546
+ const declared = rule.properties.filter((property) => isDeclared(templateProperties[property]));
16547
+ if (declared.length <= 1) continue;
16548
+ const winnerCertain = !rule.properties.slice(0, rule.properties.indexOf(declared[0])).some((property) => isUnresolvedIntrinsic(templateProperties[property]));
16549
+ violations.push({
16550
+ resourceType,
16551
+ rule,
16552
+ declared,
16553
+ winnerCertain
16554
+ });
16555
+ }
16556
+ return violations;
16557
+ }
16558
+ /**
16559
+ * Render one violation as a per-resource error line.
16560
+ *
16561
+ * For a `firstDeclaredWins` rule the message names the key that would actually
16562
+ * be SENT, which is what makes the remedy actionable.
16563
+ *
16564
+ * It deliberately stops short of promising the deployed resource is unaffected.
16565
+ * Pre-flight has no state, so it cannot know which destination the LIVE
16566
+ * resource was created from: for a route deployed with only
16567
+ * `DestinationIpv6CidrBlock` whose template later GAINS
16568
+ * `DestinationCidrBlock`, deleting the IPv6 key makes the CIDR key the sole
16569
+ * destination — a create-only change that REPLACES the route. Saying "removing
16570
+ * the others leaves the resource unchanged" would be false exactly there.
16571
+ *
16572
+ * The sentence is omitted entirely when
16573
+ * {@link MutuallyExclusiveViolation.winnerCertain} is false — naming a winner
16574
+ * the raw template does not determine would be a confident falsehood.
16575
+ */
16576
+ function buildMutuallyExclusiveMessage(logicalId, violation) {
16577
+ const { resourceType, rule, declared, winnerCertain } = violation;
16578
+ const winnerNote = rule.firstDeclaredWins && winnerCertain ? ` cdkd would send only ${declared[0]}; ${declared.slice(1).join(" / ")} would be dropped. Deleting the dropped keys changes nothing cdkd sends — but if the LIVE resource was created from one of them, making ${declared[0]} the sole value is a create-only change that REPLACES it.` : "";
16579
+ return ` - ${logicalId} (${resourceType}) declares ${declared.join(" and ")}\n ${rule.rationale}${winnerNote}\n Declare at most one of: ${rule.properties.join(" / ")}`;
16580
+ }
16581
+
16443
16582
  //#endregion
16444
16583
  //#region src/provisioning/provider-registry.ts
16445
16584
  /**
@@ -16738,11 +16877,41 @@ var ProviderRegistry = class {
16738
16877
  * the property check is a no-op (`findSilentDropProperties` returns `[]`
16739
16878
  * for non-Tier-1 / unknown types).
16740
16879
  *
16880
+ * Since issue [#1634](https://github.com/go-to-k/cdkd/issues/1634) this
16881
+ * ALSO runs the mutually-exclusive-property check
16882
+ * ({@link validateMutuallyExclusiveProperties}), which throws BEFORE any
16883
+ * routing decision is logged — a template CloudFormation itself rejects
16884
+ * should not first produce a page of routing chatter.
16885
+ *
16741
16886
  * @see findAutoRouteHits for the pure-functional pre-deploy plan-builder
16742
16887
  * that returns the same information without logging.
16743
16888
  */
16744
16889
  validateResourceProperties(resources) {
16745
- this.reportSilentDropDecisions(resources);
16890
+ const materialized = [...resources];
16891
+ this.validateMutuallyExclusiveProperties(materialized);
16892
+ this.reportSilentDropDecisions(materialized);
16893
+ }
16894
+ /**
16895
+ * Reject a template that declares two or more MUTUALLY EXCLUSIVE top-level
16896
+ * properties on one resource (issue
16897
+ * [#1634](https://github.com/go-to-k/cdkd/issues/1634)).
16898
+ *
16899
+ * Aggregated into ONE error listing every offending resource, mirroring
16900
+ * {@link validateResourceTypes} — a template with three bad routes should
16901
+ * report three, not fail three deploys in a row. There is deliberately no
16902
+ * `--allow-*` escape hatch: the combination is invalid at CloudFormation and
16903
+ * at the service API, so the only correct outcome is a template edit (see
16904
+ * the rule module's header).
16905
+ *
16906
+ * Unlike the provider-side refusal this fires even when the resource already
16907
+ * exists and the deploy diff classifies NO_CHANGE, which is the gap the
16908
+ * issue was filed for.
16909
+ */
16910
+ validateMutuallyExclusiveProperties(resources) {
16911
+ const lines = [];
16912
+ for (const { logicalId, resourceType, properties } of resources) for (const violation of findMutuallyExclusiveViolations(resourceType, properties)) lines.push(buildMutuallyExclusiveMessage(logicalId, violation));
16913
+ if (lines.length === 0) return;
16914
+ throw new Error(`The following resources declare mutually exclusive properties:\n` + lines.join("\n") + "\n\nCloudFormation rejects these combinations too — edit the template to declare only one of each set.");
16746
16915
  }
16747
16916
  /**
16748
16917
  * Info-log every silent-drop routing decision (auto-route via CC API) and
@@ -19352,7 +19521,7 @@ const FLUSH_INTERVAL_MS = 2e3;
19352
19521
  const FLUSH_EVENT_THRESHOLD = 50;
19353
19522
  /** Build-time cdkd version, with a dev fallback for non-built contexts. */
19354
19523
  function getCdkdVersion() {
19355
- return "0.280.26";
19524
+ return "0.280.28";
19356
19525
  }
19357
19526
  /**
19358
19527
  * Generate a time-sortable unique run id, e.g.
@@ -21485,4 +21654,4 @@ var DeployEngine = class {
21485
21654
 
21486
21655
  //#endregion
21487
21656
  export { requireConfigObject as $, AssemblyReader as $t, green as A, __exportAll as An, runDockerForeground as At, disableInstanceApiTermination as B, resolveCaptureObservedState as Bt, MULTI_REGION_RECREATE_BLOCKED_TYPES as C, StackTerminationProtectionError as Cn, getBootstrapMarkerKey as Ct, bold as D, isCdkdError as Dn, buildDockerImage as Dt, formatResourceLine as E, formatError as En, validateContainerRepoName as Et, clearOnUpdateRemoval as F, synthesisStatusMessage as Ft, WAFv2WebACLProvider as G, stateBucketExistenceConfirmed as Gt, IntrinsicFunctionResolver as H, resolveStateBucketWithDefault as Ht, ProviderRegistry as I, getDefaultStateBucketName as It, assertRegionMatch as J, CFN_TEMPLATE_URL_LIMIT as Jt, normalizeAwsTagsToCfn as K, warnDeprecatedNoPrefixCliFlag as Kt, findActionableSilentDrops as L, getLegacyStateBucketName as Lt, yellow as M, AssetManifestLoader as Mt, IAMRoleProvider as N, getDockerImageBySourceHash as Nt, cyan as O, normalizeAwsError as On, formatDockerLoginError as Ot, collectInlinePolicyNamesManagedBySiblings as P, Synthesizer as Pt, requireConfigArray as Q, expectedOwnerParam as Qt, CloudControlProvider as R, resolveApp as Rt, extractDeploymentEventError as S, StackHasActiveImportsError as Sn, ensureAssetStorage as St, renderStatefulReason as T, SynthesisError as Tn, validateAssetBucketName as Tt, cfnRefValueFromPhysicalId as U, resolveStateBucketWithDefaultAndSource as Ut, isTerminationProtectionPropagationError as V, resolveSkipPrefix as Vt, refStateLookupFromResource as W, resolveUseCdkBootstrapAssets as Wt, readConfigString as X, findLargeInlineResources as Xt, configStringRefusal as Y, MIGRATE_TMP_PREFIX as Yt, replayWarn as Z, uploadCfnTemplate as Zt, createPreDeleteFinalSnapshot as _, NestedStackChildDirectDestroyError as _n, createAssetRedirectResolver as _t, DeploymentEventsStore as a, resetAwsClients as an, isRetryableTransientError as at, unsupportedFinalSnapshotError as b, ResourceTimeoutError as bn, AssetModeResolver as bt, replayFailedOperations as c, CdkdError as cn, TemplateParser as ct, IMPLICIT_DELETE_DEPENDENCIES as d, DeployCancelledError as dn, rebuildClientForBucketRegion as dt, processStackMessages as en, requireConfigString as et, computeImplicitDeleteEdges as f, LocalInvokeBuildError as fn, shouldRetainResource as ft, ccRoutedFinalSnapshotError as g, MissingCdkCliError as gn, buildAssetRedirectMap as gt, buildFinalSnapshotIdentifier as h, LockError as hn, WorkGraph as ht, DeploymentEventsReader as i, getAwsClients as in, withRetry as it, red as j, runDockerStreaming as jt, gray as k, withErrorHandling as kn, getDockerCmd as kt, replayRollback as l, ConfigError as ln, LockManager as lt, PRE_DELETE_SNAPSHOT_TYPES as m, LocalStartServiceError as mn, stringifyValue as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, resolveBucketRegion as nn, DiffCalculator as nt, planFailedOps as o, setAwsClients as on, isThrottlingError as ot, ATOMIC_FINAL_SNAPSHOT_TYPES as p, LocalMigrateError as pn, AssetPublisher as pt, resolveExplicitPhysicalId as q, CFN_TEMPLATE_BODY_LIMIT as qt, DeployEngine as r, AwsClients as rn, describeTypeWithThrottleRetry as rt, planRollback as s, AssetError as sn, DagBuilder as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, clearBucketRegionCache as tn, applyRoleArnIfSet as tt, withResourceDeadline as u, DependencyError as un, S3StateBackend as ut, isFinalSnapshotError as v, PartialFailureError as vn, loadPublishableAssetManifest as vt, isStatefulRecreateTargetSync as w, StateError as wn, parseBootstrapMarker as wt, makeCanonicalizePropertiesFn as x, ResourceUpdateNotSupportedError as xn, BOOTSTRAP_MARKER_PREFIX as xt, refusesFinalSnapshot as y, ProvisioningError as yn, rewriteTemplateAssetReferences as yt, slowCcOperationTimeoutMs as z, resolveAutoAssetStorage as zt };
21488
- //# sourceMappingURL=deploy-engine-Sb8KbTlA.js.map
21657
+ //# sourceMappingURL=deploy-engine-Dkep5i7x.js.map