@go-to-k/cdkd 0.280.21 → 0.280.23

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 requireConfigString, $t as processStackMessages, A as red, At as runDockerStreaming, B as isTerminationProtectionPropagationError, Bt as resolveSkipPrefix, C as isStatefulRecreateTargetSync, Ct as parseBootstrapMarker, D as cyan, Dn as normalizeAwsError, Dt as formatDockerLoginError, E as bold, Et as buildDockerImage, F as ProviderRegistry, Ft as getDefaultStateBucketName, G as normalizeAwsTagsToCfn, Gt as warnDeprecatedNoPrefixCliFlag, H as cfnRefValueFromPhysicalId, Ht as resolveStateBucketWithDefaultAndSource, I as findActionableSilentDrops, It as getLegacyStateBucketName, J as configStringRefusal, Jt as MIGRATE_TMP_PREFIX, K as resolveExplicitPhysicalId, Kt as CFN_TEMPLATE_BODY_LIMIT, L as CloudControlProvider, Lt as resolveApp, M as IAMRoleProvider, Mt as getDockerImageBySourceHash, N as collectInlinePolicyNamesManagedBySiblings, Nt as Synthesizer, O as gray, On as withErrorHandling, Ot as getDockerCmd, P as clearOnUpdateRemoval, Pt as synthesisStatusMessage, Q as requireConfigObject, Qt as AssemblyReader, R as slowCcOperationTimeoutMs, Rt as resolveAutoAssetStorage, S as MULTI_REGION_RECREATE_BLOCKED_TYPES, Sn as StackTerminationProtectionError, St as getBootstrapMarkerKey, T as formatResourceLine, Tt as validateContainerRepoName, U as refStateLookupFromResource, Ut as resolveUseCdkBootstrapAssets, V as IntrinsicFunctionResolver, Vt as resolveStateBucketWithDefault, W as WAFv2WebACLProvider, Wt as stateBucketExistenceConfirmed, X as replayWarn, Xt as uploadCfnTemplate, Y as readConfigString, Yt as findLargeInlineResources, Z as requireConfigArray, Zt as expectedOwnerParam, _ as createPreDeleteFinalSnapshot, _n as PartialFailureError, _t as loadPublishableAssetManifest, a as DeploymentEventsStore, an as setAwsClients, at as isThrottlingError, b as unsupportedFinalSnapshotError, bn as ResourceUpdateNotSupportedError, bt as BOOTSTRAP_MARKER_PREFIX, c as replayFailedOperations, ct as LockManager, d as IMPLICIT_DELETE_DEPENDENCIES, dn as LocalInvokeBuildError$1, dt as shouldRetainResource, et as applyRoleArnIfSet, f as computeImplicitDeleteEdges, fn as LocalMigrateError, ft as AssetPublisher, g as ccRoutedFinalSnapshotError, gn as NestedStackChildDirectDestroyError, gt as createAssetRedirectResolver, h as buildFinalSnapshotIdentifier, hn as MissingCdkCliError, ht as buildAssetRedirectMap, i as DeploymentEventsReader, in as resetAwsClients, it as isRetryableTransientError, j as yellow, jt as AssetManifestLoader, k as green, kt as runDockerForeground, l as replayRollback, lt as S3StateBackend, m as PRE_DELETE_SNAPSHOT_TYPES, mt as WorkGraph, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as AwsClients, nt as describeTypeWithThrottleRetry, o as planFailedOps, ot as DagBuilder, p as ATOMIC_FINAL_SNAPSHOT_TYPES, pn as LocalStartServiceError, pt as stringifyValue, q as assertRegionMatch, qt as CFN_TEMPLATE_URL_LIMIT, r as DeployEngine, rn as getAwsClients, rt as withRetry, s as planRollback, sn as CdkdError, st as TemplateParser, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as resolveBucketRegion, tt as DiffCalculator, u as withResourceDeadline, un as DeployCancelledError, ut as rebuildClientForBucketRegion, v as isFinalSnapshotError, vn as ProvisioningError, vt as rewriteTemplateAssetReferences, w as renderStatefulReason, wt as validateAssetBucketName, x as extractDeploymentEventError, xn as StackHasActiveImportsError, xt as ensureAssetStorage, y as refusesFinalSnapshot, yn as ResourceTimeoutError, yt as AssetModeResolver, z as disableInstanceApiTermination, zt as resolveCaptureObservedState } from "./deploy-engine-2LVq2qWa.js";
2
+ import { $ as requireConfigString, $t as processStackMessages, A as red, At as runDockerStreaming, B as isTerminationProtectionPropagationError, Bt as resolveSkipPrefix, C as isStatefulRecreateTargetSync, Ct as parseBootstrapMarker, D as cyan, Dn as normalizeAwsError, Dt as formatDockerLoginError, E as bold, Et as buildDockerImage, F as ProviderRegistry, Ft as getDefaultStateBucketName, G as normalizeAwsTagsToCfn, Gt as warnDeprecatedNoPrefixCliFlag, H as cfnRefValueFromPhysicalId, Ht as resolveStateBucketWithDefaultAndSource, I as findActionableSilentDrops, It as getLegacyStateBucketName, J as configStringRefusal, Jt as MIGRATE_TMP_PREFIX, K as resolveExplicitPhysicalId, Kt as CFN_TEMPLATE_BODY_LIMIT, L as CloudControlProvider, Lt as resolveApp, M as IAMRoleProvider, Mt as getDockerImageBySourceHash, N as collectInlinePolicyNamesManagedBySiblings, Nt as Synthesizer, O as gray, On as withErrorHandling, Ot as getDockerCmd, P as clearOnUpdateRemoval, Pt as synthesisStatusMessage, Q as requireConfigObject, Qt as AssemblyReader, R as slowCcOperationTimeoutMs, Rt as resolveAutoAssetStorage, S as MULTI_REGION_RECREATE_BLOCKED_TYPES, Sn as StackTerminationProtectionError, St as getBootstrapMarkerKey, T as formatResourceLine, Tt as validateContainerRepoName, U as refStateLookupFromResource, Ut as resolveUseCdkBootstrapAssets, V as IntrinsicFunctionResolver, Vt as resolveStateBucketWithDefault, W as WAFv2WebACLProvider, Wt as stateBucketExistenceConfirmed, X as replayWarn, Xt as uploadCfnTemplate, Y as readConfigString, Yt as findLargeInlineResources, Z as requireConfigArray, Zt as expectedOwnerParam, _ as createPreDeleteFinalSnapshot, _n as PartialFailureError, _t as loadPublishableAssetManifest, a as DeploymentEventsStore, an as setAwsClients, at as isThrottlingError, b as unsupportedFinalSnapshotError, bn as ResourceUpdateNotSupportedError, bt as BOOTSTRAP_MARKER_PREFIX, c as replayFailedOperations, ct as LockManager, d as IMPLICIT_DELETE_DEPENDENCIES, dn as LocalInvokeBuildError$1, dt as shouldRetainResource, et as applyRoleArnIfSet, f as computeImplicitDeleteEdges, fn as LocalMigrateError, ft as AssetPublisher, g as ccRoutedFinalSnapshotError, gn as NestedStackChildDirectDestroyError, gt as createAssetRedirectResolver, h as buildFinalSnapshotIdentifier, hn as MissingCdkCliError, ht as buildAssetRedirectMap, i as DeploymentEventsReader, in as resetAwsClients, it as isRetryableTransientError, j as yellow, jt as AssetManifestLoader, k as green, kt as runDockerForeground, l as replayRollback, lt as S3StateBackend, m as PRE_DELETE_SNAPSHOT_TYPES, mt as WorkGraph, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as AwsClients, nt as describeTypeWithThrottleRetry, o as planFailedOps, ot as DagBuilder, p as ATOMIC_FINAL_SNAPSHOT_TYPES, pn as LocalStartServiceError, pt as stringifyValue, q as assertRegionMatch, qt as CFN_TEMPLATE_URL_LIMIT, r as DeployEngine, rn as getAwsClients, rt as withRetry, s as planRollback, sn as CdkdError, st as TemplateParser, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as resolveBucketRegion, tt as DiffCalculator, u as withResourceDeadline, un as DeployCancelledError, ut as rebuildClientForBucketRegion, v as isFinalSnapshotError, vn as ProvisioningError, vt as rewriteTemplateAssetReferences, w as renderStatefulReason, wt as validateAssetBucketName, x as extractDeploymentEventError, xn as StackHasActiveImportsError, xt as ensureAssetStorage, y as refusesFinalSnapshot, yn as ResourceTimeoutError, yt as AssetModeResolver, z as disableInstanceApiTermination, zt as resolveCaptureObservedState } from "./deploy-engine-DmxpjXP8.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-FOwZJoaD.js";
4
+ import { t as ASGProvider } from "./asg-provider-DsxUvLEc.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";
@@ -4984,7 +4984,14 @@ var S3BucketProvider = class S3BucketProvider {
4984
4984
  /**
4985
4985
  * Apply versioning configuration if specified
4986
4986
  */
4987
- async applyVersioning(bucketName, versioningConfig) {
4987
+ async applyVersioning(bucketName, versioningConfig, onUnusable) {
4988
+ if (onUnusable) {
4989
+ const refusal = configStringRefusal(versioningConfig, "Status", "Suspended", "AWS::S3::Bucket VersioningConfiguration");
4990
+ if (refusal !== void 0) {
4991
+ onUnusable(`${refusal}. No PutBucketVersioning is issued rather than applying the Suspended default, which would turn object versioning OFF on an existing bucket (on a replayed create it means the bucket is left unversioned); the same value is REFUSED on a template-path create`);
4992
+ return;
4993
+ }
4994
+ }
4988
4995
  const status = readConfigString(versioningConfig, "Status", "Suspended", "AWS::S3::Bucket VersioningConfiguration");
4989
4996
  await this.s3Client.send(new PutBucketVersioningCommand({
4990
4997
  Bucket: bucketName,
@@ -5262,7 +5269,14 @@ var S3BucketProvider = class S3BucketProvider {
5262
5269
  * - LogFilePrefix -> SDK TargetPrefix
5263
5270
  * SDK: PutBucketLogging with BucketLoggingStatus.LoggingEnabled
5264
5271
  */
5265
- async applyLoggingConfiguration(bucketName, loggingConfig) {
5272
+ async applyLoggingConfiguration(bucketName, loggingConfig, onUnusable) {
5273
+ if (onUnusable) {
5274
+ const refusal = configStringRefusal(loggingConfig, "DestinationBucketName", "", "AWS::S3::Bucket LoggingConfiguration") ?? configStringRefusal(loggingConfig, "LogFilePrefix", "", "AWS::S3::Bucket LoggingConfiguration");
5275
+ if (refusal !== void 0) {
5276
+ onUnusable(`${refusal}. No PutBucketLogging is issued rather than clearing access logging, which would turn it OFF on an existing bucket (on a replayed create it means the bucket comes up with no access logging); the same value is REFUSED on a template-path create`);
5277
+ return;
5278
+ }
5279
+ }
5266
5280
  const destinationBucket = readConfigString(loggingConfig, "DestinationBucketName", "", "AWS::S3::Bucket LoggingConfiguration");
5267
5281
  if (!loggingConfig || destinationBucket === "") {
5268
5282
  await this.s3Client.send(new PutBucketLoggingCommand({
@@ -5589,11 +5603,20 @@ var S3BucketProvider = class S3BucketProvider {
5589
5603
  const s3Dest = this.resolveS3BucketDestination(rawDest, inventoryDestPath, onUnusable);
5590
5604
  if (s3Dest === void 0 && rawDest != null) continue;
5591
5605
  const destPath = s3BucketDestinationPath(rawDest, inventoryDestPath);
5606
+ const scheduleFrequencyRefusal = onUnusable ? configStringRefusal(config, "ScheduleFrequency", "Weekly", "AWS::S3::Bucket InventoryConfigurations[]") : void 0;
5607
+ if (scheduleFrequencyRefusal !== void 0 && config["Schedule"] == null) {
5608
+ onUnusable?.(`${scheduleFrequencyRefusal}. Skipping this inventory configuration: there is no Schedule.Frequency to fall back to, and defaulting to Weekly would silently re-cadence a live inventory report; the same value is REFUSED on a template-path create`);
5609
+ continue;
5610
+ }
5611
+ if (scheduleFrequencyRefusal !== void 0) onUnusable?.(`${scheduleFrequencyRefusal}. Falling back to Schedule.Frequency; the same value is REFUSED on a template-path create`);
5612
+ const useScheduleFrequency = config["ScheduleFrequency"] !== void 0 && scheduleFrequencyRefusal === void 0;
5613
+ if (!useScheduleFrequency && this.skipOnUnusableConfigString(config["Schedule"], "Frequency", "Weekly", "AWS::S3::Bucket InventoryConfigurations[].Schedule", "Skipping this inventory configuration", onUnusable)) continue;
5614
+ const frequency = useScheduleFrequency ? requireConfigString(config["ScheduleFrequency"], "Weekly", "AWS::S3::Bucket InventoryConfigurations[].ScheduleFrequency") : readConfigString(config["Schedule"], "Frequency", "Weekly", "AWS::S3::Bucket InventoryConfigurations[].Schedule");
5592
5615
  const inventoryConfig = {
5593
5616
  Id: id,
5594
5617
  IsEnabled: config["Enabled"] ?? true,
5595
5618
  IncludedObjectVersions: readConfigString(config, "IncludedObjectVersions", "All", "AWS::S3::Bucket InventoryConfigurations[]"),
5596
- Schedule: { Frequency: config["ScheduleFrequency"] !== void 0 ? requireConfigString(config["ScheduleFrequency"], "Weekly", "AWS::S3::Bucket InventoryConfigurations[].ScheduleFrequency") : readConfigString(config["Schedule"], "Frequency", "Weekly", "AWS::S3::Bucket InventoryConfigurations[].Schedule") },
5619
+ Schedule: { Frequency: frequency },
5597
5620
  Destination: { S3BucketDestination: s3Dest ? {
5598
5621
  Bucket: s3Dest["BucketArn"] ?? s3Dest["Bucket"],
5599
5622
  AccountId: s3Dest["BucketAccountId"],
@@ -5762,9 +5785,10 @@ var S3BucketProvider = class S3BucketProvider {
5762
5785
  * in both places would double-PUT.
5763
5786
  */
5764
5787
  async applyConfiguration(bucketName, properties, options = {}) {
5765
- const { skipTags = false, skipDiffManaged = false } = options;
5788
+ const { skipTags = false, skipDiffManaged = false, context } = options;
5789
+ const replayOnUnusable = replayWarn(this.logger, context).onUnusable;
5766
5790
  const versioningConfig = properties["VersioningConfiguration"];
5767
- if (!skipDiffManaged && versioningConfig != null) await this.applyVersioning(bucketName, versioningConfig);
5791
+ if (!skipDiffManaged && versioningConfig != null) await this.applyVersioning(bucketName, versioningConfig, replayOnUnusable);
5768
5792
  const tags = properties["Tags"];
5769
5793
  if (!skipTags && tags && Array.isArray(tags) && tags.length > 0) await this.applyTags(bucketName, tags);
5770
5794
  const ownershipControls = properties["OwnershipControls"];
@@ -5897,8 +5921,10 @@ var S3BucketProvider = class S3BucketProvider {
5897
5921
  const previousVersioning = previousProperties["VersioningConfiguration"];
5898
5922
  const nextVersioning = properties["VersioningConfiguration"];
5899
5923
  const versioningChanged = JSON.stringify(previousVersioning ?? null) !== JSON.stringify(nextVersioning ?? null);
5900
- const desiredVersioningStatus = readConfigString(nextVersioning, "Status", "Suspended", "AWS::S3::Bucket VersioningConfiguration");
5901
- if (versioningChanged && nextVersioning != null && desiredVersioningStatus !== "Suspended") await this.applyVersioning(bucketName, nextVersioning);
5924
+ const versioningRefusal = configStringRefusal(nextVersioning, "Status", "Suspended", "AWS::S3::Bucket VersioningConfiguration");
5925
+ if (versioningRefusal !== void 0 && versioningChanged) this.logger.warn(`Bucket ${bucketName}: ${versioningRefusal}. Leaving the bucket's LIVE versioning state unchanged — neither enabling nor suspending it; the same value is REFUSED on a template-path create`);
5926
+ const desiredVersioningStatus = versioningRefusal === void 0 ? readConfigString(nextVersioning, "Status", "Suspended", "AWS::S3::Bucket VersioningConfiguration") : void 0;
5927
+ if (versioningRefusal === void 0 && versioningChanged && nextVersioning != null && desiredVersioningStatus !== "Suspended") await this.applyVersioning(bucketName, nextVersioning, (m) => this.logger.warn(m));
5902
5928
  await this.diffSubConfig(bucketName, S3BucketProvider.emptyListConfigToUndefined(previousProperties["OwnershipControls"], "Rules"), S3BucketProvider.emptyListConfigToUndefined(properties["OwnershipControls"], "Rules"), async (cfg) => this.applyOwnershipControls(bucketName, cfg), async () => {
5903
5929
  await this.s3Client.send(new DeleteBucketOwnershipControlsCommand({ Bucket: bucketName }));
5904
5930
  this.logger.debug(`Deleted ownership controls on bucket ${bucketName}`);
@@ -5925,7 +5951,7 @@ var S3BucketProvider = class S3BucketProvider {
5925
5951
  await this.s3Client.send(new DeleteBucketWebsiteCommand({ Bucket: bucketName }));
5926
5952
  this.logger.debug(`Deleted website configuration on bucket ${bucketName}`);
5927
5953
  });
5928
- await this.diffSubConfig(bucketName, previousProperties["LoggingConfiguration"], properties["LoggingConfiguration"], async (cfg) => this.applyLoggingConfiguration(bucketName, cfg), async () => this.applyLoggingConfiguration(bucketName, void 0));
5954
+ await this.diffSubConfig(bucketName, previousProperties["LoggingConfiguration"], properties["LoggingConfiguration"], async (cfg) => this.applyLoggingConfiguration(bucketName, cfg, (m) => this.logger.warn(m)), async () => this.applyLoggingConfiguration(bucketName, void 0));
5929
5955
  await this.diffSubConfig(bucketName, previousProperties["NotificationConfiguration"], properties["NotificationConfiguration"], async (cfg) => this.applyNotificationConfiguration(bucketName, cfg), async () => this.applyNotificationConfiguration(bucketName, void 0));
5930
5956
  await this.diffSubConfig(bucketName, previousProperties["ReplicationConfiguration"], properties["ReplicationConfiguration"], async (cfg) => this.applyReplicationConfiguration(bucketName, cfg, (m) => this.logger.warn(m)), async () => {
5931
5957
  await this.s3Client.send(new DeleteBucketReplicationCommand({ Bucket: bucketName }));
@@ -5989,7 +6015,9 @@ var S3BucketProvider = class S3BucketProvider {
5989
6015
  const lifecycleConfig = properties["LifecycleConfiguration"];
5990
6016
  if (lifecycleConfig?.Rules && Array.isArray(lifecycleConfig.Rules) && lifecycleConfig.Rules.length > 0) await this.applyLifecycleConfiguration(bucketName, lifecycleConfig, replayOnUnusable);
5991
6017
  const loggingConfig = properties["LoggingConfiguration"];
5992
- if (readConfigString(loggingConfig, "DestinationBucketName", "", "AWS::S3::Bucket LoggingConfiguration") !== "") await this.applyLoggingConfiguration(bucketName, loggingConfig);
6018
+ const loggingRefusal = replayOnUnusable ? configStringRefusal(loggingConfig, "DestinationBucketName", "", "AWS::S3::Bucket LoggingConfiguration") : void 0;
6019
+ if (loggingRefusal !== void 0) replayOnUnusable?.(`${loggingRefusal}. Skipping the logging configuration; the same value is REFUSED on a template-path create`);
6020
+ else if (readConfigString(loggingConfig, "DestinationBucketName", "", "AWS::S3::Bucket LoggingConfiguration") !== "") await this.applyLoggingConfiguration(bucketName, loggingConfig, replayOnUnusable);
5993
6021
  const websiteConfig = properties["WebsiteConfiguration"];
5994
6022
  if (websiteConfig) await this.applyWebsiteConfiguration(bucketName, websiteConfig);
5995
6023
  const accelerateConfig = properties["AccelerateConfiguration"];
@@ -6032,7 +6060,7 @@ var S3BucketProvider = class S3BucketProvider {
6032
6060
  else throw createError;
6033
6061
  }
6034
6062
  try {
6035
- await this.applyConfiguration(bucketName, properties);
6063
+ await this.applyConfiguration(bucketName, properties, { ...context ? { context } : {} });
6036
6064
  await this.applyAllSubConfigsForCreate(bucketName, properties, context);
6037
6065
  } catch (innerError) {
6038
6066
  if (createdNewBucket) try {
@@ -11857,6 +11885,51 @@ function readCapacityNumber(block, member) {
11857
11885
  return Number.isFinite(value) ? value : void 0;
11858
11886
  }
11859
11887
  /**
11888
+ * Whether a desired `GlobalSecondaryIndexes[]` entry declares BOTH capacity
11889
+ * members usably — the same test the BillingMode flip's per-index block applies
11890
+ * before it forwards the index, expressed as a predicate for the pre-flip
11891
+ * removal's look-ahead (issue #1617).
11892
+ *
11893
+ * BOTH members are required, exactly as in the flip block: an entry declaring
11894
+ * only one is treated like an absent one there, so accepting it here would let
11895
+ * the pre-flip delete run ahead of a flip AWS still rejects.
11896
+ */
11897
+ function hasUsableDeclaredCapacity(entry) {
11898
+ const declared = entry?.["ProvisionedThroughput"];
11899
+ return readCapacityNumber(declared, "ReadCapacityUnits") !== void 0 && readCapacityNumber(declared, "WriteCapacityUnits") !== void 0;
11900
+ }
11901
+ /**
11902
+ * Whether the desired TABLE-level `ProvisionedThroughput` is one the flip can
11903
+ * actually send (issue #1617 PR review).
11904
+ *
11905
+ * Deliberately NOT {@link hasUsableDeclaredCapacity}: the flip defaults each
11906
+ * member to 5 when absent (`Number(pt['ReadCapacityUnits'] ?? 5)`), so a
11907
+ * half-declared TABLE capacity is sendable while a half-declared per-INDEX one
11908
+ * is not.
11909
+ *
11910
+ * It mirrors the flip's GATE as well as its arithmetic, which the first version
11911
+ * did not and a second review round caught — both halves of that miss are real:
11912
+ *
11913
+ * - the gate is plain TRUTHINESS (`properties['ProvisionedThroughput']`), so a
11914
+ * non-object truthy value sends `{5, 5}` and SUCCEEDS. Requiring a plain
11915
+ * object refused it and skipped a removal that would have been fine.
11916
+ * - a DECLARED member that coerces to `0` (`''`, `false`, `[]`) is finite, so
11917
+ * an arithmetic-only check accepted it — while AWS rejects a capacity below
11918
+ * 1, which is precisely the deterministic rejection this predicate exists to
11919
+ * keep a delete from running ahead of.
11920
+ */
11921
+ function hasUsableTableCapacity(value) {
11922
+ if (!value) return false;
11923
+ if (typeof value !== "object" || Array.isArray(value)) return true;
11924
+ const pt = value;
11925
+ for (const member of ["ReadCapacityUnits", "WriteCapacityUnits"]) {
11926
+ if (pt[member] === void 0) continue;
11927
+ const n = Number(pt[member]);
11928
+ if (!Number.isFinite(n) || n < 1) return false;
11929
+ }
11930
+ return true;
11931
+ }
11932
+ /**
11860
11933
  * `DescribeTable` poll budget (seconds, 1 poll/s) after an ordinary
11861
11934
  * `UpdateTable` — capacity, TTL, tags, table class. Calibrated on those and
11862
11935
  * sufficient for them.
@@ -11997,6 +12070,41 @@ var DynamoDBTableProvider = class {
11997
12070
  const billingMode = billingModeUnusable ? prevBillingMode : requestedBillingMode;
11998
12071
  const billingOrThroughputChanged = billingMode !== prevBillingMode || JSON.stringify(properties["ProvisionedThroughput"]) !== JSON.stringify(previousProperties["ProvisionedThroughput"]);
11999
12072
  if (billingOrThroughputChanged && billingMode === "PROVISIONED" && (prevBillingMode === "PAY_PER_REQUEST" || liveBillingMode === "PAY_PER_REQUEST") && properties["OnDemandThroughput"] !== void 0) throw new ProvisioningError(`AWS::DynamoDB::Table ${logicalId}: the template flips BillingMode to PROVISIONED${properties["BillingMode"] === void 0 ? " (BillingMode is absent from the template, which CloudFormation resets to PROVISIONED)" : ""} while still declaring OnDemandThroughput, which AWS accepts only on a PAY_PER_REQUEST table. Nothing was applied. Remove OnDemandThroughput, or keep BillingMode: PAY_PER_REQUEST.`, logicalId, resourceType);
12073
+ const liveIndexes = table?.GlobalSecondaryIndexes ?? [];
12074
+ const desiredIndexByName = /* @__PURE__ */ new Map();
12075
+ for (const entry of Array.isArray(properties["GlobalSecondaryIndexes"]) ? properties["GlobalSecondaryIndexes"] : []) {
12076
+ const name = entry?.["IndexName"];
12077
+ if (typeof name === "string") desiredIndexByName.set(name, entry);
12078
+ }
12079
+ const preFlipDeletedIndexNames = /* @__PURE__ */ new Set();
12080
+ const desiredIndexesUnusable = properties["GlobalSecondaryIndexes"] != null && !Array.isArray(properties["GlobalSecondaryIndexes"]);
12081
+ if (billingOrThroughputChanged && billingMode === "PROVISIONED" && liveBillingMode === "PAY_PER_REQUEST" && liveIndexes.length > 0 && !desiredIndexesUnusable) {
12082
+ const previousIndexNames = new Set((Array.isArray(previousProperties["GlobalSecondaryIndexes"]) ? previousProperties["GlobalSecondaryIndexes"] : []).map((entry) => entry?.["IndexName"]).filter((name) => typeof name === "string"));
12083
+ const removable = [];
12084
+ const remainingWithoutCapacity = [];
12085
+ for (const live of liveIndexes) {
12086
+ const indexName = live.IndexName;
12087
+ if (typeof indexName !== "string") continue;
12088
+ if (!desiredIndexByName.has(indexName)) {
12089
+ if (previousIndexNames.has(indexName)) removable.push(indexName);
12090
+ else remainingWithoutCapacity.push(indexName);
12091
+ continue;
12092
+ }
12093
+ if (!hasUsableDeclaredCapacity(desiredIndexByName.get(indexName))) remainingWithoutCapacity.push(indexName);
12094
+ }
12095
+ const tableCapacityUnusable = !hasUsableTableCapacity(properties["ProvisionedThroughput"]);
12096
+ if (removable.length > 0 && (remainingWithoutCapacity.length > 0 || tableCapacityUnusable)) {
12097
+ const reasons = [];
12098
+ if (remainingWithoutCapacity.length > 0) reasons.push(`index(es) ${remainingWithoutCapacity.join(", ")} stay live and declare no usable per-index ProvisionedThroughput (both ReadCapacityUnits and WriteCapacityUnits are required there)`);
12099
+ if (tableCapacityUnusable) reasons.push(`the template declares no usable table-level ProvisionedThroughput`);
12100
+ this.logger.warn(`AWS::DynamoDB::Table ${logicalId}: this deploy removes global secondary index(es) ${removable.join(", ")} and flips BillingMode to PROVISIONED. The removal would normally be applied BEFORE the flip, but ${reasons.join(" and ")}, so AWS rejects the flip either way. Nothing was removed.`);
12101
+ } else if (removable.length > 0) {
12102
+ this.logger.debug(`Deleting GSI(s) ${removable.join(", ")} on DynamoDB table ${physicalId} before the BillingMode flip to PROVISIONED`);
12103
+ await this.runGsiOps(physicalId, removable.map((name) => ({ Delete: { IndexName: name } })), void 0);
12104
+ for (const name of removable) preFlipDeletedIndexNames.add(name);
12105
+ }
12106
+ }
12107
+ const currentLiveIndexNames = table ? new Set(liveIndexes.map((live) => live.IndexName).filter((name) => typeof name === "string").filter((name) => !preFlipDeletedIndexNames.has(name))) : void 0;
12000
12108
  const gsiHandledByBillingFlip = /* @__PURE__ */ new Set();
12001
12109
  if (billingOrThroughputChanged || tableClassChanged) {
12002
12110
  const updateInput = { TableName: physicalId };
@@ -12009,20 +12117,14 @@ var DynamoDBTableProvider = class {
12009
12117
  WriteCapacityUnits: Number(pt["WriteCapacityUnits"] ?? 5)
12010
12118
  };
12011
12119
  }
12012
- const liveIndexes = table?.GlobalSecondaryIndexes ?? [];
12013
- if (billingOrThroughputChanged && billingMode === "PROVISIONED" && liveBillingMode === "PAY_PER_REQUEST" && liveIndexes.length > 0) {
12014
- const desiredIndexes = Array.isArray(properties["GlobalSecondaryIndexes"]) ? properties["GlobalSecondaryIndexes"] : [];
12015
- const desiredByName = /* @__PURE__ */ new Map();
12016
- for (const entry of desiredIndexes) {
12017
- const name = entry?.["IndexName"];
12018
- if (typeof name === "string") desiredByName.set(name, entry);
12019
- }
12120
+ const flipLiveIndexes = liveIndexes.filter((live) => typeof live.IndexName !== "string" || !preFlipDeletedIndexNames.has(live.IndexName));
12121
+ if (billingOrThroughputChanged && billingMode === "PROVISIONED" && liveBillingMode === "PAY_PER_REQUEST" && flipLiveIndexes.length > 0) {
12020
12122
  const indexUpdates = [];
12021
12123
  const unspecified = [];
12022
- for (const live of liveIndexes) {
12124
+ for (const live of flipLiveIndexes) {
12023
12125
  const indexName = live.IndexName;
12024
12126
  if (typeof indexName !== "string") continue;
12025
- const declared = desiredByName.get(indexName)?.["ProvisionedThroughput"];
12127
+ const declared = desiredIndexByName.get(indexName)?.["ProvisionedThroughput"];
12026
12128
  const read = readCapacityNumber(declared, "ReadCapacityUnits");
12027
12129
  const write = readCapacityNumber(declared, "WriteCapacityUnits");
12028
12130
  if (read === void 0 || write === void 0) {
@@ -12039,7 +12141,7 @@ var DynamoDBTableProvider = class {
12039
12141
  gsiHandledByBillingFlip.add(indexName);
12040
12142
  }
12041
12143
  if (indexUpdates.length > 0) updateInput.GlobalSecondaryIndexUpdates = indexUpdates;
12042
- if (unspecified.length > 0) this.logger.warn(`AWS::DynamoDB::Table ${logicalId}: flipping BillingMode to PROVISIONED, but the template declares no usable ProvisionedThroughput for live index(es) ${unspecified.join(", ")}. AWS requires per-index capacity in the same UpdateTable and will reject the flip naming them. Declare GlobalSecondaryIndexes[].ProvisionedThroughput (both ReadCapacityUnits and WriteCapacityUnits) for each. If an index is listed here because this deploy REMOVES it, drop it in a separate deploy BEFORE the flip AWS demands capacity for every index still live at flip time, so the two cannot be combined.`);
12144
+ if (unspecified.length > 0) this.logger.warn(`AWS::DynamoDB::Table ${logicalId}: flipping BillingMode to PROVISIONED, but the template declares no usable ProvisionedThroughput for live index(es) ${unspecified.join(", ")}. AWS requires per-index capacity in the same UpdateTable and will reject the flip naming them. Declare GlobalSecondaryIndexes[].ProvisionedThroughput (both ReadCapacityUnits and WriteCapacityUnits) for each. An index this deploy REMOVES is deleted BEFORE the flip automatically (issue #1617), so a name here is one the template still keeps, one that exists in AWS but in neither the template nor cdkd state (created out of band), or one whose pre-flip removal was skipped for the reason warned above.`);
12043
12145
  }
12044
12146
  await this.dynamoDBClient.send(new UpdateTableCommand(updateInput));
12045
12147
  if (billingMode !== liveBillingMode) await this.waitForTableAndIndexesActive(physicalId, BILLING_FLIP_ACTIVE_WAIT_ATTEMPTS);
@@ -12117,7 +12219,7 @@ var DynamoDBTableProvider = class {
12117
12219
  this.logger.debug(`Disabled DynamoDB Stream on table ${physicalId}`);
12118
12220
  }
12119
12221
  }
12120
- if (JSON.stringify(properties["GlobalSecondaryIndexes"]) !== JSON.stringify(previousProperties["GlobalSecondaryIndexes"])) await this.applyGsiUpdates(physicalId, resourceType, logicalId, previousProperties["GlobalSecondaryIndexes"], properties["GlobalSecondaryIndexes"], properties["AttributeDefinitions"], gsiHandledByBillingFlip);
12222
+ if (JSON.stringify(properties["GlobalSecondaryIndexes"]) !== JSON.stringify(previousProperties["GlobalSecondaryIndexes"])) await this.applyGsiUpdates(physicalId, resourceType, logicalId, previousProperties["GlobalSecondaryIndexes"], properties["GlobalSecondaryIndexes"], properties["AttributeDefinitions"], gsiHandledByBillingFlip, currentLiveIndexNames);
12121
12223
  if (JSON.stringify(properties["PointInTimeRecoverySpecification"]) !== JSON.stringify(previousProperties["PointInTimeRecoverySpecification"])) await this.applyPointInTimeRecovery(physicalId, properties["PointInTimeRecoverySpecification"], previousProperties["PointInTimeRecoverySpecification"]);
12122
12224
  if (JSON.stringify(properties["TimeToLiveSpecification"]) !== JSON.stringify(previousProperties["TimeToLiveSpecification"])) {
12123
12225
  this.assertNoActiveTtlAttributeNameChange(logicalId, resourceType, physicalId, properties["TimeToLiveSpecification"], previousProperties["TimeToLiveSpecification"]);
@@ -12555,13 +12657,20 @@ var DynamoDBTableProvider = class {
12555
12657
  * a remove-then-add pair, so this method throws on such a change rather than
12556
12658
  * silently dropping it.
12557
12659
  */
12558
- async applyGsiUpdates(physicalId, resourceType, logicalId, previousGsis, desiredGsis, desiredAttributeDefinitions, handledByBillingFlip = /* @__PURE__ */ new Set()) {
12660
+ async applyGsiUpdates(physicalId, resourceType, logicalId, previousGsis, desiredGsis, desiredAttributeDefinitions, handledByBillingFlip = /* @__PURE__ */ new Set(), liveIndexNames) {
12559
12661
  const prev = previousGsis ?? [];
12560
12662
  const desired = desiredGsis ?? [];
12561
12663
  const prevByName = new Map(prev.filter((g) => g.IndexName).map((g) => [g.IndexName, g]));
12562
12664
  const desiredByName = new Map(desired.filter((g) => g.IndexName).map((g) => [g.IndexName, g]));
12563
12665
  const ops = [];
12564
- for (const name of prevByName.keys()) if (!desiredByName.has(name)) ops.push({ Delete: { IndexName: name } });
12666
+ for (const name of prevByName.keys()) {
12667
+ if (desiredByName.has(name)) continue;
12668
+ if (liveIndexNames !== void 0 && !liveIndexNames.has(name)) {
12669
+ this.logger.debug(`GSI ${name} is recorded in cdkd state but not live on DynamoDB table ${physicalId}; skipping its Delete (already removed)`);
12670
+ continue;
12671
+ }
12672
+ ops.push({ Delete: { IndexName: name } });
12673
+ }
12565
12674
  for (const [name, gsi] of desiredByName) if (!prevByName.has(name)) {
12566
12675
  if (!gsi.KeySchema) throw new ProvisioningError(`GlobalSecondaryIndex ${name} on DynamoDB table ${logicalId} is missing KeySchema`, resourceType, logicalId, physicalId);
12567
12676
  ops.push({ Create: {
@@ -12579,6 +12688,18 @@ var DynamoDBTableProvider = class {
12579
12688
  ProvisionedThroughput: gsi.ProvisionedThroughput
12580
12689
  } });
12581
12690
  }
12691
+ await this.runGsiOps(physicalId, ops, desiredAttributeDefinitions);
12692
+ }
12693
+ /**
12694
+ * Issue a list of `GlobalSecondaryIndexUpdates` ops, one per `UpdateTable`,
12695
+ * waiting for the table AND every index to return to ACTIVE between each.
12696
+ *
12697
+ * Extracted from {@link applyGsiUpdates} so the pre-flip GSI removal (issue
12698
+ * #1617) drives the identical call + wait sequence rather than a second copy
12699
+ * of it — the wait is what makes the next op (there, the BillingMode flip)
12700
+ * legal.
12701
+ */
12702
+ async runGsiOps(physicalId, ops, desiredAttributeDefinitions) {
12582
12703
  for (const op of ops) {
12583
12704
  const input = {
12584
12705
  TableName: physicalId,
@@ -70152,7 +70273,7 @@ function createMigrateCommand() {
70152
70273
  */
70153
70274
  function buildProgram() {
70154
70275
  const program = new Command();
70155
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.280.21");
70276
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.280.23");
70156
70277
  program.hook("preAction", (_thisCommand, actionCommand) => {
70157
70278
  const { profile } = actionCommand.optsWithGlobals();
70158
70279
  if (profile !== void 0) process.env["AWS_PROFILE"] = profile;