@go-to-k/cdkd 0.280.28 → 0.280.30

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/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { $ as requireConfigObject, $t as AssemblyReader, A as green, At as runDockerForeground, B as disableInstanceApiTermination, Bt as resolveCaptureObservedState, C as MULTI_REGION_RECREATE_BLOCKED_TYPES, Cn as StackTerminationProtectionError, Ct as getBootstrapMarkerKey, D as bold, Dt as buildDockerImage, E as formatResourceLine, Et as validateContainerRepoName, F as clearOnUpdateRemoval, Ft as synthesisStatusMessage, G as WAFv2WebACLProvider, Gt as stateBucketExistenceConfirmed, H as IntrinsicFunctionResolver, Ht as resolveStateBucketWithDefault, I as ProviderRegistry, It as getDefaultStateBucketName, J as assertRegionMatch, Jt as CFN_TEMPLATE_URL_LIMIT, K as normalizeAwsTagsToCfn, Kt as warnDeprecatedNoPrefixCliFlag, L as findActionableSilentDrops, Lt as getLegacyStateBucketName, M as yellow, Mt as AssetManifestLoader, N as IAMRoleProvider, Nt as getDockerImageBySourceHash, O as cyan, On as normalizeAwsError, Ot as formatDockerLoginError, P as collectInlinePolicyNamesManagedBySiblings, Pt as Synthesizer, Q as requireConfigArray, Qt as expectedOwnerParam, R as CloudControlProvider, Rt as resolveApp, S as extractDeploymentEventError, Sn as StackHasActiveImportsError, St as ensureAssetStorage, T as renderStatefulReason, Tt as validateAssetBucketName, U as cfnRefValueFromPhysicalId, Ut as resolveStateBucketWithDefaultAndSource, V as isTerminationProtectionPropagationError, Vt as resolveSkipPrefix, W as refStateLookupFromResource, Wt as resolveUseCdkBootstrapAssets, X as readConfigString, Xt as findLargeInlineResources, Y as configStringRefusal, Yt as MIGRATE_TMP_PREFIX, Z as replayWarn, Zt as uploadCfnTemplate, _ as createPreDeleteFinalSnapshot, _n as NestedStackChildDirectDestroyError, _t as createAssetRedirectResolver, a as DeploymentEventsStore, an as resetAwsClients, at as isRetryableTransientError, b as unsupportedFinalSnapshotError, bn as ResourceTimeoutError, bt as AssetModeResolver, c as replayFailedOperations, cn as CdkdError, ct as TemplateParser, d as IMPLICIT_DELETE_DEPENDENCIES, dn as DeployCancelledError, dt as rebuildClientForBucketRegion, en as processStackMessages, et as requireConfigString, f as computeImplicitDeleteEdges, fn as LocalInvokeBuildError$1, ft as shouldRetainResource, g as ccRoutedFinalSnapshotError, gn as MissingCdkCliError, gt as buildAssetRedirectMap, h as buildFinalSnapshotIdentifier, ht as WorkGraph, i as DeploymentEventsReader, in as getAwsClients, it as withRetry, j as red, jt as runDockerStreaming, k as gray, kn as withErrorHandling, kt as getDockerCmd, l as replayRollback, lt as LockManager, m as PRE_DELETE_SNAPSHOT_TYPES, mn as LocalStartServiceError, mt as stringifyValue, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as resolveBucketRegion, nt as DiffCalculator, o as planFailedOps, on as setAwsClients, ot as isThrottlingError, p as ATOMIC_FINAL_SNAPSHOT_TYPES, pn as LocalMigrateError, pt as AssetPublisher, q as resolveExplicitPhysicalId, qt as CFN_TEMPLATE_BODY_LIMIT, r as DeployEngine, rn as AwsClients, rt as describeTypeWithThrottleRetry, s as planRollback, st as DagBuilder, t as DEFAULT_RESOURCE_TIMEOUT_MS, tt as applyRoleArnIfSet, u as withResourceDeadline, ut as S3StateBackend, v as isFinalSnapshotError, vn as PartialFailureError, vt as loadPublishableAssetManifest, w as isStatefulRecreateTargetSync, wt as parseBootstrapMarker, x as makeCanonicalizePropertiesFn, xn as ResourceUpdateNotSupportedError, xt as BOOTSTRAP_MARKER_PREFIX, y as refusesFinalSnapshot, yn as ProvisioningError, yt as rewriteTemplateAssetReferences, z as slowCcOperationTimeoutMs, zt as resolveAutoAssetStorage } from "./deploy-engine-Dkep5i7x.js";
2
+ import { $ as requireConfigObject, $t as AssemblyReader, A as green, At as runDockerForeground, B as disableInstanceApiTermination, Bt as resolveCaptureObservedState, C as MULTI_REGION_RECREATE_BLOCKED_TYPES, Cn as StackTerminationProtectionError, Ct as getBootstrapMarkerKey, D as bold, Dt as buildDockerImage, E as formatResourceLine, Et as validateContainerRepoName, F as clearOnUpdateRemoval, Ft as synthesisStatusMessage, G as WAFv2WebACLProvider, Gt as stateBucketExistenceConfirmed, H as IntrinsicFunctionResolver, Ht as resolveStateBucketWithDefault, I as ProviderRegistry, It as getDefaultStateBucketName, J as assertRegionMatch, Jt as CFN_TEMPLATE_URL_LIMIT, K as normalizeAwsTagsToCfn, Kt as warnDeprecatedNoPrefixCliFlag, L as findActionableSilentDrops, Lt as getLegacyStateBucketName, M as yellow, Mt as AssetManifestLoader, N as IAMRoleProvider, Nt as getDockerImageBySourceHash, O as cyan, On as normalizeAwsError, Ot as formatDockerLoginError, P as collectInlinePolicyNamesManagedBySiblings, Pt as Synthesizer, Q as requireConfigArray, Qt as expectedOwnerParam, R as CloudControlProvider, Rt as resolveApp, S as extractDeploymentEventError, Sn as StackHasActiveImportsError, St as ensureAssetStorage, T as renderStatefulReason, Tt as validateAssetBucketName, U as cfnRefValueFromPhysicalId, Ut as resolveStateBucketWithDefaultAndSource, V as isTerminationProtectionPropagationError, Vt as resolveSkipPrefix, W as refStateLookupFromResource, Wt as resolveUseCdkBootstrapAssets, X as readConfigString, Xt as findLargeInlineResources, Y as configStringRefusal, Yt as MIGRATE_TMP_PREFIX, Z as replayWarn, Zt as uploadCfnTemplate, _ as createPreDeleteFinalSnapshot, _n as NestedStackChildDirectDestroyError, _t as createAssetRedirectResolver, a as DeploymentEventsStore, an as resetAwsClients, at as isRetryableTransientError, b as unsupportedFinalSnapshotError, bn as ResourceTimeoutError, bt as AssetModeResolver, c as replayFailedOperations, cn as CdkdError, ct as TemplateParser, d as IMPLICIT_DELETE_DEPENDENCIES, dn as DeployCancelledError, dt as rebuildClientForBucketRegion, en as processStackMessages, et as requireConfigString, f as computeImplicitDeleteEdges, fn as LocalInvokeBuildError$1, ft as shouldRetainResource, g as ccRoutedFinalSnapshotError, gn as MissingCdkCliError, gt as buildAssetRedirectMap, h as buildFinalSnapshotIdentifier, ht as WorkGraph, i as DeploymentEventsReader, in as getAwsClients, it as withRetry, j as red, jt as runDockerStreaming, k as gray, kn as withErrorHandling, kt as getDockerCmd, l as replayRollback, lt as LockManager, m as PRE_DELETE_SNAPSHOT_TYPES, mn as LocalStartServiceError, mt as stringifyValue, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as resolveBucketRegion, nt as DiffCalculator, o as planFailedOps, on as setAwsClients, ot as isThrottlingError, p as ATOMIC_FINAL_SNAPSHOT_TYPES, pn as LocalMigrateError, pt as AssetPublisher, q as resolveExplicitPhysicalId, qt as CFN_TEMPLATE_BODY_LIMIT, r as DeployEngine, rn as AwsClients, rt as describeTypeWithThrottleRetry, s as planRollback, st as DagBuilder, t as DEFAULT_RESOURCE_TIMEOUT_MS, tt as applyRoleArnIfSet, u as withResourceDeadline, ut as S3StateBackend, v as isFinalSnapshotError, vn as PartialFailureError, vt as loadPublishableAssetManifest, w as isStatefulRecreateTargetSync, wt as parseBootstrapMarker, x as makeCanonicalizePropertiesFn, xn as ResourceUpdateNotSupportedError, xt as BOOTSTRAP_MARKER_PREFIX, y as refusesFinalSnapshot, yn as ProvisioningError, yt as rewriteTemplateAssetReferences, z as slowCcOperationTimeoutMs, zt as resolveAutoAssetStorage } from "./deploy-engine-POCXyXPl.js";
3
3
  import { a as getLiveRenderer, c as PATTERN_B_RESOURCE_TYPES, d as generateResourceNameWithFallback, f as withSkipPrefix, i as runStackBuffered, n as getLogger, o as PATTERN_B_NAME_OPTIONS, p as withStackName, s as PATTERN_B_NAME_PROPERTIES, u as generateResourceName } from "./logger-BYMEE-BS.js";
4
- import { t as ASGProvider } from "./asg-provider-C3rBE_Ni.js";
4
+ import { t as ASGProvider } from "./asg-provider-DZ05yRbJ.js";
5
5
  import { AsyncLocalStorage } from "node:async_hooks";
6
6
  import { createHash, randomBytes, randomUUID } from "node:crypto";
7
7
  import { CopyObjectCommand, CreateBucketCommand, DeleteBucketAnalyticsConfigurationCommand, DeleteBucketCommand, DeleteBucketCorsCommand, DeleteBucketEncryptionCommand, DeleteBucketIntelligentTieringConfigurationCommand, DeleteBucketInventoryConfigurationCommand, DeleteBucketLifecycleCommand, DeleteBucketMetricsConfigurationCommand, DeleteBucketOwnershipControlsCommand, DeleteBucketPolicyCommand, DeleteBucketReplicationCommand, DeleteBucketTaggingCommand, DeleteBucketWebsiteCommand, DeleteObjectsCommand, GetBucketAccelerateConfigurationCommand, GetBucketCorsCommand, GetBucketEncryptionCommand, GetBucketLifecycleConfigurationCommand, GetBucketLocationCommand, GetBucketLoggingCommand, GetBucketNotificationConfigurationCommand, GetBucketOwnershipControlsCommand, GetBucketPolicyCommand, GetBucketReplicationCommand, GetBucketTaggingCommand, GetBucketVersioningCommand, GetBucketWebsiteCommand, GetObjectCommand, GetObjectLockConfigurationCommand, GetPublicAccessBlockCommand, HeadBucketCommand, ListBucketAnalyticsConfigurationsCommand, ListBucketIntelligentTieringConfigurationsCommand, ListBucketInventoryConfigurationsCommand, ListBucketMetricsConfigurationsCommand, ListObjectVersionsCommand, ListObjectsV2Command, NoSuchBucket, PutBucketAccelerateConfigurationCommand, PutBucketAnalyticsConfigurationCommand, PutBucketCorsCommand, PutBucketEncryptionCommand, PutBucketIntelligentTieringConfigurationCommand, PutBucketInventoryConfigurationCommand, PutBucketLifecycleConfigurationCommand, PutBucketLoggingCommand, PutBucketMetricsConfigurationCommand, PutBucketNotificationConfigurationCommand, PutBucketOwnershipControlsCommand, PutBucketPolicyCommand, PutBucketReplicationCommand, PutBucketTaggingCommand, PutBucketVersioningCommand, PutBucketWebsiteCommand, PutObjectCommand, PutObjectLockConfigurationCommand, PutPublicAccessBlockCommand, S3Client, S3ServiceException } from "@aws-sdk/client-s3";
@@ -17989,6 +17989,67 @@ function narrowRouteDestinations(properties) {
17989
17989
  narrowed
17990
17990
  };
17991
17991
  }
17992
+ /** The `AuthorizeSecurityGroupIngress` default protocol: "all protocols". */
17993
+ const SG_INGRESS_IP_PROTOCOL_DEFAULT = "-1";
17994
+ /** The refusal path both the create guard and the canonicalizer report under. */
17995
+ const SG_INGRESS_IP_PROTOCOL_PATH = "AWS::EC2::SecurityGroupIngress IpProtocol";
17996
+ /**
17997
+ * Resolve the `IpProtocol` an `AWS::EC2::SecurityGroupIngress` rule actually
17998
+ * SENDS, plus the bag that describes it (issue #1633).
17999
+ *
18000
+ * ONE source for a decision three call sites have to agree on, exactly as
18001
+ * `narrowRouteDestinations` is for the Route destinations: the create arm
18002
+ * (which authorizes the rule), the update arm (which revokes then re-creates
18003
+ * it), and the DIFF-side canonicalizer. If those disagreed, state and template
18004
+ * would each be normalized to a different protocol and the fix would become
18005
+ * the bug — the lesson `.claude/rules/providers.md` records from #1591.
18006
+ *
18007
+ * Two shapes reach the wire as something other than what the template wrote,
18008
+ * and BOTH have to be recorded or they become permanent phantom drift, because
18009
+ * `readSecurityGroupIngressCurrentState` can only ever return what AWS holds:
18010
+ *
18011
+ * - a MALFORMED value (`''` / `{}` / `true` / an explicit `null`), which
18012
+ * `requireConfigString` refuses on the template-path create and — under
18013
+ * `onUnusable` — warn-substitutes the default on the replay-reachable paths;
18014
+ * - a finite NUMBER, the accepted-by-design shape from an unquoted YAML
18015
+ * `IpProtocol: -1` (issue #1513), which is stringified before it is sent.
18016
+ *
18017
+ * The second is not announced by a warning, and does not need to be: unlike a
18018
+ * dropped destination key it loses NOTHING — `-1` and `'-1'` name the same
18019
+ * protocol — so there is no silent loss for `effectiveProperties` to launder
18020
+ * into a clean record, which is the hazard the "already ANNOUNCED" clause of
18021
+ * the `effectiveProperties` contract exists to guard against. Recording the
18022
+ * string additionally fixes the delete path, which forwards the state record
18023
+ * to `buildIpPermission` and would otherwise hand the EC2 API a number.
18024
+ *
18025
+ * `narrowed` is a FRESH object only when the sent value differs from the
18026
+ * declared one, so an ordinary `IpProtocol: 'tcp'` bag is returned untouched
18027
+ * and compares byte-for-byte as before.
18028
+ *
18029
+ * @param onUnusable Passed straight through to `requireConfigString`: absent,
18030
+ * a malformed value THROWS (the template-path create, where the user can fix
18031
+ * the template); supplied, it warns and takes the default. The canonicalizer
18032
+ * passes a no-op — a diff must never throw, and it must never warn either,
18033
+ * since the provisioning path already announces the same substitution.
18034
+ */
18035
+ function narrowIngressIpProtocol(properties, onUnusable) {
18036
+ const declared = properties["IpProtocol"];
18037
+ const ipProtocol = requireConfigString(declared, SG_INGRESS_IP_PROTOCOL_DEFAULT, SG_INGRESS_IP_PROTOCOL_PATH, {
18038
+ coerceNumber: true,
18039
+ ...onUnusable && { onUnusable }
18040
+ });
18041
+ if (declared === void 0 || declared === ipProtocol) return {
18042
+ ipProtocol,
18043
+ narrowed: properties
18044
+ };
18045
+ return {
18046
+ ipProtocol,
18047
+ narrowed: {
18048
+ ...properties,
18049
+ IpProtocol: ipProtocol
18050
+ }
18051
+ };
18052
+ }
17992
18053
  /**
17993
18054
  * AWS EC2 Networking Provider
17994
18055
  *
@@ -19170,10 +19231,8 @@ var EC2Provider = class {
19170
19231
  this.logger.debug(`Creating SecurityGroupIngress ${logicalId}`);
19171
19232
  const groupId = properties["GroupId"];
19172
19233
  if (!groupId) throw new ProvisioningError(`GroupId is required for SecurityGroupIngress ${logicalId}`, resourceType, logicalId);
19173
- const ipProtocol = requireConfigString(properties["IpProtocol"], "-1", "AWS::EC2::SecurityGroupIngress IpProtocol", {
19174
- coerceNumber: true,
19175
- ...onUnusableProtocol && { onUnusable: onUnusableProtocol }
19176
- });
19234
+ const { ipProtocol, narrowed } = narrowIngressIpProtocol(properties, onUnusableProtocol);
19235
+ const effectiveProperties = narrowed === properties ? void 0 : narrowed;
19177
19236
  const fromPort = properties["FromPort"];
19178
19237
  const toPort = properties["ToPort"];
19179
19238
  try {
@@ -19188,7 +19247,8 @@ var EC2Provider = class {
19188
19247
  this.logger.debug(`Successfully created SecurityGroupIngress ${logicalId}: ${physicalId}`);
19189
19248
  return {
19190
19249
  physicalId,
19191
- attributes: {}
19250
+ attributes: {},
19251
+ ...effectiveProperties && { effectiveProperties }
19192
19252
  };
19193
19253
  } catch (error) {
19194
19254
  if (error instanceof Error && error.message.includes("already exists")) {
@@ -19196,7 +19256,8 @@ var EC2Provider = class {
19196
19256
  this.logger.debug(`SecurityGroupIngress ${logicalId} already exists, treating as success`);
19197
19257
  return {
19198
19258
  physicalId,
19199
- attributes: {}
19259
+ attributes: {},
19260
+ ...effectiveProperties && { effectiveProperties }
19200
19261
  };
19201
19262
  }
19202
19263
  const cause = error instanceof Error ? error : void 0;
@@ -19215,7 +19276,8 @@ var EC2Provider = class {
19215
19276
  return {
19216
19277
  physicalId: createResult.physicalId,
19217
19278
  wasReplaced: true,
19218
- ...createResult.attributes && { attributes: createResult.attributes }
19279
+ ...createResult.attributes && { attributes: createResult.attributes },
19280
+ ...createResult.effectiveProperties && { effectiveProperties: createResult.effectiveProperties }
19219
19281
  };
19220
19282
  } catch (error) {
19221
19283
  if (error instanceof CdkdError) throw error;
@@ -20293,7 +20355,9 @@ var EC2Provider = class {
20293
20355
  }
20294
20356
  }
20295
20357
  /**
20296
- * The diff-side half of the Route multi-destination narrowing (issue #1591).
20358
+ * The diff-side half of the two EC2 narrowings that report
20359
+ * `effectiveProperties` — Route multi-destination (issue #1591) and
20360
+ * SecurityGroupIngress `IpProtocol` (issue #1633).
20297
20361
  *
20298
20362
  * `createRoute` sends exactly one destination key and reports the narrowed
20299
20363
  * bag via `effectiveProperties`, so state carries one key. Without the same
@@ -20306,8 +20370,21 @@ var EC2Provider = class {
20306
20370
  *
20307
20371
  * Shares `narrowRouteDestinations` with the provisioning path so the two
20308
20372
  * cannot disagree about which key survives.
20373
+ *
20374
+ * The SecurityGroupIngress arm is the same shape and needs the same second
20375
+ * half for the same reason: `IpProtocol` is create-only on that type in the
20376
+ * registry schema, so normalizing state alone would make the template's
20377
+ * original value read as a changed immutable property — a REPLACEMENT, whose
20378
+ * create passes no context and so gets no `onUnusable` downgrade, turning a
20379
+ * previously-green no-op deploy into a hard failure. Normalizing BOTH sides
20380
+ * is also what keeps the fix correct for records written BEFORE it existed,
20381
+ * which still carry the un-narrowed value.
20309
20382
  */
20310
20383
  canonicalizeDesiredProperties(resourceType, properties) {
20384
+ if (resourceType === "AWS::EC2::SecurityGroupIngress") {
20385
+ const { narrowed } = narrowIngressIpProtocol(properties, () => {});
20386
+ return narrowed;
20387
+ }
20311
20388
  if (resourceType !== "AWS::EC2::Route") return properties;
20312
20389
  const { declared, narrowed } = narrowRouteDestinations(properties);
20313
20390
  return declared.length > 1 ? narrowed : properties;
@@ -20789,12 +20866,33 @@ function flattenIpPermissions(perms, direction) {
20789
20866
  * Direction-sensitive: ingress uses `Source*` fields, egress uses
20790
20867
  * `Destination*`.
20791
20868
  */
20869
+ /**
20870
+ * The `IpProtocol` component of a security-group rule identity key.
20871
+ *
20872
+ * A NUMBER is stringified so a record written BEFORE issue #1633 still matches:
20873
+ * AWS always reports the protocol as a string, so a legacy record holding the
20874
+ * number `-1` keyed as `{"p":-1}` against AWS's `{"p":"-1"}` and matched
20875
+ * nothing — `readSecurityGroupIngressCurrentState` returned `undefined` and
20876
+ * `cdkd drift` reported the rule as GONE, forever. The canonicalizer makes such
20877
+ * a record's diff NO_CHANGE, so no deploy ever rewrites it either; normalizing
20878
+ * at the KEY is what heals the existing population with no migration.
20879
+ *
20880
+ * Anything that is neither a string nor a number is passed through UNCHANGED
20881
+ * rather than blanket-`String()`-ed: a malformed object would otherwise key as
20882
+ * the useless `'[object Object]'`, silently collapsing every malformed record
20883
+ * onto one bucket. Such a rule matches nothing either way — this keeps the
20884
+ * non-match honest instead of manufacturing a fake identity.
20885
+ */
20886
+ function sgProtocolKey(value) {
20887
+ if (typeof value === "number") return String(value);
20888
+ return value ?? "-1";
20889
+ }
20792
20890
  function sgRuleKey(rule, direction) {
20793
20891
  const peerKey = direction === "egress" ? rule["DestinationSecurityGroupId"] : rule["SourceSecurityGroupId"];
20794
20892
  const prefixKey = direction === "egress" ? rule["DestinationPrefixListId"] : rule["SourcePrefixListId"];
20795
20893
  const peerOwner = direction === "ingress" ? rule["SourceSecurityGroupOwnerId"] : void 0;
20796
20894
  return JSON.stringify({
20797
- p: rule["IpProtocol"] ?? "-1",
20895
+ p: sgProtocolKey(rule["IpProtocol"]),
20798
20896
  f: rule["FromPort"] ?? null,
20799
20897
  t: rule["ToPort"] ?? null,
20800
20898
  c4: rule["CidrIp"] ?? null,
@@ -53260,7 +53358,7 @@ function isServiceManagedTagKey(key) {
53260
53358
  * provider binds it, and that tag is REQUIRED for managed scaling. Because any
53261
53359
  * CDK ASG declares at least a `Name` tag, `Tags` is template-DECLARED, so the
53262
53360
  * #1498 carve-out (undeclared + captured-empty) correctly does not apply — and
53263
- * the tag list is an ARRAY, which {@link findRevertDroppedAwsKeys}'s walk
53361
+ * the tag list is an ARRAY, which {@link findRevertUnbaselinedAwsKeys}'s walk
53264
53362
  * compares wholesale and never descends into. Post-revert the ASG kept the
53265
53363
  * capacity provider with its managed scaling silently broken (verified live
53266
53364
  * 2026-08-10).
@@ -53332,43 +53430,48 @@ function findRevertPreservedTagKeys(drifts, desiredProperties, awsProperties) {
53332
53430
  return [...preserved].sort();
53333
53431
  }
53334
53432
  /**
53335
- * The AWS-authored property paths a `--revert` is about to DROP, for a
53336
- * resource whose state predates observed-capture (issue #1478).
53433
+ * The AWS-authored property paths a `--revert` LEAVES UNTOUCHED, for a
53434
+ * resource whose state predates observed-capture (issue #1478, semantics
53435
+ * settled by issue #1626).
53337
53436
  *
53338
- * The mechanism, and why this is not a Glue fix: `runRevert` picks the revert
53339
- * baseline as `observedProperties ?? properties`. When `observedProperties` is
53340
- * absent, the desired side is the raw TEMPLATE while the previous side is the
53341
- * AWS-CURRENT snapshot — so {@link buildRevertNewProperties}, which overwrites
53342
- * each drifted top-level key with the desired sub-shape wholesale, replaces
53343
- * every AWS-authored key inside that subtree with the template's version. For
53344
- * a Glue Iceberg table that silently wipes `table_type` /
53345
- * `metadata_location`, the same outcome as issue #1461 reached by a different
53346
- * trigger. The exposure is general: ANY resource where AWS writes into a bag
53347
- * the template does not fully declare, on state written before
53437
+ * The mechanism: `runRevert` picks the revert baseline as
53438
+ * `observedProperties ?? properties`. When `observedProperties` is absent, the
53439
+ * desired side is the raw TEMPLATE while the previous side is the AWS-CURRENT
53440
+ * snapshot — so {@link buildRevertNewProperties}, which overwrites each drifted
53441
+ * top-level key with the desired sub-shape wholesale, has no value to carry for
53442
+ * any AWS-authored key inside that subtree. For a Glue Iceberg table that
53443
+ * reaches `table_type` / `metadata_location`, the same exposure as issue #1461
53444
+ * by a different trigger; the general case is ANY resource where AWS writes
53445
+ * into a bag the template does not fully declare, on state written before
53348
53446
  * observed-capture.
53349
53447
  *
53350
- * The user DID ask to push state over AWS, so this is not silent-wrong the way
53351
- * #1461 was but it is almost certainly not what they meant, and nothing told
53352
- * them. So the chosen semantic is **warn and proceed**: it is the cheapest
53353
- * honest step, preserves today's behavior, and does not foreclose the stricter
53354
- * options (refuse outright, or treat "no observedProperties" as "previous is
53355
- * unknown" so the merge preserves).
53448
+ * #1478 shipped the **warn and proceed** semantic the values were still
53449
+ * erased, the user was merely told first while explicitly leaving the door
53450
+ * open to "treat 'no observedProperties' as 'previous is unknown' so the merge
53451
+ * preserves". Issue #1626 walked through that door for the reasons in
53452
+ * {@link mergeUntemplatedValue}: on this baseline cdkd cannot tell an
53453
+ * AWS-authored value from an out-of-band change, and resetting on a coin flip
53454
+ * is the worse error. So the paths below are now REPORTED AS PRESERVED, and
53455
+ * `mergeUntemplatedValue` is what makes that true by merging them into the bag
53456
+ * `--revert` actually SENDS — which is the only side a wholesale-replace
53457
+ * provider consults. This function is unchanged in what it computes — only in
53458
+ * what the caller does about it.
53356
53459
  *
53357
53460
  * Scoped deliberately:
53358
53461
  *
53359
53462
  * - **Only drifted top-level keys.** Non-drifted keys keep their AWS-current
53360
- * value in `buildRevertNewProperties`, so nothing under them is dropped.
53463
+ * value in `buildRevertNewProperties`, so nothing under them is at stake.
53361
53464
  * - **Only when `observedProperties` is absent.** With it present the desired
53362
53465
  * side is the deploy-time AWS snapshot, which already carries AWS-authored
53363
- * fields — dropping one there is a legitimate revert of a real console
53364
- * change, and warning about it would be noise on every run.
53466
+ * fields — removing one there is a legitimate revert of a real console
53467
+ * change, and reporting it would be noise on every run.
53365
53468
  * - **Only keys that vanish.** A key present on both sides with a different
53366
- * VALUE is the drift the user asked to revert, not a silent loss.
53469
+ * VALUE is the drift the user asked to revert, and it reverts normally.
53367
53470
  *
53368
53471
  * @returns dotted paths, sorted, e.g. `['Parameters.metadata_location']`.
53369
53472
  */
53370
- function findRevertDroppedAwsKeys(drifts, desiredProperties, awsProperties) {
53371
- const dropped = /* @__PURE__ */ new Set();
53473
+ function findRevertUnbaselinedAwsKeys(drifts, desiredProperties, awsProperties) {
53474
+ const missing = /* @__PURE__ */ new Set();
53372
53475
  const driftedTopLevelKeys = /* @__PURE__ */ new Set();
53373
53476
  for (const d of drifts) {
53374
53477
  const topLevelKey = d.path.split(".", 1)[0];
@@ -53376,9 +53479,9 @@ function findRevertDroppedAwsKeys(drifts, desiredProperties, awsProperties) {
53376
53479
  }
53377
53480
  for (const key of driftedTopLevelKeys) {
53378
53481
  if (!(key in desiredProperties)) continue;
53379
- collectMissingPaths(awsProperties[key], desiredProperties[key], key, dropped, isTagListKey(key) ? isServiceManagedTagKey : void 0);
53482
+ collectMissingPaths(awsProperties[key], desiredProperties[key], key, missing, isTagListKey(key) ? isServiceManagedTagKey : void 0);
53380
53483
  }
53381
- return [...dropped].sort();
53484
+ return [...missing].sort();
53382
53485
  }
53383
53486
  /**
53384
53487
  * Walk the AWS-side value against the desired-side value, recording every path
@@ -53412,11 +53515,12 @@ function findRevertDroppedAwsKeys(drifts, desiredProperties, awsProperties) {
53412
53515
  */
53413
53516
  function collectMissingPaths(awsValue, desiredValue, path, out, isPreservedKey) {
53414
53517
  if (isKeyedList(awsValue)) {
53415
- if (!isKeyedList(desiredValue)) {
53518
+ const desiredIsEmptyArray = Array.isArray(desiredValue) && desiredValue.length === 0;
53519
+ if (!isKeyedList(desiredValue) && !desiredIsEmptyArray) {
53416
53520
  if (desiredValue === void 0) out.add(path);
53417
53521
  return;
53418
53522
  }
53419
- const desiredKeys = new Set(desiredValue.map((e) => e.Key));
53523
+ const desiredKeys = new Set(isKeyedList(desiredValue) ? desiredValue.map((e) => e.Key) : []);
53420
53524
  for (const entry of awsValue) {
53421
53525
  if (desiredKeys.has(entry.Key)) continue;
53422
53526
  if (isPreservedKey?.(entry.Key)) continue;
@@ -53441,10 +53545,12 @@ function collectMissingPaths(awsValue, desiredValue, path, out, isPreservedKey)
53441
53545
  /**
53442
53546
  * A NON-EMPTY array whose every element is an object carrying a string `Key`.
53443
53547
  *
53444
- * Emptiness is deliberately excluded: `[]` carries no identities, so treating
53445
- * it as keyed would report nothing on the AWS side and — as the DESIRED side
53446
- * would claim every AWS entry is dropped, which is already what the
53447
- * whole-list arm reports more legibly.
53548
+ * Emptiness is excluded on the AWS side: `[]` carries no identities, so there
53549
+ * is nothing to report. It is NOT excluded on the DESIRED side any more
53550
+ * (issue #1626) — an empty baseline list means every AWS entry is unbaselined,
53551
+ * and since the only caller runs on the template-only baseline where
53552
+ * {@link mergeUntemplatedValue} PRESERVES all of them, reporting nothing would
53553
+ * leave the plan under-claiming what survives.
53448
53554
  */
53449
53555
  function isKeyedList(value) {
53450
53556
  return Array.isArray(value) && value.length > 0 && value.every((e) => typeof e === "object" && e !== null && typeof e.Key === "string");
@@ -53453,6 +53559,95 @@ function isPlainRecord(value) {
53453
53559
  return typeof value === "object" && value !== null && !Array.isArray(value);
53454
53560
  }
53455
53561
  /**
53562
+ * Merge the AWS-current value with the revert baseline, KEEPING every path the
53563
+ * baseline does not declare (issue
53564
+ * [#1626](https://github.com/go-to-k/cdkd/issues/1626) items 2 + 3).
53565
+ *
53566
+ * ## The contract this settles
53567
+ *
53568
+ * `provider.update(logicalId, physicalId, type, newProperties,
53569
+ * previousProperties)` has ONE parameter list and TWO callers:
53570
+ *
53571
+ * - the deploy engine passes the state-recorded `properties` — the
53572
+ * LAST-DEPLOYED TEMPLATE — so a path present on the previous side and absent
53573
+ * from the desired side means exactly one thing: **the user removed it from
53574
+ * the template**;
53575
+ * - `runRevert` passes the FULL `readCurrentState` snapshot, so the same
53576
+ * absence ALSO covers **a path the template never declared at all**.
53577
+ *
53578
+ * Both provider shapes then destroy that path, by different routes. One that
53579
+ * KEY-DIFFS a collection reads the absence as a REMOVAL — ELBv2's
53580
+ * `LoadBalancerAttributes`, where `readCurrentState` deliberately emits every
53581
+ * attribute AWS reports, puts ~18 untemplated attributes on the removal path
53582
+ * against a template declaring one or two. One that REPLACES a bag wholesale
53583
+ * (`PutBucketTagging` is documented full-replace; the same holds for every
53584
+ * `Put*Configuration`) never consults the previous side at all and simply
53585
+ * sends a bag the untemplated path is missing from.
53586
+ *
53587
+ * That second shape is why the fix lands on the DESIRED side rather than by
53588
+ * trimming `previousProperties`: trimming stops the key-diffing removal but
53589
+ * leaves the wholesale-replace drop untouched, so half the reported paths
53590
+ * would still be erased while the plan claimed otherwise. Merging into the bag
53591
+ * cdkd actually SENDS covers both, and is what makes the plan's "left
53592
+ * untouched" line true rather than aspirational.
53593
+ *
53594
+ * ## Scoped to the baseline that cannot tell the two apart
53595
+ *
53596
+ * Applied ONLY when `observedProperties` is absent — the same gate as
53597
+ * {@link findRevertUnbaselinedAwsKeys}'s notice, and for the same reason. With
53598
+ * observed-capture present the baseline IS a deploy-time AWS snapshot, so a
53599
+ * path AWS reports and the baseline lacks was genuinely added out-of-band and
53600
+ * removing it is the revert the user asked for (the `drift-revert` integ
53601
+ * fixture requires exactly that for an injected tag). Without it the baseline
53602
+ * is the raw TEMPLATE, and "AWS has it, the template does not" is
53603
+ * indistinguishable from "AWS authored it" — so the honest answer is to leave
53604
+ * it alone. `findRevertUnbaselinedAwsKeys`'s docstring already named this as
53605
+ * one of the stricter options #1478 did not foreclose ("treat 'no
53606
+ * observedProperties' as 'previous is unknown' so the merge preserves"); this
53607
+ * is that option.
53608
+ *
53609
+ * The structural rules mirror {@link collectMissingPaths} case for case —
53610
+ * plain records descend, KEYED `[{Key, Value}]` lists merge by `Key`,
53611
+ * positional arrays and scalars are taken from the baseline wholesale — so
53612
+ * every path the plan names as preserved IS kept, and a unit test pins that
53613
+ * correspondence by diffing the flag-on and flag-off outputs.
53614
+ *
53615
+ * A service-authored tag is absent from both sides of that comparison and
53616
+ * stays consistent: `findRevertUnbaselinedAwsKeys` omits it
53617
+ * (`isServiceManagedTagKey`) because the #1501 carve-out already preserves it
53618
+ * on the DEFAULT path, so it is not something this flag rescues and reporting
53619
+ * it would warn about a loss that cannot happen. The test pins that it
53620
+ * survives under BOTH settings.
53621
+ *
53622
+ * A path the baseline DOES declare always wins, which is what keeps the revert
53623
+ * a revert: a drifted value is reset, and one AWS dropped is re-added.
53624
+ */
53625
+ function mergeUntemplatedValue(awsValue, desiredValue) {
53626
+ const desiredIsEmptyArray = Array.isArray(desiredValue) && desiredValue.length === 0;
53627
+ if (isKeyedList(awsValue) && (isKeyedList(desiredValue) || desiredIsEmptyArray)) {
53628
+ const desiredEntries = isKeyedList(desiredValue) ? desiredValue : [];
53629
+ const desiredByKey = new Map(desiredEntries.map((e) => [e.Key, e]));
53630
+ const merged = [];
53631
+ const emitted = /* @__PURE__ */ new Set();
53632
+ for (const entry of awsValue) {
53633
+ if (emitted.has(entry.Key)) continue;
53634
+ emitted.add(entry.Key);
53635
+ merged.push(desiredByKey.get(entry.Key) ?? entry);
53636
+ }
53637
+ for (const entry of desiredEntries) {
53638
+ if (emitted.has(entry.Key)) continue;
53639
+ emitted.add(entry.Key);
53640
+ merged.push(entry);
53641
+ }
53642
+ return merged;
53643
+ }
53644
+ if (!isPlainRecord(awsValue) || !isPlainRecord(desiredValue)) return desiredValue;
53645
+ const merged = {};
53646
+ for (const [key, value] of Object.entries(awsValue)) merged[key] = key in desiredValue ? mergeUntemplatedValue(value, desiredValue[key]) : value;
53647
+ for (const [key, value] of Object.entries(desiredValue)) if (!(key in awsValue)) merged[key] = value;
53648
+ return merged;
53649
+ }
53650
+ /**
53456
53651
  * Build the `newProperties` object passed to `provider.update` during
53457
53652
  * `--revert`. Strategy:
53458
53653
  *
@@ -53494,8 +53689,16 @@ function isPlainRecord(value) {
53494
53689
  * (`Tags[0].Value`) — array drifts surface as a single entry on the
53495
53690
  * parent path — so `path.split('.', 1)` is always safe to extract the
53496
53691
  * top-level key.
53692
+ *
53693
+ * `preserveUntemplated` (issue #1626) switches the overlay from WHOLESALE to a
53694
+ * deep merge for the drifted subtrees, keeping every path the baseline does
53695
+ * not declare. `runRevert` sets it exactly when the resource has no
53696
+ * `observedProperties` — see {@link mergeUntemplatedValue} for why that is the
53697
+ * only baseline on which it is correct, and why the fix has to live on this
53698
+ * side rather than on `previousProperties`.
53497
53699
  */
53498
- function buildRevertNewProperties(drifts, desiredProperties, awsProperties) {
53700
+ function buildRevertNewProperties(drifts, desiredProperties, awsProperties, options = {}) {
53701
+ const preserveUntemplated = options.preserveUntemplated === true;
53499
53702
  const result = { ...awsProperties };
53500
53703
  for (const d of drifts) {
53501
53704
  const topLevelKey = d.path.split(".", 1)[0];
@@ -53503,8 +53706,11 @@ function buildRevertNewProperties(drifts, desiredProperties, awsProperties) {
53503
53706
  if (topLevelKey in desiredProperties) {
53504
53707
  const desiredValue = desiredProperties[topLevelKey];
53505
53708
  const awsValue = awsProperties[topLevelKey];
53506
- const baselineIsTagList = isCfnTagList(desiredValue) || Array.isArray(desiredValue) && desiredValue.length === 0;
53507
- result[topLevelKey] = isTagListKey(topLevelKey) && baselineIsTagList && isCfnTagList(awsValue) ? mergeTagListForRevert(desiredValue, awsValue) : desiredValue;
53709
+ if (preserveUntemplated) result[topLevelKey] = mergeUntemplatedValue(awsValue, desiredValue);
53710
+ else {
53711
+ const baselineIsTagList = isCfnTagList(desiredValue) || Array.isArray(desiredValue) && desiredValue.length === 0;
53712
+ result[topLevelKey] = isTagListKey(topLevelKey) && baselineIsTagList && isCfnTagList(awsValue) ? mergeTagListForRevert(desiredValue, awsValue) : desiredValue;
53713
+ }
53508
53714
  }
53509
53715
  }
53510
53716
  return result;
@@ -53567,7 +53773,7 @@ async function runRevert(reports, providerRegistry, stateConfig, awsClients, opt
53567
53773
  provisionedBy: stateResource.provisionedBy
53568
53774
  }).provider;
53569
53775
  const desiredProperties = stateResource.observedProperties ?? stateResource.properties ?? {};
53570
- const newProperties = buildRevertNewProperties(outcome.changes, desiredProperties, outcome.awsProperties);
53776
+ const newProperties = buildRevertNewProperties(outcome.changes, desiredProperties, outcome.awsProperties, { preserveUntemplated: stateResource.observedProperties === void 0 });
53571
53777
  try {
53572
53778
  await withRetry(() => provider.update(outcome.logicalId, stateResource.physicalId, outcome.resourceType, newProperties, outcome.awsProperties), outcome.logicalId, { logger: { debug: (msg) => logger.debug(msg) } });
53573
53779
  totalSucceeded++;
@@ -53634,16 +53840,17 @@ function printRevertPlan(reports) {
53634
53840
  const tagWord = preserved.length === 1 ? "tag" : "tags";
53635
53841
  process.stdout.write(` ! reverting this tag list KEEPS ${preserved.length} AWS-authored ${tagWord} the baseline does not carry:\n`);
53636
53842
  for (const path of preserved) process.stdout.write(` ${path}\n`);
53637
- process.stdout.write(" Every other tag reverts normally. A service may require these (ECS needs AmazonECSManaged for managed scaling); 'aws:'-prefixed keys are AWS-reserved and cannot be removed by hand.\n");
53843
+ const othersRevert = stateResource.observedProperties === void 0 ? "" : `Every other tag reverts normally. `;
53844
+ process.stdout.write(` ${othersRevert}A service may require these (ECS needs AmazonECSManaged for managed scaling); 'aws:'-prefixed keys are AWS-reserved and cannot be removed by hand.\n`);
53638
53845
  }
53639
53846
  }
53640
53847
  if (stateResource && stateResource.observedProperties === void 0) {
53641
- const dropped = findRevertDroppedAwsKeys(o.changes, stateResource.properties ?? {}, o.awsProperties);
53642
- if (dropped.length > 0) {
53643
- const word = dropped.length === 1 ? "value" : "values";
53644
- process.stdout.write(` ! this resource has no observed-capture baseline, so the revert pushes the raw TEMPLATE and will DROP ${dropped.length} AWS-authored ${word}:\n`);
53645
- for (const path of dropped) process.stdout.write(` ${path}\n`);
53646
- process.stdout.write(" Re-deploy the stack first to populate observedProperties if you want these preserved.\n");
53848
+ const unbaselined = findRevertUnbaselinedAwsKeys(o.changes, stateResource.properties ?? {}, o.awsProperties);
53849
+ if (unbaselined.length > 0) {
53850
+ const word = unbaselined.length === 1 ? "value" : "values";
53851
+ process.stdout.write(` ! this resource has no observed-capture baseline, so the revert pushes the raw TEMPLATE and LEAVES ${unbaselined.length} AWS-authored ${word} untouched:\n`);
53852
+ for (const path of unbaselined) process.stdout.write(` ${path}\n`);
53853
+ process.stdout.write(` The template does not declare these, so cdkd cannot tell an AWS-authored value from an out-of-band change and will not reset either (issue #1626). Run 'cdkd state refresh-observed ${report.stackName}' (or re-deploy) to populate observedProperties if you want them reverted too.\n`);
53647
53854
  }
53648
53855
  }
53649
53856
  }
@@ -70595,7 +70802,7 @@ function createMigrateCommand() {
70595
70802
  */
70596
70803
  function buildProgram() {
70597
70804
  const program = new Command();
70598
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.280.28");
70805
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.280.30");
70599
70806
  program.hook("preAction", (_thisCommand, actionCommand) => {
70600
70807
  const { profile } = actionCommand.optsWithGlobals();
70601
70808
  if (profile !== void 0) process.env["AWS_PROFILE"] = profile;